2 คะแนน โดย GN⁺ 2025-08-23 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • เดิมทีมีการใช้โมเดลแบบอิงอีเวนต์หลากหลายรูปแบบ เช่น 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 ความคิดเห็น

 
GN⁺ 2025-08-23
ความคิดเห็นจาก 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 ที่ข้ามเคอร์เนลไปเลยเหมือนกัน