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

Capstone — kaen-kvstore ครบ​วงจร และ​เส้น​แบ่ง std กับ async

เจ็ด​บท​ที่​ผ่าน​มา​เรา​ต่อ kaen-kvstore ขึ้น​ที​ละ​ชิ้น: บท1 วาง wire protocol กับ server/client TCP; บท2 ทำให้​ข้อมูล​รอด​การ​ปิด​โปรแกรม​ด้วย append-only log + hash index (model Bitcask); บท3 เปลี่ยน “append แล้ว” ให้​เป็น “durable แล้ว” ด้วย​ลำดับ write→fsync→ack; บท4 ทวง​คืน​พื้นที่​ด้วย tombstone + compaction; บท5 รับ​หลาย client พร้อม​กัน​ด้วย ThreadPool; บท6 พิสูจน์ crash-consistency ด้วย SIGKILL จริง; บท7 วัด​แล้ว​เจอ​ว่า throughput ติด​เพดาน​เพราะ fsync บท​นี้​เป็น​บท​ปิด — เรา​จะ ประกอบ​ทุก​ชิ้น​เข้า​เป็น​โปรแกรม​เดียว​ที่​รัน​จริง, ตอบ​คำถาม​ค้าง​จาก​บท7, แล้ว ตั้ง​ชื่อ​เส้น​แบ่ง ระหว่าง std กับ async ให้​ชัด ก่อน​ส่ง​ไม้​ต่อ​ให้​คอร์ส #23 (Distributed Systems)

📦 kaen-kvstore

code ลงมือ​ของ​คอร์ส​นี้​อยู่​ใน repo kaen-kvstore (code ตัวอย่าง​กำลัง​จัด​ทำ) — ตลอด 8 บท​เรา​สร้าง key-value store บน​เครือข่าย​ที่​กู้​คืน​จาก crash ได้ หนึ่ง​ตัว ด้วย Rust std ล้วน (ไม่มี async/tokio, ไม่มี serde) บท​นี้​ไม่มี​ชิ้น​ส่วน​ใหม่​ที่ load-bearing — เรา​ประกอบ protocol บท1 + ThreadPool บท5 + store (log/index/fsync/tombstone/compaction) บท2-4 เข้า​เป็น main เดียว​ที่​รัน​จริง แล้ว​ปิด​คอร์ส​ด้วย​เส้น​แบ่ง std↔async และ​การ​ส่ง​ไม้​ต่อ​ให้ #23 โปรแกรม capstone ทั้ง​ก้อน​ถูก compile และ​รัน​จริง​บน musl — output ที่​ยก​มา​ใน​บท​นี้​คือ​ผล​รัน​จริง

ก่อน​ประกอบ ขอ​ทวน​เส้น​เรื่อง​เดียว​ที่​ร้อย​ทั้ง​คอร์ส — kaen-kvstore ไม่ใช่​ของเล่น​ที่​เรา​คิด​ขึ้น​เอง แต่​เป็น storage engine จริง​ใน​ระดับ toy ทุก​ชิ้น​มีชื่อ​ใน​ตำรา (Petrov Database Internals, Kleppmann DDIA):

ชิ้น​ใน kaen-kvstoreแนวคิด storage engine จริงบท
append-only log (ทุก set/delete เขียน​ต่อ​ท้าย)WAL / commit logบท2-3
HashMap<key, offset> ใน​หน่วย​ความ​จำBitcask hash index (key อยู่​ใน RAM, value บน disk)บท2
record แบบ [key_len][val_len][key][val] (LE)on-disk record framingบท1-2
write_all → sync_data → update index → ackWAL force policy (log-before-ack)บท3
replay ตอน boot: scan log ต้น→จบ, last-write-winscrash recovery / redo from logบท3
tombstone (val_len == u32::MAX)delete markerบท4
compact(): เขียน​เฉพาะ record ที่​มี​ชีวิต​ลง file ใหม่segment merge & compactionบท4
torn-tail ผ่าน read_exactUnexpectedEofpartial-write detectionบท3, บท6
หนึ่ง fsync ต่อ writeจุด​ที่ group commit เข้า​มา​แก้บท7, บท​นี้
ThreadPool + Arc<Mutex<KvStore>>thread-per-request pool + concurrency controlบท5

