ข้าม​ไป​ยัง​เนื้อหา

ล็อก​คือ​ความ​จริง ไม่ใช่​บันทึก​เหตุการณ์

broker ที่​คอร์ส​นี้​จะ​สร้าง​ไม่มี​คิว ไม่มี​ตาราง​สถานะ และ​ไม่มี​ที่พัก​ของ​ข้อความ​ที่​ยัง​ไม่มี​ใคร​อ่าน

มัน​มี​ของ​อยู่​สอง​อย่าง คือ file ที่​ต่อ​ท้าย​ได้​อย่าง​เดียว กับ​จำนวนเต็ม​หนึ่ง​ตัว​ที่​บอก​ว่า​เขียน​ไป​ถึง​ลำดับ​ไหน​แล้ว

กลไก​ที่​เหลือ​ของ broker ทั้งหมด ทั้ง​สารบัญ การ​จอด​คำขอ การ​ลืม การ​กัน​ซ้ำ และ​การ​ทำ​สำเนา เป็น​ผล​ที่​ตาม​มา​จาก​การ​ยอมรับ​ข้อ​นั้น​ข้อ​เดียว ไม่ใช่ feature ที่​เติม​เข้า​มา​ทีหลัง

📦 code ของ​บท​นี้

crate ของ​คอร์ส​อยู่​ที่ scripts/log-broker/ และ​ชื่อ binary คือ klog ทั้ง​คอร์ส​เขียน​ด้วย Rust standard library ล้วน ตรวจ​ได้​จาก Cargo.toml ที่​ไม่มี​หัวข้อ [dependencies] และ Cargo.lock ที่​มี package ตัว​เดียว​คือ​ตัว​มัน​เอง

บล็อก code กับ​บล็อก​ผล​รัน​ทุก​อัน​ใน​บท​เรียน​นี้​ถูก​คัด​มา​จาก scripts/log-broker/src/record.rs และ​จาก scripts/log-broker/expected/01-record.txt ทีละ byte โดย​มี​เทสต์​เทียบ​ไว้ ถ้า​แก้​ใน​บท​เรียน​อย่าง​เดียว​โดย​ไม่​แก้​ที่​ต้นทาง เทสต์​จะ​แดง​ทันที

ผล​รัน​ทุก​บรรทัด​ใน​บท​นี้​มา​จาก​คำ​สั่ง​เดียว คือ klog dump-record --hex --key ORD-1042 --now-ms 1700000000000 ซึ่ง​เทสต์​รัน​ซ้ำ​แล้ว​เทียบ​ผล​ที​ละ byte ทุก​ครั้ง

หัวข้อ​นี้​แยก​คำ​สอง​ความ​หมาย​ออก​จาก​กัน แล้ว​บอก​ว่า​คอร์ส​นี้​ใช้​คำ​ไหน

ใน​วง​สนทนา​ของ developer ไทย คำ​ว่า​ล็อก​มัก​หมาย​ถึง​บรรทัด​ที่ logger พิมพ์​ออก​มา เป็น​ข้อความ​ที่​คน​อ่าน เขียน​ไว้​เผื่อ​วัน​ที่​ระบบ​พัง

ของ​แบบ​นั้น​มี​ผู้​อ่าน​เป็น​มนุษย์ รูป​แบบ​ของ​มัน​จึง​ยืดหยุ่น​ได้ ตกหล่น​ได้ และ​ไม่มี​ใคร​สร้าง​สถานะ​ของ​ระบบ​ขึ้น​ใหม่​จาก​มัน

log ที่ broker เก็บ​เป็น​คนละ​อย่าง มัน​คือ append-only logappend-only logfile ที่​เขียน​ต่อ​ท้าย​อย่าง​เดียว ไม่​แก้​ที่​เดิม · ใน broker มัน​ไม่ใช่​ชั้น​รอง​ของ​โครงสร้าง​อื่น มัน​คือ​โครงสร้าง​ข้อมูล​หลัก​ที่​ผู้​อ่าน​ทุก​ราย​เดิน​อยู่ ที่​โปรแกรม​อ่าน รูป​แบบ​ของ​มัน​ตายตัว​ที​ละ byte และ​มัน​เป็น​แหล่ง​เดียว​ที่​ผู้​อ่าน​ทุก​ราย​ใช้​สร้าง​สถานะ​ของ​ตัว​เอง

ชื่อ​บท​จึง​เขียน​ว่า​ล็อก​คือ​ความ​จริง ไม่ใช่​บันทึก​เหตุการณ์ ประโยค​นี้​ไม่​ได้​แปล​ว่า​มัน​ไม่​ได้​เก็บ​เหตุการณ์ มัน​แปล​ว่า​เหตุการณ์​ไม่​ได้​เกิด​ที่​อื่น​แล้ว​ค่อย​ถูก​จด​ตาม​มา การ​เขียน​ลง log คือ​ตัว​เหตุการณ์​เอง

ตั้งแต่​บรรทัด​นี้​ไป คอร์ส​ใช้​คำ​ว่า log ใน​รูป​อังกฤษ​เสมอ เพื่อ​ไม่​ให้​ปน​กับ​ความ​หมาย​แรก

หัวข้อ​นี้​แสดง​ว่า​ทำไม log ถึง​ไม่ใช่​คิว โดย​ดู​จาก​สิ่ง​ที่​ผู้​อ่าน​แต่ละ​ราย​ถือ​อยู่

คิว​มี​สมบัติ​ข้อ​หนึ่ง​ที่​ทุก​คน​คุ้น คือ​การ​อ่าน​ทำให้​ของ​หาย​ไป ผู้​อ่าน​ราย​ที่​สอง​จะ​ไม่​เห็น​สิ่ง​ที่​ราย​แรก​หยิบ​ไป​แล้ว

log ไม่มี​สมบัติ​ข้อ​นั้น​เลย การ​อ่าน​ไม่​ได้​ลบ​อะไร​ทั้ง​สิ้น ผู้​อ่าน​แต่ละ​ราย​ถือ​จำนวนเต็ม​ของ​ตัว​เอง​ไว้​ว่า​อ่าน​ถึง​ไหน​แล้ว แล้ว​เดิน​ต่อ​จาก​ตรง​นั้น

