Steven
Steven8 phút đọc

Một cú rớt kết nối không nên làm sập cả ứng dụng — nhưng của chúng tôi thì có

Khi backend của chúng tôi rớt mạng giữa cuộc họp, nó không chỉ tạm dừng việc chép lời — nó làm sập toàn bộ ứng dụng. Nguyên nhân là một sự kiện đơn lẻ không được xử lý, và bản vá chỉ mười dòng. Đây là cụm bản phát hành đã khiến GeekBye giữ được đăng nhập và giữ được kết nối xuyên qua những thứ từng giết chết nó.

Kỹ thuật
Độ tin cậy
Electron
Bản phát hành GeekBye
Một cú rớt kết nối không nên làm sập cả ứng dụng — nhưng của chúng tôi thì có

Có những bug làm gián đoạn một tính năng. Những cái tệ nhất kéo sập mọi thứ xung quanh nó. Ở một bản dựng đầu đời của GeekBye, nếu backend rớt mạng khi bạn đang giữa cuộc họp, việc chép lời không chỉ dừng lại — cả ứng dụng sập. Một cú rớt kết nối, và cả cửa sổ biến mất.

Cụm bản phát hành đã sửa chuyện này — v1.6.8 đến v1.6.11 — là một bài học bậc thầy về công việc chẳng hào nhoáng gì mang tên "giữ đăng nhập, giữ kết nối". Bản vá tiêu điểm chỉ mười dòng. Đây là mười dòng đó là gì, và mọi thứ khác đã ra mắt cùng chúng.

Mười dòng giữa một cú rớt và một cú sập

Cú sập nằm trong đoạn mã âm thanh. Khi một phiên chép lời tháo dỡ các WebSocket của nó, nó gọi removeAllListeners() trên chúng — dọn dẹp hợp lý. Nhưng đây là chi tiết Node.js biến việc dọn dẹp thành thảm họa: một sự kiện error không có trình lắng nghe thì không bị bỏ qua. Nó bị ném lại thành một ngoại lệ không bắt.

Vậy hãy hình dung trình tự khi backend rớt mạng: socket có một sự kiện error đang xếp hàng chờ phát ra. Việc dọn dẹp chạy removeAllListeners(), bóc đi trình xử lý lẽ ra đã bắt được nó. Một khoảnh khắc sau, lỗi đang xếp hàng phát ra — vào hư không. Node thấy một sự kiện error không ai lắng nghe, và làm điều duy nhất mà giao ước của nó cho phép: ném nó ra thành một ngoại lệ không bắt, khiến tiến trình sập. Backend của người dùng nấc lên hai giây và ứng dụng của họ tan biến.

Bản vá (v1.6.8) gần như hụt hẫng vì đơn giản. Ngay sau khi bóc các trình lắng nghe, hãy gắn lại một trình xử lý error cố ý rỗng:

this.micWebSocket.removeAllListeners()
this.micWebSocket.on('error', () => {}) // absorb late error events

Hàm rỗng đó chính là toàn bộ ý nghĩa. Nó cho lỗi đang chờ một chỗ để đáp xuống — một điểm hứng — nên Node có trình lắng nghe và không bao giờ ném lại. Cùng commit đó còn dọn dẹp các socket mở dở khi một lần thử kết nối thất bại giữa chừng, để một cú kết nối hỏng không thể để lại một socket còn sống lủng lẳng với một lỗi chực chờ phát ra. Bình luận commit nói thẳng: "Không có cái này, các lỗi đang chờ trở thành ngoại lệ không bắt và làm sập ứng dụng." Mười dòng biến một cú sập toàn bộ thành một cú rớt kết nối lặng lẽ, phục hồi được.

Bài học đáng giá hơn cả bản vá: bất cứ khi nào bạn gọi removeAllListeners() trên một socket hay stream mà bạn có thể vẫn đang tháo dỡ, hãy gắn lại một điểm hứng error rỗng trước đã. Một sự kiện error không được xử lý là một cú sập, không phải một dòng log.

Giữ đăng nhập: làm mới trước, và làm mới khi bị bật ngược

Nửa "giữ kết nối" có một người song sinh là "giữ đăng nhập". Trước cụm này, token xác thực của GeekBye cứ thế hết hạn — sau một lúc, bạn bị hất phũ phàng về màn hình đăng nhập, giữa chừng luồng công việc, chẳng vì lý do nào bạn thấy được.

