+9

📨🧠 Kafka Cluster & High Availability – Trái Tim Bất Tử Của Hệ Thống Dữ Liệu - Kafka 101 P6

Kafka Cluster & High Availability – Trái Tim Bất Tử Của Hệ Thống Dữ Liệu

Hãy tưởng tượng bạn đang vận hành một hệ thống xử lý giao dịch thời gian thực cho một ngân hàng số. Vào lúc 2 giờ chiều thứ Sáu - thời điểm lượng giao dịch trong tuần đạt đỉnh - một trong các máy chủ vật lý chạy Kafka bỗng dưng bị cháy card mạng hoặc sập nguồn đột ngột do sự cố phần cứng.

Trong các hệ thống cơ sở dữ liệu truyền thống hoặc các message queue thế hệ cũ, sự cố này thường đi kèm với những tiếng chuông cảnh báo đỏ, sự hoảng loạn của đội ngũ SRE (Site Reliability Engineering), và hàng giờ đồng hồ gián đoạn hệ thống để chuyển đổi dự phòng (failover) bằng tay.

Nhưng với Apache Kafka, dòng chảy giao dịch vẫn tiếp tục trôi đi như chưa hề có cuộc chia ly. Phía Client (các ứng dụng Backend) chỉ nhận thấy độ trễ tăng nhẹ trong vài giây, trước khi mọi thứ trở lại quỹ đạo ổn định. Kafka đã tự động phát hiện nút chết, tái định tuyến lưu lượng, và bầu chọn một máy chủ mới tiếp quản công việc mà không cần bất kỳ sự can thiệp thủ công nào từ con người.

Làm thế nào Kafka đạt được khả năng "bất tử" này? Cơ chế vận hành phân tán của cụm Kafka Cluster đứng sau sự kỳ diệu đó hoạt động ra sao? Bài viết này sẽ mổ xẻ kiến trúc High Availability (HA) của Kafka để giúp bạn hiểu rõ cách thiết kế một hệ thống phân tán chịu lỗi thực thụ.


Câu chuyện từ Production: Sự cố chiều thứ Sáu và cú tự cứu mình của Kafka

Để thấy rõ sức mạnh của tính sẵn sàng cao, hãy nhìn vào một kịch bản thực tế tại một cổng thanh toán lớn. Cụm Kafka của họ gồm 3 Broker (Broker 1, Broker 2, Broker 3). Một topic quan trọng tên là payment-transactions được chia làm 3 partition, mỗi partition có cấu hình replication.factor = 3 (nhân bản 3 lần).

Sự phân bổ partition trên cụm máy chủ lúc bình thường trông như sau:

  • Partition 0: Leader nằm ở Broker 1, Followers nằm ở Broker 2 và Broker 3.
  • Partition 1: Leader nằm ở Broker 2, Followers nằm ở Broker 1 và Broker 3.
  • Partition 2: Leader nằm ở Broker 3, Followers nằm ở Broker 1 and Broker 2.
       [Broker 1]             [Broker 2]             [Broker 3]
  +------------------+   +------------------+   +------------------+
  |  P0 (Leader)     |   |  P0 (Follower)   |   |  P0 (Follower)   |
  |  P1 (Follower)   |   |  P1 (Leader)     |   |  P1 (Follower)   |
  |  P2 (Follower)   |   |  P2 (Follower)   |   |  P2 (Leader)     |
  +------------------+   +------------------+   +------------------+

Đúng 14:05, Broker 1 bị sập nguồn hoàn toàn.

Ngay lập tức, cụm Kafka phát hiện Broker 1 không còn phản hồi nhịp tim (heartbeat). Chỉ trong vòng chưa đầy 2 giây, một quy trình ngầm diễn ra:

  1. Broker 2 (đang giữ vai trò Controller của cụm) phát hiện Broker 1 đã chết.
  2. Controller xem xét danh sách các bản sao còn hoạt động của Partition 0 (nằm trên Broker 2 và Broker 3).
  3. Controller chọn Broker 2 làm Leader mới cho Partition 0.
  4. Client nhận được thông tin Metadata cập nhật từ Controller, tự động gửi tiếp các tin nhắn của Partition 0 sang Broker 2.
  5. Luồng ghi đọc tiếp tục thông suốt. Dữ liệu lịch sử của Partition 0 hoàn toàn nguyên vẹn nhờ đã được sao chép sang Broker 2 từ trước.

Những hiểu lầm tai hại về cơ chế Replication của Kafka

Trước khi đi sâu vào chi tiết, chúng ta cần đập tan hai ngộ nhận phổ biến nhất mà ngay cả nhiều lập trình viên có kinh nghiệm cũng mắc phải:

