ทำไม io_uring ถึงช้ากว่า syscall ถ้าไม่มี readahead — เจาะลึกจากงานจริงใน Turso
io_uring Without Readahead
บทความวันที่ 31 สิงหาคม 2026 โดย Fernando Simões เจาะลึกคำถามที่ดูเรียบง่ายแต่ผลลัพธ์พลิกความคาดหมาย คือทำไมการทำ readahead ที่ระดับแอปพลิเคชันจึงช่วยให้ io_uring ในฐานข้อมูล Turso เร็วขึ้นอย่างมีนัยสำคัญ โดยเขาวัดผลกระทบต่อ I/O concurrency การรวมคำขอ (request merging) ต้นทุน CPU polling และพฤติกรรมของแคช
หัวใจของการค้นพบคือ io_uring ที่ใช้ร่วมกับ O_DIRECT ซึ่งปิด readahead ของเคอร์เนล จะส่งคำสั่งอ่านทีละหนึ่งคำขอ ทำให้ block layer ของเคอร์เนลไม่มีโอกาสรวมคำขอที่อยู่ติดกันเข้าด้วยกัน แต่พอทำ readahead เอง เช่นสั่งอ่าน page 100 ถึง 131 พร้อมกัน เคอร์เนลจึงรวมได้เป็น I/O ก้อนใหญ่
ตัวเลขจากเบนช์มาร์ก TPC-H Q6 บนฐานข้อมูลขนาด 1.2 GiB ชัดเจนมาก จำนวน SQE ที่ส่งอยู่ที่ 195,207 (ไม่มี readahead) เทียบกับ 218,212 (มี readahead) ซึ่งใกล้เคียงกัน แต่จำนวนคำขอที่ลงไปถึงอุปกรณ์จริงต่างกันมหาศาลคือราว 196,000 เทียบกับราว 16,300 ขนาดคำขอเฉลี่ยขยับจาก 4.37 KiB เป็น 56.53 KiB และอัตราการรวมคำขอไปจากราว 0% เป็น 91-93%
กรณีที่แย่ที่สุดคือเมื่อปิด sqpoll ด้วย io_uring ใช้เวลา 8.55 วินาที ขณะที่ syscall แบบธรรมดาใช้เพียง 3.02 วินาที และ io_uring ยังเกิด cache miss เพิ่มขึ้นอีก 4.8 ล้านครั้ง
ข้อสรุปสำหรับนักพัฒนาคือ io_uring ที่ส่งทีละ SQE ให้ผลแย่ที่สุดเพราะรวมคำขอไม่ได้ สำหรับ sqpoll ใช้ได้ดีเมื่อมี vCPU หลายตัวแต่เสี่ยงแย่งทรัพยากรบนระบบที่งานหนัก ส่วน io_uring แบบ batch ธรรมดานั้นใช้ได้จริง แต่ต้องทำ batching ที่ระดับแอปเองเพื่อรีดประสิทธิภาพ และ O_DIRECT คือการแลกแคช CPU ที่ยังอุ่นจากการ copy ของเคอร์เนล ไปกับ cache miss ที่จะมาเก็บบิลตอนรันคิวรีแทน