topic orders ของ​คอร์ส​นี้​มี​ผู้​อ่าน​สาม​ราย และ​ทั้ง​สาม​ราย​อ่าน log ก้อน​เดียวกัน

  • kitchen — ครัว​รับ​ออ​เด​อร์​ไป​ทำ จึง​ตาม​หลัง​ปลาย log อยู่​ไม่​กี่​ลำดับ
  • courier — คน​ส่ง​ของ​ตาม​ช้า​กว่า​ครัว เพราะ​งาน​ของ​มัน​เริ่ม​หลังค​รัว​ทำ​เสร็จ
  • analytics — ทีม​ข้อมูล​เริ่ม​อ่าน​จาก​ต้น log และ​ย้อน​กลับ​ไป​อ่าน​ซ้ำ​ได้​ทุก​เมื่อ

เอกสาร​ออกแบบ​ของ Kafka พูด​ข้อ​เดียวกัน​นี้​ด้วย​คำ​ของ​โครงการ​เอง ว่า topic ถูก​แบ่ง​เป็น “totally ordered partitions” และ​ตำแหน่ง​ของ​ผู้​อ่าน​ใน​แต่ละ partition คือ “just a single integer, the offset of the next message to consume”

เอกสาร​หน้า​เดียวกัน​ยอมรับ​ตรง​ไป​ตรง​มา​ด้วย​ว่า การ​ที่​ผู้​อ่าน​ย้อน​กลับ​ไป​อ่าน​ของ​เก่า​ได้​นั้น “violates the common contract of a queue”

flowchart LR
    P["producer · ส่ง batch เข้ามา"] --> B["broker · แจกลำดับแล้วต่อท้าย"]
    B --> L
    subgraph L["log ของ orders-0 · เขียนต่อท้ายอย่างเดียว"]
        direction LR
        N0["0"] --> N1["1"] --> N2["2"] --> N3["3"] --> N4["4"] --> N5["5"] --> N6["6"] --> N7["7"]
    end
    A["analytics · อ่านอยู่ที่ 0"] -.-> N0
    C["courier · อ่านอยู่ที่ 4"] -.-> N4
    K["kitchen · อ่านอยู่ที่ 7"] -.-> N7

คำ​บรรยาย​ภาพ: ภาพ​นี้​เป็น​ตัวอย่าง ไม่ใช่​ผล​รัน · หัว​เขียน​มี​หัว​เดียว​และ​อยู่​ที่​ปลาย log เสมอ ส่วน​หัว​อ่าน​มี​กี่​หัว​ก็ได้ ยืน​คนละ​ที่​ได้ และ​การ​อ่าน​ของ​หัว​หนึ่ง​ไม่​ทำให้​ของ​หาย​ไป​จาก​อีก​หัว​หนึ่ง

หัวข้อ​นี้​บอก​ว่า​ลำดับ​ของ record ทำ​หน้าที่​แทน​เวลา และ​ใคร​เป็น​คน​กำหนด​ลำดับ​นั้น

offsetoffsetลำดับ​ของ record ใน partition ที่ broker เป็น​คน​แจก เริ่ม​ที่ 0 และ​เพิ่ม​ที​ละ 1 · ต่าง​จาก #22 ที่​ใช้​คำ​นี้​แทน​ตำแหน่ง byte ใน file — ความ​หมาย​นั้น​อยู่​ที่ key `byte position` ของ​คอร์ส​นี้ คือ​ลำดับ​ของ record ใน partition เริ่ม​ที่​ศูนย์​แล้ว​เพิ่ม​ที​ละ​หนึ่ง มัน​ไม่ใช่​ตัวแทน​ของ​เวลา มัน​คือ​เวลา​ของ log ก้อน​นั้น

ผล​ที่​ตาม​มา​คือ​สถานะ​ทั้ง​ก้อน​ของ​ผู้​อ่าน​หนึ่ง​ราย ย่อ​ลง​เหลือ​จำนวนเต็ม​หนึ่ง​ตัว ไม่​ต้อง​มี​ตาราง​ว่า record ไหน​ถูก​อ่าน​ไป​แล้ว​บ้าง

ข้อ​ได้​เปรียบ​ที่แท้​จริง​ของ​นาฬิกา​แบบ​นี้​คือ​มัน​ไม่​ผูก​กับ​นาฬิกา​ของ​เครื่อง​ไหน​เลย สอง​เครื่อง​ที่​นาฬิกา​เดิน​ไม่​ตรง​กัน​ยัง​ตกลง​กัน​ได้​ว่า​อะไร​มา​ก่อน เพราะ​ลำดับ​ไม่​ได้​มา​จาก​นาฬิกา​ของ​ใคร

🔁 คอร์ส​ที่ 23 เป็น​เจ้าของ vector clock ไม่ใช่​คอร์ส​นี้

คอร์ส Distributed Systems สอน​นาฬิกา​แบบ vector clock กับ Lamport clock ไว้​แล้ว ทั้ง​สอง​ตัว​มี​ไว้​เรียง​เหตุการณ์​ที่​เกิด​คนละ​ที่​และ​ไม่มี​ใคร​เป็น​คนกลาง

kaen-log ไม่มี​ของ​สอง​อย่าง​นั้น​เลย และ​ไม่​ต้อง​มี เพราะ​มัน​มี​คนกลาง​อยู่​แล้ว​คือ broker ที่​เป็น​ผู้​เขียน​ราย​เดียว​ของ partition นั้น ลำดับ​จึง​มา​จาก​การ​เขียน ไม่​ได้​มา​จาก​การ​เทียบ​นาฬิกา

และ​คน​ที่​แจก​เลข​นั้น​คือ broker ไม่ใช่ producer producer ประกอบ batch แล้ว​ส่ง​เข้า​มา​โดยที่​ช่อง base_offset ยัง​เป็น​ศูนย์ ตัว broker เป็น​คน​เขียน​ทับ​ช่อง​นั้น​ตอน append

Kafka ทำ​แบบ​เดียวกัน LogValidator.assignOffsetsNonCompressed เรียก batch.setLastOffset(offsetCounter.value - 1) และ​ตัว setLastOffset เอง​มี​บรรทัด​เดียว คือ buffer.putLong(BASE_OFFSET_OFFSET, offset - lastOffsetDelta())

เขียน​ทับ​แล้ว​จบ ไม่มี​การ​คิด checksum ใหม่ เหตุผล​ว่า​ทำไม​ถึง​ทำ​แบบ​นั้น​ได้​อยู่​ที่​ตำแหน่ง​ของ checksum ซึ่ง​อีก​สาม​หัวข้อ​ข้าง​ล่าง​จะ​กาง​ให้​เห็น

offset ใน​คอร์ส​ที่ 22 กับ​ใน​คอร์ส​นี้ ไม่ใช่​ของ​อย่าง​เดียวกัน

