Reliability & Ordering
ISR (In-Sync Replicas)
ISR คือ subset ของ replica ที่ sync ทันกับ leader จริงๆ ณ ขณะนั้น (ไม่ใช่ทุก replica ที่ ถูก assign ไว้ตาม replication factor) follower ที่ sync ช้าเกินไปจะถูกเตะออกจาก ISR ชั่วคราว จนกว่าจะตามทัน
Partition 0: replicas = [broker1(leader), broker2, broker3]
ISR ปกติ = [broker1, broker2, broker3]
ถ้า broker3 lag เกิน replica.lag.time.max.ms:
ISR = [broker1, broker2] // broker3 หลุดจาก ISR ชั่วคราว
"ISR = ทีมที่ตามทันงานจริงๆ ไม่ใช่ทีมที่ชื่ออยู่ในรายชื่อ" — replication factor คือจำนวนคนที่ "ถูกมอบหมาย" ให้ดูแลงานนี้ แต่ ISR คือคนที่ "ตามงานทันจริงๆ ตอนนี้" เท่านั้น คนที่ตามไม่ทันจะถูก เอาชื่อออกจากทีมชั่วคราว (ไม่ใช่ไล่ออกถาวร) จนกว่าจะตามทัน
ถ้า ISR เหลือ broker เดียว (leader เดี่ยวๆ) แล้ว leader ล่มไปพอดี มี 2 ทางเลือกที่ขัดแย้งกัน
ซึ่งควบคุมด้วย unclean.leader.election.enable: false (ค่า default แนะนำ) คือ ยอม
ให้ partition นั้น unavailable ชั่วคราว (ไม่มีใคร elect เป็น leader ใหม่) จนกว่า broker
เดิมจะกลับมา เพื่อป้องกัน data loss อย่างเด็ดขาด ส่วน true คือยอมให้ replica ที่ตามไม่ทัน
(นอก ISR) ขึ้นเป็น leader ใหม่ได้ทันที (unclean leader election) ซึ่งแลก availability
สูงขึ้นกับความเสี่ยง data loss (message ที่ replica นั้นยังตามไม่ทันจะหายไปถาวร) — นี่คือ
trade-off ระหว่าง CP (consistency) กับ AP (availability) ตาม CAP theorem ที่เกิดขึ้นจริงใน
Kafka configuration ไม่ใช่แค่ทฤษฎีลอยๆ
acks — Producer รอ Confirm แค่ไหนถึงจะถือว่าสำเร็จ
acks=0 # ไม่รอ confirm เลย เร็วสุดแต่เสี่ยงข้อมูลหายสูงสุด
acks=1 # รอแค่ leader confirm (default เก่า) — leader ตายก่อน replicate ข้อมูลหายได้
acks=all # รอทุก ISR confirm — ปลอดภัยสุด แลกกับ latency สูงขึ้น (= acks=-1)
"acks คือจะรอใบเสร็จจากใครก่อนเดินออกจากร้าน": 0 = ไม่รอใบเสร็จเลยเดินออกทันที, 1 = รอ
แคชเชียร์คนเดียวยืนยัน, all = รอให้ทุกสาขาที่ควรมีสำเนาใบเสร็จยืนยันครบก่อนค่อยเดินออก
acks=all ไม่ได้แปลว่าปลอดภัย 100% โดยอัตโนมัติ ถ้าไม่ตั้งคู่กับ
min.insync.replicas ให้สอดคล้องกัน — acks=all แปลว่า "รอให้ ISR ปัจจุบัน ยืนยันครบ"
แต่ถ้า ISR เหลือแค่ 1 (broker อื่น lag หลุดออกไปหมด) การันตีจาก acks=all ก็เท่ากับ
acks=1 ไปโดยปริยาย (เพราะ ISR มีแค่ leader ตัวเดียว) ต้องตั้ง min.insync.replicas=2
(หรือมากกว่า) ควบคู่กัน เพื่อบอก broker ว่า ถ้า ISR เหลือน้อยกว่าค่านี้ ให้ producer เขียน
ไม่สำเร็จ (throw exception) แทนที่จะยอมเขียนแบบเสี่ยงๆ การตั้ง acks=all อย่างเดียวโดยไม่
ตั้ง min.insync.replicas คือความเข้าใจผิดที่พบบ่อยที่สุดเรื่อง Kafka durability ในวงการ
Idempotent Producer & Transactions
Idempotent producer (enable.idempotence=true) ป้องกัน duplicate message ที่เกิดจาก
producer retry เองตอน network error (ไม่ใช่ duplicate จาก consumer อ่านซ้ำ) — broker ใส่
sequence number ให้แต่ละ message ต่อ producer session แล้วทิ้ง duplicate ที่ sequence ซ้ำกัน
ทิ้งให้อัตโนมัติ Transaction ต่อยอดจากนี้ให้เขียนหลาย partition/topic เป็น atomic unit
(ทั้งหมดสำเร็จ หรือทั้งหมด rollback)
"Idempotent producer = เลขที่ใบเสร็จกันพิมพ์ซ้ำ, Transaction = ใบเสร็จรวมหลายรายการเป็นบิลเดียว" — sequence number ป้องกันการพิมพ์ใบเสร็จซ้ำเวลาเครื่องพิมพ์ค้างแล้วลองใหม่ ส่วน transaction รวมหลายการซื้อ (หลาย partition) เข้าบิลเดียวที่ "จ่ายครบทุกอย่างหรือไม่จ่ายเลย" ไม่มีจ่ายครึ่งๆ กลางๆ
Kafka transaction ทำงานผ่านกลไกที่เรียกว่า transaction marker — message ที่เขียนใน
transaction จะถูกเขียนลง partition ก่อนจริงๆ (visible ใน log ตั้งแต่ตอนนั้น) แต่ consumer ที่
ตั้งค่า isolation.level=read_committed (ไม่ใช่ default ซึ่งคือ read_uncommitted) จะ
ไม่เห็น message เหล่านั้นจนกว่า transaction marker ที่บอกว่า "commit แล้ว" จะถูกเขียนตามมา
— ถ้า transaction ถูก abort, broker จะเขียน abort marker แทน และ consumer ที่ตั้ง
read_committed จะข้าม message เหล่านั้นไปทั้งหมดโดยอัตโนมัติ เป็นกลไกที่ทำให้
exactly-once semantics เกิดขึ้นได้จริงโดยไม่ต้องลบ message ที่ abort ออกจาก log จริงๆ
(ซึ่งขัดกับ design พื้นฐานของ Kafka ที่เป็น append-only log) ทีมที่ไม่ตั้ง
isolation.level=read_committed ที่ consumer จะยังเห็น message จาก transaction ที่ถูก
abort ไปแล้วอยู่ดี ทำให้ "exactly-once" ที่ตั้งใจไว้ไม่ทำงานจริงตามที่คาดหวัง
Ordering Guarantee
Kafka รับประกัน order ภายใน partition เดียวเท่านั้น (message ที่ producer ส่งเข้า partition เดียวกันตามลำดับ จะถูกอ่านตามลำดับเดียวกันเป๊ะ) ไม่รับประกัน order ข้าม partition เลย
"Order การันตีแค่ในเลนเดียว ข้ามเลนไม่การันตี" — รถในเลนเดียวกันเรียงคิวตามลำดับที่เข้ามาเป๊ะ แต่รถคนละเลนไม่มีใครรับประกันว่าใครจะถึงปลายทางก่อนหลัง แม้จะออกจากจุดเริ่มต้นพร้อมกันก็ตาม
Order ที่ "การันตีภายใน partition" ยังมีเงื่อนไขซ่อนอยู่ที่คนมักมองข้าม: ถ้าตั้ง
max.in.flight.requests.per.connection > 1 (ส่งหลาย request พร้อมกันก่อนรอ response —
ค่า default คือ 5) พร้อมกับ retry เปิดอยู่ (ซึ่งเปิดอัตโนมัติเมื่อ
enable.idempotence=true) มีโอกาสที่ request ที่ส่งทีหลังแต่ retry สำเร็จก่อน request
ที่ส่งก่อนหน้า ทำให้ order ใน partition สลับกันได้แม้จะมาจาก producer เดียวกัน! วิธีป้องกัน
คือ idempotent producer (ซึ่ง broker version ใหม่ของ Kafka ทำให้ max.in.flight.requests. per.connection สูงสุดได้ถึง 5 โดยยังรักษา ordering ไว้ได้อย่างปลอดภัย เพราะใช้ sequence
number ตรวจสอบลำดับที่ broker แทนที่จะพึ่งลำดับการมาถึงของ network request อย่างเดียว) — ถ้า
ไม่เปิด idempotent producer และตั้ง in-flight requests มากกว่า 1 ด้วย ต้องยอมรับความเสี่ยง
เรื่อง ordering สลับได้ในสถานการณ์ retry
In-Sync Replicas (ISR) — not the raw replication factor — determine real durability; a lagging
follower drops out of ISR until it catches up. unclean.leader.election.enable embodies a
direct CAP-theorem trade-off: false favors consistency (partition goes unavailable rather than
lose data), true favors availability (an out-of-ISR replica can become leader, risking data
loss). acks=all only guarantees as much as the current ISR — pair it with
min.insync.replicas or durability silently degrades to acks=1 when ISR shrinks. Idempotent
producers use per-session sequence numbers to drop retry-caused duplicates; transactions add
atomicity across partitions via commit/abort markers, visible to consumers only under
isolation.level=read_committed (not the default). Kafka guarantees order only within a single
partition — and even that can break if max.in.flight.requests.per.connection > 1 without
idempotence enabled, since retried requests can complete out of order.