Có một luật đơn giản về dư địa: khoảng cách giữa cái bạn đòi hỏi và cái bạn có chính là số đơn vị bạn được phép mất mà vẫn ổn. Đặt đòi hỏi bằng đúng tổng số có, dư địa về không — và khi ấy một việc hoàn toàn bình thường như vá một máy cũng trở thành sự cố. min.insync.replicas là chỗ luật này hiện ra rõ nhất trong Kafka, và tôi sẽ đo đúng cái giá của việc quên nó.

min.insync.replicas là tham số quyết định lúc nào Kafka thà từ chối ghi còn hơn nhận rồi mất. Bài này đo cả ba giá trị ở cả ba mức hỏng.

Nó làm gì

Khi producer gửi với acks=all, broker chỉ xác nhận sau khi mọi bản sao trong ISR đã ghi. Vấn đề: ISR có thể co lại. Còn một bản sao trong ISR thì "mọi bản sao" là một, và acks=all lặng lẽ biến thành acks=1.

min.insync.replicas chặn chuyện đó: ISR nhỏ hơn con số này thì broker từ chối ghi với lỗi NOT_ENOUGH_REPLICAS.

Nó là cấu hình của topic, không phải của producer — nên nó là chỗ duy nhất người quản trị áp được luật lên mọi ứng dụng đang ghi vào topic đó.

Đo cả ba giá trị

Ba topic giống hệt nhau, replication-factor=3, chỉ khác min.insync.replicas. Gửi 200 tin với acks=all:

3 broker sống Giết 1 broker Giết 2 broker
min.insync=1 200 gửi được 200 gửi được 0 — hết giờ
min.insync=2 200 gửi được 200 gửi được 0 — hết giờ
min.insync=3 200 gửi được 0 — NOT_ENOUGH_REPLICAS 0 — hết giờ

Ô đáng chú ý là ô giữa của hàng cuối.

min.insync.replicas=3 với replication-factor=3 khiến mọi lần bảo trì một broker đều làm dừng ghi. Không có dư địa nào. Vá hệ điều hành, nâng phiên bản Kafka, thay ổ đĩa — mỗi việc đó đều thành sự cố toàn phần.

Đây là sai lầm dễ mắc vì nó nghe hợp lý: "muốn an toàn nhất thì đòi cả ba bản sao". Thực tế nó đổi một rủi ro hiếm (mất dữ liệu khi hai broker chết cùng lúc) lấy một chắc chắn thường xuyên (dừng ghi mỗi lần bảo trì).

Luật thực dụng: min.insync.replicas phải nhỏ hơn replication-factor. Chênh lệch đó chính là số broker bạn được phép mất.

Giết hai broker: chuyện khác hẳn

Hàng cuối cùng của bảng — cột "giết 2 broker" — trông như min.insync đang làm việc của nó. Không phải.

Cụm của tôi có ba node, mỗi node mang cả hai vai broker và controller. Đây là cấu hình mặc định và phổ biến nhất cho cụm nhỏ. Giết hai node là mất luôn quorum controller — KRaft cần đa số, tức 2 trên 3.

min.insync = 1   ->  0 tin, TimeoutException
đọc              ->  0 tin
describe         ->  vẫn in "Leader: 1, Isr: 1,2"

Ngay cả min.insync=1 cũng không ghi được — không phải vì thiếu bản sao, mà vì không còn ai bầu leader mới. Đây là kiểu hỏng khác hẳn, và không cấu hình topic nào chạm tới được.

Chi tiết đáng nhớ nhất: describe vẫn trả lời như thể mọi thứ bình thường. Nó in ra Leader: 1, Isr: 1,2 — thông tin từ bản siêu dữ liệu đã lưu sẵn, không hỏi lại controller. Cụm đã chết mà công cụ chẩn đoán vẫn báo khoẻ.

Nếu bạn từng gặp cảnh "Kafka không ghi được nhưng describe nói ổn", đây là một trong những nguyên nhân. Cách kiểm đúng là hỏi thẳng quorum:

kafka-metadata-quorum.sh --bootstrap-server kb1:9092 describe --status

Trên cụm ba node gộp vai, mất hai node là mất tất cả — bất kể replication-factor hay min.insync.replicas bằng bao nhiêu. Cụm năm node chịu được hai node chết. Đó là lý do cụm sản xuất lớn tách controller ra riêng, thường là năm controller nhỏ, để việc bảo trì broker không bao giờ chạm tới quorum.

Bộ ba dùng thật

replication-factor  = 3
min.insync.replicas = 2
acks                = all

Chịu được một broker chết mà không mất tin và không dừng ghi. Hai broker chết thì dừng ghi — và đó là lựa chọn có chủ ý, không phải lỗi: thà dừng còn hơn nhận tin rồi mất.

Đặt trên topic:

kafka-configs.sh --bootstrap-server kb1:9092 --alter \
  --entity-type topics --entity-name ten-topic \
  --add-config min.insync.replicas=2

Đổi được lúc đang chạy, có hiệu lực ngay, không cần khởi động lại gì.

Ba chỗ nó không cứu được