หัวข้อ​ที่​มีชื่อ​ว่า “offset ใน​คอร์ส​ที่ 22 กับ​ใน​คอร์ส​นี้ ไม่ใช่​ของ​อย่าง​เดียวกัน”

หัวข้อ​นี้​เตือน​คำ​เดียว​ที่​เปลี่ยน​ความ​หมาย​ระหว่าง​สอง​คอร์ส

คอร์ส kaen-kvstore ใช้​คำ​ว่า offset แทน​ตำแหน่ง byte ใน file สารบัญ​ขอ​งมันแม็ป key ไปหา offset แล้ว seek ตรง​ไป​ที่​ตำแหน่ง​นั้น​แล้ว​อ่าน

คอร์ส​นี้​ใช้​คำ​เดียวกัน​แทน​ลำดับ​ของ record ตำแหน่ง byte ยัง​อยู่​ครบ แต่​มัน​มีชื่อ​ของ​ตัว​เอง​แล้ว​ว่า byte position

ความ​ต่าง​ข้อ​นี้​จะ​สำคัญ​ตั้งแต่​บท​ที่ 2 เพราะ​สารบัญ​ของ kaen-log แม็ป offset ไปหา byte position ถ้า​ใช้​คำ​เดียวกัน​ทั้ง​สอง​ฝั่ง​ของ​ลูก​ศร ประโยค​จะ​อ่าน​ไม่​ได้​ความ

ความ​กว้าง​ของ​จำนวนเต็ม​ก็​ถูก​ตรึง​ไว้​ตั้งแต่​บท​นี้ offset ใน​หน่วย​ความ​จำเป็น u64 เสมอ ส่วน i64 โผล่​ได้ที่​ขอบ​ของ​การ encode ลง wire เท่านั้น เพราะ field ของ Kafka เป็น signed ทุก​ตัว

src/record.rs · ขอบ​เดียว​ที่​ความ​กว้าง​ของ offset เปลี่ยน
/// u64 ในหน่วยความจำ → i64 บน wire · field ของ Kafka เป็น signed ทั้งหมด
pub fn wire_i64(o: u64) -> i64 {
o.cast_signed()
}
/// i64 บน wire → u64 ในหน่วยความจำ · offset ติดลบไม่ใช่สิ่งที่ kaen-log ถือได้
pub fn from_wire_i64(v: i64) -> io::Result<u64> {
u64::try_from(v).map_err(|_| bad(format!("offset บน wire ติดลบ: {v}")))
}

สอง​ตัว​นี้​คือ​ที่​เดียว​ใน crate ที่​ความ​กว้าง​ของ offset เปลี่ยน และ​ฝั่ง​ขา​เข้า​ปฏิเสธ​ค่า​ติดลบ​ทันที เพราะ offset ติดลบ​ไม่ใช่​สิ่ง​ที่ kaen-log ถือ​ได้

🔁 ของ​ที่​คอร์ส​ที่ 22 สอน​ไว้​แล้ว และ​คอร์ส​นี้​ถือว่า​รู้

คอร์ส​นี้​ไม่​สอน​สาม​ข้อ​นี้​ซ้ำ ถ้า​ยัง​ไม่​เคย​เจอ ให้​ย้อน​ไป​อ่าน​ที่ คอร์ส kaen-kvstore ก่อน

  • การ​หุ้ม frame ด้วย​ความ​ยาว​นำ​หน้า แล้ว​อ่าน​กลับ​ด้วย read_exact บน std::net
  • write_all ไป​ได้​แค่ page cache ไม่​ได้​แปล​ว่า byte ถึง storage แล้ว
  • ลำดับ write แล้ว fsync แล้ว​ค่อย ack ซึ่ง​บท​ที่ 3 ของ​คอร์ส​นี้​จะ​รับ​มา​ทั้ง​ลำดับ แล้ว​เถียง​เรื่อง​เหตุผล​ของ​มัน​ใหม่

หัวข้อ​นี้​ยก​สอง​ประโยค​ที่​ขัด​กัน​จาก​หน้า​เดียวกัน แล้ว​ตัดสิน​ด้วย code ที่ broker ใช้​แจก​เลข​นั้น

หน้า Implementation › Log ของ​เอกสาร Kafka 4.0 เขียน​ไว้​ว่า “Each message is uniquely identified by a 64-bit integer offset giving the byte position of the start of this message in the stream of all messages ever sent to that topic on that partition”

ย่อหน้า​ถัด​ลง​ไป​ใน​หน้า​เดียวกัน​เขียน​ว่า โครงการ “decided to use a simple per-partition atomic counter”

สอง​ประโยค​นี้​อยู่​คนละ​ความ​หมาย ประโยค​แรก​บอก​ว่า offset คือ​ตำแหน่ง byte ประโยค​หลัง​บอก​ว่า​มัน​คือ​ตัว​นับ​ที่​เพิ่ม​ที​ละ​หนึ่ง

ประโยค​หลัง​คือ​ของ​จริง และ​ตัดสิน​ได้​จาก code ที่​แจก​เลข​นั้น ไม่​ต้อง​เชื่อ​ประโยค​ไหน​เป็น​พิเศษ

หน้า Message Format กำหนด​ให้ batch เก็บ baseOffset ไว้​ค่า​เดียว​ที่​หัว แล้ว​ให้ record แต่ละ​ตัว​เก็บ offsetDelta ต่อ​จาก​นั้น รูป​นี้​เข้า​ได้​กับ​ทั้ง​สอง​ความ​หมาย เพราะ delta เป็น varint ที่​เก็บ​ระยะ​ห่าง​เท่าไร​ก็ได้ มัน​จึง​ไม่ใช่​ตัว​ตัดสิน

ตัว​ตัดสิน​อยู่​ที่ LogValidator.assignOffsetsNonCompressed มัน​วน record ที​ละ​ตัว​ใน​หนึ่ง batch แล้ว​สั่ง offsetCounter.value++ หนึ่ง​ครั้ง​ต่อ​หนึ่ง record จาก​นั้น​ค่อย​ประทับ setLastOffset(offsetCounter.value - 1) ลง​หัว batch

เลข​ที่​เพิ่ม​ที​ละ​หนึ่ง​ต่อ​หนึ่ง record คือ​ตัว​นับ ไม่ใช่​ตำแหน่ง byte เพราะ​ตำแหน่ง byte ต้อง​เพิ่ม​ตาม​ความ​ยาว​ของ record ที่​เพิ่ง​ผ่าน​ไป ซึ่ง​ไม่​เท่ากับ​หนึ่ง