Ngộ nhận 1: Follower giúp gánh tải đọc/ghi (Read/Write Scaling)

Trong nhiều hệ thống dữ liệu như Redis hay MongoDB, mô hình Master-Slave cho phép Client ghi vào Master và đọc từ Slave để mở rộng băng thông đọc (Read scalability). Kỹ sư mới thường nghĩ Kafka cũng hoạt động tương tự: Ghi vào Leader partition và đọc từ bất kỳ Follower partition nào.

Thực tế: Trong Kafka, theo mặc định, tất cả mọi yêu cầu đọc (Read/Fetch) và ghi (Write/Produce) từ Client đều phải đi qua Leader partition.

                       [ CLIENT ]
                       /        \
              (Write) /          \ (Read)
                     v            v
             +----------------------------+
             |    Broker 1 (P0 Leader)    |
             +----------------------------+
                           ||
                    (Sync Replication)
                           ||
                           v
             +----------------------------+
             |   Broker 2 (P0 Follower)   |
             +----------------------------+

Các Follower partition không bao giờ trả lời yêu cầu của Client; nhiệm vụ duy nhất của chúng là đóng vai trò như các "cái bóng" (shadow), liên tục kéo dữ liệu từ Leader về để đồng bộ hóa. Lý do của thiết kế này là để đảm bảo sự nhất quán tuyệt đối (Strong Consistency) trong việc đọc ghi. Nếu cho phép đọc từ Follower, Client có thể nhận được dữ liệu cũ hoặc bị lệch thứ tự nếu Follower đó đang bị trễ mạng so với Leader.

(Lưu ý: Từ phiên bản Kafka 2.4, tính năng "Follower Fetching" đã được giới thiệu để tối ưu chi phí mạng trong kiến trúc đa vùng - Multi-Region, nhưng đây là cấu hình nâng cao và Client chỉ đọc từ Follower gần nhất về mặt địa lý khi được thiết lập rõ ràng, không phải để load-balance thông thường).

Ngộ nhận 2: Khi leader chết, hệ thống phải dừng lại để cấu hình thủ công

Nhiều người nghĩ rằng việc khôi phục một phân vùng bị mất leader yêu cầu sự can thiệp của Quản trị viên hệ thống để gán lại vai trò Leader bằng tay hoặc chạy các script khôi phục phức tạp. Thực tế, quy trình này diễn ra hoàn toàn tự động ở tầng giao thức nội bộ của Kafka thông qua sự điều phối của Controller Broker.


3 Trụ cột cốt lõi của Kiến trúc High Availability trong Kafka

Để kiến tạo nên khả năng chịu lỗi kỳ diệu này, Kafka dựa trên 3 khái niệm nền tảng:

1. In-Sync Replicas (ISR) - Tập hợp những bản sao đáng tin cậy

Không phải tất cả các Follower đều giống nhau. Có những Follower bám rất sát Leader (đồng bộ dữ liệu chỉ chậm vài mili giây), nhưng cũng có những Follower bị tụt lại rất xa do nghẽn mạng hoặc thiếu tài nguyên CPU.

Để quản lý điều này, Kafka phân loại các bản sao thành hai nhóm:

  • ISR (In-Sync Replicas): Danh sách các bản sao được coi là "đang đồng bộ" với Leader. Leader luôn theo dõi tiến độ của các Follower. Nếu một Follower sao chép kịp thời dữ liệu mới nhất trong khoảng thời gian quy định (mặc định là 30 giây, cấu hình bởi tham số replica.lag.time.max.ms), nó được giữ lại trong danh sách ISR.
  • OSR (Out-of-Sync Replicas): Những follower bị rớt lại phía sau quá thời gian cấu hình sẽ bị trục xuất khỏi ISR.

Khi Leader partition chết, chỉ những Broker nằm trong danh sách ISR mới có quyền ứng cử để trở thành Leader mới. Quy tắc này đảm bảo rằng Leader mới luôn có dữ liệu gần như khớp hoàn toàn với Leader cũ, tránh hiện tượng mất mát dữ liệu nghiêm trọng.

2. Controller Broker - Bộ não điều phối của cả cụm

Trong một cụm Kafka Cluster, luôn có một Broker đảm nhận vai trò Controller. Đây là Broker chịu trách nhiệm quản lý trạng thái của tất cả các partition và replica trong toàn bộ cluster.

