เซิร์ฟเวอร์ HTTPS แบบศูนย์ system call ด้วย io_uring, kTLS และ Rust
(blog.habets.se)- เดิมทีมีการใช้โมเดลแบบอิงอีเวนต์หลากหลายรูปแบบ เช่น select(), poll(), epoll เพื่อสร้าง เว็บเซิร์ฟเวอร์ประสิทธิภาพสูง
- แต่ด้วยข้อจำกัดด้านประสิทธิภาพของ system call เหล่านี้ จึงเกิด io_uring ขึ้นมา โดยนำแนวทางให้เคอร์เนลประมวลผลแบบอะซิงโครนัสผ่านการใส่คำขอลงในคิว
- kTLS ให้เคอร์เนลรับหน้าที่ประมวลผลการเข้ารหัส TLS ทำให้สามารถทำ optimization เพิ่มเติมได้ เช่น ความสามารถในการใช้ sendfile() และการ offload ไปยังฮาร์ดแวร์
- การมาของ Descriptorless files มอบแนวทางการเข้าถึงที่เหมาะกับ io_uring โดยไม่ต้องส่ง file descriptor โดยตรง
- ผ่าน โปรเจ็กต์โอเพนซอร์ส tarweb ที่ผสาน Rust, io_uring และ kTLS เข้าด้วยกัน ทำให้ให้บริการ HTTPS ได้โดยไม่ต้องมี system call เพิ่มต่อคำขอ และยังมีการพูดถึงประเด็นเรื่องความปลอดภัยและการจัดการหน่วยความจำ
วิวัฒนาการของสถาปัตยกรรมเว็บเซิร์ฟเวอร์ประสิทธิภาพสูง
- ตั้งแต่ช่วงต้นทศวรรษ 2000 ความต้องการเว็บเซิร์ฟเวอร์ที่รองรับปริมาณงานสูงเพิ่มขึ้น
- ในระยะแรก แนวทางที่สร้างโปรเซสใหม่สำหรับแต่ละคำขอเป็นเรื่องปกติ แต่เพราะมีต้นทุนสูงจึงเกิดเทคนิค preforking ขึ้นมา
- หลังจากนั้นก็พัฒนาไปสู่การใช้ เธรด และการใช้งาน select(), poll() เพื่อลดต้นทุนจากการสลับคอนเท็กซ์
- อย่างไรก็ตาม วิธีแบบ select() และ poll() ก็ยังมีข้อจำกัดด้านการขยายระบบ เพราะเมื่อจำนวนการเชื่อมต่อเพิ่มขึ้น จะต้องส่งอาร์เรย์ขนาดใหญ่ให้เคอร์เนลบ่อยครั้ง
การมาของ epoll
- บน Linux มีการนำ epoll มาใช้ ทำให้จัดการการเชื่อมต่อจำนวนมากได้มีประสิทธิภาพกว่าวิธีเดิม
- epoll ประมวลผลเฉพาะส่วนที่เปลี่ยนแปลง (delta) จึงลดการใช้ทรัพยากรที่ไม่จำเป็น
- แม้จะไม่ได้กำจัด system call ออกไปทั้งหมด แต่ก็ลดต้นทุนลงได้มาก
ภาพรวมของ io_uring
- io_uring ไม่ได้เรียก system call สำหรับทุกคำขอ แต่จะเพิ่มคำขอลงในคิวบนหน่วยความจำเพื่อให้เคอร์เนลประมวลผลแบบอะซิงโครนัสได้
- ตัวอย่างเช่น หากใส่ accept() ลงในคิว เคอร์เนลจะประมวลผลและส่งผลลัพธ์กลับมาใน completion queue
- เว็บเซิร์ฟเวอร์ทำงานโดยเพิ่มคำขอลงในคิว และตรวจสอบผลลัพธ์จากพื้นที่หน่วยความจำอีกส่วนหนึ่ง
- เพื่อหลีกเลี่ยง busy loop หากไม่มีความเปลี่ยนแปลงในคิว ทั้งเว็บเซิร์ฟเวอร์และเคอร์เนลจะเรียก system call เฉพาะเมื่อจำเป็น ทำให้ประหยัดพลังงานได้
- หากใช้ไลบรารีที่เหมาะสม เซิร์ฟเวอร์ที่กำลังทำงานอยู่สามารถประมวลผลคำขอได้โดยไม่ต้องมี system call เพิ่มเติม
สภาพแวดล้อมแบบหลายคอร์และ NUMA
- เมื่อพิจารณาสภาพแวดล้อม CPU แบบหลายคอร์สมัยใหม่ กลยุทธ์ที่มีประสิทธิภาพคือรัน หนึ่งเธรดต่อหนึ่งคอร์ และลดการแชร์โครงสร้างข้อมูลให้น้อยที่สุด
- ในสภาพแวดล้อม NUMA สามารถปรับแต่งให้แต่ละเธรดเข้าถึงเฉพาะหน่วยความจำของ local node ของตนเองได้
- การกระจายคำขอให้สมดุลอย่างสมบูรณ์ยังต้องมีการศึกษาเพิ่มเติม
การจัดสรรหน่วยความจำ
- ทั้งฝั่งเคอร์เนลและเว็บเซิร์ฟเวอร์ยังคงมีการจัดสรรหน่วยความจำอยู่ และการจัดสรรใน user space เองก็สุดท้ายเชื่อมโยงไปถึง system call
- ฝั่งเว็บเซิร์ฟเวอร์มีการจัดสรรบล็อกหน่วยความจำขนาดคงที่ล่วงหน้าต่อหนึ่งการเชื่อมต่อ เพื่อป้องกันปัญหาการแตกกระจายและหน่วยความจำไม่พอ
- ฝั่งเคอร์เนลเองก็ต้องมีบัฟเฟอร์ I/O ต่อการเชื่อมต่อ และสามารถปรับแต่งได้บางส่วนผ่านตัวเลือกของซ็อกเก็ต
- หากเกิดภาวะหน่วยความจำไม่เพียงพอ อาจนำไปสู่ความขัดข้องร้ายแรงได้
แนะนำ kTLS (Kernel TLS)
- kTLS เป็นความสามารถที่ให้ Linux kernel รับผิดชอบงานเข้ารหัสและถอดรหัส
- ขั้นตอนแฮนด์เชคยังคงจัดการในแอปพลิเคชัน แต่หลังจากนั้นเคอร์เนลจะจัดการการส่งข้อมูลเสมือนเป็นข้อความล้วน
- ทำให้สามารถใช้ sendfile() ได้ และลดการคัดลอกหน่วยความจำระหว่าง user space กับ kernel space
- หากการ์ดเครือข่ายรองรับ ก็ยังมีข้อดีที่สามารถ offload งานเข้ารหัสไปยังฮาร์ดแวร์ได้
Descriptorless Files
- เป็นแนวทางที่เกิดขึ้นเพื่อลด overhead จากการส่ง file descriptor โดยตรงจาก user space ไปยัง kernel space
- ใช้ register_files เพื่อให้ io_uring ใช้หมายเลขไฟล์แบบ 'จำนวนเต็ม' แยกต่างหากซึ่งมีผลใช้ได้เฉพาะใน io_uring และจะไม่แสดงใน /proc/pid/fd
- อย่างไรก็ตาม ข้อจำกัด ulimit ของระบบยังคงมีผลอยู่
แนะนำโปรเจ็กต์ tarweb
- tarweb คือโปรเจ็กต์เว็บเซิร์ฟเวอร์โอเพนซอร์สตัวอย่างที่นำเทคนิคทั้งหมดข้างต้นมาใช้
- มีโครงสร้างที่ให้บริการเนื้อหาจากไฟล์ tar เพียงไฟล์เดียว โดยผสาน Rust, io_uring, kTLS และเทคโนโลยีประสิทธิภาพสูงสมัยใหม่เข้าด้วยกัน
- ระหว่างการใช้งานจริง พบปัญหาความเข้ากันได้ระหว่าง io_uring กับ kTLS (เช่น ไม่รองรับ setsockopt) จึงมีการแก้บางประเด็นผ่าน Pull Request
- โปรเจ็กต์ยังอยู่ในขั้นไม่สมบูรณ์ และไลบรารี rustls ของ Rust อาจมีการจัดสรรหน่วยความจำระหว่างขั้นตอนแฮนด์เชค
- ประเด็นสำคัญคือ สามารถให้บริการ HTTPS ได้โดยไม่มี system call เพิ่มต่อแต่ละคำขอ
เบนช์มาร์กและการวัดประสิทธิภาพ
- ผู้เขียนยังไม่ได้ทำเบนช์มาร์กอย่างเพียงพอ และมีแผนจะทดสอบประสิทธิภาพหลังจากปรับโค้ดให้เรียบร้อย
ประเด็นด้านความปลอดภัยของ io_uring และ Rust
- ต่างจาก system call แบบซิงโครนัส ใน io_uring บัฟเฟอร์หน่วยความจำจะต้องไม่ถูกปล่อยก่อนที่อีเวนต์เสร็จสิ้นจะมาถึง
- crate ของ io-uring ไม่ได้รับประกันความปลอดภัยระดับคอมไพล์ไทม์แบบที่ Rust ควรให้ และการตรวจสอบตอนรันไทม์ก็ยังไม่เพียงพอ
- หากใช้งานผิดพลาด อาจนำไปสู่ปัญหาร้ายแรงคล้ายกับ C++ ได้ ทำให้ความปลอดภัยโดยธรรมชาติของ Rust อ่อนลง
- จึงจำเป็นต้องมี crate safer-ring แยกต่างหากที่ใช้ pinning และ borrow checker อย่างจริงจัง
- ประเด็นนี้กำลังมีการพูดคุยกันอยู่ในคอมมูนิตี้แล้ว
อ้างอิงและลิงก์เพิ่มเติม
- เนื้อหานี้เป็นโพสต์ที่ถูกพูดถึงบน HackerNews ณ วันที่ 2025-08-22
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
เวลาใช้ io_uring เพื่อส่งงานเขียน ต้องมั่นใจว่าตำแหน่งหน่วยความจำจะไม่ถูกคืนหรือถูกเขียนทับ แต่ดูเหมือนว่า API ของ crate io-uring จะไม่ได้ให้ Rust borrow checker ช่วยในจุดนี้ และก็ไม่มีการตรวจสอบตอนรันไทม์ด้วย
เคยอ่านทั้งบทความและคอมเมนต์ที่พูดถึงสถานการณ์แบบนี้แล้ว สรุปได้ความว่าการทำไลบรารี async ของ Rust ที่ครอบ io_uring แบบปลอดภัยนั้นยากมากจริงๆ
จำได้ด้วยว่า Alice จากทีม tokio เพิ่งพูดไม่นานมานี้ว่าไม่ได้สนใจผลักดันให้ข้ามปัญหานี้มากนัก
เพราะตอนนี้ประสิทธิภาพอยู่ในระดับที่ "ดีพอแล้ว"
อ้างอิง: https://boats.gitlab.io/blog/post/io-uring/
มีหลายอย่างใน Rust async ที่น่าเสียดาย และนี่ก็เป็นหนึ่งในนั้น
Rust async ถูกออกแบบมาในยุคที่ epoll เป็นมาตรฐาน และแทบไม่ได้ใส่ใจกับ IOCP เลย
ที่ synchronous syscall ไม่มีปัญหาแบบนี้ก็เพราะตอนเรียก read เราส่ง mutable reference ของบัฟเฟอร์ให้เคอร์เนล ซึ่งเข้ากับโมเดล ownership/borrow แบบ native ของ Rust ได้ดี
แต่ I/O แบบ completion-based ถ้าจะให้เข้ากับโมเดล ownership จริงๆ ต้องรับประกันได้ว่าโค้ดฝั่งผู้ใช้จะไม่รันต่อจนกว่างานจะเสร็จ ซึ่งทำไม่ได้ด้วยโครงสร้าง state machine polling
โมเดลแบบ threading หรือ green thread เหมาะกับกรณีนี้มากกว่า
ถ้า Rust เพิ่ม "เป้าหมายเฉพาะสำหรับ async" เข้ามา ก็น่าจะดีกว่านี้
ทีมพัฒนา Rust เคยคาดหวังกับโมเดล async แบบ stackless polling มากพอสมควร และตอนนี้ก็กำลังรอดูบทสรุปของมันอยู่
คิดว่ามีโมเดล ownership แบบหนึ่งที่ Rust borrow checker ยังรองรับได้ไม่ดีพอ
ขอเรียกชั่วคราวว่า "hot potato ownership" คือส่งบัฟเฟอร์ออกไปชั่วคราวแล้วค่อยรับกลับมา
พอจะเขียนแพตเทิร์นแบบนี้ให้ปลอดภัยใน Rust มันยากมากและโค้ดก็ยุ่งเหยิงพอสมควร
ต่างจากที่ Alice จากทีม tokio พูด ฝั่ง file IO ยังมีความสนใจอยู่
file IO ตอนนี้ใช้วิธี spawn_blocking อยู่แล้ว จึงเจอปัญหาบัฟเฟอร์แบบเดียวกับ io_uring อยู่แล้ว และการย้ายไป io_uring ก็ไม่ได้ยากมาก
แต่ API เดิมของ tokio::net เข้ากันไม่ได้กับ buffer API ที่อิง io_uring ดังนั้นแม้จะทำ readiness check ได้ แต่ก็ยากที่จะรองรับได้ครบถ้วน
ถ้าจะทำอินเทอร์เฟซ io_uring แบบปลอดภัย วิธีที่เหมาะที่สุดน่าจะเป็นให้รับบัฟเฟอร์ที่ ring เป็นเจ้าของมาใช้ แล้วค่อยคืนกลับไปตอนเริ่มเขียน
ไม่จำเป็นต้องแทนทุกอย่างด้วย borrows เสมอไป
ใช้โครงสร้างข้อมูลอย่าง Slab ก็ทำให้ cancel safe ได้
อ้างอิง: https://github.com/steelcake/io2
อ่านบทความนี้สนุกมาก
รอผลทดสอบประสิทธิภาพอยู่ แต่ประทับใจที่ผู้เขียนบอกว่าจะจัดโค้ดให้สะอาดก่อนค่อยทำเบนช์มาร์ก
ในยุคที่ทุกอย่างเน้นแต่ตัวเลขเบนช์มาร์ก การได้เห็นคนคิดแบบนี้รู้สึกสดใหม่มาก
ตอนอายุราว 11 ขวบเคยพยายามทำฐานข้อมูลแล้วได้เจอ cgi-bin และเพิ่งมารู้ตอนนี้เองว่ามันเป็นวิธีที่สร้างโปรเซสใหม่ทุกครั้งต่อหนึ่งคำขอ
sendfile เคยเป็นตัวเปลี่ยนเกมตอนฟอรัมเกมขนาดใหญ่ต้องแจกเดโมให้คนดาวน์โหลดพร้อมกัน และพอเห็นผลลัพธ์อย่างกรณี Netflix ลดได้ 40ms หรือ GTA 5 ลดเวลาโหลดลง 70% ก็ยิ่งรู้สึกว่ายังมีงานวิศวกรรมที่ทรงอิมแพ็กต์ซ่อนอยู่อีกมาก
ลิงก์ที่เกี่ยวข้อง: Common Gateway Interface, กรณี 40ms ของ Netflix, ลดเวลาโหลด GTA Online
ไม่ใช่แค่ CGI เท่านั้น แต่ HTTP session ของสาย CERN และ Apache ในยุคก่อนก็ทำงานด้วยการ fork ทั้งเซิร์ฟเวอร์ด้วย
ต่อมาก็ค่อยๆ ดีขึ้น แต่เพราะรูปแบบการคอนฟิกของ Apache ทำให้เซิร์ฟเวอร์เบาๆ ที่ถูกออกแบบมาเป็น event-based I/O ตั้งแต่ต้นอย่าง nginx ได้รับความนิยมอย่างมาก
ยังสงสัยในประสิทธิภาพของ sendfile
แม้มันจะดังมากช่วงปลายยุค 90 แต่คิดว่าผลด้านประสิทธิภาพจริงๆ ค่อนข้างน้อย
orchestrator ของคลาวด์เวิร์กโหลดส่วนใหญ่ (CloudRun, GKE, EKS, Docker บนเครื่องตัวเอง ฯลฯ) ปิด io_uring ไว้เป็นค่าเริ่มต้น
ถ้าจุดนี้ยังไม่ดีขึ้น io_uring ก็น่าจะยังเป็นเทคโนโลยีเฉพาะทางที่ใช้งานได้จำกัดไปอีกพักใหญ่
เลยอดสงสัยไม่ได้ว่าทำไมพวกเขาถึงปิด io_uring ไว้
ถ้าเป็นแบบนี้ก็คงต้องกลับไป self-hosting อีกครั้ง
อ่านแล้วสนุกมากจริงๆ
จะรอเบนช์มาร์ก จะค่อยๆ ทำก็ได้ และทัศนคติของผู้เขียนที่ให้ความสำคัญกับการจัดระเบียบโค้ดก่อนเบนช์มาร์กนั้นน่าประทับใจมาก
ช่วงนี้มีหลายโปรเจกต์ที่ทุ่มสุดตัวกับคะแนนเบนช์มาร์ก แนวคิดแบบนี้จึงทั้งสดใหม่และน่านับถือจริงๆ
ไม่เคยรู้เลยว่า ktls หรือ io_uring จะถูกนำไปใช้ได้หลากหลายขนาดนี้
สถานการณ์ของ async ในตอนนี้ประมาณนี้
Rust: ต้องเข้าใจ Futures, Pin, Waker, async runtime, ข้อกำหนด Send/Sync, async trait object และแนวคิดอีกหลายอย่าง
C++20: coroutines
Go: goroutines
Java21+: virtual threads
C++ coroutine ใช้ heap allocation เพื่อหลีกเลี่ยงปัญหาที่ Pin เข้ามาแก้
นี่ถือว่าออกห่างจากหลักการ "zero overhead" ที่ C++ ยึดถืออยู่มาก
เหตุผลที่ Rust ใช้เวลานานมากกว่าจะเพิ่ม async trait เข้ามาในภายหลังก็เพราะ Rust ไม่ heap-allocate futures
trade-off ระหว่างประสิทธิภาพ/การพกพา กับความซับซ้อนนั้นมีคุณค่าต่างกันไปในแต่ละโปรเจกต์
ข้อจำกัดเกี่ยวกับ Send/Sync ยังมีความหมายในภาษาอื่นๆ เช่นกัน และถ้าไม่มีข้อจำกัดนี้ก็จะเขียนโค้ดที่ผิดแบบแนบเนียนได้ง่ายขึ้น
ถ้าคุณเขียน Rust ในระดับที่ "ดีพอใช้" และใช้ mid-level primitive ที่คนอื่นทำไว้แล้ว ก็ไม่ได้จำเป็นต้องเข้าใจทุกแนวคิดพวกนั้นทั้งหมด
Rust บังคับว่าถ้าไม่เข้าใจแนวคิดพวกนี้ก็จะคอมไพล์ไม่ผ่านตั้งแต่ต้น
ส่วน Go นั้น goroutine ไม่ใช่ async และถ้าไม่เข้าใจ channel ก็ไม่อาจเข้าใจ goroutine ได้
การ implement channel ของ Go มีความเฉพาะตัว ทำให้พฤติกรรมในกรณีขอบเขตคาดเดาตามสัญชาตญาณได้ยาก
Go ยังเขียนโค้ดได้แม้จะไม่เข้าใจเชิงลึก ซึ่งก็มีทั้งข้อดีและข้อเสีย
"เธรดราคาถูก" ไม่ได้เท่ากับ async
tarweb (เซิร์ฟเวอร์ในบล็อก) เป็นโครงสร้าง single-thread บน event loop ที่อิง io_uring โดยมีแนวคิดให้หนึ่งเธรดต่อหนึ่งคอร์ CPU
แทนที่จะเรียกว่า "สภาพปัจจุบันของ concurrency ขนาดใหญ่" อาจเรียกว่า "สภาพปัจจุบันของ cheap thread" จะตรงกว่า
ความต่างใหญ่ที่สุดระหว่าง cheap thread กับ async loop คือมันให้เหตุผลกับโค้ดได้ง่ายกว่า
แน่นอนว่าก็มีข้อเสีย เพราะแต่ละเธรดแม้จะเบาแต่ก็ยังต้องใช้ขนาดสแตก
kTLS ถือเป็นความก้าวหน้าชัดเจน
เมื่อหลายปีก่อนผมก็เคยทำเซิร์ฟเวอร์ที่มี syscall ต่อ request เป็นศูนย์จริงๆ แล้วเขียนบล็อกไว้ (https://wjwh.eu/posts/2021-10-01-no-syscall-server-iouring.html)
แต่ก็มีข้อเสียคือต้อง busy-looping ตลอดเวลา
io_uring พัฒนาได้เร็วอย่างน่าประทับใจมากในช่วงไม่กี่ปีที่ผ่านมา
โปรเจกต์นี้เจ๋งมาก และเพราะคิดเรื่องคล้ายๆ กันมานานแล้ว เลยดีใจที่มีคนลงมือทำจริง
ถ้าจะเขียน BPF ด้วย rust ก็แนะนำ Aya
Github ของโปรเจกต์ Aya
สงสัยเหมือนกันว่าสถานะปัจจุบันของ kTLS เป็นอย่างไร
ไม่นานมานี้ผมถามนักพัฒนา Cilium คนหนึ่ง เขาบอกว่า Thomas Graf ก็คาดหวังกับมันอยู่ แต่ในทางปฏิบัติยังมีดิสโทรลินุกซ์จำนวนมากที่รองรับในเคอร์เนลไม่พอ จึงยังอีกไกลกว่าจะเปิดใช้เป็นค่าเริ่มต้นได้
แม้จะน่าเสียดาย แต่ก็สงสัยเหมือนกันว่าการเปิดใช้นั้นยากแค่ไหน
ต้องคอมไพล์เคอร์เนลเองหรือเปล่า หรือเปิดได้จากรันไทม์เลย
FreeBSD มี kTLS ในเคอร์เนล/openssl ตั้งแต่เวอร์ชัน 13 และสามารถสลับเปิดปิดตอนรันไทม์ได้ด้วย sysctl (
kern.ipc.tls.enable=1)ใน FreeBSD-15 ค่าปริยายจะเปลี่ยนเป็นเปิดใช้งาน และที่ Netflix ก็ใช้ kTLS เข้ารหัสทราฟฟิกมานานเกือบ 10 ปีแล้ว
โดยรวมแล้ว kTLS ให้ความรู้สึกเหมือนเป็นไอเดียที่ไม่ค่อยดีนัก
สงสัยว่าโครงสร้างหนึ่งเธรดต่อหนึ่งคอร์จะเหมาะกับระบบที่ใช้ time slice จริงหรือไม่
จากประสบการณ์ของผม แนวทาง "oversubscribing" (มีเธรดมากกว่าจำนวนคอร์) ให้ผลดีในเวลาแบบ wall-clock จริง
ถ้าไม่มี preemptive scheduling หรือเป็นหนึ่งคอร์ต่อหนึ่งเธรดก็น่าจะเหมาะกว่า
แน่นอนว่าถ้าแบบนั้นก็คงไม่ใช่เรื่องของ Unix แล้ว
ถ้าต้องการ latency ต่ำและ throughput สูง การแยกคอร์แล้ว pin เธรดไว้กับคอร์เป็นวิธีที่ได้ผล
วิธีนี้ใช้ได้ดีบน Linux และนิยมในระบบเทรดดิ้ง แม้จะต้องยอมรับความไม่มีประสิทธิภาพบางส่วน
คอร์ส่วนใหญ่แทบจะว่างและหมุนรออยู่ ทั้งที่จริงๆ ไม่มีงาน แต่ในแง่ latency และ throughput ถือว่าดีที่สุด
กับโครงสร้าง thread-per-core จุดอันตรายคือการคิดว่า "หยิบมาใช้เฉพาะส่วนที่สะดวกก็พอ"
จริงๆ แล้วมันแทบเป็นทางเลือกแบบเอาทั้งหมดหรือไม่ใช้เลย
การทำแบบครึ่งๆ กลางๆ มักไม่มีประสิทธิภาพเลย
แต่ถ้าออกแบบถูกต้อง มันมีประสิทธิภาพสูงมากในแทบทุกสถานการณ์
อย่างไรก็ตาม นักพัฒนาที่เข้าใจเทคนิคการออกแบบ TPC (เช่น load balancing ระหว่างคอร์) อย่างลึกซึ้งนั้นมีน้อย
thread-per-core จะมีประสิทธิภาพก็ต่อเมื่อเป็นงาน "CPU-bound"
ถ้าเหมือนโปรเจกต์เซิร์ฟเวอร์นี้ที่งานส่วนใหญ่เป็น async และ event-based เซิร์ฟเวอร์ก็แทบไม่ต้องรอ I/O หรือ syscall และจะไปต่อคำขอถัดไปได้ทันที ดังนั้นในทางทฤษฎีหนึ่งเธรดต่อหนึ่งคอร์จึงเป็นโครงสร้างที่ถูกต้อง
แต่ในโลกจริงแทบไม่มีสถานการณ์อุดมคติแบบนั้น จึงต้องจำไว้ว่าการจำกัดให้มีแค่ nproc เธรดแบบตายตัวนั้นเสี่ยง
ในโลกของ io_uring การมี user thread แค่หนึ่งตัวต่อหนึ่งคอร์ก็ไม่ได้เป็นตัวเลือกที่แย่นัก
เพราะฝั่งเคอร์เนลเองทำงานเป็น thread pool อยู่แล้ว
อยากเห็นแนวทางแบบ DPDK ที่ข้ามเคอร์เนลไปเลยเหมือนกัน
ลิงก์งานวิจัย: https://www.usenix.org/system/files/atc23-zhu-lingjun.pdf