ผล​ที่​ตาม​มา​คือ last_offset_delta ใน​หัว batch เท่ากับ​จำนวน record ใน​นั้น​ลบ​หนึ่ง​เสมอ delta ของ record จึง​เรียง​ติด​กัน​โดย​ไม่มี​ช่อง​ว่าง ส่วน​ตำแหน่ง byte ไม่มี​ทาง​เรียง​แบบ​นั้น

ประโยค​แรก​เป็น​ซาก​ที่​ค้าง​มา​จาก​ยุค​แรก​ของ​โครงการ ตอน​นั้น offset เป็น​ตำแหน่ง byte จริง และ​เลข​ของ record จึง​เพิ่ม​ขึ้น​แบบ​ไม่​ต่อ​เนื่อง

⚠ เอกสาร​ของ​โครงการ​ใหญ่​ขัด​กันเอง​ได้ และ​จะ​ไม่มี​ใคร​มาบ​อก

สอง​ประโยค​นั้น​อยู่​ห่าง​กัน​ไม่​ถึง​หน้า​จอ​เดียว และ​หน้า​นั้น​ยัง​เผย​แพร่​อยู่​ใน​เอกสาร​ของ​รุ่น 4.0

วิธี​ตัดสิน​ไม่ใช่​การ​ดู​ว่า​ประโยค​ไหน​เขียน​ก่อน และ​ไม่ใช่​การ​เปิด​รูป​บน disk ด้วย เพราะ​รูป​แบบ base บวก delta เข้า​ได้​กับ​ทั้ง​สอง​ความ​หมาย

ตัว​ที่​ตัดสิน​คือ code ที่​แจก​เลข​นั้น มัน​ต้อง compile และ​ต้อง​รัน​ได้ ส่วน​ย่อหน้า​อธิบาย​ไม่​ต้อง

หัวข้อ​นี้​กาง​ความ​กว้าง​ของ​แต่ละ field ใน​หัว batch แล้ว​ชี้​ว่า checksum ครอบ​ตั้งแต่​ตรง​ไหน

หน่วย​ที่ producer ส่ง​และ broker เขียน​ลง disk ไม่ใช่ record เดี่ยว แต่​เป็น record batchrecord batchหน่วย​ที่ producer ส่ง​และ broker เขียน​ลง log จริง หนึ่ง batch ห่อ​หลาย record ไว้​ใต้ header กับ checksum เดียวกัน ซึ่ง​ห่อ record หลาย​ตัว​ไว้​ใต้ header เดียวกัน​และ checksum เดียวกัน

src/record.rs · สาม​ค่าที่​ตรึง​รูป​ของ​หัว batch
/// หัว batch แบบ v2 ยาว 61 byte พอดี — เท่ากับ `RECORD_BATCH_OVERHEAD` ของ Kafka 4.0.0
pub const BATCH_HEADER_LEN: usize = 61;
/// CRC ครอบตั้งแต่ `attributes` ไปจนจบ batch · `base_offset` กับ `partition_leader_epoch`
/// อยู่ **นอก** ช่วงนี้ เพื่อให้ broker ประทับค่าทับได้โดยไม่ต้องคิด CRC ใหม่
pub const CRC_COVERED_FROM: usize = 21;
/// `batch_length` ไม่ได้นับทั้ง batch · ขนาดจริงบน disk = `batch_length + 12`
/// (`OFFSET_LENGTH` 8 + `SIZE_LENGTH` 4 ของ Kafka)
pub const LOG_OVERHEAD: usize = 12;

BATCH_HEADER_LEN เท่ากับ RECORD_BATCH_OVERHEAD ของ Kafka 4.0.0 ซึ่ง​ไม่ใช่​ตัวเลข​ที่​คัด​มา​จาก​ย่อหน้า​ใน​เอกสาร แต่​บวก​ขึ้น​มา​จาก constant ที่​ต่อ​กัน​เป็น​ลูกโซ่​ใน DefaultRecordBatch.java โดย​แต่ละ​ตัว​เขียน​ว่า​ตำแหน่ง​ของ​ตัว​เอง​คือ​ตำแหน่ง​ก่อน​หน้า​บวก​ความ​ยาว​ก่อน​หน้า

src/record.rs · ตำแหน่ง​ของ​ทุก field ใน​หัว batch
const BASE_OFFSET_AT: usize = 0;
const BATCH_LENGTH_AT: usize = 8;
const LEADER_EPOCH_AT: usize = 12;
const MAGIC_AT: usize = 16;
const CRC_AT: usize = 17;
const ATTRIBUTES_AT: usize = 21;
const LAST_OFFSET_DELTA_AT: usize = 23;
const BASE_TIMESTAMP_AT: usize = 27;
const MAX_TIMESTAMP_AT: usize = 35;
const PRODUCER_ID_AT: usize = 43;
const PRODUCER_EPOCH_AT: usize = 51;
const BASE_SEQUENCE_AT: usize = 53;
const RECORDS_COUNT_AT: usize = 57;

ตัวเลข​ทั้ง​ชุด​ข้าง​บน​คือ​ผล​บวก​นั้น ไม่ใช่​ค่าที่​ถอด​ออก​มา​จาก​ตาราง​ใน​เอกสาร

batch_length มี​กับ​ดัก​ของ​ตัว​เอง​อยู่​หนึ่ง​ข้อ มัน​ไม่​ได้​นับ​ทั้ง batch ต้อง​บวก LOG_OVERHEAD กลับ​เข้าไป​ถึง​จะ​ได้​ขนาด​จริง​บน disk

ตัว klog พิมพ์​ตาราง​นี้​ออก​มา​เอง​จาก​หัว batch ที่​มัน​เพิ่ง​สร้าง ไม่ใช่​ตาราง​ที่​พิมพ์​ใส่​ไว้​ใน​บท​เรียน

expected/01-record.txt (ตัด​มา​บาง​ส่วน)
== หัว batch 61 byte · big-endian ==
byte field ค่า
00..08 base_offset 0
08..12 batch_length 125
12..16 partition_leader_epoch -1
16..17 magic 2
17..21 crc 0x98867f12
21..23 attributes 0
23..27 last_offset_delta 0
27..35 base_timestamp 1700000000000
35..43 max_timestamp 1700000000000
43..51 producer_id -1
51..53 producer_epoch -1
53..57 base_sequence -1
57..61 records_count 1
61.. records
ขนาดจริงบน disk = batch_length + 12 = 137 byte
CRC ครอบ byte 21..137 · ไม่ครอบ base_offset และไม่ครอบ
partition_leader_epoch — สอง field นี้เป็นของ broker ไม่ใช่ของ producer

