← All posts

Cơ chế phát hiện dịch vụ của chúng tôi đã bắt được lỗi của chính nó và tự tắt đi

Authagonal·July 1, 2026
authdistributed-systemsclusteringleader-electionazureaks

Chúng tôi có một cluster ba bản sao liên tục bất đồng với chính nó. Các tác vụ nền chạy lặp lại hai, ba lần. Câu trả lời không nằm trong log; nó nằm trong chính mã của chúng tôi. Thủ tục phát hiện peer có một khối catch, và dòng chú thích bên trong đại khái nói: "multicast thất bại, đã vô hiệu hóa phát hiện." Cơ chế phát hiện dịch vụ của chúng tôi đang bắt được lỗi của chính nó rồi lặng lẽ tự tắt đi, và nó đã làm đúng như vậy trong môi trường production ngay từ ngày đầu tiên.

Đây là câu chuyện về lý do vì sao một giao thức clustering vốn đúng đắn trên một máy chủ đặt trong rack lại là công cụ sai ngay khi bạn đưa nó lên một nền tảng được quản lý, và vì sao cách khắc phục là xóa nó đi thay vì sửa nó.

Cluster thực sự dùng để làm gì

Một auth server chạy nhiều hơn một bản sao để đảm bảo tính sẵn sàng. Các bản sao phần lớn độc lập với nhau: bất kỳ bản nào cũng có thể xác thực một token hoặc kiểm tra một mật khẩu. Nhưng vài tác vụ phải chạy một lần, chứ không phải một lần cho mỗi bản sao. Tác vụ quét giữ liệu xóa các bản ghi đã hết hạn. Lượt chạy kích hoạt các webhook của khách hàng. Bất cứ thứ gì vươn ra ngoài và tạo ra một tác động phụ. Với những thứ đó, bạn cần hai điều mà cluster phải thống nhất: ai là thành viên, và bản nào trong số đó là leader chạy công việc singleton.

Câu trả lời ban đầu của chúng tôi là câu trả lời kinh điển: gossip. Mỗi node trò chuyện với các peer của nó, danh sách thành viên là một thuộc tính nổi lên từ việc ai đang liên lạc được, và nhóm bầu ra một leader từ tập hợp đã thống nhất. Đó là một thiết kế tuyệt đẹp. Nó cũng giả định rằng các node có thể tìm thấy nhau, và cách mà mã cũ tìm chúng là multicast: hô lớn trên phân đoạn mạng cục bộ rồi xem ai trả lời.

Vì sao nó hỏng, một cách âm thầm

Kubernetes được quản lý không truyền tải multicast. Gần như mọi mạng cloud mà bạn không tự dựng cũng vậy. Tiếng hô phát đi và chẳng có gì vọng lại, lần nào cũng thế. Thế là việc phát hiện "thất bại," khối catch kích hoạt, và mỗi node kết luận rằng nó cô độc một mình trên đời.

Với một bản sao thì điều đó vô hại. Với ba bản thì đó là split-brain theo đúng cấu trúc: ba node, mỗi node chắc chắn mình là thành viên duy nhất, mỗi node tự bầu chính nó làm leader, mỗi node chạy công việc singleton. Tác vụ quét giữ liệu chạy trên cả ba. Lượt webhook kích hoạt webhook của mỗi khách hàng ba lần. Không có gì trong số đó ném ra lỗi cả, bởi từ góc nhìn của mỗi node cô độc thì mọi thứ đều ổn. Bug không phải là một cú crash; đó là ba chương trình, mỗi chương trình hành xử hoàn toàn đúng đắn trong một thế giới mà chúng hiểu sai.

Cách khắc phục là ngừng hẳn việc phát hiện peer