v1.6.8 đã sửa điều này từ cả hai hướng:

  • Chủ động: một bộ hẹn giờ làm mới token trước khi nó hết hạn, được lên lịch tại thời điểm hết hạn trừ đi một khoảng đệm thông minh (một phần trong tuổi thọ của token, có sàn và trần). Bạn được làm mới trước khi kịp nhận ra.
  • Phản ứng: nếu một yêu cầu vẫn trả về 401 — lệch đồng hồ, một nhịp hẹn giờ bị lỡ, một chiếc laptop vừa nguội — client làm mới một lần và thử lại chính yêu cầu ban đầu đúng một lần. Một cờ canh duy nhất chặn lần thử lại đó, để một 401 dai dẳng không bao giờ có thể xoáy thành vòng lặp vô tận.

Phần tinh tế nhưng cốt tử: cả hai đường đều nằm sau một mutex. Nếu mười yêu cầu cùng đụng 401 trong một khoảnh khắc, chúng không kích hoạt mười lần làm mới — chúng đều chờ một lần làm mới đang diễn ra rồi mới thử lại. Và khi một lần làm mới thật sự không thể cứu vãn, ứng dụng cho phiên hết hạn một cách êm ả — một thông báo mười giây rõ ràng "phiên của bạn đã hết hạn, vui lòng đăng nhập lại" — thay vì một cú đá lặng lẽ về màn hình đăng nhập.

Kết nối lại: thêm ở client, rồi chuyển sang backend

v1.6.9 làm cho một cú rớt kết nối chép lời trở nên phục hồi được thay vì chí mạng. Khi backend báo rằng kết nối giọng nói thượng nguồn đã rớt, client thôi phơi ra một lỗi chí mạng và thay vào đó kết nối lại — với trải nghiệm cố ý điềm tĩnh: một thông báo hổ phách "Đang kết nối lại…" (không phải mỗi lần thử một cái), một thông báo xanh "Đã kết nối lại" khi nó trở về, và lỗi cuối cùng "vui lòng khởi động lại" chỉ xuất hiện sau khi mọi lần thử đã cạn.

Rồi điều tôi thích ở cụm này: một bản phát hành sau, ở v1.6.10, chúng tôi xóa đoạn logic kết nối lại phía client đó — khoảng 113 dòng — và chuyển việc kết nối lại sang backend, nơi giờ phát ra những thông điệp trạng thái "đang kết nối lại" lặng lẽ mà client chỉ việc phản chiếu. Chúng tôi xây phần phục hồi ở client, quyết định backend mới là chủ nhân đúng đắn, và chuyển nó đi. Đó không phải sự do dự; đó là vòng đời trung thực của một bài toán khó. Logic độ tin cậy bị chẻ ra hai đầu của một kết nối là một mùi khó chịu — hãy chọn một chủ sở hữu. (Backend rốt cuộc trở thành chủ sở hữu dứt khoát của việc này, chính là câu chuyện kết nối lại được kể trong vì sao trình ghi chú AI của bạn dừng khi Wi-Fi tệ.)

Cùng bản phát hành đó làm cho giới hạn thời gian ghi âm trở nên nhân văn: thay vì một lỗi trần trụi, là một thông báo cảnh báo cho phép ghi âm tiếp tục, và một thông điệp rõ ràng "Đã đạt giới hạn ghi âm" khi nó thật sự dừng — câu chữ thân thiện được đưa ra, tiền tố nội bộ được bóc đi.

Một lần thử lại thống lĩnh tất cả

Đến v1.6.11, cùng một đoạn logic thử-lại-kèm-backoff đã bị sao chép dán vào ba nơi khác nhau — xác thực, cập nhật, tháo dỡ phiên. Nên nó được hợp nhất thành một mô-đun duy nhất với một quy tắc chung về rốt cuộc cái gì đáng để thử lại: các trục trặc mạng thoáng qua (kết nối bị đặt lại, hết thời gian chờ, DNS nấc, socket đứt) được thử lại với backoff theo cấp số nhân; còn một lỗi máy chủ thật sự thì thất bại ngay lập tức, vì thử lại một 4xx đúng nghĩa chỉ tổ phí thời gian của người dùng. Một bộ phân loại, vài preset có tên, ba bên gọi được chuyển lên nó.