สอง​บรรทัด​สุดท้าย​คือ​หัวใจ​ของ​ทั้ง​บท CRC ครอบ​ตั้งแต่ attributes ไป​จน​จบ batch ส่วน base_offset กับ partition_leader_epoch ยืน​อยู่​ก่อน​หน้า​นั้น จึง​อยู่​นอก​ช่วง​ที่ CRC ครอบ

เอกสาร Message Format ของ Kafka บอก​เหตุผล​ไว้​เอง ว่า “The partition leader epoch field is not included in the CRC computation to avoid the need to recompute the CRC when this field is assigned for every batch that is received by the broker”

อ่าน​ประโยค​นั้น​กลับ​ด้าน​จะ​ได้​ข้อ​อ้าง​ของ​บท​นี้ รูป​แบบ​ถูก​ออกแบบ​มา​ให้ broker เขียน​ทับ​ช่อง​ที่​เป็น​ของ broker ได้ โดย​ไม่​ต้อง​คิด checksum ใหม่ ตำแหน่ง​ของ field ใน​หัว batch จึง​ไม่ใช่​รสนิยม แต่​เป็น​ผล​ของ คำถาม​ว่า​ใคร​เป็น​เจ้าของ​ช่อง​ไหน

ธง --hex สั่ง​ให้ klog พิมพ์ byte ทั้ง​ก้อน​ออก​มา​ให้​อ่าน​เทียบ​กับ​ตาราง​ข้าง​บน​ได้ที​ละ​ช่อง

expected/01-record.txt (ตัด​มา​บาง​ส่วน)
== hex dump · ทั้ง batch 137 byte ==
00000000 00 00 00 00 00 00 00 00 00 00 00 7d ff ff ff ff |...........}....|
00000010 02 98 86 7f 12 00 00 00 00 00 00 00 00 01 8b cf |................|
00000020 e5 68 00 00 00 01 8b cf e5 68 00 ff ff ff ff ff |.h.......h......|
00000030 ff ff ff ff ff ff ff ff ff 00 00 00 01 94 01 00 |................|
00000040 00 00 10 4f 52 44 2d 31 30 34 32 78 7b 22 65 76 |...ORD-1042x{"ev|
00000050 65 6e 74 22 3a 22 4f 72 64 65 72 50 6c 61 63 65 |ent":"OrderPlace|
00000060 64 22 2c 22 6f 72 64 65 72 5f 69 64 22 3a 22 4f |d","order_id":"O|
00000070 52 44 2d 31 30 34 32 22 2c 22 74 6f 74 61 6c 22 |RD-1042","total"|
00000080 3a 31 32 39 39 30 30 7d 00 |:129900}.|

แปด​ตัว​แรก​ของ​แถว​บน​คือ base_offset ที่​ยัง​เป็น​ศูนย์ ต่อ​ด้วย​สี่​ตัว​ที่​เป็น batch_length แล้ว ff ff ff ff คือ partition_leader_epoch ที่​ยัง​ไม่มี​ค่า ซึ่ง​เขียน​แทน​ด้วย​ลบ​หนึ่ง

หัวข้อ​นี้​บอก​ว่า​ทำไม crate ถึง​ต้อง​เขียน CRC ขึ้น​เอง และ​เทียบ​กับ​อะไร​ถึง​จะ​รู้​ว่า​ตาราง​ถูก

CRC-32CCRC-32Cchecksum แบบ Castagnoli ที่ Kafka ใช้​ตรวจ​ว่า batch เสีย​หาย​หรือ​ไม่ · คนละ polynomial กับ​ของ zlib ที่ Kafka ใช้​ไม่ใช่ CRC ตัว​เดียว​กับ​ที่ zlib ใช้ เอกสาร Message Format ระบุ​ไว้​ตรง​ตัว​ว่า “The CRC-32C (Castagnoli) polynomial is used for the computation”

standard library ของ Rust ไม่มี checksum สัก​ตัว​ใน API สาธารณะ และ DefaultHasher ใช้​แทน​ไม่​ได้ เพราะ​เอกสาร​ของ​มัน​เขียน​ไว้​เอง​ว่า algorithm ข้าง​ใน​ไม่​ได้​ระบุ​ไว้ ค่าที่​มัน​คืน​จึง​เชื่อ​ถือ​ข้าม​รุ่น​ของ std ไม่​ได้

ค่าที่​ไม่​รับ​ประกัน​ความ​เสถียร ห้าม​เขียน​ลง​รูป​แบบ​ของ file เด็ด​ขาด

crate จึง​กาง​ตาราง 256 ช่อง​ขึ้น​เอง​ตอน compile

src/record.rs · ตาราง CRC-32C ที่​กาง​ตอน compile
/// polynomial ของ Castagnoli ในรูปกลับด้าน · **ไม่ใช่** 0xEDB88320 ของ zlib
/// ถ้าใช้ตัวหลัง byte ที่ออกมาจะไม่ใช่ byte ของ Kafka และไม่มีอะไรเตือน
const CRC32C_POLY_REVERSED: u32 = 0x82F6_3B78;
const fn crc32c_table() -> [u32; 256] {
let mut table = [0u32; 256];
let mut i = 0usize;
while i < 256 {
let mut crc = i as u32;
let mut bit = 0;
while bit < 8 {
crc = if crc & 1 != 0 { (crc >> 1) ^ CRC32C_POLY_REVERSED } else { crc >> 1 };
bit += 1;
}
table[i] = crc;
i += 1;
}
table
}
static CRC32C_TABLE: [u32; 256] = crc32c_table();

กับ​ดัก​อยู่​ตรง​บรรทัด​ของ polynomial ถ้า​หยิบ 0xedb88320 ของ zlib มา​ใส่​แทน ตาราง​จะ​ยัง​คำนวณ​ได้​ปกติ​ทุก​ครั้ง ไม่มี error ไม่มี​คำ​เตือน แต่ byte ที่​ออก​มา​ไม่ใช่ byte ของ Kafka

ทาง​เดียว​ที่​จะ​รู้ คือ​เทียบ​กับ​ค่า​ตรวจ​ที่​มี​คน​ประกาศ​ไว้​แล้ว แค​ต​ตา​ล็อก CRC ของ Greg Cook ระบุ​ค่า​ตรวจ​ของ CRC-32C สำหรับ input 123456789 ไว้​ที่ 0xe3069283

