- เดิมทีมีการใช้โมเดลแบบอิงอีเวนต์หลากหลายรูปแบบ เช่น 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
ยังไม่มีความคิดเห็น