1. Producer dùng acks=1 hoặc acks=0. Broker chỉ kiểm min.insync.replicas khi producer đòi acks=all. Đặt min.insync.replicas=2 mà ứng dụng gửi acks=1 thì con số đó hoàn toàn vô nghĩa — phần 4 đã đo: với hai broker chết, acks=1 báo gửi thành công 100 tin trong khi acks=all từ chối.

Không có cách nào ép từ phía broker. Cách duy nhất là kiểm ở phía ứng dụng, hoặc kiểm định kỳ bằng cách đọc cấu hình producer từ số liệu JMX.

2. Topic tạo tự động. auto.create.topics.enable bật mặc định. Topic sinh ra kiểu đó nhận mặc định của broker — thường là replication-factor=1 và min.insync.replicas=1. Một lỗi gõ sai tên topic trong producer là đủ tạo ra một topic không có bản sao nào, và nó chạy êm cho tới khi broker đó chết.

Trên cụm sản xuất, tắt nó đi:

auto.create.topics.enable=false

3. Mất quorum controller. Như đo ở trên. Đây là bài toán tô-pô cụm, không phải bài toán cấu hình topic.

Kiểm nhanh toàn cụm

Liệt kê mọi topic không đạt chuẩn:

for t in $(kafka-topics.sh --bootstrap-server kb1:9092 --list); do
  rf=$(kafka-topics.sh --bootstrap-server kb1:9092 --describe --topic "$t" \
       | head -1 | grep -oE "ReplicationFactor: [0-9]+" | cut -d' ' -f2)
  mi=$(kafka-configs.sh --bootstrap-server kb1:9092 --describe \
       --entity-type topics --entity-name "$t" \
       | grep -oE "min.insync.replicas=[0-9]+" | cut -d= -f2)
  [ "$rf" -lt 3 ] || [ "${mi:-1}" -lt 2 ] && echo "$t  rf=$rf  min.insync=${mi:-mặc định}"
done

Chạy lệnh này trên một cụm đã sống vài năm thường cho ra một danh sách bất ngờ dài — phần lớn là topic tạo tự động mà không ai nhớ.

Cùng một luật "dư địa là khoảng cách giữa cần và có", đổi ngăn xếp

Chuyện min.insync=3 với 3 bản sao dừng ghi mỗi lần bảo trì không phải đặc sản Kafka. Nó là hệ quả của một luật dư địa mà mọi hệ có năng lực hữu hạn đều phải theo: đặt đòi hỏi bằng đúng tổng số có là để lại số không dư địa.

Trong thế giới cơ sở dữ liệu, đây đúng là lý do PostgreSQL giữ riêng superuser_reserved_connections: nếu max_connections bị ứng dụng dùng cạn tới đúng trần, quản trị viên không vào nổi để cứu — nên hệ chừa sẵn vài suất. Một connection pool đặt bằng đúng tải đỉnh mà không dư sẽ cạn ngay khi một truy vấn chậm giữ một kết nối lâu hơn thường; đĩa để đầy 100% thì bất cứ thứ gì tăng thêm là sự cố, nên người ta cảnh báo ở 75%.

Rộng hơn, đây chính là dự phòng N+1: bạn cấp nhiều hơn mức tối thiểu cần đúng một (hoặc hai) đơn vị, để còn rút một ra bảo trì mà vẫn đáp ứng yêu cầu. replication-factor=3 với min.insync.replicas=2 chính là N+1 — cần 2, có 3, được phép mất 1.

Mẫu số chung: một ràng buộc đặt bằng đúng toàn bộ năng lực (đòi hỏi = tổng số có) để lại không dư địa, nên mọi mất mát dù chỉ một đơn vị — kể cả việc bảo trì bình thường, vốn là chuyện thường chứ không phải ngoại lệ — đều biến thành gián đoạn; dư địa an toàn chính là khoảng cách giữa 'cần' và 'có', và bạn phải chừa nó ra đủ rộng để chịu được lượng churn dự kiến, chứ không chỉ churn thảm hoạ. Nên câu hỏi đúng khi đặt bất kỳ ngưỡng tối thiểu nào không phải "đặt cao nhất cho an toàn" mà là "khoảng cách giữa cái tôi đòi hỏi và cái tôi có là bao nhiêu — vì đó đúng bằng số đơn vị tôi được phép mất cho một lần vá máy bình thường; nếu nó bằng 0 thì mỗi lần bảo trì là một sự cố". Và một hệ quả về chẩn đoán: khi hệ chết ở tầng dưới (mất quorum), công cụ đọc từ bản siêu dữ liệu đã lưu sẵn vẫn báo khoẻ — hỏi thẳng quorum mới ra sự thật.

Muốn kiểm chính xác chỗ topic của bạn sẽ gãy, chạy thử:

kafka-topics.sh --bootstrap-server kb1:9092 --describe --topic ten-topic | head -1
kafka-configs.sh --bootstrap-server kb1:9092 --describe \
  --entity-type topics --entity-name ten-topic | grep min.insync

Lấy ReplicationFactor trừ min.insync.replicas. Kết quả là số broker bạn được phép mất trước khi việc ghi dừng lại. Nếu nó bằng 0, mỗi lần bảo trì là một lần gián đoạn.

Phần sau đo lưu trữ: dữ liệu ở lại bao lâu, và điều gì thật sự xảy ra khi hết hạn.