Cụm này đã dạy điều gì

  1. Một sự kiện error không được xử lý là một cú sập, không phải một dòng log. Node ném lại các sự kiện error không có trình lắng nghe. Nếu bạn bóc các trình lắng nghe trên một socket mà bạn có thể vẫn đang tháo dỡ, bạn phải gắn lại một điểm hứng error rỗng — nếu không một cú rớt kết nối sẽ kéo cả ứng dụng đi theo.
  2. Làm mới thông tin đăng nhập trước khi hết hạn phản ứng khi gặp 401 — với một mutex ở cả hai. Chủ động giữ cho bạn đăng nhập; phản ứng bắt các trường hợp biên; mutex nghĩa là N bên gọi đồng thời chỉ gây đúng một lần làm mới, và trần thử lại nghĩa là một token hỏng không thể lặp mãi.
  3. Chỉ thử lại với các lỗi thoáng qua, và tập trung hóa bộ phân loại. "Cái này có đáng thử lại không?" là một quyết định thuộc về một chỗ. Thử lại một lỗi thật sự chỉ là một thất bại chậm hơn.
  4. Quyết định ai sở hữu việc kết nối lại. Chúng tôi thêm nó ở client, rồi chuyển sang backend. Logic phục hồi sống ở cả hai đầu là một cỗ máy sinh bug; hãy cho nó một mái nhà duy nhất.

Đây là chương thứ ba của câu chuyện độ tin cậy trở thành GeekBye v2. Với chương trước, xem chúng tôi xóa 5.000 dòng mã âm thanh và bản chép lời bắt đầu hiện ra hai lần (v1.6.0); với nơi việc kết nối lại rốt cuộc an vị, vì sao trình ghi chú AI của bạn dừng khi Wi-Fi tệ; và với toàn bộ cung đường, giải phẫu học của việc phát hành phần mềm đến độ hoàn hảo.

Bài Viết Liên Quan

Cơ chế kiểm tra an toàn khiến ứng dụng của chúng tôi không thể tắt nổi
Steven
Steven8 phút đọc

Cơ chế kiểm tra an toàn khiến ứng dụng của chúng tôi không thể tắt nổi

Auto-update là tính năng khó ship nhất mà chúng tôi từng làm — sáu bản phát hành trong bốn ngày chỉ để nó ngừng làm hỏng ứng dụng. Con bug tệ nhất lại chính là thứ chúng tôi tự tạo ra khi cố gắng cẩn thận: một bước kiểm tra "an toàn" dài 500 mili-giây biến một bản cập nhật thất bại thành một tiến trình mà bạn thực sự không thể tắt được.

Kỹ thuật
Electron
Độ tin cậy
Giải phẫu quá trình đưa phần mềm đến độ hoàn hảo: Cách đánh giá mã bắt được lỗi mà kiểm thử bỏ sót
Steven
Steven9 phút đọc

Giải phẫu quá trình đưa phần mềm đến độ hoàn hảo: Cách đánh giá mã bắt được lỗi mà kiểm thử bỏ sót

Xuyên suốt loạt bài GeekBye v2, cùng một điều cứ lặp đi lặp lại: một bản sửa lỗi vượt qua mọi bài kiểm thử trên máy của lập trình viên, rồi khâu đánh giá mã chứng minh rằng nó sẽ thất bại với gần như tất cả mọi người. Đây là quy trình đằng sau chín bản phát hành — cửa kiểm soát đánh giá, những phát hiện bắt lỗi ngay từ đầu, và kỷ luật kiểm thử trước khi phát hành đã biến "nó chạy được trên máy tôi" thành "nó chạy được".

Kỹ thuật
Quy trình
Đánh giá mã
Ngày ứng dụng của chúng tôi tự DDoS chính mình
Steven
Steven7 phút đọc

Ngày ứng dụng của chúng tôi tự DDoS chính mình

Một đống bản tải lên còn tồn đọng, được xả ra cùng một lúc lúc khởi động, đã biến mỗi client GeekBye thành một đợt tấn công từ chối dịch vụ nhỏ nhắm vào chính máy chủ của chúng tôi. Cách sửa — và cái thang connection-liveness mà nó buộc chúng tôi phải dựng — là một trong những thứ hữu ích nhất mà v2 đã dạy chúng tôi.

Độ tin cậy
Mạng
Kỹ thuật