QUIC สำหรับเคอร์เนลลินุกซ์
(lwn.net)- มีการส่งแพตช์แรกสำหรับการผสานโปรโตคอล QUIC เข้าสู่ เคอร์เนลลินุกซ์ อย่างเป็นทางการ
- มีเป้าหมายเพื่อแก้ข้อจำกัดของ TCP เดิม เช่น ความหน่วง, head-of-line blocking, และการแข็งตัวของโปรโตคอลจากอุปกรณ์ตัวกลาง
- QUIC ทำงานบน UDP รองรับมัลติสตรีมและการเข้ารหัสแบบ End-to-End ซึ่งเมื่อเข้าไปอยู่ในเคอร์เนลแล้วจะเพิ่มโอกาสในการใช้งานบนแพลตฟอร์มและฮาร์ดแวร์ที่กว้างขึ้น
- ประสิทธิภาพของการติดตั้งใช้งานในเคอร์เนลช่วงแรกยังวัดได้ต่ำกว่า TCP เดิมและ kernel TLS แต่คาดว่าจะปรับปรุงได้ในอนาคตผ่าน hardware offloading และการเพิ่มประสิทธิภาพ
- ขณะนี้มีการหารือเรื่องการรองรับใน Samba, SMB/NFS บนเคอร์เนล, curl และอื่น ๆ อย่างคึกคัก แต่คาดว่ายังต้องใช้เวลาอีกพอสมควรกว่าจะถูกรวมเข้า mainline
ที่มาของโปรโตคอล QUIC และข้อจำกัดของ TCP
- QUIC ถูกสร้างขึ้นมาเพื่อแก้ปัญหาหลายอย่างของ TCP บนอินเทอร์เน็ตแบบเดิม
- ความหน่วงจาก 3-way handshake ในขั้นตอนเชื่อมต่อของ TCP, การรองรับมัลติสตรีมที่ยังไม่ดีพอ, และปัญหา head-of-line blocking เมื่อเกิด packet loss ล้วนทำให้ประสบการณ์ใช้งานเว็บแย่ลง
- metadata ของ TCP ถูกส่งโดยไม่เข้ารหัส ทำให้มีความเสี่ยงด้านการรั่วไหลของข้อมูล และ middlebox (อุปกรณ์ตัวกลาง) บนอินเทอร์เน็ตก็กรองทราฟฟิกตามข้อมูลการเชื่อมต่อ จนนำไปสู่ การแข็งตัวของโปรโตคอล (ossification)
- แม้จะมีความพยายามปรับปรุง TCP (เช่น Multipath TCP) ก็ยังอยู่ในสภาพที่หากไม่ปลอมตัวเป็น TCP แบบเดิมก็ทำงานได้ไม่สมบูรณ์
คุณลักษณะและข้อได้เปรียบทางเทคนิคของ QUIC
- QUIC ทำงานบน UDP และสามารถตั้งค่าการเชื่อมต่อได้รวดเร็วโดยไม่ต้องมี 3-way handshake แยกต่างหาก
- มีการออกแบบการส่งแบบ มัลติสตรีม เพื่อไม่ให้ packet loss กระทบทั้งสตรีมทั้งหมด
- ข้อมูลการส่งที่เกี่ยวข้องกับ QUIC จะถูก เข้ารหัสแบบ end-to-end (อิง TLS) ตลอดเวลา ทำให้อุปกรณ์ตัวกลางไม่สามารถเข้าถึงข้อความภายในได้
- หากเป็นสภาพแวดล้อมเครือข่ายที่แพ็กเก็ต UDP ผ่านได้ QUIC ก็สามารถทำงานได้ตามปกติเช่นกัน
ภาพรวมของแพตช์ผสาน QUIC เข้าสู่เคอร์เนลลินุกซ์
- ในแพตช์ที่ส่งเข้ามามีการเพิ่มชนิดโปรโตคอลใหม่ชื่อ IPPROTO_QUIC ทำให้สามารถใช้ system call socket() แบบเดิมได้
- สามารถใช้คำสั่งเรียกอย่าง bind(), connect(), listen(), accept() ได้คล้ายกับ TCP แต่แนวทางการทำงานหลังจากนั้นมีความแตกต่าง
- การจัดการ TLS session และกระบวนการยืนยันตัวตน/เข้ารหัส จะประมวลผลใน userspace และหลังเชื่อมต่อแล้วแต่ละฝั่งต้องทำ TLS handshake ให้เสร็จก่อนจึงจะรับส่งข้อมูลได้
- หลังการเชื่อมต่อครั้งแรกสามารถ แคชผลการเจรจา TLS เอาไว้ เพื่อเร่งความเร็วอย่างมากเมื่อสองระบบเชื่อมต่อกันใหม่
ความท้าทายด้านประสิทธิภาพและแนวโน้ม
- การติดตั้งใช้งาน QUIC ภายในเคอร์เนล ที่ส่งเข้ามายังด้อยกว่า kernel TLS และ TCP เดิมในด้านประสิทธิภาพ
- throughput ต่ำกว่า in-kernel TLS มากกว่า 3 เท่า และแม้ปิดการเข้ารหัสก็ยังมี throughput ต่ำกว่า TCP ได้สูงสุดถึง 4 เท่า
- สาเหตุที่ถูกชี้ถึงได้แก่ ยังไม่รองรับ segmentation offloading, มีการคัดลอกข้อมูลเพิ่มในเส้นทางส่งข้อมูล และมีขั้นตอนเข้ารหัสส่วนหัว
- ในอนาคตหากเพิ่มการรองรับ hardware offloading และปรับแต่ง implementation ในเคอร์เนลให้ดีขึ้น ก็คาดว่าประสิทธิภาพจะดีขึ้น
สถานะการนำไปใช้และแนวโน้มต่อจากนี้
- มีการหารือเรื่อง การรองรับ QUIC ในเคอร์เนล อย่างคึกคักในหลาย โปรเจกต์ เช่น เซิร์ฟเวอร์/ไคลเอนต์ Samba, ระบบไฟล์ SMB และ NFS บนเคอร์เนล, รวมถึง curl
- แพตช์มีขนาดราว 9,000 บรรทัด และตอนนี้ยังมีเพียงโค้ดรองรับระดับล่างเท่านั้น โดย implementation ทั้งหมดยังจะตามมาในแพตช์เพิ่มเติม
- การรีวิวโค้ดและการหารือเพื่อรวมเข้าระบบเพิ่งเริ่มต้น จึงคาดว่าจะยังต้องใช้เวลาอีกมากกว่าจะใช้งานจริงได้
- เมื่อดูจากกรณีของโปรโตคอล Homa ที่ต้องส่งถึง 11 ครั้งในช่วง 9 เดือนกว่าจะถูกรวมเข้าเคอร์เนล ก็มีแนวโน้มว่า QUIC เองอาจได้เข้าสู่ mainline หลังปี 2026
1 ความคิดเห็น
ความเห็นจาก Hacker News
ssl_preread_server_nameในการตั้งค่า NGINX เพื่อproxy_passคำขอของบางโดเมนไปยัง NGINX อีกอินสแตนซ์หนึ่งอินสแตนซ์แรกทำหน้าที่เพียงส่งต่อสตรีม TLS ดิบ (
proxy_protocolรวมอยู่ด้วย) และอินสแตนซ์ที่สองเป็นผู้ทำ TLS termination จริงวิธีนี้มีประสิทธิภาพเมื่อทำระบบ failover — ถ้าเส้นทางหลักของเซิร์ฟเวอร์ล่ม ก็อัปเดต DNS A record ให้ชี้ไปยัง NGINX ของเครื่อง failover แล้วอินสแตนซ์นั้นจะ route คำขอของโดเมนที่กำหนดผ่านเส้นทางแยกกลับไปยังแบ็กเอนด์เดิม
สะดวกเพราะไม่ต้องทำสำเนาคอนฟิก TLS ทั้งหมด
แต่ข้อเสียคือวิธีนี้ใช้กับ HTTP/3 ไม่ได้
HTTP/3 อยู่บน QUIC, ทำงานบน UDP และเข้ารหัส SNI ระหว่าง handshake จึงไม่สามารถทำ domain-based routing ด้วย
ssl_preread_server_nameได้เลยสงสัยว่ามีทางเลือกอื่นที่รองรับ SNI-based routing บน HTTP/3 หรือไม่ หรือถ้าต้องการฟีเจอร์นี้จริง ๆ ก็ยังต้องคง HTTP/1.1 หรือ HTTP/2 over TLS ต่อไป
ในทางปฏิบัติพฤติกรรมต่างกันตาม implementation ของไคลเอนต์ (ดูสถานะการรองรับ HTTPS record ของ Chromium ในลิงก์ issue) แต่เมื่อการเชื่อมต่อ QUIC ล้มเหลว ไคลเอนต์จะ fallback ไปใช้ HTTP/1.1/2 แบบโปร่งใส และยังเคารพ header Alt-Svc ด้วย
ถ้าเป็น failover ที่วางแผนไว้ ก็อาจหยุดส่ง Alt-Svc header แล้วรอให้ timeout ไปยังอินสแตนซ์สำรองได้
ถ้าจำเป็นต้อง route QUIC จริง ๆ โชคดีที่ข้อมูล SNI จะอยู่ในแพ็กเก็ตแรกเสมอ จึงสามารถ route ได้ด้วยการตรวจสอบแพ็กเก็ต
udpgrm ของ Cloudflare น่าจะเป็นตัวอย่างอ้างอิงที่ดี และใช้ได้เมื่อไม่มี ECH (Encrypted Client Hello)
ถ้ามี ECH เราเตอร์ต้องมีคีย์ถอดรหัสจึงจะตัดสินใจ route ได้ และในระดับโปรโตคอลก็สามารถออกแบบ failover แบบ cascade ได้เช่นกัน
ดูโค้ดตัวอย่างจริงได้จาก ตัวอย่าง udpgrm
ถ้าผู้โจมตีเข้าถึงเซิร์ฟเวอร์นั้นได้ การออกใบรับรอง SSL ใหม่ก็ทำได้ง่ายอยู่แล้ว ดังนั้นแทนที่จะกังวลกับระบบ failover ที่ซับซ้อน การทำ TLS termination โดยตรงน่าจะสมเหตุสมผลกว่า
ส่วนตัวไม่เคยทำซ้ำให้เห็นข้อดีด้านประสิทธิภาพและความน่าเชื่อถือของ QUIC ได้จริง
ทดสอบซ้ำมาหลายปี แต่สุดท้ายก็มักปิดใช้งานเพราะเหตุผลด้านประสิทธิภาพเป็นหลัก
failover แบบอิง DNS เองก็มักใช้เวลาหลายนาทีกว่าจะมีผลจริง และไคลเอนต์ธรรมดาอย่างเบราว์เซอร์ก็มัก failover ได้ไม่ดี
เลยใช้วิธีเขียน onerror handler เองเพื่อโหลดเส้นทางที่สอง
ตัวอย่างเช่น ใช้โค้ดลักษณะนี้กับระบบติดตามโฆษณา และยัง wrap
fetchAPI แบบเดียวกันให้ใช้งานด้วยวิธีนี้มีประสิทธิภาพกว่าสิ่งที่เคยลองมาทั้งหมดมาก
ต่อให้เบราว์เซอร์เชื่อมต่อ QUIC ไม่ได้ (แม้จะประกาศไว้ใน DNS) มันก็จะ fallback ไปใช้ HTTP/1 หรือ HTTP/2 over TLS อัตโนมัติ จึงยังใช้แนวทาง failover เดิมได้เหมือนเดิม
ลักษณะการออกแบบของ HTTP/3 คือไม่เปิดเผยข้อมูล endpoint จนถึงชั้น TLS
ส่วนตัวมองว่านี่เป็นข้อดี
HAProxy สามารถทำ raw TLS proxy ได้ แต่ไม่สามารถ route ตาม hostname ได้
Cloudflare tunnel มีความสามารถพิเศษที่ route ตาม hostname ได้โดยไม่ต้องทำ TLS termination แต่จะใช้ได้ก็ต่อเมื่อ DNS ผูกกับ Cloudflare ด้วย
ดูภาพประกอบในการ์ตูน xkcd ที่เกี่ยวข้อง
สงสัยว่าบนสภาพแวดล้อม TCP+TLS ถ้าใช้ Encrypted Client Hello ก็จะมีข้อจำกัดเดียวกันหรือไม่
คิดว่าคำตอบก็น่าจะเกือบเหมือนกัน
การถกเถียงครั้งนี้ทำให้รู้สึกว่าเป็นทิศทางในการค่อย ๆ แก้ปัญหาเหล่านั้น
ในอนาคตก็ยังมีโอกาสที่การ์ดเครือข่ายจะรองรับในระดับฮาร์ดแวร์
แต่ทุกวันนี้ทราฟฟิกอินเทอร์เน็ตส่วนใหญ่ไหลระหว่างมือถือกับเซิร์ฟเวอร์ ดังนั้นในช่วงนั้น QUIC และ HTTP/3 จึงแสดงศักยภาพได้เต็มที่
สำหรับงานอื่นก็ยังใช้ TCP ต่อไปได้
น่าจะดูเหมือนมีหลาย connection แบบเดิม แต่ภายในมีการ cache ไว้
ส่วนตัวอยากได้แบบรับ connection object อย่างชัดเจนแล้วค่อยเปิด stream แยกต่างหาก แต่ตอนนี้รูปแบบปัจจุบันก็พอรับได้
จากการพูดคุยที่เกี่ยวข้อง ถ้านี่ไม่ใช่ extension ฝั่งเซิร์ฟเวอร์ก็น่าจะสร้าง stream ใหม่ได้หลังจากตั้ง connection แล้ว
ฝั่งไคลเอนต์สิ่งที่เป็น stream จริง ๆ แต่แยก abstraction ให้เหมือน “connection” ดูทำได้ยาก และโดยพื้นฐานแล้วน่าจะต้องใช้ API abstraction แบบใหม่ทั้งหมด
เดาว่าอาจเป็นโครงสร้างที่รับ file descriptor ผ่าน
recvmsgสำหรับแต่ละ stream ใหม่อยากได้ความทนทานต่อปัญหาเครือข่ายแบบ Mosh แต่ยังคงใช้ความสามารถทั้งหมดของ OpenSSH (SFTP, SOCKS, port forwarding, state management, roaming ฯลฯ) ได้เหมือนเดิม
สงสัยว่า OpenSSH จะใช้ประโยชน์จากการรองรับในเคอร์เนลได้หรือไม่
ดู Mosh
อาจจะดีกว่าถ้าสร้างโปรโตคอลล็อกอินแยกใหม่บน QUIC ไปเลย
ตอนนี้มีหลายแนวทางที่กำลังอยู่ในขั้น prototype
ขณะเดียวกันก็มีข้อมูลว่าการ implement QUIC ในเคอร์เนลตอนนี้ช้ากว่า Linux อยู่ 3–4 เท่า และส่วนต่างด้านประสิทธิภาพน่าจะลดลงในไม่ช้า
ถ้าความเร็วคือข้อดีของ QUIC แต่ในการใช้งานจริงกลับช้ากว่า แล้วเหตุผลในการใช้ QUIC คืออะไร
ผู้เขียน PR เองก็บอกว่าส่วนหนึ่งของประสิทธิภาพที่ลดลงมาจากการออกแบบโปรโตคอล เลยสงสัยว่า TCP ยังมีปัญหาอะไรที่ต้องแก้อีกหรือไม่
ส่วนใหญ่สรุปได้ว่า “ยังไม่ได้ optimize”
เช่น ยังไม่รองรับ segment offload, มีการคัดลอกข้อมูลเพิ่มในเส้นทางส่งข้อมูล, มี overhead จากการเข้ารหัส header เป็นต้น ซึ่งทั้งหมดมีโอกาสแก้ได้สูง
การ benchmark ที่นี่ทำในสภาพแวดล้อมที่อุดมคติมาก
แต่ในโลกจริง โดยเฉพาะบนมือถือ ความผันผวนของเครือข่ายสูงมากจนข้อจำกัดเชิงโครงสร้างของ TCP ชัดเจนขึ้น
ในทางปฏิบัติก็มีหลายกรณีที่ทำฟีเจอร์คล้าย QUIC อยู่บน TCP แล้ว เช่น HTTP/2
สุดท้าย QUIC คือ networking stack แบบครบชุดที่ทำงานตั้งแต่ชั้น OSI 5 ขึ้นไป ขณะที่ TCP เป็นเพียงเอนจินระดับชั้น 3 จึงเทียบเชิงโครงสร้างกันได้ยาก
ที่สำคัญ QUIC เด่นเรื่องการเชื่อมต่อและเชื่อมต่อซ้ำได้รวดเร็วกว่า และยังคงความต่อเนื่องของ session ได้แม้ IP เปลี่ยน
โครงสร้างแบบ multiplexing และ non-blocking stream ช่วยลดความซับซ้อนของการออกแบบโปรโตคอลชั้นบนอย่างมาก
ถ้าโครงสร้างนี้เข้าไปอยู่ในเคอร์เนล ก็ยังมีพื้นที่ให้ optimize ด้านประสิทธิภาพอีกมหาศาล
ต่อไปแทนที่จะสร้างโซลูชันหลายชั้นคร่อมอยู่บนข้อจำกัดของ TCP เราน่าจะใช้เทคโนโลยีฐานที่ก้าวหน้ากว่าอย่าง QUIC ให้เป็นเรื่องปกติมากขึ้น
เมื่อเกิด packet loss การส่งทั้งหมดหลังจากนั้นจะหน่วงจนกว่าจะกู้คืนได้ (HOL blocking) ซึ่งเป็นข้อจำกัดเชิงโครงสร้างที่สำคัญ
มันไม่ใช่แค่เรื่องความเร็ว แต่คือการลดความหน่วง
ดูเอกสารอธิบายทางเทคนิค
คอขวดหลักคือ context switch ระหว่างเคอร์เนลกับ user space
user-space networking (เช่น เข้าถึง NIC โดยตรง) ช่วยตัดการเข้าเคอร์เนลออกไป
ในทางกลับกัน การมีฟังก์ชันในเคอร์เนล (เช่น
sendfile, in-kernel TLS, NIC offloading, DMA ตรงจากดิสก์ไป NIC) ก็ช่วยลดทั้ง context switch และการคัดลอกข้อมูลโดยรวมได้QUIC stack ปัจจุบันยังใช้ข้อดีของทั้งสองฝั่งไม่ได้เต็มที่
การรับส่งแพ็กเก็ตยังพึ่ง syscall และยังหลีกเลี่ยงการคัดลอกข้อมูลไม่ได้
แม้จะใช้ batch I/O ผ่าน
io_uringเพื่อลดการสลับ context แต่ก็ยังลดตัวการคัดลอกข้อมูลเองไม่ได้มีอยู่สองแนวทางคือ kernel bypass+DMA หรือไม่ก็แบบ
sendfile/ktlsที่ตัด user space ออกไปการ implement QUIC ในเคอร์เนลตอนนี้ยังไม่ได้ข้อดีเต็ม ๆ จากทั้งสองแบบ
หากเขียนถึง NIC ได้ตรงผ่าน DMA หรือส่งผ่าน syscall ของเคอร์เนล ความต่างด้านประสิทธิภาพจะมีนัยสำคัญ
ดังนั้น user-space networking จะน่าสนใจก็ต่อเมื่อมีโครงสร้าง privilege transition และ DMA แบบนี้รองรับ
มักใช้กันเฉพาะบริษัทขนาดใหญ่มาก (MOFAANG ฯลฯ)
ในทางทฤษฎีมีความหวังว่า
io_uringจะทำให้ประโยชน์เหล่านี้กลายเป็นเรื่องทั่วไปได้ แต่ตอนนี้ยังไม่ถึงขั้นใช้งานจริงแพร่หลายนั่นจึงเป็นเหตุผลที่ TCP/IP ยังอยู่ในเคอร์เนลของระบบปฏิบัติการหลัก
เดิมคิดว่าหน้าที่ของเคอร์เนลคือจัดการหน่วยความจำ ฮาร์ดแวร์ และงานต่าง ๆ แล้วโปรโตคอลที่อยู่เหนือ IP ควรถูกจัดการใน userland มากกว่าหรือไม่
ในทางกลับกัน การแยก stack เหล่านี้ไปไว้ใน user space ก็ช่วยเรื่องประสิทธิภาพได้ในบางกรณีเช่นกัน
เพราะ TCP/UDP ในเคอร์เนลทำหน้าที่ไกล่เกลี่ยการ route socket ตามพอร์ต ทำให้หลายโปรแกรมใช้ TCP/UDP พร้อมกันได้
QUIC ทำงานอยู่บน UDP ดังนั้นประเด็นที่ถกกันก็ยังมีน้ำหนัก
จุดที่อยากเน้นคือโปรโตคอลที่อยู่เหนือ IP โดยตรงเท่านั้นที่ไม่สามารถรันใน user space ได้ง่าย
หวังว่าอินเทอร์เน็ตจะเร็วขึ้นอีกเล็กน้อยในอนาคต
ในสภาพแวดล้อมสื่อสารอย่าง 5G อาจไม่รู้สึกถึงความต่าง แต่ก็ยังเป็นพัฒนาการที่มีคุณค่า
น่าสนใจที่มันมีโครงสร้าง handshake ของลิงก์แยกต่างหาก
เดิมเคยคิดว่า QUIC ฝัง TLS ไว้ในตัวทั้งหมด ซึ่งไม่ตรงนัก
แต่ก็คิดว่า latency ของเกมอาจลดลงได้จากเทคโนโลยีนี้
เมื่อทรัพยากรคอมพิวต์และประสิทธิภาพเครือข่ายดีขึ้น อุปสงค์ก็เพิ่มขึ้นตาม
สำหรับเกมหรือวิทยาศาสตร์คำนวณ เรื่องนี้อาจไม่เป็นปัญหาเพราะเป้าหมายคือ “ผลลัพธ์ที่ดีกว่า”
แต่บนเว็บ มักเกิดผลย้อนกลับจากโฆษณา การติดตาม และ JavaScript ที่เพิ่มขึ้น
bind(),connect(),listen(),accept()คล้าย TCP แต่หลังจากนั้นโครงสร้างจะต่างออกไป โดยใช้ syscallsendmsg()และrecvmsg()อยากให้มีคำอธิบายด้วยว่าทำไมจึงเลือกแนวทางนี้ และทำไมไม่สร้าง system call แยกสำหรับ QUIC โดยเฉพาะ