`HashMap<key, offset>` และ `Arc<Mutex<KvStore>>` ใน​ตาราง​คือ​ของ​ชิ้น​เดียว​กับ​ที่​เรา เขียน code จริง มา​แล้ว — capstone แค่​เอา​มัน​มา​ต่อ​กัน

โปรแกรม capstone คือ composition ล้วนๆ: write_frame/read_frame จาก​บท1, KvStore (พร้อม open/set/get/remove/compact และ replay) จาก​บท2-4, และ ThreadPool จาก​บท5 — ทั้งหมด​อยู่​ใน​โปรแกรม​เดียว ไม่​แก้​ของ​เดิม สิ่ง​ที่ ใหม่ มี​แค่​ชั้น glue บางๆ ที่​ให้ protocol บท1 คุย​กับ store บท2-4 ผ่าน pool บท5:

type SharedStore = Arc<Mutex<KvStore>>;
fn handle_connection(stream: TcpStream, store: SharedStore) -> io::Result<()> {
let mut reader = BufReader::new(stream.try_clone()?);
let mut writer = BufWriter::new(stream);
loop {
let mut op = [0u8; 1];
match reader.read_exact(&mut op) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(()), // client วางสายสะอาด
Err(e) => return Err(e),
}
match op[0] {
OP_GET => {
let key = read_frame(&mut reader)?; // socket I/O: ยังไม่ถือ lock
let got = store.lock().unwrap().get(&key)?; // ถือ lock เฉพาะตอนแตะ store
match got {
Some(v) => { writer.write_all(&[ST_OK])?; write_frame(&mut writer, &v)?; }
None => { writer.write_all(&[ST_NOT_FOUND])?; write_frame(&mut writer, &[])?; }
}
}
OP_SET => {
let key = read_frame(&mut reader)?;
let val = read_frame(&mut reader)?;
store.lock().unwrap().set(&key, &val)?; // guard drop ที่ `;` ก่อน flush ข้างล่าง
writer.write_all(&[ST_OK])?; write_frame(&mut writer, &[])?;
}
OP_DEL => {
let key = read_frame(&mut reader)?;
let ok = store.lock().unwrap().remove(&key)?;
writer.write_all(&[if ok { ST_OK } else { ST_NOT_FOUND }])?;
write_frame(&mut writer, &[])?;
}
_ => { writer.write_all(&[ST_ERR])?; write_frame(&mut writer, b"unknown opcode")?;
writer.flush()?; return Ok(()); }
}
writer.flush()?; // สำคัญเสมอ: ไม่ flush = คำตอบค้างใน buffer
}
}

สังเกต​วินัย lock scope จาก​บท5 ที่​ยัง​ยึด​อยู่: อ่าน frame จาก socket นอก lock แล้ว​ค่อย store.lock().unwrap().set(...) เป็น expression เดียว — guard ปล่อย​ที่ ; ก่อน​เรา​จะ​ไป​เขียน​ตอบ​กลับ socket จุด​ที่​ต้อง​พูดตรงๆ: KvStore::get ต้อง seek reader จึง​เป็น &mut self — store ก้อน​นี้​จึง share ด้วย `Arc<Mutex<KvStore>>` ไม่ใช่ RwLock (ผู้​อ่าน​ขนาน​กัน​ไม่​ได้​เมื่อ get ก็ mutate ตำแหน่ง reader) และ​เพราะ set ทำ fsync ใต้ lock ทุก write จึง​ต่อ​คิว​กัน​จริง — ตรง​กับ​ตัวเลข throughput จาก​บท7 เป๊ะ นี่​คือ trade-off ที่​เรา​เลือก​อย่าง​รู้ตัว ไม่ใช่ bug

