- ทราฟฟิก HTTP ของ Firefox ราว 20% ใช้ HTTP/3 ซึ่งทำงานอยู่บน QUIC และ UDP
- มีการแทนที่ชั้นเน็ตเวิร์ก I/O เดิมอย่าง NSPR ด้วย quinn-udp ที่สร้างด้วย Rust เพื่อเสริมทั้ง ประสิทธิภาพและความปลอดภัยของหน่วยความจำ
- มีการปรับแต่งประสิทธิภาพโดยใช้ system call สมัยใหม่ของแต่ละระบบปฏิบัติการ อย่างจริงจัง (เช่น multi-message, segmentation offloading)
- บน Windows และ MacOS ฟีเจอร์บางส่วนถูกจำกัดจากปัญหาความเข้ากันได้และปัญหาไดรเวอร์ แต่บน Linux ยืนยันได้ถึงประสิทธิภาพสูงสุด
- ประสบการณ์จากการลองผิดลองถูกและการแก้บั๊กด้าน การรองรับ QUIC ECN และ UDP I/O บนแต่ละแพลตฟอร์ม น่าจะเป็นประโยชน์ต่อ โครงการในอนาคตและระบบนิเวศโอเพนซอร์ส ด้วย
แรงจูงใจ
- ราว 20% ของทราฟฟิก HTTP บน Firefox ใช้ HTTP/3 ซึ่งทำงานผ่าน QUIC และสุดท้ายถูกนำไปใช้งานบน UDP
- ในอดีต Firefox ใช้ไลบรารี NSPR สำหรับเน็ตเวิร์ก I/O แต่ความสามารถที่เกี่ยวกับ UDP I/O นั้น เก่าและมีข้อจำกัด (ฟังก์ชันหลักคือ
PR_SendTo, PR_RecvFrom)
- ช่วงหลังระบบปฏิบัติการต่าง ๆ ได้เพิ่ม multi-message system call (เช่น
sendmmsg, recvmmsg) และ segmentation offload (GSO, GRO) เพื่อช่วยเพิ่มประสิทธิภาพเครือข่าย
- เทคโนโลยีเหล่านี้สามารถ ยกระดับประสิทธิภาพของ UDP I/O ได้อย่างมาก
- Firefox จึงสำรวจว่าตนจะได้รับประโยชน์หรือไม่ หาก เปลี่ยนสแตก UDP I/O เดิมไปใช้ system call สมัยใหม่
ภาพรวม
- โครงการนี้เริ่มขึ้นช่วงกลางปี 2024 โดยมีเป้าหมายคือ สร้างสแตก QUIC UDP I/O ของ Firefox ขึ้นใหม่ด้วย system call สมัยใหม่บนทุก OS ที่รองรับ
- นอกจากการปรับปรุงประสิทธิภาพแล้ว ยังมุ่งเพิ่มความปลอดภัยด้วยการใช้ Rust ที่รับประกันความปลอดภัยของหน่วยความจำ ในส่วน UDP I/O
- ตัว QUIC เองถูกเขียนด้วย Rust อยู่แล้ว จึงพัฒนาต่อบน ไลบรารี quinn-udp ที่สร้างด้วย Rust
- ความแตกต่างของ system call ระหว่างแต่ละ OS ทำให้การพัฒนายากขึ้น แต่ quinn-udp ช่วยให้ความเร็วในการพัฒนาดีขึ้นอย่างมาก
- ณ กลางปี 2025 การนำไปใช้กำลังขยายไปสู่ผู้ใช้ Firefox ส่วนใหญ่ และผลเบนช์มาร์กด้านประสิทธิภาพแสดงให้เห็นว่าความเร็วเพิ่มขึ้นอย่างมากจน สูงสุด 4Gbit/s
โครงสร้าง UDP I/O และแนวทางเพิ่มประสิทธิภาพสมัยใหม่
การส่งดาตาแกรมเดี่ยว
- วิธีเดิมใช้
sendto และ recvfrom ส่งและรับได้เพียง UDP datagram เดี่ยว ต่อครั้ง
- ต้นทุนจากการสลับระหว่าง user space กับ kernel space ต้องจ่ายต่อหนึ่งดาตาแกรม จึง ไม่มีประสิทธิภาพ ในสภาพแวดล้อมทราฟฟิกสูง
- ตัวอย่างเช่น หากต้องส่งแพ็กเก็ตขนาดต่ำกว่า 1500 ไบต์ด้วยอัตราหลายร้อย Mbit ต่อวินาที จะเกิดโอเวอร์เฮดจำนวนมาก
การส่งแบบแบตช์หลายดาตาแกรม
- Linux และบาง OS อื่น ๆ รองรับ multi-message system call เช่น
sendmmsg และ recvmmsg
- ทำให้สามารถส่งและรับหลายดาตาแกรมได้ในครั้งเดียว และ ลดโอเวอร์เฮดลงได้มาก
ดาตาแกรมขนาดใหญ่แบบแบ่งเซกเมนต์
- เทคโนโลยี offload อย่าง GSO (ส่ง) และ GRO (รับ) ช่วยให้ UDP datagram ขนาดใหญ่ถูกแบ่งโดยอัตโนมัติที่ OS หรือ NIC ก่อนส่ง
- อินเทอร์เฟซเครือข่ายจะจัดการแยกเป็นแพ็กเก็ต คำนวณ checksum และเพิ่ม header ให้
- ทำให้ฝั่งแอปพลิเคชันสามารถ จัดการแพ็กเก็ตจริงจำนวนมากได้ด้วย system call เพียงครั้งเดียว
- เมื่อเปิดใช้ GSO เครื่องมือเครือข่ายบางตัวอย่าง Wireshark อาจรองรับการวิเคราะห์แพ็กเก็ตได้ไม่สมบูรณ์
กระบวนการแทนที่ NSPR ของ Firefox
- เริ่มจากแทนที่ NSPR ด้วย quinn-udp ในโครงสร้าง การส่งและรับดาตาแกรมเดี่ยว
- รีแฟกเตอร์ไปป์ไลน์จัดการดาตาแกรมของ implementation QUIC ให้ รองรับการส่งรับแบบแบตช์และการแบ่งเซกเมนต์
- ใช้ทั้ง multi-message call และ segmentation offload call ให้เหมาะกับสถานการณ์
- มีการเพิ่มการจัดการข้อยกเว้นเฉพาะแพลตฟอร์มและฟีเจอร์ปรับปรุง I/O หลากหลายรายการ
รายละเอียดแยกตามแพลตฟอร์ม
Windows
- Windows มี
WSASendMsg/WSARecvMsg สำหรับรองรับทั้งดาตาแกรมขนาด MTU แบบดั้งเดิม หรือดาตาแกรมขนาดใหญ่แบบแบ่งเซกเมนต์
- ฟีเจอร์ฝั่ง Windows ที่สอดคล้องกับ GSO/GRO ของ Linux คือ USO (ส่ง) / URO (รับ)
- ในช่วงแรกใช้เพียง system call สำหรับดาตาแกรมเดี่ยวและไม่มีปัญหา แต่เมื่อเปิด URO พบว่ามีบั๊กในบางสภาพแวดล้อม (เช่น Windows on ARM + WSL) ทำให้ ไม่สามารถระบุความยาวของแพ็กเก็ต QUIC ได้ และหน้าเว็บโหลดไม่สำเร็จ
- มีการใช้ USO/การส่งด้วยเช่นกัน แต่พบผลข้างเคียงอย่าง packet loss เพิ่มขึ้นและไดรเวอร์เครือข่ายล่ม ในสภาพแวดล้อมที่ติดตั้ง Firefox บน Windows
- ปัจจุบัน Firefox จึงยังคง ปิดใช้งาน URO/USO และกำลังดีบักเพิ่มเติม
MacOS
- บน MacOS มีการนำ quinn-udp มาใช้โดยเปลี่ยนจาก
sendto/recvfrom เดิมไปเป็น sendmsg/recvmsg
- ยังไม่ได้เปิดใช้ฟีเจอร์ segmentation offload
- แทนที่จะใช้วิธีนั้น มีการรองรับ การส่งแบบแบตช์ ผ่าน
sendmsg_x/recvmsg_x ที่ไม่ได้มีเอกสารอย่างเป็นทางการ และนำมาใช้กับ quinn-udp แบบไม่เป็นทางการ
- เนื่องจาก Apple อาจถอด call เหล่านี้ออกในอนาคต จึง ทดสอบโดยไม่เปิดใช้เป็นค่าเริ่มต้น และไม่ได้รวมไว้ในรีลิซจริง
Linux
- รองรับทั้ง sendmmsg/recvmmsg และ GSO/GRO โดย quinn-udp จะให้ความสำคัญกับ GSO เป็นค่าเริ่มต้นในฝั่งส่ง
- Firefox ใช้ UDP socket แยกต่อหนึ่งการเชื่อมต่อ เพื่อเสริมความเป็นส่วนตัว (แยกตาม 4-tuple)
- ในโครงสร้างนี้ ข้อดีของ segmentation offload จะเด่นชัดมากที่สุด ขณะที่ข้อดีของ sendmmsg/recvmmsg ในการส่งข้ามกันมีจำกัด
- นอกจากการเปลี่ยนแปลงเล็กน้อย เช่น network sandbox และการตรวจสอบการรองรับ GSO ระหว่างรันไทม์ ก็สามารถนำมาใช้ได้สำเร็จโดยไม่เจออุปสรรคใหญ่
Android
- Android ต่างจาก Linux ตรงเส้นทางประมวลผล system call และตัวกรองความปลอดภัย (เช่น seccomp)
- มีประเด็นด้านความเข้ากันได้หลายอย่าง เช่น การรองรับแพลตฟอร์มเก่ามาก อย่าง Android 5 บน x86, การเลี่ยง
socketcall, และการจัดการข้อผิดพลาด
- ในบางสภาพแวดล้อม เมื่อเรียกส่งโดยเปิด ECN bit จะเกิดข้อผิดพลาด (EINVAL) และ quinn-udp ใช้กลยุทธ์ ลองใหม่และปิด option ดังกล่าว
- ด้วยประโยชน์จากการปรับปรุงหลายอย่างในชุมชน Quinn ทำให้ Firefox ได้รับผลดีจากการปรับปรุงเหล่านั้นโดยอัตโนมัติ
การรองรับ ECN (Explicit Congestion Notification)
- การนำ system call สมัยใหม่มาใช้ ทำให้รองรับการส่งและรับ ancillary data ได้ และ รองรับ QUIC ECN ได้
- แม้จะมีบั๊กเล็กน้อย แต่บน Firefox Nightly มากกว่าครึ่งของการเชื่อมต่อ QUIC ทำงานผ่านเส้นทาง outbound ของ ECN
- เมื่อเทคโนโลยีใหม่อย่าง L4S ได้รับความสนใจมากขึ้น ความสำคัญและการใช้งานของ ECN ก็เพิ่มขึ้นตามไปด้วย
สรุปบทสรุป
- มีการแทนที่ชั้น QUIC UDP I/O ของ Firefox ด้วย implementation แบบ Rust ที่ใช้ quinn-udp ทำให้ได้ทั้งประสิทธิภาพและความปลอดภัย
- แทนที่จะใช้ system call แบบเก่า ก็หันไปใช้ I/O system call สมัยใหม่ของแต่ละ OS เพื่อเพิ่ม throughput และรองรับ ECN
- ฟีเจอร์เพิ่มประสิทธิภาพบางอย่างบน Windows และบางแพลตฟอร์มยังต้องปรับปรุงเพิ่มเติมเพราะปัญหาความเข้ากันได้
- เมื่ออัตราการใช้งาน QUIC เพิ่มขึ้นอย่างต่อเนื่อง การรองรับในระดับ OS และไดรเวอร์ก็น่าจะพัฒนาต่อไปในอนาคต
1 ความคิดเห็น
ความคิดเห็นใน Hacker News
ใจความสำคัญของบทความซ่อนอยู่ตรงกลาง
การปรับปรุงที่กล่าวถึงตรงนี้แม้จะจำเป็นสำหรับความเร็วระดับสูงมากจริง ๆ (100Gb/s ขึ้นไป) แต่ 4Gb/s เองก็ไม่ได้เร็วมากนัก
เพราะ 500MB/s หมายความว่าต้องมีคอขวดที่ช้ามากอย่างร้ายแรงอยู่ตรงไหนสักแห่ง
มีการบอกว่า kernel context switch อยู่ระดับ 1us ซึ่งจริง ๆ ก็ถือว่าสูงสำหรับ system call
แต่ถ้าขนาดแพ็กเก็ตเฉลี่ยแค่ประมาณ 500 ไบต์ ก็ทำ 500MB/s หรือ 4Gb/s ได้แล้ว
ตัวเลข 1Gb/s ก่อนหน้านี้น่าจะเป็นตอนที่ขนาดแพ็กเก็ตเล็กกว่านี้ และการแค่ยัดแพ็กเก็ต UDP เข้า NIC buffer ก็เป็นงานที่แม้แต่ความเร็วการคัดลอกหน่วยความจำก็รองรับได้สบาย
ต่อให้บอกว่าการเข้ารหัสช้า จริง ๆ ก็ไม่ค่อยใช่
ตัวอย่างเช่น Intel i5-6500 เคยทำความเร็ว AES-128 GCM ได้ 1729MB/s
CPU ปัจจุบันทำได้ 3-5GB/s ต่อคอร์ หรือ 25-40Gb/s ดังนั้นตัวเลข 4Gb/s ที่พูดถึงจึงต่ำเกินไปมาก
(ลิงก์อ้างอิงประสิทธิภาพ AES-128 GCM)
มีการบอกว่า latency ของ system call สูง ซึ่งสาเหตุอาจมาจากมาตรการรับมือ spectre & meltdown
TCP มี path binding แต่ UDP ไม่มี จึงมีความต่างกันมากในขั้นตอนการตั้งค่าเส้นทาง
การบอกว่าการเข้ารหัสช้านั้นเป็นจริงสำหรับ PDU (protocol data unit) ขนาดเล็ก
งานส่วนใหญ่ถูกปรับแต่งและทำเบนช์มาร์กโดยอิงจาก TCP frame ขนาดใหญ่ ดังนั้นในแพ็กเก็ตขนาดเล็ก ต้นทุนของการตั้งค่าสถานะจึงเด่นชัดมาก
ถ้ารันไมโครบेंช์มาร์กใน tight loop ตัวเลขจะออกมาดี แต่ในสภาพแวดล้อมจริงที่มีความสุ่ม ประสิทธิภาพการใช้แคชจะลดลง และแพ็กเก็ตต่ำกว่า 1KB จะเสียประสิทธิภาพอย่างมาก
ยังมี framing overhead เพิ่มเติม การตรวจสอบความถูกต้องของข้อมูล out-of-band ภายนอกอีก ซึ่งล้วนมีต้นทุนสูงพอสมควร
หน่วยความจำบัฟเฟอร์ UDP เองก็มีค่าเริ่มต้นน้อยเกินไปจนมีปัญหาในการใช้งานจริง
ตอนดูแล TCP มีการเพิ่มขนาดบัฟเฟอร์ต่อเนื่อง แต่ UDP ยังติดอยู่กับค่าค่อนข้างอนุรักษ์นิยมแบบยุค 90-00
API ที่ควรมีจริง ๆ คือการ fork fd เพื่อให้รองรับ connect(2) และ route binding ได้เต็มรูปแบบ จากนั้นค่อยต่อด้วย submission queue-based (uring, rio ฯลฯ)
ในด้านการเข้ารหัส วิธีแบบ KDF สามารถลดต้นทุนด้านสถานะได้มาก
วิธีแบบ PSP มีผู้ขายบางรายยอมรับ แต่ถูกปฏิเสธบ่อยจาก IETF และที่อื่น ๆ จึงไม่แพร่หลาย
ในการทดสอบ concurrent scale ขนาดใหญ่ของเหล่า vendor นั้น มันให้ตัวเลขการสเกลที่สูงกว่า TLS แบบเดิมอย่างชัดเจน
ไม่ได้บอกเลยว่า CPU ที่ใช้เบนช์มาร์กเป็นระดับไหน
และ overhead จากการเข้ารหัสก็คือค่าต้นทุนของตัวโปรโตคอล QUIC เอง
QUIC ยังด้อยกว่า TCP ในด้าน encryption offload (การประมวลผลด้วยฮาร์ดแวร์) โดย TCP ยังพอให้ NIC ช่วยประมวลผลได้ผ่าน kTLS offload
คอนเทนต์เชิงเทคนิคแบบนี้น่าพอใจมากจริง ๆ
อยากให้เอกสารเชิงเทคนิคของ Mozilla ทุกชิ้นลึกและเขียนอย่างเข้าใจงานจริงโดยวิศวกรแบบนี้
ไม่มีแง่มองโลกสวยแบบ alegría อะไรทั้งนั้น จึงคุ้มค่ากับการอ่านมาก
ไม่เข้าใจว่าทำไมยังรองรับ Android 5 อยู่
ผ่านมาเกิน 10 ปีแล้ว และผู้ใช้เครื่องเหล่านั้นก็เป็นกลุ่ม legacy ยิ่งกว่าเดิมอีก
เว็บสมัยนี้หนักเกินไปมาก จนแค่จะท่องเว็บบนอุปกรณ์เก่าแบบนั้นก็น่าจะลำบากแล้ว เลยสงสัยว่าทำไมยังต้องรองรับ
คงมีแต่พวกแฮ็กเกอร์ที่ซ่อมเครื่องเก่าอย่าง OnePlus ยุคก่อน ๆ เสียบชาร์จทิ้งไว้ ไม่ลงรอมมหาชนอย่าง LineageOS แล้วใช้ Firefox จากสโตร์ทางเลือกเท่านั้น
ในทางปฏิบัติมันคือค่าใช้จ่ายที่ทำให้ความเร็วการพัฒนาทั้งหมดช้าลง
ขอแนะนำบล็อกของทีมพัฒนา Factorio ตอน "The map download struggle" ซึ่งมีเกร็ดที่น่าสนใจเกี่ยวกับเรื่องนี้เช่นกัน
(บล็อกโพสต์ที่เกี่ยวข้อง)
ถ้าเคยจัดการปัญหาเครือข่ายจริง ๆ จะยิ่งอินกับเรื่องนี้เพราะแพ็กเก็ต runt ลึกลับพวกนั้น
อุปกรณ์เครือข่ายส่วนใหญ่จัดการแพ็กเก็ตแบบนี้ได้ไม่ดี
ทราฟฟิกที่อิงกับ UDP หรือ QUIC จะตกเป็นเป้าโจมตีได้ง่าย ถ้าไม่ใช่สภาพแวดล้อมคลาวด์ขนาดใหญ่ระดับหนึ่ง
ด้วยเหตุนี้ผู้ให้บริการโฮสติ้งขนาดเล็กหรือแบบดูแลเองจึงยิ่งดำเนินงานลำบากขึ้น และสุดท้ายจะเหลือแต่ผู้เล่นที่รับมือทราฟฟิกขนาดใหญ่ได้
ดังนั้นในสภาพแวดล้อม LAN ส่วนใหญ่ตอนนี้จึงดรอปทราฟฟิก UDP ไปเกือบทั้งหมด และจัดการเฉพาะส่วนที่จำเป็นด้วย rate limit
ตัวติดตามบั๊กของ Mozilla
ตอนนี้บน macOS และ Fedora เมื่อเข้าเว็บที่ Cloudflare โฮสต์ด้วย Firefox ก็ยังเจออาการเดิมอยู่
เพิ่งรู้ครั้งนี้เองว่า Windows และ MacOS ก็มีฟีเจอร์คล้าย GSO/GRO (การจัดการแพ็กเก็ตเครือข่ายขนาดใหญ่) ด้วย
แต่น่าเสียดายที่เหมือนจะมีบั๊กเยอะในทางปฏิบัติ
และก็รู้สึกว่าไม่น่าจะมีแค่ GSO/GRO อย่างเดียวที่มีบั๊ก
มีใครอธิบายได้ไหมว่า UDP GSO/GRO ทำงานเชิงโครงสร้างอย่างไร
UDP เป็นแพ็กเก็ตที่ไม่มีลำดับ แล้วถ้าแพ็กเก็ต QUIC หนึ่งก้อนถูกแบ่งเป็นหลายแพ็กเก็ต UDP โดยที่ในเฮดเดอร์ก็ไม่มีข้อมูลลำดับ ฝั่งรับจะรวมกลับให้ถูกลำดับได้อย่างไร
มันเหมือนกับว่าเคอร์เนลใส่หลาย datagram ลงในโครงสร้างเดียวกัน แล้วส่งต่อโดยยังคงขอบเขตระหว่างกันในแต่ละชั้นไว้ (เช่น data fragments ใน sk_buff)
ผมไม่ใช่ผู้เชี่ยวชาญแบบจริงจัง แต่ระหว่างหาวิธีว่ามันทำงานอย่างไร ก็ไปเจอบทความนี้เข้า บทความนี้
มีการบอกว่า "เราเริ่มพัฒนาบน quinn-udp ซึ่งเป็นไลบรารี UDP I/O ของโครงการ Quinn และสิ่งนี้ช่วยให้เราพัฒนาได้เร็วขึ้นมาก"
ถ้าอย่างนั้นก็สงสัยว่าได้สนับสนุนโครงการ Quinn บ้างหรือเปล่า
(ลิงก์สนับสนุน Quinn)
ลองถามตรง ๆ เรื่องการสนับสนุนทางการเงินแล้ว ทาง Senior Principal Software Engineer ของ Mozilla ตอบว่า "Mozilla ไม่มีเงินครับ"
แต่พวกเขาช่วย contribute โค้ดมาเยอะมาก ซึ่งน่าขอบคุณจริง ๆ
(ผมเป็นเมนเทนเนอร์หลักของ Quinn)
สำหรับคำถามว่า "ได้สนับสนุนไหม" ก็มีความเห็นว่า นี่แหละสไตล์ Mozilla ที่ไม่จำเป็นต้องสนับสนุนโอเพนซอร์ส แต่กลับจ่ายเพิ่มให้เงินเดือน CEO อีกหลายล้านดอลลาร์
ทั้งที่แม้แต่ผลิตภัณฑ์หลัก (Firefox) เองก็ยังทรุดลงเรื่อย ๆ
ถ้ามีส่วนช่วยในรูปแบบอื่น เช่น โค้ด ก็อยากรู้เหมือนกัน
น่าแปลกใจที่ sendmmsg/recvmmsg ถูกเรียกว่า “ใหม่”
จริง ๆ แล้วมันเป็น system call ที่มีมานานพอสมควรแล้ว
น่าเสียดายที่ในเนื้อหาฝั่ง Linux ไม่มีการพูดถึง io_uring
อย่างดีที่สุดก็แค่ส่งคำขอ sendmsg, recvmsg หลายรายการพร้อมกัน
คำตอบที่ถูกคือต้องใช้ GSO/GRO
sendmmsg/recvmmsg เป็นเทคโนโลยีที่เก่ามากแล้ว และในหมู่นักพัฒนาเคอร์เนลก็มีคนที่อยากเลิกใช้มันแล้วด้วย
(การสนทนาใน GitHub ที่เกี่ยวข้อง)