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)
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 ที่ยกมาในบทนี้คือผลรันจริง
ทวนแผนที่: ทุกชิ้น map กับแนวคิดจริงของ storage engine
หัวข้อที่มีชื่อว่า “ทวนแผนที่: ทุกชิ้น map กับแนวคิดจริงของ storage engine”ก่อนประกอบ ขอทวนเส้นเรื่องเดียวที่ร้อยทั้งคอร์ส — 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 → ack | WAL force policy (log-before-ack) | บท3 |
| replay ตอน boot: scan log ต้น→จบ, last-write-wins | crash recovery / redo from log | บท3 |
tombstone (val_len == u32::MAX) | delete marker | บท4 |
compact(): เขียนเฉพาะ record ที่มีชีวิตลง file ใหม่ | segment merge & compaction | บท4 |
torn-tail ผ่าน read_exact → UnexpectedEof | partial-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 แค่เอามันมาต่อกัน
ประกอบ kaen-kvstore ทั้งตัว
หัวข้อที่มีชื่อว่า “ประกอบ kaen-kvstore ทั้งตัว”โปรแกรม 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 -> NoneAct1 (networked): DELETE edition -> true ; GET edition -> NoneAct2 (replay): reopened live keys=1 log=71 bytes ; GET lang -> Some("rust-2024") ; GET edition -> NoneAct2 (compact): log 71 -> 21 bytes ; after reboot GET lang -> Some("rust-2024") ; live keys=1kaen-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: throughput ติดเพดานเพราะอะไร
หัวข้อที่มีชื่อว่า “ตอบคำถามค้างจากบท7: throughput ติดเพดานเพราะอะไร”บท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
Segment: สะพานสู่ Distributed Systems #23
หัวข้อที่มีชื่อว่า “Segment: สะพานสู่ Distributed Systems #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 log | log ต่อ replica ที่ replication stream ข้ามเครื่อง | DDIA ch5 |
write→fsync→ack (durability ในเครื่อง) | สิ่งที่ quorum ack นับ — “W replica fsync แล้ว” คือ ack ก้อนนี้ที่ถูกนับ | DDIA ch5 |
| tombstone / compaction | anti-entropy (ปรับ replica ให้ตรงกัน) | DDIA ch5 |
| hash index ในหน่วยความจำ | จุดแขวน logical clock / version vector ต่อ key | DDIA 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 ใหม่
honesty spine: สรุปความจริงทั้งคอร์ส
หัวข้อที่มีชื่อว่า “honesty spine: สรุปความจริงทั้งคอร์ส”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
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- Designing Data-Intensive Applications — ch5 “Replication” (Kleppmann, 2017/2026; เข้าถึง 2026-07-24) — replication streams the log, quorum acknowledgement นับ local durability ที่แต่ละ replica; substrate ที่ #23 ต่อยอด
- Designing Data-Intensive Applications — ch9 “Consistency and Consensus” (Kleppmann, 2017/2026; เข้าถึง 2026-07-24) — logical clock / version vector และเหตุที่ consensus correctness reproduce ในเครื่องเดียวไม่ได้
- Designing Data-Intensive Applications — ch3 “Storage and Retrieval” (Kleppmann, 2017/2026; เข้าถึง 2026-07-24) — segment ที่ immutable เป็นหน่วยที่ compaction/replication ทำงานด้วย
- Database Internals — ch5 “Transaction Processing and Recovery” (Petrov, 2019; เข้าถึง 2026-07-24) — fsync เป็นต้นทุนหลักของ durable write, group commit amortize มันข้ามหลาย operation
- The Rust Programming Language — ch21 “Final Project: Building a Multithreaded Web Server” (เข้าถึง 2026-07-24) — thread-per-connection +
ThreadPoolฝั่ง blocking ของเส้นแบ่ง (เคยเป็นบท 20 — อ้างบท 21 เท่านั้น) - The Rust Programming Language — ch17 “Fundamentals of Asynchronous Programming” (เข้าถึง 2026-07-24) — บท async ที่แยกต่างหาก คือฝั่ง C10k ของเส้นแบ่งที่เลื่อนไป #23
- std
sync::mpsc(เข้าถึง 2026-07-24) —channel()unbounded (ไม่มี backpressure) vssync_channel(n)bounded (block sender)
เช็กความเข้าใจ — บทที่ 8
ข้อ 1 / 3หลักคิดเรื่องเส้นแบ่ง std กับ async ที่คอร์สนี้ยึดคืออะไร?