Đây là góc nhìn mới khiến toàn bộ vấn đề thu gọn lại còn vài dòng: chúng tôi đang cố dựng lại, bằng một giao thức lắm lời, một sự thật mà nền tảng vốn đã lưu trữ giúp chúng tôi với tính nhất quán mạnh. Chúng tôi chạy trên một cloud cấp cho chúng tôi một kho lưu trữ nhất quán mạnh. Việc bầu leader không cần đến đồng thuận giữa các peer nếu đã có sẵn một nơi mà mọi người đều có thể đồng ý.

Thế là vai trò leader trở thành một blob lease. Một blob, một lease. Ai giữ lease thì người đó là leader. Lease có thời gian hết hạn, nên một leader chết đi sẽ ngừng gia hạn và lease tự giải phóng cho người tiếp theo giành lấy. Không có phát hiện peer, không có tính toán quorum, không có multicast, và không có khối catch nào chực chờ vô hiệu hóa nó. Việc điều phối và các sự kiện chạy trên một bảng nhỏ đóng vai trò một bus giữa các node.

Thuộc tính mà tôi không ngờ là mình lại thích đến vậy: bạn có thể mở Storage Explorer và nhìn thấy tâm trí của cluster. Ai đang giữ lease ngay lúc này. Cái gì đang xếp hàng trên bus. Danh sách thành viên thôi không còn là thứ bạn suy ra từ hành vi của một giao thức nữa, mà trở thành một dòng dữ liệu bạn có thể đọc. Khi cái quyết định ai chạy các tác vụ phá hủy của bạn là một giá trị bạn có thể nhìn vào, thì việc gỡ lỗi thôi không còn giống khảo cổ học nữa.

Phần mà chúng tôi cố tình xóa đi một thứ thông minh

Nhân tiện làm luôn, chúng tôi xóa đi một bộ giới hạn tốc độ phân tán dựng trên một conflict-free replicated data type, một CRDT hợp nhất các bộ đếm của từng node thành một con số toàn cục mà không cần điều phối. Nó thực sự thanh lịch. Nhưng nó cũng đang giải quyết một bài toán mà chúng tôi đã dời đi nơi khác. Giới hạn tốc độ toàn cục thuộc về phần rìa, nơi các request đi vào, chứ không phải được dựng lại bên trong cluster bằng một cấu trúc dữ liệu cần cả một đoạn văn để giải thích. Xóa.

Xóa đi mã đang chạy tốt cảm giác như một mất mát, cho đến khi bạn tính xem mình thôi không phải bảo trì những gì. Chúng tôi gỡ bỏ lớp gossip, giả định về multicast, khối catch tự vô hiệu hóa, và CRDT, rồi thay tất cả bằng một lease và một bảng. Số dòng mã giảm đi, và số trạng thái mà hệ thống có thể rơi vào cũng giảm theo.

Bài học, đúc kết lại

Danh sách thành viên kiểu gossip và multicast là công cụ đúng đắn trên bare metal hoặc trên một mạng phẳng mà bạn kiểm soát. Chúng là công cụ sai trên một nền tảng có mạng không truyền tải nổi chính cái cơ chế mà chúng phụ thuộc vào, và kiểu lỗi không phải là một cú crash ầm ĩ; đó là một cú dự phòng lặng lẽ trông như đang hoạt động bình thường. Trước khi đưa một mẫu thiết kế hệ phân tán lên hạ tầng được quản lý, hãy tự hỏi nó giả định gì về mạng, và hãy tự hỏi nền tảng đã cho bạn sẵn những gì miễn phí. Của chúng tôi vốn đã chạy một kho lưu trữ nhất quán mạnh. Một lease trong kho đó là một cơ chế bầu leader đơn giản hơn, đúng đắn hơn so với giao thức mà chúng tôi đã mang vác bấy lâu, và đó là cơ chế chúng tôi có thể quan sát được.

Nếu bạn vận hành auth, những phần giữ khóa của bạn và quyết định ai chạy các tác vụ phá hủy của bạn nên là những thứ nhàm chán nhất và dễ soi xét nhất mà bạn sở hữu. Chúng tôi đã làm cho phần của mình trở nên nhàm chán. Đó là một sự nâng cấp.