Khi một Broker trong cụm bị chết hoặc kết nối lại, Controller là thực thể phát hiện ra đầu tiên thông qua các phiên làm việc với hệ thống quản lý metadata (ZooKeeper hoặc cơ chế KRaft mới). Controller sẽ thực hiện:

  • Cập nhật danh sách các Broker còn sống.
  • Thực hiện bầu chọn Leader mới cho tất cả các partition vốn có Leader nằm trên Broker vừa chết.
  • Phát tán (broadcast) thông tin metadata mới này đến tất cả các Broker còn lại để Client có thể cập nhật bản đồ định tuyến của mình.

3. Cơ chế KRaft (Kafka Raft) - Kỷ nguyên metadata không ZooKeeper

Trước đây, Kafka phụ thuộc vào Apache ZooKeeper để lưu trữ metadata và thực hiện bầu chọn Controller. Khi ZooKeeper gặp sự cố hoặc cụm có quá nhiều partition (vượt quá 100,000), việc đồng bộ metadata qua ZooKeeper trở thành một cơn ác mộng vì nghẽn cổ chai.

Hiện nay, Kafka đã chuyển sang cơ chế KRaft (Kafka Raft Metadata Mode). Với KRaft, metadata được lưu trữ trực tiếp trong một topic nội bộ của chính Kafka và được đồng bộ qua thuật toán đồng thuận Raft giữa một nhóm nhỏ các Broker đặc quyền (Controller Quorum). Điều này giúp quá trình bầu chọn Leader khi có sự cố diễn ra nhanh hơn gấp hàng chục lần, cho phép một cluster quản lý hàng triệu partition mà không hề suy giảm hiệu năng.


Góc nhìn kiến trúc: Thiết lập cấu hình chống mất mát và cân bằng HA

Để cụm Kafka của bạn thực sự đạt được trạng thái High Availability tối ưu, bạn cần hiểu rõ các tham số cấu hình sau dưới lăng kính của một Senior Architect:

1. Cuộc chiến tham số: replica.lag.time.max.ms

Tham số này quyết định thời gian tối đa một Follower được phép không gửi yêu cầu fetch dữ liệu đến Leader trước khi bị đá khỏi danh sách ISR.

  • Nếu đặt quá nhỏ (ví dụ < 5000ms): Trong môi trường mạng Cloud không ổn định, các rung động mạng tạm thời (network jitter) hoặc các đợt Garbage Collection (GC) kéo dài trên Follower sẽ khiến chúng liên tục bị loại khỏi ISR rồi lại được thêm vào. Điều này tạo ra một lượng lớn request cập nhật Metadata chạy khắp cụm, làm quá tải Controller.
  • Nếu đặt quá lớn (ví dụ > 60000ms): Nếu một Follower thực sự bị treo hoặc chậm tiến độ nghiêm trọng, phải mất một phút hệ thống mới nhận diện và loại nó ra khỏi ISR. Nếu trong lúc này Leader bị sập, Controller có nguy cơ bầu chọn một Leader mới có dữ liệu quá cũ (nếu cấu hình cho phép).

2. Sự đánh đổi cực đoan: unclean.leader.election.enable

Đây là quyết định kiến trúc mang tính triết học: Bạn chọn Nhất quán dữ liệu (Consistency) hay Tính sẵn sàng (Availability)?

Giả sử bạn có cụm 3 broker, một partition có Leader ở Broker 1, hai Follower ở Broker 2 và Broker 3 bị lỗi mạng nên đều bị loại khỏi ISR. Lúc này danh sách ISR chỉ còn đúng Broker 1.

Đột nhiên, Broker 1 bị cháy nổ. Bây giờ, không có broker nào còn sống nằm trong danh sách ISR. Bạn có hai lựa chọn:

                  [ CẢM TỬ BẦU CHỌN - UNCLEAN LEADER ELECTION ]
                               Broker 1 (Leader - ISR) [ĐÃ CHẾT]
                                      /        \
                                     /          \
      [LỰA CHỌN A: unclean=false]   /            \   [LỰA CHỌN B: unclean=true]
      ---------------------------  /              \  --------------------------
      * Không bầu Leader mới.     v                v * Bầu Broker 2 hoặc 3 làm Leader.
      * Partition dừng hoạt động.                     * Dữ liệu cũ bị ghi đè.
      * AN TOÀN DỮ LIỆU.                             * HỆ THỐNG TIẾP TỤC CHẠY.
  • Lựa chọn A (unclean.leader.election.enable = false): Kafka sẽ từ chối bầu chọn Broker 2 hoặc Broker 3 làm Leader vì chúng không thuộc ISR. Partition này sẽ ngừng hoạt động hoàn toàn cho đến khi Broker 1 sống lại. Bạn chọn Consistency (dữ liệu không bao giờ bị mất hoặc sai lệch), chấp nhận hi sinh Availability.
  • Lựa chọn B (unclean.leader.election.enable = true): Kafka chấp nhận bầu chọn Broker 2 hoặc Broker 3 làm Leader mới dù dữ liệu của chúng bị thiếu hụt. Khi Broker 1 sống lại, nó sẽ phải xóa bỏ phần dữ liệu mới của mình để đồng bộ theo Leader mới. Bạn chọn Availability (hệ thống tiếp tục hoạt động), chấp nhận mất mát dữ liệu.