ชั้น accept ห่อ pool บท5 ไว้ตรงๆ — capstone นี้​ขับ client เดียว​เพื่อ​เดโม จึง​รับ1 connection แล้ว​ปล่อย​ให้ pool drop ทำ graceful shutdown:

fn run_server(listener: TcpListener, store: SharedStore) -> io::Result<()> {
let pool = ThreadPool::new(4);
if let Some(stream) = listener.incoming().next() {
let stream = stream?;
let store = Arc::clone(&store);
pool.execute(move || {
if let Err(e) = handle_connection(stream, store) {
eprintln!("connection error: {e}");
}
});
}
Ok(()) // pool drop ที่นี่ -> ปิด channel -> worker ทุกตัว join -> shutdown สะอาด
}

แล้ว main เดิน​เรื่อง​เป็น​สอง​องก์: องก์​ที่ 1 คือ store บน​เครือข่าย​จริง (protocol + pool + shared store) รับคำ​สั่ง SET/GET/DELETE ผ่าน loopback; องก์​ที่ 2 จำลอง “รีส​ตาร์ต process” — ปล่อย handle เดิม​ทิ้ง แล้ว KvStore::open ตัว​ใหม่​บน directory เดิม ให้​มัน replay log ที่ durable แล้ว สร้าง​สถานะ​กลับ​มา (นี่​คือ crash recovery จาก​บท3 ตรงๆ) จบ​ด้วย​การ compact() แล้ว reboot อีกรอบ​เพื่อ​ยืนยัน​ว่า compaction รอด boot:

fn main() -> io::Result<()> {
let dir = unique_dir();
// ---- องก์ที่ 1: store บนเครือข่าย (protocol บท1 + pool บท5 + shared store บท2-4) ----
let listener = TcpListener::bind("127.0.0.1:0")?;
let addr = listener.local_addr()?;
let store: SharedStore = Arc::new(Mutex::new(KvStore::open(&dir)?));
let server = thread::spawn({
let store = Arc::clone(&store);
move || run_server(listener, store)
});
let mut client = Client::connect(&addr.to_string())?;
client.set(b"lang", b"rust")?;
client.set(b"edition", b"2024")?;
client.set(b"lang", b"rust-2024")?; // overwrite -> append ใหม่ชนะ
let lang = client.get(b"lang")?;
let deleted = client.delete(b"edition")?;
let edition_after = client.get(b"edition")?;
drop(client); // วางสาย -> handler จบ -> pool join -> server thread จบ
server.join().unwrap()?;
drop(store); // ปล่อย handle องก์ที่ 1 = จำลองรีสตาร์ต process
// ---- องก์ที่ 2: crash recovery — open ใหม่ replay จาก log ที่ durable ----
let mut reopened = KvStore::open(&dir)?;
assert_eq!(reopened.get(b"lang")?.as_deref(), Some(&b"rust-2024"[..])); // overwrite ล่าสุดรอด
assert_eq!(reopened.get(b"edition")?, None); // tombstone รอด replay
assert_eq!(reopened.len(), 1);
// ---- องก์ที่ 2b: compaction ทวงคืน dead bytes แล้วรอด reboot ----
let size_before = reopened.log_size()?;
reopened.compact()?;
let size_after = reopened.log_size()?;
drop(reopened);
let mut after_compact = KvStore::open(&dir)?;
assert_eq!(after_compact.get(b"lang")?.as_deref(), Some(&b"rust-2024"[..]));
assert!(size_after < size_before);
fs::remove_dir_all(&dir).ok();
Ok(())
}

ทั้ง​ก้อน (frames + KvStore + ThreadPool + server + client + main) คือ โปรแกรม​เดียว ที่ compile และ​รัน​จริง​บน musl ได้ output นี้:

Act1 (networked): GET lang -> Some("rust-2024")
Act1 (networked): GET missing -> None
Act1 (networked): DELETE edition -> true ; GET edition -> None
Act2 (replay): reopened live keys=1 log=71 bytes ; GET lang -> Some("rust-2024") ; GET edition -> None
Act2 (compact): log 71 -> 21 bytes ; after reboot GET lang -> Some("rust-2024") ; live keys=1
kaen-kvstore capstone: all acts OK