src/record.rs · ค่า​ตรวจ​ที่​ประกาศ​ไว้ กับ​ตัว​คิด CRC
/// ค่าตรวจมาตรฐานของ CRC-32C สำหรับ input `123456789` · ถ้าตารางผิด เทสต์ตัวนี้แดง
pub const CRC32C_CHECK_VALUE: u32 = 0xE306_9283;
pub fn crc32c(data: &[u8]) -> u32 {
let mut crc = 0xFFFF_FFFFu32;
for &b in data {
let idx = usize::from((crc as u8) ^ b);
crc = (crc >> 8) ^ CRC32C_TABLE[idx];
}
!crc
}
expected/01-record.txt (ตัด​มา​บาง​ส่วน)
== CRC-32C · std ไม่มี checksum สักตัว จึงต้องกางตาราง 256 ช่องเอง ==
polynomial 0x82f63b78 (Castagnoli แบบกลับด้าน)
ของ zlib 0xedb88320 (คนละตัว · ใช้ผิดแล้ว byte ไม่ใช่ของ Kafka)
ค่าตรวจของ input `123456789`
ที่ตารางนี้คิดได้ 0xe3069283
ค่าที่ประกาศไว้ 0xe3069283
ตรงกัน ใช่

บรรทัด​สุดท้าย​คือ​คำ​ตอบ ตาราง​ที่ crate กาง​เอง​ให้​ค่า​ตรง​กับ​ค่าที่​ประกาศ​ไว้

ด่าน​ที่​ยืนยัน​ด้าน​เดียว ยัง​ไม่​ได้​ยืนยัน​อะไร

เทสต์​ที่​บอก​แค่​ว่า​ค่าที่​คิด​ได้​เท่ากับ​ค่าที่​ประกาศ​ไว้ จะ​เขียว​ทันที​ถ้า​มี​คน​แก้​ค่าที่​ประกาศ​ไว้​ให้​ตรง​กับ ตาราง​ที่​ผิด

src/record.rs จึง​มี​เทสต์​อีก​ตัว​ที่​ยืนยัน​ด้าน​กลับ ว่า​ค่าที่​ได้​ต้อง ไม่ใช่ 0xcbf43926 ซึ่ง​เป็น​ค่า​ตรวจ​ของ CRC-32 แบบ zlib สำหรับ input เดียวกัน ตาราง​ที่​ใช้ polynomial ผิด​จะ​ผ่าน​ด่าน​แรก​ไม่​ได้​และ​ตก​ด่าน​ที่​สอง​พร้อม​กัน

ทุก​ความ​ยาว​ใน batch เป็น zigzag varint และ​ลบ​หนึ่ง​แปล​ว่า null

หัวข้อ​ที่​มีชื่อ​ว่า “ทุก​ความ​ยาว​ใน batch เป็น zigzag varint และ​ลบ​หนึ่ง​แปล​ว่า null”

หัวข้อ​นี้​อธิบาย​การ encode ที่​ทุก​ความ​ยาว​ใน batch ใช้ แล้ว​ชี้​ค่า​ลบ​หนึ่ง​ที่​บท​ที่ 6 จะ​สร้าง tombstone จาก​มัน

หัว batch ตรึง​ความ​กว้าง​ไว้​ทุก​ช่อง แต่​ข้าง​ใน batch ตรง​กัน​ข้าม​โดย​สิ้น​เชิง ทุก​ความ​ยาว​และ​ทุก delta ของ record เป็น zigzag varintzigzag varintการ encode จำนวนเต็ม​ด้วย​ความ​ยาว​ไม่​คงที่ โดย​แม็ป​เลข​ติดลบ​สลับ​กับ​เลข​บวก​ก่อน ค่าที่​เล็ก​จึง​กิน​ที่​น้อย

varint เก็บ 7 bit ต่อ​หนึ่ง byte แล้ว​ใช้ bit สูงสุด​บอก​ว่า​ยัง​มี byte ถัด​ไป​หรือ​ไม่ ค่าที่​เล็ก​จึง​กิน​ที่​น้อย และ​ค่าที่​ใหญ่​ค่อย​กิน​เพิ่ม

ปัญหา​คือ varint เป็น​ค่าที่​ไม่มี​เครื่องหมาย เลข​ติดลบ​ใน​รูป two’s complement จะมี bit สูงสุด​ติด​หมด และ​กิน​ครบ​สิบ byte ทุก​ครั้ง​ที่​ใช้

zigzag แก้​ข้อ​นั้น​ด้วย​การ​สลับ​เลข​บวก​กับ​เลข​ลบ​เข้าหา​กัน​ก่อน​จะ encode เอกสาร​ของ Protobuf เขียน​สูตร​ไว้​ว่า (n << 1) ^ (n >> 31) สำหรับ sint32 และ (n << 1) ^ (n >> 63) สำหรับ​รุ่น 64 bit

src/record.rs · zigzag ทั้ง​สอง​ความ​กว้าง
/// `(n << 1) ^ (n >> 31)` · 0→0 · -1→1 · 1→2 · -2→3
pub fn zigzag_i32(n: i32) -> u32 {
(n.cast_unsigned() << 1) ^ (n >> 31).cast_unsigned()
}
pub fn unzigzag_i32(v: u32) -> i32 {
(v >> 1).cast_signed() ^ -(v & 1).cast_signed()
}
/// `(n << 1) ^ (n >> 63)` · รูปเดียวกับ `sint64` ของ Protobuf
pub fn zigzag_i64(n: i64) -> u64 {
(n.cast_unsigned() << 1) ^ (n >> 63).cast_unsigned()
}
pub fn unzigzag_i64(v: u64) -> i64 {
(v >> 1).cast_signed() ^ -(v & 1).cast_signed()
}

รูป​นี้​ไม่ใช่​รสนิยม​ของ kaen-log เหมือน​กัน เอกสาร Message Format ของ Kafka เขียน​ไว้​เอง​ว่า “We use the same varint encoding as Protobuf”

expected/01-record.txt (ตัด​มา​บาง​ส่วน)
== zigzag varint · รูปที่ทุกความยาวใน batch ใช้ ==
n zigzag varint
0 0 00
-1 1 01
1 2 02
-2 3 03
64 128 80 01
-64 127 7f
129900 259800 d8 ed 0f