Những kịch bản sụp đổ dây chuyền trên Production

Dù có thiết kế HA hoàn hảo, cụm Kafka vẫn có thể bị quật ngã bởi những hiệu ứng dây chuyền sau:

1. Hiện tượng Metadata Storm (Bão Metadata)

Khi cluster có quá nhiều partition (ví dụ 200,000 partition) và chạy trên phần cứng yếu. Khi một broker chứa 50,000 partition bị chết, Controller phải thực hiện 50,000 cuộc bầu chọn Leader mới và gửi thông tin cập nhật metadata này đến tất cả các broker còn lại. Quá trình này tiêu tốn cực kỳ nhiều tài nguyên CPU và băng thông mạng, làm tê liệt Controller và khiến toàn bộ các broker khác bị mất kết nối (heartbeat timeout), dẫn đến việc sập toàn bộ cụm dây chuyền.

2. Split-Brain (Hội chứng hai đầu não)

Trong các phiên bản cũ dùng ZooKeeper, nếu mạng giữa Controller và ZooKeeper bị nghẽn nhưng mạng giữa Controller và các Broker khác vẫn bình thường. ZooKeeper nghĩ Controller cũ đã chết và bầu Controller mới. Kết quả là trong cụm tồn tại 2 Controller cùng ra lệnh bầu chọn Leader, dẫn đến việc dữ liệu metadata bị sai lệch nghiêm trọng. Hiện nay, KRaft với cơ chế đồng thuận nhóm (Quorum) đã giải quyết triệt để rủi ro này.


Bài học cốt lõi từ thiết kế cụm

Tính sẵn sàng cao của Kafka không phải là phép thuật; nó là một bài toán phân bổ xác suất và cấu hình tài nguyên hợp lý:

  • Hãy luôn duy trì replication.factor = 3 cho các môi trường production nghiêm túc.
  • Phân bổ các Broker của bạn trên các vùng vật lý khác nhau (Rack Awareness / AZ Awareness) để tránh sự cố sập nguồn toàn bộ trung tâm dữ liệu.
  • Tận dụng cơ chế KRaft trên các phiên bản Kafka mới nhất để loại bỏ sự phụ thuộc vào ZooKeeper, tăng tốc thời gian khôi phục hệ thống từ vài phút xuống còn vài mili giây.

Hiểu được cách thức hoạt động phân tán của cụm Kafka Broker giúp chúng ta yên tâm về mặt kiến trúc. Tuy nhiên, để dữ liệu được ghi xuống đĩa cứng của từng máy chủ riêng lẻ một cách hiệu quả nhất mà không làm giảm đi tốc độ hàng triệu tin nhắn mỗi giây, Kafka phải sở hữu một cấu trúc lưu trữ tập tin vô cùng đặc biệt dưới ổ đĩa.

Bài viết tiếp theo sẽ đưa bạn đi sâu vào bí mật bên trong "bộ nhớ" của Kafka: Cách nó tổ chức các phân đoạn log (Segments), xây dựng các tệp index và dọn dẹp dữ liệu cũ mà không gây ảnh hưởng tới hiệu năng hệ thống.


🤝 Đồng hành cùng TechCraft

TechCraft là nơi chia sẻ kiến thức về Backend Engineering, Database, Distributed Systems và Production Architecture thông qua các bài viết, video và những series được xây dựng theo lộ trình.

Nếu bạn yêu thích cách tiếp cận này, hãy tiếp tục đồng hành cùng TechCraft trên các nền tảng bên dưới.

Và nếu muốn học chuyên sâu hơn, Dev Insider sẽ là nơi tập trung toàn bộ các nội dung premium được cập nhật liên tục mỗi tuần.

🚀 Dev Insider https://www.patreon.com/techcraft_official/posts/vi-sao-dev-ra-161163881?collection=2220113

📘 Facebook https://www.facebook.com/techcraft.official

🎥 YouTube https://www.youtube.com/@techcraft.official

🎵 TikTok https://www.tiktok.com/@techcraft.official

Think Beyond Code. Build Better Systems.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí