- Khoảng 20% lưu lượng HTTP của Firefox sử dụng HTTP/3, vốn hoạt động trên QUIC và UDP
- Bằng cách thay thế lớp I/O mạng hiện có là NSPR bằng quinn-udp dựa trên Rust, Firefox tăng cường hiệu năng và độ an toàn bộ nhớ
- Firefox chủ động tận dụng các system call hiện đại theo từng hệ điều hành (multi-message, segmentation offloading, v.v.) để tối ưu hiệu năng
- Trên Windows và MacOS, một số tính năng bị giới hạn do vấn đề tương thích, driver và các yếu tố khác, nhưng trên Linux đã xác nhận được hiệu năng tối ưu
- Những kinh nghiệm thử nghiệm, vấp váp và sửa lỗi liên quan đến hỗ trợ QUIC ECN và UDP I/O trên nhiều nền tảng được kỳ vọng sẽ hữu ích cho các dự án tương lai và hệ sinh thái mã nguồn mở
Động lực
- Khoảng 20% lưu lượng HTTP của Firefox sử dụng HTTP/3, công nghệ này hoạt động thông qua QUIC, và được triển khai trên UDP
- Về mặt lịch sử, Firefox dùng thư viện NSPR cho I/O mạng, nhưng các tính năng liên quan đến UDP I/O đã cũ và hạn chế (các hàm chính là
PR_SendTo, PR_RecvFrom)
- Gần đây, các hệ điều hành đã cung cấp các system call đa thông điệp (ví dụ
sendmmsg, recvmmsg) và các tối ưu mạng như segmentation offload (GSO, GRO)
- Những kỹ thuật này có thể cải thiện đáng kể hiệu năng của UDP I/O
- Firefox đã tìm cách xem liệu có thể thay thế stack UDP I/O hiện có bằng các system call hiện đại để tận dụng các lợi ích này hay không
Tổng quan
- Dự án bắt đầu vào giữa năm 2024, với mục tiêu xây dựng lại stack QUIC UDP I/O của Firefox bằng các system call hiện đại trên mọi hệ điều hành được hỗ trợ
- Bên cạnh cải thiện hiệu năng, dự án cũng hướng tới tăng cường bảo mật bằng cách sử dụng Rust bảo đảm an toàn bộ nhớ cho UDP I/O
- Bản thân QUIC đã được triển khai bằng Rust, nên việc phát triển được thực hiện trên thư viện quinn-udp dựa trên Rust
- Sự khác biệt về system call giữa các hệ điều hành làm tăng độ khó phát triển, nhưng nhờ quinn-udp mà tốc độ phát triển được cải thiện đáng kể
- Tính đến giữa năm 2025, việc triển khai đang được áp dụng cho phần lớn người dùng Firefox, và benchmark hiệu năng cho thấy mức tăng lớn, lên tới 4Gbit/s
Cấu trúc UDP I/O và các phương thức tối ưu hiện đại
Truyền một datagram đơn
- Cách làm cũ dùng
sendto và recvfrom, chỉ gửi và nhận một UDP datagram mỗi lần
- Chi phí chuyển đổi giữa user space và kernel space phải trả theo từng datagram, nên trong môi trường lưu lượng lớn là kém hiệu quả
- Ví dụ: để gửi các gói nhỏ hơn 1500 byte ở mức hàng trăm Mbit/s mỗi giây sẽ phát sinh overhead đáng kể
Truyền theo lô nhiều datagram
- Linux và một số hệ điều hành khác hỗ trợ các system call đa thông điệp như
sendmmsg và recvmmsg
- Có thể gửi và nhận nhiều datagram trong một lần, từ đó giảm mạnh overhead
Một datagram phân đoạn cỡ lớn duy nhất
- Các công nghệ offload như GSO (gửi), GRO (nhận) cho phép tự động chia một UDP datagram lớn thành nhiều phần tại hệ điều hành hoặc NIC để truyền đi
- Giao diện mạng xử lý việc tách theo từng gói, tính checksum và gắn header
- Nhờ đó, phía ứng dụng có thể xử lý nhiều gói thực tế chỉ với một system call duy nhất
- Khi bật GSO, một số công cụ mạng như Wireshark hỗ trợ phân tích gói chưa tốt
Quá trình thay thế NSPR trong Firefox
- Trước tiên, Firefox thay thế NSPR bằng quinn-udp trong cấu trúc gửi và nhận datagram đơn
- Pipeline xử lý datagram của triển khai QUIC được refactor để hỗ trợ gửi/nhận theo lô và segmentation
- Cả lời gọi multi-message lẫn segmentation offload đều được sử dụng tùy theo tình huống
- Đồng thời bổ sung xử lý ngoại lệ theo từng nền tảng và nhiều cải tiến I/O khác nhau
Chi tiết theo từng nền tảng
Windows
- Windows cung cấp
WSASendMsg/WSARecvMsg, hỗ trợ datagram theo kích thước MTU truyền thống hoặc datagram phân đoạn cỡ lớn
- Tương ứng với GSO/GRO trên Linux là USO (gửi) / URO (nhận) trên Windows
- Ban đầu chỉ dùng lời gọi datagram đơn nên không có vấn đề, nhưng khi bật URO thì ở một số môi trường cụ thể (ví dụ Windows on ARM + WSL) đã xuất hiện lỗi tải trang thất bại do không thể xác định độ dài gói QUIC
- Firefox cũng đã dùng USO/gửi, nhưng phát hiện tác dụng phụ như tăng mất gói và crash driver mạng trong môi trường cài đặt Firefox trên Windows
- Hiện tại Firefox đang giữ URO/USO ở trạng thái vô hiệu hóa và tiếp tục debug thêm
MacOS
- Trên MacOS, Firefox đưa quinn-udp vào bằng cách dùng
sendmsg/recvmsg thay cho sendto/recvfrom
- Tính năng segmentation offload không được bật
- Thay vào đó,
sendmsg_x/recvmsg_x không được tài liệu hóa chính thức hỗ trợ truyền theo lô, và đã được áp dụng không chính thức vào quinn-udp
- Do Apple có thể loại bỏ các lời gọi này trong tương lai, tính năng này không được bật mặc định, chỉ thử nghiệm và không được đưa vào bản phát hành thực tế
Linux
- Hỗ trợ đầy đủ sendmmsg/recvmmsg và GSO/GRO, và quinn-udp mặc định ưu tiên GSO khi gửi
- Firefox dùng socket UDP riêng cho từng kết nối để tăng cường quyền riêng tư (phân biệt theo 4-tuple)
- Trong cấu trúc này, lợi ích của segmentation offload được tối đa hóa, còn lợi ích truyền chéo của sendmmsg/recvmmsg thì bị hạn chế
- Ngoài các thay đổi nhỏ như sandbox mạng và kiểm tra hỗ trợ GSO lúc chạy, việc đưa vào đã thành công mà không gặp khó khăn lớn
Android
- Khác với Linux, Android có đường xử lý system call và bộ lọc bảo mật (ví dụ: seccomp) khác biệt
- Tồn tại nhiều vấn đề tương thích như hỗ trợ các nền tảng rất cũ như Android 5 dựa trên x86, обход
socketcall, xử lý lỗi, v.v.
- Trong một số môi trường, lời gọi gửi khi bật bit ECN gây lỗi (EINVAL), nên quinn-udp áp dụng chiến lược thử lại và tắt tùy chọn
- Nhờ nhiều cải tiến khác nhau từ cộng đồng Quinn, Firefox cũng có thể tự động hưởng lợi từ các cải tiến đó
Hỗ trợ ECN (Explicit Congestion Notification)
- Nhờ áp dụng các system call hiện đại, việc gửi và nhận ancillary data (dữ liệu phụ trợ) được hỗ trợ, từ đó có thể hỗ trợ QUIC ECN
- Dù có một số lỗi nhỏ, trên Firefox Nightly hơn một nửa các kết nối QUIC hoạt động qua đường outbound ECN
- Khi các công nghệ mới như L4S ngày càng được chú ý, tầm quan trọng và khả năng ứng dụng của hỗ trợ ECN cũng tăng lên
Tóm tắt kết luận
- Firefox đã thay thế lớp QUIC UDP I/O bằng triển khai Rust dựa trên quinn-udp, đồng thời đạt được cả hiệu năng lẫn bảo mật
- Thay vì các system call cũ, Firefox tận dụng các system call I/O hiện đại theo từng hệ điều hành, qua đó cải thiện thông lượng và hỗ trợ ECN
- Một số tính năng tối ưu trên Windows và các nền tảng khác vẫn cần cải thiện thêm do vấn đề tương thích
- Khi tỷ lệ sử dụng QUIC tiếp tục tăng, hỗ trợ ở cấp hệ điều hành/driver cũng sẽ tiếp tục phát triển trong tương lai
1 bình luận
Ý kiến trên Hacker News
Ý chính của bài lại nằm ẩn ở giữa
Cải tiến ở đây đúng là cần thiết để đạt tốc độ cực cao thực sự (trên 100Gb/s), nhưng 4Gb/s thì thật ra không nhanh lắm
500MB/s nghĩa là ở đâu đó đang có một nút thắt cổ chai nghiêm trọng
Chuyển ngữ cảnh kernel được nói là ở mức 1us, mà thật ra với system call thì khá cao
Tuy vậy, chỉ cần trung bình mỗi gói cỡ 500 byte là đã có thể đạt 500MB/s, tức 4Gb/s
Mức 1Gb/s trước đây là khi kích thước gói nhỏ hơn, và việc chỉ đẩy gói UDP vào buffer của NIC thì ngay cả tốc độ sao chép bộ nhớ cũng dư sức làm được
Dù nói mã hóa chậm, thực tế lại không hẳn như vậy
Ví dụ, Intel i5-6500 từng đạt tốc độ AES-128 GCM là 1729MB/s
CPU hiện nay có thể đạt 3-5GB/s mỗi lõi, tức 25-40Gb/s, nên mức 4Gb/s được nói ở đây là quá thấp
(liên kết tham khảo hiệu năng AES-128 GCM)
Có nói độ trễ system call cao, nhưng nguyên nhân có thể là do các biện pháp giảm thiểu spectre & meltdown
TCP có path binding, còn UDP thì không, nên có khác biệt lớn trong cách thiết lập đường đi
Nói mã hóa chậm thì đúng với PDU (đơn vị dữ liệu giao thức) nhỏ
Phần lớn tối ưu hóa và benchmark đều dựa trên khung TCP dung lượng lớn, nên trong thực tế với gói nhỏ thì chi phí thiết lập trạng thái nổi bật hơn nhiều
Nếu chạy microbenchmark trong tight loop thì số liệu sẽ đẹp, nhưng trong môi trường thực có tính ngẫu nhiên thì hiệu quả tận dụng cache cũng thấp hơn, và với gói dưới 1KB thì hiệu suất tụt mạnh
Thêm vào đó còn có overhead framing bổ sung, kiểm tra tính hợp lệ của dữ liệu out-of-band, v.v., đều khá tốn kém
Bộ nhớ buffer UDP mặc định cũng thiếu, gây nhiều vấn đề khi dùng thực tế
Người ta đã liên tục tăng kích thước buffer khi vận hành TCP, còn UDP thì vẫn kẹt ở các giá trị bảo thủ từ thập niên 90~00
API thực sự cần thiết là fork fd, hỗ trợ đầy đủ
connect(2)và route binding, rồi sau đó là dựa trên submission queue (uring,rio, v.v.)Về mặt mã hóa, cách tiếp cận KDF có thể giảm đáng kể chi phí trạng thái
Cách làm PSP được một số vendor công nhận nhưng bị từ chối khá nhiều ở IETF và nơi khác nên không phổ biến rộng rãi
Trong các bài kiểm tra đồng thời quy mô lớn của vendor, nó cho thấy mức độ mở rộng vượt trội hơn hẳn các loại TLS truyền thống
Hoàn toàn không hề nhắc đến CPU dùng để benchmark thuộc phân khúc nào
Và overhead mã hóa là chi phí xử lý của chính giao thức QUIC
QUIC vẫn kém TCP về khả năng offload mã hóa (xử lý bằng phần cứng), trong khi TCP phần nào có thể giao cho NIC xử lý thông qua offload kTLS
Nội dung kỹ thuật kiểu này thật sự rất thỏa mãn
Giá mà mọi tài liệu kỹ thuật của Mozilla đều sâu và chắc tay như thế này, do kỹ sư thực chiến viết bài bản
Không có kiểu lạc quan sáo rỗng, rất đáng đọc
Không hiểu vì sao vẫn còn hỗ trợ Android 5
Nó đã phát hành hơn 10 năm rồi, và người dùng thiết bị đó còn là dạng legacy cũ hơn nữa
Web ngày nay quá nặng, nên ngay cả việc duyệt web bình thường trên mấy thiết bị cổ như vậy cũng đã khó, nên khá tò mò vì sao vẫn phải hỗ trợ
Có lẽ chỉ là các hacker sửa lại mấy máy đời cũ kiểu OnePlus ngày xưa, cắm sạc cố định, không cài cả các bản ROM đại trà như LineageOS, rồi dùng Firefox qua kho ứng dụng thay thế
Trên thực tế đây là cái giá làm chậm toàn bộ tốc độ phát triển
Cũng nên đọc một giai thoại thú vị liên quan trong blog phát triển Factorio có tựa đề "The map download struggle"
(bài blog liên quan)
Ai từng xử lý vấn đề mạng ngoài đời thật sẽ còn đồng cảm hơn vì mấy mystery packet runts
Phần lớn thiết bị mạng xử lý loại gói này rất kém
Lưu lượng dựa trên UDP hoặc QUIC rất dễ bị tấn công nếu không phải ở môi trường cloud đủ lớn
Vì thế các nhà cung cấp hosting nhỏ hoặc tự vận hành ngày càng khó sống, và chỉ còn những nơi có năng lực xử lý lưu lượng lớn tồn tại được
Bởi vậy trong phần lớn môi trường LAN, người ta đang drop hầu hết lưu lượng UDP và chỉ xử lý phần cần thiết bằng rate limit
Trình theo dõi lỗi của Mozilla
Trên macOS và Fedora, khi truy cập các trang do Cloudflare host bằng Firefox, tôi vẫn gặp hiện tượng y hệt
Hôm nay mới biết là trên Windows và macOS cũng có tính năng tương tự GSO/GRO (xử lý gói mạng kích thước lớn)
Chỉ tiếc là nghe nói thực tế có rất nhiều lỗi
Có cảm giác không chỉ riêng GSO/GRO mới có bug
Có ai giải thích được về mặt cấu trúc UDP GSO/GRO hoạt động như thế nào không?
UDP là gói không có thứ tự, vậy khi một gói QUIC bị tách thành nhiều gói UDP mà trong header cũng không có thông tin thứ tự, thì phía nhận làm sao ghép lại đúng thứ tự?
Ý tưởng là kernel đặt nhiều datagram vào cùng một cấu trúc, nhưng vẫn giữ ranh giới giữa chúng qua các tầng khác nhau (ví dụ data fragments trong
sk_buff)Tôi không phải chuyên gia chính xác về chuyện này, nhưng khi tìm hiểu cách nó hoạt động tôi đã tham khảo bài viết này
Có nhắc rằng "chúng tôi bắt đầu phát triển dựa trên quinn-udp, thư viện UDP I/O của dự án Quinn, nhờ đó tốc độ phát triển nhanh hơn rất nhiều"
Vậy không biết họ có tài trợ cho dự án Quinn hay không
(liên kết tài trợ Quinn)
Tôi đã hỏi trực tiếp về chuyện hỗ trợ tài chính, thì một Senior Principal Software Engineer của Mozilla trả lời rằng "Mozilla không có tiền"
Nhưng họ đóng góp cực kỳ nhiều mã nguồn nên chúng tôi thật sự rất biết ơn
(Tôi là maintainer chính của Quinn)
Với câu hỏi "Có tài trợ không?", có ý kiến cho rằng cách Mozilla làm là không cần tài trợ open source, mà thay vào đó chi thêm vài triệu USD cho lương CEO
Trong khi ngay cả sản phẩm chủ lực (Firefox) cũng đang xuống dốc
Nếu họ có đóng góp theo cách khác như mã nguồn thì tôi cũng muốn biết
Việc
sendmmsg/recvmmsgđược gọi là “hiện đại” thật đáng ngạc nhiênThật ra đây là các system call đã tồn tại khá lâu rồi
Cũng hơi tiếc vì trong nội dung liên quan Linux lại không nhắc đến
io_uringio_uringkhông có khả năng batch thực sự để xử lý nhiều UDP datagram cùng lúcCố lắm thì cũng chỉ là gửi yêu cầu nhiều
sendmsg,recvmsgcùng lúcGSO/GRO mới là đáp án đúng
sendmmsg/recvmmsgđã là công nghệ rất cũ, và trong số các kernel developer giờ còn có người muốn loại bỏ nó(thảo luận GitHub liên quan)