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

ยังไม่มีความคิดเห็น

ยังไม่มีความคิดเห็น