อ่าน​ผล: องก์​ที่ 1 พิสูจน์​ว่า protocol + pool + store ต่อ​กัน​แล้ว​ทำงาน​จริง​บน​เครือข่าย (overwrite ชนะ, delete ได้ tombstone); องก์​ที่ 2 คือ crash recovery — process ใหม่​ที่ open directory เดิม replay log 71 byte แล้ว​ได้​สถานะ​กลับ​มา​ครบ (lang=rust-2024, edition หาย​เพราะ tombstone); องก์​ที่ 2b compact() บีบ log จาก 71 เหลือ 21 byte (ทิ้ง record ที่​ตาย​แล้ว) และ​สถานะ​ยัง​ถูกต้อง​หลัง reboot ทุก​อย่าง​ใน​คอร์ส​นี้​บรรจบ​ที่​โปรแกรม​ก้อน​นี้

บท7 จบ​ด้วย​คำถาม: ทำไม throughputthroughputจำนวน operation ต่อ​วินาที ตัวเลข​ที่​ชี้​ว่า​เมื่อไร​ถึง​ต้อง​มี async — จำนวน operation ต่อ​วินาที — ถึง​อยู่​แค่​หลัก​ร้อย (วัด​ได้ ~250 ops/s บน​กล่อง​นี้) ทั้ง​ที่ CPU แทบ​ว่าง คำ​ตอบ​อยู่​ใน​ลำดับ write→fsync→ack ของ​บท3: ทุก set ต้อง​รอ sync_data คืน​ค่า ก่อน​ถึง​จะ ack ได้ — และ fsync คือ​การ​รอ storage จริง ไม่ใช่​งาน CPU throughput จึง​ถูก​จำกัด​ด้วย durability ไม่ใช่ compute (durability-bound, not CPU-bound)

ทาง​แก้​ที่​อยู่​ใน​ขอบเขต​ของ storage engine คือ group commit: แทนที่​จะ fsync ทีละ write ให้​สะสม​หลาย write ที่​ค้าง​อยู่​แล้ว fsync รวด​เดียว — ค่า fsync หนึ่ง​ครั้ง​ถูก amortize ข้าม​หลาย operation ตำรา​ฐาน​ข้อมูล (Petrov ch5) บอกว่า​นี่​คือ​วิธี​ที่ DB จริง​ดัน throughput ขึ้น​โดย​ไม่​ทิ้ง durability คอร์ส​นี้​จงใจ​ไม่​ทำ group commit เพื่อ​ให้​เห็น​เพดาน​ดิบๆ ชัดๆ ก่อน — แต่ ชื่อ ของ​ทาง​แก้​อยู่​ตรง​นี้​แล้ว

เส้น​แบ่ง std กับ async — การ​ตั้ง​ชื่อ​เส้น​แบ่ง​คือ​บทเรียน

หัวข้อ​ที่​มีชื่อ​ว่า “เส้น​แบ่ง std กับ async — การ​ตั้ง​ชื่อ​เส้น​แบ่ง​คือ​บทเรียน”

ตลอด​คอร์ส concurrency ของ​เรา​คือ std::thread ล้วน — ThreadPool แบบ thread-per-request ที่ share store ผ่าน `Arc<Mutex<KvStore>>` model นี้ ถูกต้อง​และ​ง่าย: มัน​คือ final project ของ​หนังสือ The Rust Programming Language บท 21 ตรงๆ คำถาม​ที่​วิศวกร​มัก​ถาม​ผิด​จังหวะ​คือ “ทำไม​ไม่​ใช้ tokio/async ตั้งแต่​แรก” คำ​ตอบ​คือ​หลัก​คิด​ที่​เรา​ย้ำ​มา​ทั้ง​คอร์ส:

thread เป็น​ค่า​ปริยาย​ที่​ถูกต้อง จนกว่า​จำนวน connection — ไม่ใช่ CPU — จะ​กลาย​เป็น​คอ​ขวด

