ผู้ตาม fetch เหมือนผู้บริโภค · high watermark ตามมาช้าหนึ่งรอบ
เจ็ดบทที่ผ่านมา kaen-log มี broker ตัวเดียว บทนี้ต่อตัวที่สองเข้าไป
สิ่งที่เพิ่มขึ้นไม่ใช่ protocol ใหม่ replicationreplicationการที่ follower ดึง log ของ leader มาต่อท้ายของตัวเอง ด้วย protocol เดียวกับที่ผู้อ่านทั่วไปใช้ ของ Kafka คือการที่ broker ตัวที่สอง ส่งคำขอ fetch อันเดียวกับที่ผู้บริโภคส่ง แล้วเขียนสิ่งที่ได้ลง log ของตัวเอง
สิ่งที่เพิ่มขึ้นจริงคือเส้นใหม่หนึ่งเส้นบน log นั้น เส้นที่บอกว่าตรงไหนคือของที่ทุกสำเนามีแล้ว และเส้นนั้นเดินทางกลับมาถึงผู้ตามช้ากว่าตัว record หนึ่งรอบเสมอ
crate ของคอร์สอยู่ที่ scripts/log-broker/ และชื่อ binary คือ klog
บทนี้เป็นเจ้าของ scripts/log-broker/src/replica.rs ทั้ง file
บล็อก code กับบล็อกผลรันทุกอันในบทนี้ถูกคัดมาจาก scripts/log-broker/src/replica.rs
และจาก scripts/log-broker/expected/08-replication.txt ทีละ byte
โดยมีเทสต์เทียบไว้ ถ้าแก้ในบทเรียนอย่างเดียวโดยไม่แก้ที่ต้นทาง เทสต์จะแดงทันที
ผลรันทุกบรรทัดในบทนี้มาจากคำสั่งเดียว คือ
klog replica-demo --rounds 4 --min-isr 2 --now-ms 1700000000000
ซึ่งเทสต์รันแล้วเทียบกับ expected/ ทีละ byte และรันซ้ำห้ารอบเพื่อพิสูจน์ว่าได้ผลเดิม
บรรทัดแรกของบทนี้ปิดสวิตช์ที่เจ็ดบทก่อนหน้าเปิดค้างไว้
หัวข้อที่มีชื่อว่า “บรรทัดแรกของบทนี้ปิดสวิตช์ที่เจ็ดบทก่อนหน้าเปิดค้างไว้”หัวข้อนี้อธิบายว่าทำไม broker ที่มี replica เดียวถึงไม่เคยต้องคิดเรื่อง high watermark เลย
ตั้งแต่บทที่ 3 log ของ kaen-log เลื่อนเส้น committed ตาม log end ของตัวเองทันทีที่ append เสร็จ ผู้อ่านจึงเห็นทุก record ที่เขียนลงไปแล้ว โดยไม่ต้องรอใคร
พฤติกรรมนั้นไม่ใช่ทางลัด มันคือพฤติกรรมจริงของ Kafka ที่ replication factor เท่ากับหนึ่ง เพราะเมื่อมีสำเนาเดียว เซตของสำเนาที่ตามทันก็มีสมาชิกตัวเดียว ค่าที่น้อยที่สุดในเซตนั้นจึงเป็นของ leader เอง
พอ broker ตัวที่สองเข้ามา ข้อนั้นเลิกจริงทันที และ Cluster::open ต้องปิดโหมดเดิมด้วยมือ
pub fn open(root: &Path, min_isr: usize, now_ms: i64) -> io::Result<Cluster> { let mut leader = Replica::open(1, Role::Leader, &root.join("broker-1/orders-0"), now_ms)?; let follower = Replica::open(2, Role::Follower, &root.join("broker-2/orders-0"), now_ms)?; leader.become_leader(0); // **บรรทัดที่บทนี้มีไว้เพื่อสอน** · ตราบใดที่มี replica ตัวเดียว high watermark // เดินตาม log end · พอมีตัวที่สอง มันต้องมาจากค่าน้อยที่สุดในหมู่ ISR แทน leader.partition.log_guard().stop_eager_high_watermark(); follower.partition.log_guard().stop_eager_high_watermark(); let isr = IsrState::new(leader.id, &[follower.id], min_isr, now_ms); Ok(Cluster { leader, follower, isr, link: Link::default(), correlation: 0 }) }สองบรรทัดที่เรียก stop_eager_high_watermark() คือความต่างทั้งหมดระหว่างหนึ่ง replica กับสอง
ถ้าลบมันทิ้ง ทุกรอบจะพิมพ์ HW เท่ากับ LEO และข้ออ้างหลักของบทจะมองไม่เห็น
ระหว่างสร้าง crate ตัวนี้ replica-demo เคยพิมพ์ HW เท่ากับ LEO ทุกรอบ ด้วย hash ที่นิ่งสนิท
เกตความ deterministic จึงเขียว ทั้งที่บทนี้ไม่มีอะไรให้ดูเลย
demo ของบทนี้จึงจบด้วยการตรวจตัวเองว่าเงื่อนไขที่มันอ้างเกิดขึ้นจริงหรือไม่ ถ้าไม่เกิด มันคืน error
และ expected/ ก็สร้างใหม่ไม่ได้ · ด่านที่พิสูจน์ว่า “เหมือนเดิมทุกครั้ง”
ไม่ได้พิสูจน์ว่า “อันนี้แสดงสิ่งที่มันอ้าง”
ผู้ตามคือผู้บริโภคที่พก replica id มาด้วย
หัวข้อที่มีชื่อว่า “ผู้ตามคือผู้บริโภคที่พก replica id มาด้วย”หัวข้อนี้ชี้ว่าความต่างระหว่างผู้ตามกับผู้บริโภคอยู่ที่ field เดียวในคำขอ และมันเปลี่ยนแค่เพดานการอ่าน
FetchRequest.json ของ Kafka 4.0.0 ประกาศ field ที่แยกสองฝ่ายนี้ออกจากกันไว้ตรงตัว คือ
{"name":"ReplicaId","type":"int32","versions":"0-14","default":"-1","entityType":"brokerId",
"about":"The broker ID of the follower, of -1 if this request is from a consumer."}
ค่าปริยายเป็นลบหนึ่ง แปลว่าคำขอมาจากผู้บริโภค ส่วนค่าที่ไม่ติดลบคือเลขประจำ broker ของผู้ตาม ไม่มี api key ของตัวเอง ไม่มี path ของตัวเอง
เอกสารออกแบบของ Kafka พูดข้อเดียวกันนี้ด้วยคำของโครงการเองว่า “Followers consume messages from the leader just as a normal Kafka consumer would and apply them to their own log”
ฝั่งรับ Kafka แปลง field นั้นเป็นเพดานการอ่านด้วยการแยกสามทาง ใน FetchIsolation.of คือ
if (!FetchRequest.isConsumer(replicaId)) { return LOG_END; } แล้วค่อยแยกผู้บริโภคที่ขอ
READ_COMMITTED ออกจากผู้บริโภคทั่วไปที่ได้ HIGH_WATERMARK
kaen-log ทำสองทางจากสามทางนั้น เพราะคอร์สนี้ไม่มี transaction และบทที่ 4 เขียน fetch isolation ไว้แล้วในหัวข้อของมันเอง
/// ผู้ตามคือผู้บริโภคที่มี replica id · เพดานการอ่านต่างกันแค่นั้น #[test] fn a_follower_reads_to_the_log_end_and_a_consumer_stops_at_the_high_watermark() { assert_eq!(isolation_of(2), crate::fetch::FetchIsolation::LogEnd); assert_eq!(isolation_of(-1), crate::fetch::FetchIsolation::HighWatermark); }ผู้ตามอ่านได้ถึงปลาย log ของ leader ส่วนผู้บริโภคหยุดที่ high watermark ที่เหลือทั้งหมด ตั้งแต่การ encode คำขอ การหุ้ม frame ไปจนถึงการอ่าน range ออกจาก segment เป็นทางเดียวกัน
LEO กับ high watermark เป็นเลขคนละตัว
หัวข้อที่มีชื่อว่า “LEO กับ high watermark เป็นเลขคนละตัว”หัวข้อนี้แยกเลขสองตัวที่ผู้อ่านมักเข้าใจว่าเป็นตัวเดียวกัน แล้วบอกว่าใครเป็นคนคำนวณตัวไหน
log end offset คือ offset ของ record ตัว ถัดไป ที่จะถูกเขียน ไม่ใช่ของตัวสุดท้ายที่เขียนไปแล้ว log ที่ว่างเปล่าจึงมี LEO เป็นศูนย์ ไม่ใช่ลบหนึ่ง
high watermarkhigh watermarkค่าต่ำสุดของปลาย log ในบรรดา replica ที่อยู่ใน ISR · เส้นนี้คือขอบของสิ่งที่ผู้อ่านมองเห็นได้ คือค่า LEO ที่น้อยที่สุดในหมู่สมาชิกของเซตสำเนาที่ตามทัน และมันเป็นเพดานแบบ ไม่รวมตัวมันเอง record ที่ offset น้อยกว่าค่านี้คือของที่ committed แล้ว
ความต่างข้อ “ไม่รวมตัวมันเอง” ไม่ใช่รายละเอียดของการนับ log ที่มี HW เป็นศูนย์คือ log ที่ยังไม่มีอะไร committed เลย ต่อให้มันมี record อยู่บน disk แล้วก็ตาม
advance_hw เป็นตัวที่คำนวณค่านั้น และนาฬิกาไม่ใช่ parameter ของมัน
ส่วนที่ทำให้มันอ่านนาฬิกาไม่ได้จริงคือกฎของทั้ง crate ที่ห้าม SystemTime::now() และมีด่านกวาดไว้
/// **ไม่มีนาฬิกา** · แยกออกจาก `shrink_isr` อย่างจงใจ เพื่อให้ครึ่งที่บริสุทธิ์/// พิสูจน์ได้ว่าบริสุทธิ์: นาฬิกาไม่ใช่ parameter มันจึงอ่านนาฬิกาไม่ได้////// เริ่มที่ LEO ของ leader เอง แล้วเอา **ค่าน้อยที่สุด** ในหมู่สมาชิก ISR/// · `None` แปลว่าอยู่ต่ำกว่า `min.insync.replicas` จึงห้ามเลื่อนเลย////// การไม่ถอยหลังเป็นหน้าที่ของผู้เรียก — ค่าที่คืนคือ *ค่าที่เสนอ* ไม่ใช่ค่าที่ตั้งให้แล้วpub fn advance_hw( leader_leo: u64, follower_leos: &[(i32, u64)], maximal_isr: &[i32], min_isr: usize,) -> Option<u64> { if maximal_isr.len() < min_isr { return None; } let mut candidate = leader_leo; for (id, leo) in follower_leos { if maximal_isr.contains(id) && *leo < candidate { candidate = *leo; } } Some(candidate)}ให้อ่านค่าที่ function นี้คืนว่าเป็น ค่าที่เสนอ ไม่ใช่ค่าที่ตั้งให้แล้ว signature ของมันไม่มีช่องรับค่า high watermark ปัจจุบัน มันจึงเทียบไม่ได้ว่าค่าใหม่ถอยหลังหรือไม่
การห้ามถอยหลังเป็นหน้าที่ของผู้เรียก และผู้เรียกในที่นี้คือ recompute_hw
/// เลื่อน high watermark ของ leader · ไม่เคยถอยหลัง pub fn recompute_hw(&mut self) { let leader_leo = self.leader.leo(); let candidate = advance_hw( leader_leo, &self.isr.follower_leos, &self.isr.maximal_isr, self.isr.min_isr, ); let mut log = self.leader.partition.log_guard(); if candidate.is_some_and(|new_hw| new_hw > log.high_watermark) { log.high_watermark = candidate.expect("เพิ่งตรวจว่าเป็น Some"); } }เงื่อนไข new_hw > log.high_watermark คือกฎห้ามถอยหลังทั้งข้อ และมันอยู่ที่ผู้เรียก
อย่าอ่าน advance_hw ว่าเป็น function ที่รักษาความ monotonic ไว้เอง เพราะมันทำไม่ได้
Kafka วางกฎเดียวกันไว้ใน maybeIncrementLeaderHW โดยเริ่มที่ var newHighWatermark = leaderLogEndOffset แล้วไล่ลดลงเป็นค่าของ replica ที่ตามหลัง และ doc comment ของมันบอกว่า
function นี้ถูกปลุกเมื่อ ISR เปลี่ยน หรือเมื่อ LEO ของ replica ตัวไหนก็ตามเปลี่ยน
ข้อที่ควรสังเกตคือ Kafka เทียบกับ maximal ISR ไม่ใช่ ISR ที่ committed แล้ว สำเนาที่ตามทันแต่ controller ยังไม่รับเข้าเซต ก็ถ่วง high watermark ไว้เหมือนกัน
ความช้าหนึ่งรอบ · เพราะ HW เดินทางกลับมาในคำตอบ
หัวข้อที่มีชื่อว่า “ความช้าหนึ่งรอบ · เพราะ HW เดินทางกลับมาในคำตอบ”หัวข้อนี้กางหนึ่งรอบของ harness ออกให้เห็นว่า leader รู้อะไรตอนไหน และผู้ตามรู้ทีหลังเพราะอะไร
ข้อมูลสองอย่างเดินสวนทางกันในหนึ่งรอบ ตัว record เดินจาก leader ไปหาผู้ตาม ส่วนเลข LEO ของผู้ตามเดินจากผู้ตามไปหา leader และมันติดไปกับ คำขอ ไม่ใช่กับคำตอบ
// ── ฝั่ง leader ──────────────────────────────────────────────────── let mut payload = Vec::new(); read_frame(&mut self.link.to_leader, &mut payload)?; let decoded = FetchRequestFrame::decode(&payload)?; // leader รู้ LEO ของผู้ตามจาก **คำขอ** ครั้งนี้ · ความช้าหนึ่งรอบเกิดตรงนี้เอง self.isr.observe(decoded.replica_id, decoded.fetch_offset, now_ms); self.recompute_hw();fetch_offset ในคำขอ คือ LEO ของผู้ตามพอดี เพราะผู้ตามขอ record ตัวถัดไปจากที่ตัวเองมีเสมอ
leader จึงเลื่อน high watermark ได้ตั้งแต่ก่อนอ่าน record ออกจาก log
แต่ค่าที่เพิ่งเลื่อนนั้นเดินทางกลับไปพร้อมคำตอบของ รอบนี้ ส่วน record ที่ผู้ตามเพิ่งได้รับ เป็นของที่มาถึงหลังจากค่านั้นถูกคำนวณไปแล้ว
// ── ฝั่งผู้ตาม ───────────────────────────────────────────────────── let mut back = Vec::new(); read_frame(&mut self.link.to_follower, &mut back)?; let response = FetchResponseFrame::decode(&back)?; let shipped = decode_batches(&response.records)?.iter().map(|b| b.records.len()).sum(); { let mut log = self.follower.partition.log_guard(); append_as_follower(&mut log, &response.records, now_ms)?; } let follower_leo = self.follower.leo(); // high watermark มาถึงใน **คำตอบ** · และไม่มีทางเกิน LEO ของตัวเอง self.follower.hw = self.follower.hw.max(from_wire_i64(response.high_watermark)?.min(follower_leo));FetchResponse.json ของ Kafka มี field นี้อยู่จริงต่อหนึ่ง partition คือ
{"name":"HighWatermark","type":"int64","versions":"0+","about":"The current high water mark."}
และฝั่งผู้ตามของ Kafka ก็เรียก log.maybeUpdateHighWatermark(partitionData.highWatermark)
ด้วยค่าที่ได้จากคำตอบนั้นตรงๆ
การ min กับ LEO ของตัวเองในบรรทัดสุดท้ายไม่ใช่การกันพลาด มันคือกฎ
ผู้ตามไม่มีทางมี high watermark สูงกว่าจำนวน record ที่มันถืออยู่จริง
sequenceDiagram
participant P as producer
participant L as leader id 1
participant F as follower id 2
P->>L: produce record ตัวที่หนึ่ง
Note over L: LEO 1 · HW 0
F->>L: fetch · replica_id 2 · fetch_offset 0
Note over L: leader เพิ่งรู้ว่า LEO ของผู้ตาม คือ 0
L-->>F: records หนึ่งตัว · high_watermark 0
Note over F: LEO 1 · HW 0 · ถือของไว้แล้วแต่ยังไม่รู้ว่า committed
P->>L: produce record ตัวที่สอง
F->>L: fetch · replica_id 2 · fetch_offset 1
Note over L: LEO 2 · HW 1
L-->>F: records หนึ่งตัว · high_watermark 1
Note over F: LEO 2 · HW 1 · เพิ่งรู้เรื่องตัวแรกตอนได้ตัวที่สอง
คำบรรยายภาพ: ภาพนี้เป็นสองรอบแรกของตารางผลรันข้างล่าง · ลูกศรที่พาเลข high watermark กลับมาเป็นลูกศรขาลง ไม่ใช่ขาขึ้น ผู้ตามจึงรู้ว่า record ตัวหนึ่ง committed แล้ว ก็ต่อเมื่อมันรับตัวถัดไปมาแล้ว
demo ขับสี่รอบด้วยรูปเดียวกันนี้ หนึ่งรอบคือ produce หนึ่งครั้งแล้ว fetch_once หนึ่งครั้ง
== 4 รอบ · หนึ่งรอบคือ produce หนึ่งครั้ง แล้ว fetch_once หนึ่งครั้ง == รอบ บทบาท id LEO HW record ที่ส่ง HW ในคำตอบ 1 leader 1 1 0 - - follower 2 1 0 1 0 2 leader 1 2 1 - - follower 2 2 1 1 1 3 leader 1 3 2 - - follower 2 3 2 1 2 4 leader 1 4 3 - - follower 2 4 3 1 3
ทุกรอบ ผู้ตามมี LEO สูงกว่า HW ของตัวเอง · มันถือ record ไว้แล้วแต่ยังไม่รู้ว่า record นั้น committed · ต้องรอคำตอบของรอบถัดไปถึงจะรู้high watermark เป็นเพดานแบบ **ไม่รวมตัวมันเอง** · record ที่ offset < HW คือของที่ committed แล้วช่องขวาสุดคือค่า high watermark ที่ leader ใส่กลับมาในคำตอบของรอบนั้น มันตามหลังช่อง LEO ของผู้ตามอยู่หนึ่งเสมอ ทั้งสี่รอบ
เทสต์ที่ตรึงข้ออ้างนี้ไว้ไม่ได้อ่าน expected/ มันยืนยันด้วยจำนวนเต็มตรงๆ
/// **ด่านที่หนึ่งจากสอง** · ไม่พิมพ์อะไรออก stdout และไม่ป้อน `expected/` เลย #[test] fn hw_lags_one_round_trip() { let dir = temp("hw-lag"); let now = 1_700_000_000_000; let mut c = Cluster::open(&dir, 2, now).unwrap();
c.produce(0, ACKS_LEADER, now).unwrap(); assert_eq!((c.leader.leo(), c.leader.partition.high_watermark()), (1, 0));
// รอบที่หนึ่ง · ผู้ตาม **มี** record แล้ว แต่ยังไม่รู้ว่ามัน committed let first = c.fetch_once(now).unwrap(); assert_eq!((first.follower_leo, first.follower_hw), (1, 0)); assert_eq!(first.records_shipped, 1); assert_eq!(c.leader.partition.high_watermark(), 0);
// รอบที่สอง · คำขอพก LEO=1 มาด้วย leader จึงเลื่อน HW แล้วส่งค่ากลับ let second = c.fetch_once(now).unwrap(); assert_eq!(c.leader.partition.high_watermark(), 1); assert_eq!(second.follower_hw, 1); assert_eq!(second.records_shipped, 0);
let _ = std::fs::remove_dir_all(&dir); }รอบแรกผู้ตามได้ record ไปหนึ่งตัวแล้ว แต่ HW ของมันยังเป็นศูนย์ รอบที่สองไม่มี record ใหม่เลย มีแต่เลข HW ที่ตามมา
บทที่ 7 ของคอร์ส Distributed Systems สร้าง harness ที่มี scheduler ตัวเดียว ขับเป็นรอบที่สั่งเอง และมีงบจำกัดตายตัว เพื่อให้สถานการณ์ที่เกี่ยวกับเวลาให้ผลเดิมทุกครั้ง
fetch_once() ของบทนี้คือหนึ่งรอบของ harness นั้น ยืมมาใช้ ไม่ได้คิดใหม่
สิ่งที่บทนี้เพิ่มเข้าไปคือ สายจริง ระหว่างสอง broker ที่เป็นคู่ buffer ในหน่วยความจำ
ทั้งสองฝั่งเขียนและอ่านด้วย write_frame กับ read_frame ตัวเดียวกับที่ producer ของบทที่ 3 ใช้
ISR เป็นสมาชิกของเซต ไม่ใช่การนับคะแนน
หัวข้อที่มีชื่อว่า “ISR เป็นสมาชิกของเซต ไม่ใช่การนับคะแนน”หัวข้อนี้ลากเส้นระหว่างสิ่งที่คอร์สที่ 23 พิสูจน์ไว้แล้ว กับสิ่งที่ Kafka ทำคนละอย่าง
ISRISRin-sync replicas: เซตของ replica ที่ตามทัน leader พอจะนับเป็นเจ้าของสำเนาที่ commit แล้ว · เป็นสมาชิกของเซต ไม่ใช่เลขคณิตแบบ quorum คือเซตของสำเนาที่ตามทัน leader พอที่จะนับได้ว่ามันถือของที่ commit แล้วอยู่จริง เอกสารออกแบบของ Kafka เปิดหัวข้อนี้ด้วยการปฏิเสธทางเลือกอีกทางตรงๆ ว่า “Instead of majority vote, Kafka dynamically maintains a set of in-sync replicas (ISR) that are caught-up to the leader”
กติกาการ commit จึงเป็นคนละกติกากับการนับคะแนน หน้าเดียวกันเขียนว่า “A write to a Kafka partition is not considered committed until all in-sync replicas have received the write”
ผลที่ตามมาคือเลขที่ต่างกัน หน้านั้นสรุปว่า “With this ISR model and f+1 replicas, a Kafka topic can tolerate f failures without losing committed messages” ขณะที่การนับคะแนนข้างมากต้องใช้ 2f+1
คอร์ส Distributed Systems ไล่กรณีของ R + W > N
จนครบไปแล้ว บทนี้ไม่แตะข้อนั้นเลย และผู้อ่านก็ไม่ต้องเอาข้อนั้นมาใช้ที่นี่
สิ่งที่ต่างกันมีสามข้อ และมันต่างตั้งแต่ราก
- ผู้เขียน — cluster แบบ Dynamo ไม่มี leader ทุก node รับ write ได้ ส่วน partition ของ Kafka มีผู้เขียนรายเดียว
- ลำดับ — ของแบบแรกไม่มีลำดับรวม ต้องคลี่ conflict ด้วย version vector ตอนอ่าน ส่วน log ของ broker มีลำดับสมบูรณ์อยู่แล้วตั้งแต่ตอนเขียน
- เส้นแบ่ง committed — ของแบบแรกไม่มีเส้นนั้น มีแต่การบรรจบกันในที่สุด ส่วนของบทนี้มี และมันเป็น ตำแหน่ง ไม่ใช่ผลของการนับ
ความต่างข้อสุดท้ายกินไปถึงกลไกที่หน้าตาคล้ายกันที่สุดของสองคอร์ส คือ anti-entropy กับ follower fetch
anti-entropy ด้วย gossip เอา peer สองตัวที่ต่างก็รับ write ได้ มาเทียบกันแล้วรวมสิ่งที่ขาดของแต่ละฝั่ง ทิศทางของข้อมูลจึงเป็นสองทาง และต้องมีกติกาคลี่ conflict เสมอ
follower fetch คัด log ที่เรียงเสร็จแล้วจากผู้เขียนรายเดียว ตามลำดับ ไม่มีการรวม ไม่มีการคลี่ conflict ผู้ตามเขียน byte ของ leader ลงไปโดยไม่แจก offset ใหม่ให้สักตัว
และ gossip ไม่มีอะไรเทียบเท่า high watermark เลย มันบรรจบกันในที่สุดโดยไม่มีเส้นบอกว่าตรงไหนคือของที่แน่แล้ว
ISR หดด้วยนาฬิกาที่ป้อนเข้ามา ไม่ใช่ด้วยตัวจับเวลา
หัวข้อที่มีชื่อว่า “ISR หดด้วยนาฬิกาที่ป้อนเข้ามา ไม่ใช่ด้วยตัวจับเวลา”หัวข้อนี้แสดงเงื่อนไขที่ทำให้สมาชิกหลุดออกจาก ISR และบอกว่าใครเป็นคนวัดเงื่อนไขข้อไหน
เอกสารของ Kafka บอกเงื่อนไขของคำว่าตามทันไว้สองข้อ ข้อแรกคือ “Brokers must maintain an active session with the controller in order to receive regular metadata updates” ข้อที่สองคือ “Brokers acting as followers must replicate the writes from the leader and not fall ‘too far’ behind”
ค่าที่นิยามคำว่าไกลเกินไปคือ replica.lag.time.max.ms ซึ่ง ReplicationConfigs.java ของ 4.0.0
ตั้งค่าปริยายไว้ที่ 30000 พร้อมคำอธิบายว่า ถ้าผู้ตามไม่ได้ส่งคำขอ fetch หรือยังตามไม่ถึงปลาย log
ของ leader นานเกินค่านี้ leader จะถอดมันออกจาก ISR
shrink_isr เป็นตัวเดียวใน replica.rs ที่เอาค่านาฬิกามาเทียบกับเส้น cutoff
ส่วน function อื่นใน file เดียวกันรับ now_ms ไปบันทึกหรือส่งต่อเท่านั้น
/// **function เดียวใน file นี้ที่รับนาฬิกา** /// /// leader เป็นคนตัดสินข้อนี้ · ผู้ตามที่ไม่ได้ส่งคำขอ fetch มานานเกิน /// `replica.lag.time.max.ms` ถูกถอดออกจาก ISR · leader ไม่เคยถอดตัวเอง /// /// `replica-demo` **เรียก** function นี้ โดยป้อน `now_ms` ให้เอง ซึ่งเป็นขั้นที่ /// สั่งเองพอๆ กับการถอดผู้ตามด้วยมือ · สิ่งที่ demo ไม่มีคือ *ตัวจับเวลาจริง* /// ไม่ใช่การหด ISR ตามเวลา · กฎที่สอนแล้วไม่เคยแสดงให้ดู คือกฎที่อ้างเกินสิ่งที่วัด pub fn shrink_isr(&mut self, now_ms: i64) -> bool { let cutoff = now_ms - self.lag_max_ms; let before = self.maximal_isr.len(); let leader = self.leader_id; let caught_up = self.last_caught_up_ms.clone(); self.maximal_isr.retain(|id| { if *id == leader { return true; } caught_up.iter().find(|(f, _)| f == id).is_some_and(|(_, at)| *at >= cutoff) }); before != self.maximal_isr.len() }retain ข้ามเงื่อนไขให้ leader เสมอ เพราะ leader ไม่เคยถอดตัวเองออกจาก ISR ของตัวเอง
ส่วนผู้ตามต้องมีเวลา fetch ครั้งสุดท้ายที่ไม่เก่ากว่าเส้น cutoff
demo เรียก function นี้จริง โดยป้อนค่านาฬิกาที่ข้ามเกณฑ์ไปหนึ่งมิลลิวินาที
== ISR หดตามเวลา · shrink_isr(now_ms) ==ISR ก่อนหน้า [1, 2] replica fetch ครั้งสุดท้ายเมื่อ 2 1700000000000ป้อน now_ms = 1700000030001 ซึ่งเลยเกณฑ์ 30000 ms ไปหนึ่งมิลลิวินาทีshrink_isr คืน true · ISR ตอนนี้ [1]ต่ำกว่า min.insync.replicas แล้ว: ใช่leader ไม่เคยถอดตัวเองออกจาก ISR ของตัวเอง · ที่เหลืออยู่คือ id 1การป้อน now_ms เข้ามาเองคือขั้นตอนที่สั่งเอง เท่ากับการถอดผู้ตามด้วยมือ
สิ่งที่ demo ไม่มีคือ thread ที่ตื่นมาเรียก shrink_isr เอง เพราะของแบบนั้นทำให้ผลรันไม่นิ่ง
ส่วนการรับกลับเข้า ISR ไม่ใช่หน้าที่ของนาฬิกาเลย เงื่อนไขสองข้อข้างบนมีผู้ตัดสินคนละคน
== สองเงื่อนไขที่ถอด replica ออกจาก ISR ได้ · คนละผู้ตัดสิน == ISR ตั้งต้น [1, 2, 3] leader ถอดตัวที่ตามไม่ทัน (เกณฑ์เวลา) [1, 2] controller ถอดตัวที่ขาด session กับ controller [1]เงื่อนไขข้อแรก leader เป็นคนวัด · ข้อที่สอง controller เป็นคนวัดreplica ตัวเดียวจึงหลุด ISR ได้จากสองทางที่ไม่เกี่ยวกันเลยleader วัดข้อเวลา เพราะมันเป็นฝ่ายที่เห็นคำขอ fetch ทุกใบ ส่วน controller วัดข้อ session เพราะมันเป็นฝ่ายที่ถือ session นั้นอยู่
replica ตัวเดียวจึงหลุดออกจาก ISR ได้จากสองทางที่ไม่เกี่ยวกันเลย และกลับเข้ามาได้ทางเดียว คือเมื่อ controller รับมันกลับ
acks=all พังทันทีที่ ISR ต่ำกว่าเกณฑ์
หัวข้อที่มีชื่อว่า “acks=all พังทันทีที่ ISR ต่ำกว่าเกณฑ์”หัวข้อนี้ต่อค่า min.insync.replicas เข้ากับ acks=all แล้วแยกข้อผิดพลาดสองข้อที่เกิดคนละจังหวะ
min.insync.replicasmin.insync.replicasจำนวนสมาชิก ISR ขั้นต่ำที่ต้องมี ไม่งั้น broker ปฏิเสธการเขียนที่ขอ `acks=all` ตั้งแต่ต้น คือจำนวนสมาชิก ISR ขั้นต่ำที่ต้องมี ไม่งั้น broker ปฏิเสธการเขียนที่ขอ
acks=all ตั้งแต่ก่อนแตะ log
ServerLogConfigs.java ของ 4.0.0 ตั้ง MIN_IN_SYNC_REPLICAS_DEFAULT ไว้ที่หนึ่ง
ค่าปริยายจึงยอมให้ ISR ที่มี leader ตัวเดียวตอบ acks=all ได้ ซึ่งแปลว่าความทนทานที่ผู้ใช้คิดว่าซื้อมา
ไม่ได้อยู่ที่ธง acks แต่อยู่ที่ค่านี้
kaen-log ตรวจข้อนี้ก่อน append เหมือน Kafka
/// producer เขียนหนึ่ง record ลง leader pub fn produce(&mut self, n: u64, acks: i16, now_ms: i64) -> io::Result<(u64, i16)> { // error 19 · ตรวจ **ก่อน** append · การตรวจนี้ไม่ได้เกิดพร้อมกับการเขียน if acks == ACKS_ALL && self.isr.under_min_isr() { return Ok((0, ERR_NOT_ENOUGH_REPLICAS)); }ผลที่ตามมาเห็นได้ทันทีในผลรัน หลังจาก ISR หดลงเหลือ leader ตัวเดียว
== ผลที่ตามมาทันที ==produce แบบ acks=all → error 2 (NOT_ENOUGH_REPLICAS · ตรวจก่อน append)produce แบบ acks=1 → base_offset 4 · error 0 (เขียนได้ตามปกติ)high watermark 3 → 3 (ไม่เลื่อน ขณะอยู่ต่ำกว่าเกณฑ์)acks เปลี่ยนเวลาที่ตอบ ไม่เคยเปลี่ยนสิ่งที่เขียน · ข้อนี้ยังจริงอยู่ที่นี่record ที่ offset 4 อยู่บน disk แล้ว แต่ยังไม่ committedacks=1 ยังเขียนได้ตามปกติ และได้ base_offset ของตัวเองกลับมา
ข้อที่บทที่ 3 วางไว้จึงยังจริงอยู่ที่นี่ คือ acks เปลี่ยนจังหวะการตอบ ไม่เคยเปลี่ยนสิ่งที่เขียนลง log
บรรทัดที่สำคัญที่สุดในบล็อกนั้นคือบรรทัดสุดท้าย record ตัวนั้นอยู่บน disk แล้วแต่ยังไม่ committed เพราะ high watermark ไม่เลื่อนขณะที่ ISR ต่ำกว่าเกณฑ์
และเพราะการตรวจ ISR ไม่ได้เกิดพร้อมกับการ append ข้อผิดพลาดจึงมีสองข้อ ไม่ใช่ข้อเดียว
== สองข้อผิดพลาด ไม่ใช่ข้อเดียว == สถานการณ์ คำตัดสิน HW ยังไม่ถึง offset ที่ต้องการ NotYet HW ถึงแล้ว และ ISR ยังครบ Committed HW ถึงแล้ว แต่ ISR หดหลัง append ไปแล้ว NotEnoughReplicasAfterAppendข้อแรกคือ error 19 ตรวจ **ก่อน** append · ข้อหลังคือ error 20 ตรวจ **หลัง** appendทั้งคู่ retry ได้ · ซึ่งเป็นเหตุผลว่าทำไม acks=all ถึงผลิตของซ้ำได้ถ้าไม่มี producer แบบกันซ้ำของบทที่ 7 มาช่วยErrors.java ของ Kafka 4.0.0 ให้เลขกับสองข้อนี้แยกกัน คือ
NOT_ENOUGH_REPLICAS(19, "Messages are rejected since there are fewer in-sync replicas than required.")
กับ NOT_ENOUGH_REPLICAS_AFTER_APPEND(20, "Messages are written to the log, but to fewer in-sync replicas than required.")
ผลรันข้างบนพิมพ์ error 2 ซึ่งเป็นเลขในตารางของ kaen-log ไม่ใช่เลขบน wire ของ Kafka
สองเลขนี้ไม่มีความเกี่ยวข้องกันเลย และคอร์สนี้ไม่เคยดึงตารางเลขจริงของ Kafka มาลอก
เลข 19 กับ 20 ในย่อหน้าข้างบนมาจาก Errors.java ของ 4.0.0 โดยตรง
ส่วนคำว่า ก่อน กับ หลัง append คือสิ่งที่บทนี้สอน ไม่ใช่ตัวเลข
การตัดสินฝั่ง kaen-log อยู่ใน IsrGate ซึ่งบทที่ 3 เรียกผ่าน trait ตัวเดียวกัน
impl IsrGate { pub fn verdict(&self, required_offset: u64) -> AckVerdict { if self.high_watermark < required_offset { return AckVerdict::NotYet; } if self.min_isr <= self.isr_len { AckVerdict::Committed } else { AckVerdict::NotEnoughReplicasAfterAppend } }}
impl ReplicaGate for IsrGate { fn check_enough_replicas_reached(&self, required_offset: u64) -> bool { self.verdict(required_offset) == AckVerdict::Committed }}ข้อผิดพลาดทั้งสองข้อ retry ได้ และนั่นคือรอยต่อที่บทที่ 7 มายืนพอดี producer ที่ retry คำขอที่ append ไปแล้ว จะผลิตของซ้ำ ถ้าไม่มี หน้าต่างกันซ้ำฝั่ง broker มารับไว้
leader epoch แทนที่การตัดหางถึง high watermark
หัวข้อที่มีชื่อว่า “leader epoch แทนที่การตัดหางถึง high watermark”หัวข้อนี้แสดงว่าการที่ผู้ตามตัด log ถึง HW ของตัวเองทำข้อมูลหาย และเวกเตอร์ epoch แก้ตรงไหน
leader epochleader epochเลขรุ่นของความเป็น leader เพิ่มขึ้นทุกครั้งที่เปลี่ยนตัว · ใช้ตัดหาง log ที่ตกค้างจากรุ่นก่อนทิ้ง คือเลขรุ่นของความเป็น leader ที่เพิ่มขึ้นทุกครั้งที่เปลี่ยนตัว KIP-101 นิยามไว้ว่าเป็น “A 32 bit, monotonically increasing number representing a continuous period of leadership for a single partition”
หัวข้อสถานการณ์แรกของ KIP นั้นชื่อ “Scenario 1: High Watermark Truncation followed by Immediate Leader Election” และมันเล่าเรื่องที่ต่อยอดจากความช้าหนึ่งรอบข้างบนตรงๆ
demo ขับสถานการณ์นั้นด้วยสามรอบ fetch คือ produce m1 แล้ว fetch สองครั้งให้ผู้ตามรู้ว่า m1 committed จากนั้น produce m2 แล้ว fetch อีกครั้งเดียว
== KIP-101 สถานการณ์ที่ 1 · ตัดถึง high watermark แล้วข้อมูลหาย ==leader มี LEO 2 · HW 1ผู้ตามมี LEO 2 · HW ของตัวเอง 1ผู้ตามถือ m2 ไว้แล้ว แต่ยังไม่รู้ว่ามัน committed หรือยังผู้ตาม restart แล้วตัด log ถึง HW ของตัวเอง → LEO เหลือ 1จากนั้น leader ล้ม · ผู้ตามขึ้นเป็นผู้นำด้วย epoch 1 และของเก่ากลายเป็นผู้ตามrecord ที่หายถาวร 1 ตัวจำนวนรอบเป็นเนื้อของสถานการณ์ ไม่ใช่รายละเอียด สองรอบแรกมีไว้ให้ผู้ตามรู้ก่อนว่า m1 committed แล้ว m2 ถึงจะเข้ามาเป็น record ตัวเดียวที่ผู้ตามถือไว้โดยยังไม่รู้สถานะ ซึ่งคือสถานะตั้งต้นที่ KIP บรรยาย
จากตรงนั้น ผู้ตาม restart แล้วตัด log ถึง HW ของตัวเอง record จึงหายไปหนึ่งตัวอย่างถาวร โดยที่ทุกฝ่ายทำถูกตามกติกาของตัวเองทุกก้าว
ทางที่ถูกคือผู้ตามไม่ตัดถึง high watermark ของตัวเอง แต่ถาม leader ว่า epoch ที่ตัวเองถืออยู่จบที่ offset ไหน
== ทางที่ถูก · ถามหา end offset ของ epoch ที่ตัวเองมี ==ผู้ตามอยู่ในสถานะเดียวกันเป๊ะ · LEO 2 · HW ของตัวเอง 1ผู้ตามถาม end offset ของ epoch 0 → leader ตอบ Some((0, 2))ผู้ตามตัดถึง min(LEO ของตัวเอง, end offset ที่ได้) = 2 → LEO เหลือ 2record ที่หาย 0 ตัว**ผู้ตามไม่เคยตัดถึง high watermark ของตัวเอง** มันตัดถึงคำตอบของ leaderสถานการณ์ที่ 1 คือข้อมูล **หาย** · สถานการณ์ที่ 2 ของ KIP เดียวกันคือ log **แตกสาย**ซึ่ง KIP-279 เป็นคนปิดรูที่เหลือ · ทั้งสองอย่างวาดไว้ ไม่ได้ขับการค้นในเวกเตอร์ epoch ใช้การเทียบแบบไม่เกิน ไม่ใช่แบบเท่ากันพอดี และนั่นคือส่วนที่ KIP-279 แก้
/// **`<=` ไม่ใช่ `==`** — นี่คือส่วนที่ KIP-279 แก้ /// คืน epoch ที่ใหญ่ที่สุดซึ่งไม่เกิน `e` พร้อม offset ที่ epoch นั้นจบลง /// /// `None` แปลว่าไม่พบ · **ไม่ใช่ค่าพิเศษที่ยัดลงชนิด unsigned** /// เพราะ `UNDEFINED_EPOCH` กับ `UNDEFINED_OFFSET` เป็น `-1` ทั้งคู่ /// ซึ่งใส่ `u32` หรือ `u64` ไม่ได้เลย pub fn end_offset_for_epoch(&self, e: i32, leo: u64) -> Option<(i32, u64)> { let i = self.entries.iter().rposition(|x| x.epoch <= e)?; let found = self.entries[i]; let end = self.entries.get(i + 1).map_or(leo, |n| n.start_offset); Some((found.epoch, end)) }KIP-279 เขียนกติกาการตอบไว้ว่า “the leader responds with a pair (largest epoch less than or equal to the requested epoch, the end offset of this epoch)” และอธิบายเป้าหมายว่า “the idea is to converge to the largest common epoch, and then to the largest common offset in that epoch”
ผลรันของ demo ถามหลายค่า รวมทั้ง epoch ที่ไม่เคยมีอยู่จริง และค่าที่แปลว่าไม่มี
== leader epoch · เวกเตอร์ที่ leader เติมตอน become_leader == epoch start_offset 0 0 1 5 3 6 ขอ epoch ได้ (epoch, end_offset) 0 Some((0, 5)) 1 Some((1, 6)) 2 Some((1, 6)) 3 Some((3, 6)) 4 Some((3, 6)) -1 Noneขอ epoch 2 ซึ่งไม่เคยมีอยู่ แล้วได้ epoch 1 กลับมา · นี่คือส่วนที่ KIP-279 แก้การเทียบเป็น `<=` ไม่ใช่ `==` · ผู้ตามจึงบรรจบที่ epoch ร่วมที่ใหญ่ที่สุดได้`None` แปลว่าไม่พบ · ไม่ใช่ค่าพิเศษที่ยัดลง u64 เพราะ -1 ใส่ u64 ไม่ได้ที่ขอบของ wire เท่านั้นที่ `None` กลายเป็น (-1, -1)ในหน่วยความจำมันเป็น `Option<(i32, u64)>` ตลอดทางNone ที่แถวสุดท้ายไม่ใช่การเลี่ยงปัญหา มันจำเป็น เพราะค่าที่ Kafka ใช้แทนคำว่าไม่มีคือลบหนึ่ง
ทั้งของ epoch และของ offset ซึ่งใส่ชนิด unsigned ไม่ได้เลย
Option จึงอยู่ตลอดทางในหน่วยความจำ แล้วกลายเป็นคู่ลบหนึ่งที่ขอบของ wire เท่านั้น
KRaft วาดไว้ ไม่ได้สร้าง
หัวข้อที่มีชื่อว่า “KRaft วาดไว้ ไม่ได้สร้าง”หัวข้อนี้บอกว่า consensus อยู่ตรงไหนของ Kafka จริง และทำไมมันไม่ได้อยู่ในทาง replication ของ partition
ISR ของ kaen-log เป็น Vec ตัวหนึ่งที่นั่งอยู่ในหน่วยความจำของ process เดียว ไม่มีสำเนาที่ไหนอีก
ส่วน ISR ของ Kafka เป็น field ชื่อ Isr ของ PartitionRecord ใน metadata log
ซึ่ง controller เป็นคนเขียน และ KRaft ทำสำเนา record นั้นด้วย quorum แบบ Raft
flowchart TB
subgraph M["metadata · ใช้ consensus แบบ Raft"]
C1["controller 1"] <--> C2["controller 2"]
C2 <--> C3["controller 3"]
C1 <--> C3
end
subgraph D["ข้อมูลใน partition · ใช้ ISR ไม่ใช่การนับคะแนน"]
LD["leader ของ orders-0"] --> FA["follower A"]
LD --> FB["follower B"]
end
M -. "แจกว่าใครเป็น leader และ ISR มีใครบ้าง" .-> D
คำบรรยายภาพ: ภาพนี้เป็นรูปวาด ไม่ใช่ผลรัน · KRaft คือ Raft สำหรับ metadata เท่านั้น ส่วนข้อมูลใน partition ยังเดินด้วยกติกา ISR ที่บทนี้สร้างขึ้นมาทั้งบท คอร์สนี้ไม่ได้ลงมือสร้าง KRaft และคอร์สที่ 23 ก็ประกาศไว้แต่แรกว่า consensus เป็นรูปวาด
การพูดว่า Kafka หนีจาก consensus ได้จึงไม่จริง มันย้ายที่ให้ consensus ต่างหาก เอกสารของโครงการยังระบุด้วยว่างานวิชาการที่ใกล้กับของจริงที่สุดคือ PacificA ของ Microsoft ส่วน Raft กับ Zab ถูกจัดไว้ในตระกูลการนับคะแนนข้างมาก
สิ่งที่ตามมาจากการข้ามชั้นนั้นไปคือ บทนี้ข้ามส่วนที่ยากที่สุดของเรื่องทั้งหมด ผู้อ่านไม่ควรอ่าน ISR ของ kaen-log ว่าเป็นสถานะราคาถูกที่เก็บไว้ในหน่วยความจำได้
สิ่งที่ capstone นี้ปฏิเสธที่จะอ้าง
หัวข้อที่มีชื่อว่า “สิ่งที่ capstone นี้ปฏิเสธที่จะอ้าง”หัวข้อนี้คือย่อหน้าที่สำคัญที่สุดของทั้งคอร์ส มันบอกว่าของเล่นนี้แสดงอะไรได้ และแสดงอะไรไม่ได้
== สิ่งที่ capstone นี้ปฏิเสธที่จะอ้าง ==สอง broker ในหนึ่ง process ใช้ kernel เดียว page cache เดียว และไฟฟ้าเส้นเดียวกันkaen-log แสดง **protocol** ของการทำสำเนาได้ครบ แต่แสดงความทนทานต่อความล้มเหลวไม่ได้เลยแม้แต่นิดเดียว · `acks=all` ที่ทน f ตัวตายคือข้ออ้างที่บทนี้ไม่ยอมพูดและ ISR ของ kaen-log เป็น Vec<i32> ใน struct ที่ demo ตัวนี้ถือไว้คนเดียวของ Kafka เป็น field ชื่อ Isr ของ PartitionRecord ใน metadata log · controllerเป็นคนเขียน และ KRaft ทำสำเนา record นั้นด้วย quorum แบบ Raftสอง broker ในหนึ่ง process ใช้ kernel เดียว page cache เดียว และปลั๊กไฟเส้นเดียวกัน ถ้าเครื่องดับ ทั้งสองสำเนาดับพร้อมกัน
สิ่งที่บทนี้แสดงได้ครบคือ กลไก ของการทำสำเนา ตั้งแต่คำขอ fetch ของผู้ตาม การเลื่อน high watermark
ด้วยค่าน้อยที่สุดในหมู่ ISR ความช้าหนึ่งรอบ การปฏิเสธของ acks=all และการตัดหางด้วย epoch
สิ่งที่มันไม่ได้แสดงเลยแม้แต่นิดเดียวคือความทนทานต่อความล้มเหลว
ข้ออ้างว่า acks=all ทน f ตัวตายได้ จึงเป็นข้ออ้างที่บทนี้ไม่ยอมพูด
และมีสามอย่างในบทนี้ที่วาดไว้ ไม่ได้ขับ ผลรันของ demo พิมพ์รายการนั้นออกมาเอง
== สามอย่างที่วาดไว้ ไม่ได้สร้าง ==1. ตัวจับเวลาจริง · `shrink_isr(now_ms)` ถูกขับจริงข้างบนด้วยนาฬิกาที่ป้อนเข้ามา สิ่งที่ไม่มีคือ thread ที่ตื่นมาเรียกมันเอง ซึ่งจะทำให้ผลรันไม่นิ่ง2. สถานการณ์ leader ล้มเร็วของ KIP-279 · การค้นแบบ epoch ที่ใหญ่ที่สุดแต่ไม่เกิน ถูกสร้างและมีเทสต์แล้ว · การสลับผู้นำ A→B→A เป็นรูปวาด3. KRaft กับ ELR · วาดไว้ตามสัญญาเดิมของคอร์สที่ 23 · KRaft คือ Raft สำหรับ metadata เท่านั้น ข้อมูลใน partition ยังใช้ ISR อยู่ข้อแรกแยกให้ชัดว่าอะไรถูกขับจริง shrink_isr ถูกเรียกจริงด้วยนาฬิกาที่ป้อนเข้ามา
สิ่งที่ไม่มีคือ thread ที่ตื่นมาเรียกมันเอง
ข้อที่สองแยกเหมือนกัน การค้นแบบ epoch ที่ใหญ่ที่สุดแต่ไม่เกิน ถูกสร้างและมีเทสต์แล้ว ส่วนการสลับผู้นำสามจังหวะของ KIP-279 เป็นรูปวาด
capstone ที่ค่อยๆ ลดขอบเขตของตัวเองเงียบๆ แย่กว่า capstone ที่บอกว่าส่วนไหนเป็นรูปวาด
จบคอร์ส · สิ่งที่คุณถืออยู่ตอนนี้
หัวข้อที่มีชื่อว่า “จบคอร์ส · สิ่งที่คุณถืออยู่ตอนนี้”หัวข้อนี้สรุปว่าแปดบทประกอบกันเป็นอะไร และชี้ทางต่อไปในเสา Rust
kaen-log ที่คุณเพิ่งอ่านจบเป็น broker ทรง Kafka ที่เขียนด้วย Rust standard library ล้วน ไม่มี crate ภายนอกสักตัว และไม่มี async runtime
ของที่คุณอ่าน code ของมันมาแล้วทั้งแปดบท มีดังนี้
- รูปแบบบน disk — batch ที่มี CRC-32C กับ varint และการแจก offset ที่ไม่ต้องคิด checksum ใหม่
- การเก็บ — log ที่ตัดเป็นท่อน พร้อมสารบัญที่จงใจไม่เก็บทุก record
- ทาง produce กับทาง fetch — frame ที่หุ้มด้วยความยาวนำหน้า การจอดคำขอที่ยังไม่มีข้อมูล และการกู้คืนด้วยการตัดหาง
- สถานะของผู้อ่าน — group ที่บันทึก offset ของตัวเองเป็น record ใน log ของตัวเอง
- การลืม — retention ที่ลืมเป็นท่อน และ compaction ที่ห้ามขยับ offset
- กติกาการส่ง — หน้าต่างกันซ้ำฝั่ง broker และเส้นขอบของคำว่า exactly-once
- การทำสำเนา — ทั้งบทนี้
สิ่งที่คอร์สนี้ไม่ได้ให้คือ cluster จริง ถ้าอยากได้ของนั้น ทางต่อไปอยู่ที่คอร์สอื่นในเสาเดียวกัน
- Rust Systems II · kaen-kvstore — ที่มาของ segment กับการกู้คืน และเป็นคอร์สที่คอร์สนี้ต่อยอดโดยตรง
- Rust Systems III · Distributed Systems — quorum vector clock gossip และ harness ที่บทนี้ยืมมาใช้
- Rust Systems IV · Async Rust ด้วย Tokio — ตัวจัดคิวงานที่คอร์สนี้ จงใจไม่ใช้ เพื่อให้เห็น thread กับ blocking IO ตรงๆ
- Rust Systems V · สร้างภาษาโปรแกรมของคุณเอง — คนละโดเมน แต่เป็นรูปเดียวกัน คือการอ่าน byte แล้วตัดสินใจ
- Apache Kafka 4.0 — Design (ตรวจแล้ว 2026-09-03) — ประโยคที่ปฏิเสธการนับคะแนนข้างมาก ประโยคที่ระบุว่า write ยังไม่ commit จนกว่า in-sync replica ทุกตัวจะได้รับ ประโยค f+1 ทน f ตัวตาย เงื่อนไขสองข้อของคำว่าตามทัน ประโยคที่ระบุว่าผู้ตามบริโภคเหมือนผู้บริโภคทั่วไป และชื่อ PacificA
- FetchRequest.json tag 4.0.0
(ตรวจแล้ว 2026-09-03) — นิยามของ field
ReplicaIdทั้งบรรทัด รวมค่าปริยายลบหนึ่ง · file รุ่นนี้ประกาศvalidVersionsไว้ที่ 4-17 และตั้งแต่ v15 field นี้ย้ายเข้าไปอยู่ในReplicaState - FetchResponse.json tag 4.0.0
(ตรวจแล้ว 2026-09-03) — field
HighWatermarkที่เดินทางกลับมาในคำตอบ และ fieldDivergingEpochที่ทำให้การตรวจการแตกสายติดไปกับ fetch ได้ตั้งแต่ 12+ - FetchIsolation.java tag 4.0.0
(ตรวจแล้ว 2026-09-03) — การแยกสามทางที่แปลง
replicaIdเป็นเพดานการอ่าน - Partition.scala tag 4.0.0
(ตรวจแล้ว 2026-09-03) —
maybeIncrementLeaderHWที่เริ่มจาก LEO ของ leader แล้วไล่ลดลง ตาม maximal ISR และ doc comment ที่ระบุสองเหตุการณ์ที่ปลุกมัน - ReplicaFetcherThread.scala tag 4.0.0 (ตรวจแล้ว 2026-09-03) — บรรทัดที่ผู้ตามฝั่ง Kafka รับค่า high watermark จากคำตอบของ fetch
- ReplicationConfigs.java tag 4.0.0
(ตรวจแล้ว 2026-09-03) — ค่าปริยายของ
replica.lag.time.max.msและคำอธิบายเงื่อนไขที่ leader ใช้ถอดผู้ตามออกจาก ISR - ServerLogConfigs.java tag 4.0.0
(ตรวจแล้ว 2026-09-03) —
MIN_IN_SYNC_REPLICAS_DEFAULTที่เป็นหนึ่ง - Errors.java tag 4.0.0 (ตรวจแล้ว 2026-09-03) — เลข 19 กับ 20 และข้อความของทั้งสองข้อ
- KIP-101 — Alter Replication Protocol to use Leader Epoch rather than High Watermark for Truncation (ตรวจแล้ว 2026-09-03 · สถานะ Accepted) — นิยามของ Leader Epoch ที่ยกมาทั้งประโยค และหัวข้อ Scenario 1 ที่บทนี้ขับตาม
- KIP-279 — Fix log divergence between leader and follower after fast leader fail over (ตรวจแล้ว 2026-09-03 · สถานะ Accepted) — กติกาการตอบแบบ epoch ที่ใหญ่ที่สุดแต่ไม่เกิน และประโยคที่อธิบายการบรรจบที่ epoch ร่วม
เช็กความเข้าใจ — บทที่ 8
ข้อ 1 / 3ผู้ตามเพิ่งรับ record ตัวหนึ่งมาจาก leader ในรอบ fetch นี้ แล้วเขียนลง log ของตัวเองเรียบร้อย ทำไมมันยังไม่รู้ว่า record ตัวนั้น committed แล้ว