แถว​ของ 0 กับ​แถว​ของ -1 คือ​แถว​ที่​บอก​ราคา​ของ zigzag ทั้ง​สอง​ค่า​กิน​หนึ่ง byte ถ้า​ช่อง​ความ​ยาว​เป็น i32 ความ​กว้าง​คงที่ ทั้ง​สอง​ค่า​จะ​กิน​สี่ byte เท่า​กัน สิ่ง​ที่ zigzag varint ซื้อ​มา​จึง​เป็น​ขนาด

ความ​หมาย​ของ​ลบ​หนึ่ง​เป็น​คนละ​เรื่อง​กับ​ขนาด ลบ​หนึ่ง​ใน​ช่อง value_len เป็น sentinel มัน​แปล​ว่า null ไม่ใช่​ค่าที่​ยาว​ศูนย์

src/record.rs · ความ​ยาว​ลบ​หนึ่ง​แปล​ว่า null
/// ความยาว `-1` บน wire แปลว่า **null** ไม่ใช่ slice ว่าง · นี่คือ tombstone ของบทที่ 6
pub const NULL_LEN: i32 = -1;

ตัว​ถอดรหัส​จึง​ต้อง​แยก​สอง​กรณี​นี้​ออก​จาก​กัน​ตั้งแต่​ก่อน​จะ​อ่าน byte ของ​ค่า

src/record.rs · อ่าน​ค่าที่​เป็น null ได้
fn read_nullable(cur: &mut Cursor<'_>) -> io::Result<Option<Vec<u8>>> {
let len = cur.varint_i32()?;
if len == NULL_LEN {
return Ok(None);
}
let len = usize::try_from(len).map_err(|_| bad(format!("ความยาว {len} ไม่ถูกต้อง")))?;
Ok(Some(cur.take(len)?.to_vec()))
}

klog สร้าง batch สอง​ก้อน​มา​เทียบ​ให้​ดู ก้อน​หนึ่ง​เป็น tombstone อีก​ก้อน​มี value ที่​ยาว​ศูนย์

expected/01-record.txt (ตัด​มา​บาง​ส่วน)
== null ไม่ใช่ slice ว่าง ==
value_len ขนาด batch
tombstone -1 76
value ว่างเปล่า 0 76
สอง batch ยาวเท่ากันเพราะ zigzag ให้ -1 กับ 0 กินหนึ่ง byte เท่ากัน
แต่ decode ออกมาคนละอย่าง · tombstone None · value ว่างเปล่า Some([])

สอง batch ยาว​เท่า​กัน​เป๊ะ แต่​ถอด​กลับ​ออก​มา​ได้​คนละ​อย่าง ก้อน​แรก​ได้ None ก้อน​หลัง​ได้ Some ที่​ห่อ slice ว่าง​ไว้

ความ​เท่า​กัน​นี้​ไม่​ได้​มา​จาก varint ถ้า​ช่อง​ความ​ยาว​เป็น​ความ​กว้าง​คงที่ สอง batch ก็​ยัง​ยาว​เท่า​กัน​อยู่ดี เพราะ tombstone เป็น​ค่า​หนึ่ง​ใน​ช่อง​ความ​ยาว​เอง ไม่ใช่​ช่อง​ใหม่​ที่​ต้อง​เก็บ​เพิ่ม

นั่น​คือ​ข้อ​ที่​บท​ที่ 6 จะ​ยืน​อยู่​บน​มัน และ​มัน​ไม่​ผูก​กับ​การ encode เลย

สิ่ง​ที่​บังคับ​ให้​ทั้ง crate ใช้ varint เป็น​คนละ​ข้อ คือ​การ​รักษา​รูป byte ของ Kafka ไว้​ให้​ตรง บท​ที่ 3 บท​ที่ 6 และ​บท​ที่ 7 อ่าน field ออก​จาก​รูป​นั้น ถ้า​แปดบท​เข้า​รหัส​ไม่​ตรง​กัน มัน​จะ​ขัด​กัน​ตอน​รัน ไม่ใช่​ตอน​อ่าน

หัวข้อ​นี้​แสดง​ว่าการ​แจก offset เป็นการ​เขียน​ทับ​ช่วง​เดียว​ใน​หัว batch แล้ว​จบ

ทุก​อย่าง​ข้าง​บน​มา​รวม​กัน​ที่ function สั้นๆ ตัว​นี้

src/record.rs · broker ประทับ​ลำดับ​ลง​หัว batch
/// broker ประทับ offset ทับที่เดิม 8 byte แล้วจบ · **ไม่ต้องคิด CRC ใหม่**
/// เพราะ `base_offset` อยู่นอกช่วงที่ CRC ครอบ
pub fn stamp_last_offset(buf: &mut [u8], last: u64, last_delta: i32) -> io::Result<()> {
let delta = u32::try_from(last_delta)
.map_err(|_| bad(format!("last_offset_delta ติดลบ: {last_delta}")))?;
let base = last
.checked_sub(u64::from(delta))
.ok_or_else(|| bad(format!("offset {last} เล็กกว่า delta {delta}")))?;
buf[BASE_OFFSET_AT..BASE_OFFSET_AT + 8].copy_from_slice(&wire_i64(base).to_be_bytes());
Ok(())
}

มัน​ไม่​เรียก crc32c สัก​ครั้ง เพราะ​ช่อง​ที่​มัน​เขียน​ทับ​อยู่​นอก​ช่วง​ที่ CRC ครอบ

partition_leader_epoch เป็น​ช่อง​ที่​สอง​ที่​เป็น​ของ broker และ​มัน​มี stamp_leader_epoch ของ​ตัว​เอง​ที่​เขียน​ทับ 4 byte ด้วย​เหตุผล​เดียวกัน demo ของ​บท​นี้​จึง​เรียก​สอง​ตัว​นี้​ต่อ​กัน เหมือน​ที่ LogValidator ของ Kafka เรียก setPartitionLeaderEpoch ต่อ​จาก setLastOffset ทันที

expected/01-record.txt (ตัด​มา​บาง​ส่วน)
== broker ประทับ offset ทับที่เดิม ==
base_offset partition_leader_epoch crc
ก่อน 0 -1 0x98867f12
หลัง 41 0 0x98867f12
crc เท่าเดิม และ decode ผ่าน · การแจก offset จึงเป็นการเขียนทับ 8 byte เฉยๆ

ก่อน​ประทับ base_offset เป็น​ศูนย์​และ leader epoch ยัง​ไม่มี​ค่า หลัง​ประทับ base_offset กลาย​เป็น offset 41 และ leader epoch กลาย​เป็น​ศูนย์ ส่วน crc เท่า​เดิม​ทุก bit