thread-per-connection หยุด​สเกล​ตอน​ชน​กำแพง C10k: connection ที่​ส่วน​ใหญ่ idle นับ​หมื่น​ตัว​ขึ้น​ไป​พร้อม​กัน เพราะ​แต่ละ OS thread กิน stack เป็น​หลัก MB บวก​ภาระ context switch — 10,000 thread ที่นั่ง​รอ socket เฉยๆ คือ​ความ​สิ้น​เปลือง​ที่ async เกิด​มา​แก้ (multiplex connection นับ​หมื่น​บน thread ไม่​กี่​ตัว) หนังสือ​เอง​ก็​วาง​เส้น​นี้​ไว้​ชัด: server std::net แบบ blocking อยู่ บท 21 ส่วน async/await/futures อยู่​แยก​ที่ บท 17 — เรา​เดิน​ตาม​ฝั่ง blocking ของ​เส้น​นั้น​ทั้ง​คอร์ส

จุด​ที่ std พูด​ความ​จริง​กับ​เรา​เรื่อง backpressure: std::mpsc::channel() ที่ ThreadPool ใช้​ป้อน​งาน​เป็น unbounded — ส่ง​งาน​เข้า​คิว​ได้​ไม่​จำกัด ไม่มี backpressure ถ้า producer เร็ว​กว่า worker คิว​โต​ไม่​หยุด​จน​หน่วย​ความ​จำ​หมด std มี bounded channel ตัว​เดียว​คือ sync_channel(n) ที่ block sender เมื่อ​คิว​เต็ม (นั่น​คือ backpressure แบบ synchronous) — แต่​ระบบ backpressure/scheduling เต็ม​รูปแบบ​สำหรับ connection นับ​หมื่น​คือ​โลก​ของ async runtime ที่​เรา จงใจ​ไม่​ข้าม​ไป ใน​คอร์ส​นี้ การ ตั้ง​ชื่อ เส้น​แบ่ง​นี้​ให้​ชัด​คือ​บทเรียน — ไม่ใช่​การ​ไขว่​คว้า tokio มา​ใส่​เพราะ​มัน​ดู​โมเดิร์น async เป็น​เครื่องมือ​ที่​ถูกต้อง เมื่อ​ชน​ปัญหา​ที่​มัน​แก้ เท่านั้น และ​ปัญหา​นั้น​รอ​เรา​อยู่​ที่ #23

ชิ้น​สุดท้าย​ที่​ต้อง​ตั้ง​ชื่อ​ก่อน​ส่ง​ไม้​ต่อ​คือ segmentsegmentfile log ที่​ถูก​แช่แข็ง (immutable) เป็น​สะพาน​สู่ replication ใน #23 — เมื่อ log โต​ถึง​ขนาด​หนึ่ง ระบบ​จริง (LSM engine) จะ แช่แข็ง log ที่ active ให้​กลาย​เป็น file immutable file หนึ่ง (เรียก​ว่า segment) แล้ว​เปิด log ใหม่​รับ write ต่อ; compaction ก็​ทำงาน​บน segment ที่​แช่แข็ง​แล้ว​เท่านั้น — เขียน​ไม่​ทับ อ่าน​ไม่​เปลี่ยน compact() ของ​บท4 คือ segment merge version file เดียว การ​แยก​เป็น​หลาย segment ที่ immutable คือ​ก้าว​ถัด​ไป และ​มัน​สำคัญ​เพราะ file ที่​ไม่​เปลี่ยน​คือ​สิ่ง​ที่ replication ส่ง​ข้าม​เครื่อง​ได้​ง่าย​ที่สุด

นี่​พา​เรา​มา​ถึง​ประเด็น​ปิด​คอร์ส: kaen-kvstore เป็น single-node storage engine อย่าง​จงใจ และ​ทุก primitive ที่​เรา​ต่อ​ขึ้น​มา​คือ หมุด ที่​คอร์ส #23 (Distributed Systems) จะ​เอา distribution มา​แขวน — #23 เพิ่ม​ชั้น​ทับ substrate นี้ ไม่​ได้​เขียน store ใหม่:

primitive ใน kaen-kvstore (single-node)บทบาท​ใน #23 (distributed)อ้างอิง
append-only loglog ต่อ replica ที่ replication stream ข้าม​เครื่องDDIA ch5
write→fsync→ack (durability ใน​เครื่อง)สิ่ง​ที่ quorum ack นับ — “W replica fsync แล้ว” คือ ack ก้อน​นี้​ที่​ถูก​นับDDIA ch5
tombstone / compactionanti-entropy (ปรับ replica ให้​ตรง​กัน)DDIA ch5
hash index ใน​หน่วย​ความ​จำจุด​แขวน logical clock / version vector ต่อ keyDDIA ch9
immutable segmentหน่วย​ที่ replication/merge ทำงาน​ด้วยDDIA ch3/ch5

เหตุผล​ที่​คอร์ส​นี้​พิสูจน์ durability ด้วย unit test ได้ (บท6) แต่ #23 จะ​ต้อง ถกเถียง เรื่อง consensus แทนที่​จะ​พิสูจน์​ด้วย fsync: durability เป็น​คุณสมบัติ ใน​เครื่อง​เดียว — crash แล้ว reopen เป็น​เหตุการณ์​ที่ reproduce ได้ ส่วน distributed consensus ต้องการ network partition, การ​สลับ​ลำดับ​ข้อความ, และ clock skew ที่ reproduce ใน​เครื่อง​เดียว​ไม่​ได้ นั่น​คือ​เส้น​แบ่ง​ที่แท้​จริง​ระหว่าง​สอง​คอร์ส

flowchart TB
    subgraph L23["คอร์ส #23 — Distributed Systems (ชั้นที่เพิ่มทับ)"]
        R["replication stream log ข้ามเครื่อง"]
        Q["quorum ack = นับ fsync-ack หลาย replica"]
        V["version vector / logical clock ต่อ key"]
        AE["anti-entropy บน immutable segment"]
    end
    subgraph L22["kaen-kvstore — single-node storage engine (คอร์สนี้)"]
        LOG["append-only log"]
        ACK["write to fsync to ack"]
        IDX["hash index ในหน่วยความจำ"]
        SEG["immutable segment + compaction"]
    end
    R -.แขวนบน.-> LOG
    Q -.แขวนบน.-> ACK
    V -.แขวนบน.-> IDX
    AE -.แขวนบน.-> SEG

คำ​บรรยาย​ภาพ: kaen-kvstore เป็น substrate ระดับ single-node — คอร์ส #23 วาง​ชั้น replication/quorum/clock/anti-entropy ทับ​ลง​บน primitive เดิม (log, write→fsync→ack, hash index, segment) โดย​ไม่​เขียน store ใหม่

สี่​ความ​จริง​ที่​คอร์ส​นี้​พูดตรงๆ ตั้งแต่​ต้น​จน​จบ

1. durability เป็น​กฎ​เรื่อง ลำดับ และ​เรา​พูดตรงๆ ว่า toy รับประกัน​แค่​ไหนwrite_all คืน Ok แปล​ว่า byte ถึง page cache ยัง​ไม่ ถึง disk จะ ack durable ได้​ต้อง​หลัง fsync คืน​ค่า สิ่ง​ที่ toy นี้​กัน​ได้​คือ truncation (length prefix + read_exact จับ torn tail); สิ่ง​ที่​กัน​ไม่​ได้​คือ bit-flip เงียบๆ (ต้อง per-record CRC แบบ production/Bitcask) และ fsync เอง​ก็ โกหก ได้ — Linux fsyncgate 2018, ไดรฟ์ผู้บริโภค​เลื่อน flush, macOS ต้อง F_FULLFSYNC (ซึ่ง sync_all ของ Rust สั่ง​ให้)