และ batch ที่​ประทับ​แล้ว​ยัง decode ผ่าน ซึ่ง​พิสูจน์​ว่า checksum ยัง​ตรง​จริง ไม่ใช่​แค่​ตัวเลข​เดิม​ค้าง​อยู่​ใน​ช่อง

นี่​คือ​เหตุผล​ที่ producer ไม่​ต้อง​รู้​เลย​ว่า record ของ​มัน​จะ​ไป​นั่ง​อยู่​ลำดับ​ไหน มัน​ส่ง batch ที่ checksum ถูก​ต้อง​มา​ให้ แล้ว broker เป็น​คน​ตัดสิน​ลำดับ​ตอน append

หัวข้อ​นี้​ประกาศ​เส้น​ขอบเขต​ของ kaen-log ไว้​ล่วง​หน้า แทนที่​จะ​ให้​ไป​เจอ​เอา​ตอน​กลาง​คอร์ส

  • ไม่มี compression เพราะ standard library ไม่มี codec ให้ Kafka บีบ​อัด​ทั้ง batch แล้ว​เก็บ​ของ​ที่​บีบ​แล้ว​ลง log ส่วน kaen-log เก็บ​ของ​ดิบ​เสมอ
  • ไม่มี TLS และ​ไม่มี​การ​ยืนยัน​ตัว​ตน ทุก​อย่าง​ที่​เข้า​มา​ถึง​ถือว่า​เชื่อ​ได้ ซึ่ง​ไม่​จริง​บน​เครือ​ข่าย​จริง
  • รูป​แบบ​ทรง Kafka แต่​ไม่​ได้​เข้า​กัน​ได้ที​ละ byte ทุก​ช่อง ลำดับ​ของ field ความ​กว้าง และ​ขอบ​ของ CRC ลอก​มา​ตรง แต่ client ของ​จริง​ยัง​คุย​กับ kaen-log ไม่​ได้
  • log ก้อน​นี้​เป็น partition เดียว​บน​เครื่อง​เดียว Kafka จริง​กระจาย topic orders ออก​เป็น​หลาย partition และ​รับ​ประกัน​ลำดับ ต่อ partition เท่านั้น

ข้อ​สุดท้าย​มี​ผล​กับ​โดเมน​ของ​คอร์ส​โดยตรง การ​ใช้ order_id เป็น key ซื้อ​ลำดับ​ต่อ​หนึ่ง​ออ​เด​อร์​มา​ได้ แต่​ไม่​ได้​ซื้อ​ลำดับ​รวม​ของ​ทั้ง topic และ​ไม่มี​บท​ไหน​ใน​คอร์ส​นี้​อ้าง​ว่า​มี

บท​ที่ 2 จะ​รับ log ก้อน​เดียว​นี้​ไป​ตัด​เป็น​ท่อน แล้ว​สร้าง​สารบัญ​ที่​จงใจ​ไม่​เก็บ​ทุก record ตรง​นั้น​คือ​จุด​ที่ offset กับ byte position จะ​ยืน​คนละ​ฝั่ง​ของ​ลูก​ศร​จริงๆ


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Apache Kafka 4.0 — Design (ตรวจ​แล้ว 2026-09-03) — ที่มา​ของ “totally ordered partitions” ของ​ประโยค​ที่​ว่า​ตำแหน่ง​ผู้​อ่าน​คือ จำนวนเต็ม​ตัว​เดียว และ​ของ​ประโยค​ที่​ยอมรับ​ว่าการ​อ่าน​ย้อน​หลัง​ผิด​สัญญา​ของ​คิว
  • Apache Kafka 4.0 — Implementation › Log (ตรวจ​แล้ว 2026-09-03) — หน้าที่​พูด​เรื่อง offset ไว้​สอง​อย่าง​ใน​หน้า​เดียวกัน ทั้ง​ประโยค​ตำแหน่ง byte และ​ประโยค​ตัว​นับ​ต่อ partition
  • Apache Kafka 4.0 — Implementation › Message Format (ตรวจ​แล้ว 2026-09-03) — ขอบ​ของ CRC เหตุผล​ที่ leader epoch อยู่​นอก​ขอบ​นั้น ประโยค​ที่​ระบุ​ว่า​ใช้ polynomial แบบ Castagnoli และ​ประโยค​ที่​ระบุ​ว่า varint เป็น​รูป​เดียว​กับ Protobuf
  • DefaultRecordBatch.java tag 4.0.0 (ตรวจ​แล้ว 2026-09-03) — ลูกโซ่​ของ constant ที่​บวก​กัน​ได้ RECORD_BATCH_OVERHEAD และ​ตัว setLastOffset ที่​เขียน​ทับ​ช่อง​เดียว​แล้ว​จบ
  • LogValidator.java tag 4.0.0 (ตรวจ​แล้ว 2026-09-03) — จุด​ที่ broker ฝั่ง Kafka เรียก setLastOffset ตอน​แจก​ลำดับ และ​เรียก setPartitionLeaderEpoch ต่อ​จาก​นั้น​ทันที
  • Protocol Buffers — Encoding (ตรวจ​แล้ว 2026-09-03) — สูตร zigzag ทั้ง​สอง​ความ​กว้าง และ​กติกา​ของ bit บอก​ว่า​ยัง​มี​ต่อ
  • CRC RevEng catalogue — CRC-32/ISCSI (ตรวจ​แล้ว 2026-09-03) — ค่า​ตรวจ 0xe3069283 ของ CRC-32C และ​ราย​ชื่อ​ชื่อ​พ้อง​ที่​รวม CRC-32/CASTAGNOLI ไว้​ด้วย
  • Rust std 1.97.1 — primitive i64 (ตรวจ​แล้ว 2026-09-03) — to_be_bytes ที่​คืน byte แบบ big-endian ให้​โดย​ไม่​ต้อง​พึ่ง crate ภายนอก ตัว method นี้ stable มา​ตั้งแต่ 1.32.0
  • Rust std 1.97.1 — struct DefaultHasher (ตรวจ​แล้ว 2026-09-03) — ประโยค “The internal algorithm is not specified, and so it and its hashes should not be relied upon over releases” ที่​ทำให้​มัน​ใช้​แทน checksum ใน​รูป​แบบ​บน disk ไม่​ได้

เช็กความเข้าใจ — บทที่ 1

ข้อ 1 / 3

producer ประกอบ batch เสร็จแล้วส่งเข้ามา broker ต้องแจกลำดับให้มัน ทำไม broker ถึงเขียนทับช่อง base_offset ได้โดยไม่ต้องคิด CRC ใหม่