2. compiler พิสูจน์​ความ​ปลอดภัย ใน​เครื่อง (memory/thread) ไม่ใช่​ความ​ถูกต้อง​ตอน crash/distributed — borrow checker ไม่​ช่วย​เรื่อง​การ​ถือ guard คร่อม I/O (บท5) และ​ไม่​ช่วย​เรื่อง crash-consistency ที่​ต้อง​พิสูจน์​ด้วย test (บท6 SIGKILL จริง) ส่วน distributed correctness reproduce ใน​เครื่อง​เดียว​ไม่​ได้ — จึง​เป็น​เรื่อง​ของ #23

3. std-first เป็น scaffold การ​สอน production ที่ concurrency สูง​ต้องการ async — เลื่อน​ไป #23 และ​เรา​บอกตรงๆ — thread ถูก​จนกว่า​จำนวน connection ไม่ใช่ CPU จะ​เป็น​คอ​ขวด (C10k) เรา​ตั้ง​ชื่อ​เส้น​แบ่ง​ไว้ ไม่​ข้าม​ไป tokio

4. log-structured storage คือ design จริง​เบื้องหลัง Bitcask/LSM/ฐาน​ข้อมูล​จริง ไม่ใช่​ของเล่น​ที่​เรา​คิด​เอง — ทุก​ชิ้น​มีชื่อ​ใน​ตำรา Petrov/Kleppmann คอร์ส​นี้​สอน storage engine จริง​ใน​ระดับ toy โดย​อ้าง​ต้นทาง​ทุก​ข้อ

อยาก​ต่อยอด​เอง? สาม​หมุด​ที่​วาง​ไว้​ให้

capstone นี้​เป็น​จุด​เริ่ม ไม่ใช่​จุดจบ — สาม​ทิศ​ที่​ต่อยอด​ได้​ทันที​ด้วย std ล้วน: (1) hint/snapshot file เก็บ index ลง file แยก​เพื่อ boot เร็ว​โดย​ไม่​ต้อง replay ทั้ง log; (2) นโยบาย trigger ของ compaction — เรียก compact() อัตโนมัติ​เมื่อ​สัดส่วน dead/total เกิน​เกณฑ์; (3) per-record CRC อัปเกรด​จาก truncation-tolerant เป็น corruption-tolerant (จับ bit-flip เงียบ) ทั้ง​สาม​คือ​ก้าว​จาก toy เข้า​ใกล้ production โดย​ไม่​ต้อง​ออก​จาก​โลก std

kaen-kvstore เดิน​ครบ​วงจร​แล้ว: จาก wire protocol บน​สาย TCP (บท1) สู่ persistence แบบ Bitcask (บท2), durability ที่​พิสูจน์​ได้ (บท3, บท6), การ​เก็บกวาด​พื้นที่​โดย​ไม่​เสีย​ความ​ถูกต้อง (บท4), การ​รับ​หลาย client (บท5), และ​การ​วัด​ที่​เผย​เพดาน fsync (บท7) — บท​นี้​ประกอบ​ทุก​ชิ้น​เป็น​โปรแกรม​เดียว​ที่​รัน​จริง แล้ว​ตั้ง​ชื่อ​สอง​เส้น​แบ่ง​สุดท้าย: throughput ที่ group commit จะ​ดัน​ต่อ และ เส้น std↔async ที่ async จะ​คุ้ม​ก็​ต่อ​เมื่อ​ชน C10k เรา​ปิด​ด้วย segment — สะพาน​สู่ #23 ที่​จะ​วาง replication/quorum/clock ทับ substrate นี้ ไม่​เขียน​ใหม่ ทุก snippet ใน​คอร์ส​นี้ compile และ​รัน​ได้​จริง​บน Rust 1.97.1 / edition 2024 / std ล้วน — พบ​กัน​ที่ Distributed Systems


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

บท​นี้​อิง​ต้นทาง​ที่​ลง​วัน​ที่​กำกับ อ่าน​ต่อ​ได้​โดยตรง:

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

ข้อ 1 / 3

หลักคิดเรื่องเส้นแบ่ง std กับ async ที่คอร์สนี้ยึดคืออะไร?