- zeroserve là một máy chủ HTTPS nhỏ gọn và nhanh, nhận tarball của website rồi phục vụ qua HTTP/2 và TLS 1.3, đồng thời chạy chương trình eBPF bên trong tarball như middleware sandbox không gian người dùng cho từng request
- Không cần tệp cấu hình; chương trình eBPF quyết định định tuyến theo từng request, header, xác thực, giới hạn tốc độ và proxy, hợp nhất cấu hình khai báo kiểu nginx·Caddy với một tầng script riêng thành một thể thống nhất
- Website được lập chỉ mục thành một tệp tar duy nhất và không giải nén ra đĩa; thay tarball và gửi
SIGHUP sẽ hoán đổi website, script và dữ liệu TLS một cách nguyên tử mà không làm rớt kết nối
- Trong benchmark HTTPS đơn nhân, zeroserve đạt 36,681 req/s với tệp tĩnh nhỏ, 46,945 req/s với JSON động eBPF 10ms, và 26,486 req/s với proxy nhỏ; tuy nhiên với proxy 100KB thì nginx dẫn trước ở mức 5,882 req/s
- zeroserve nhắm tới việc trở thành lựa chọn thay thế cho nginx và Caddy bằng cách kết hợp triển khai bằng một tarball duy nhất, cấu hình kiểu lập trình, eBPF không gian người dùng và TLS hiện đại, nhưng với phản hồi proxy lớn thì nginx vẫn phù hợp hơn
Tổng quan
- zeroserve là một máy chủ HTTPS nhỏ gọn, nhanh và không cần cấu hình, phục vụ một tarball website duy nhất qua HTTP/2 và TLS 1.3
- Các chương trình eBPF đặt trong tarball sẽ chạy như middleware sandbox không gian người dùng cho mọi request, có thể xử lý rewrite request, xác thực, giới hạn tốc độ và reverse proxy backend
- Đây là máy chủ hướng tới hiệu năng cao hơn nginx trên hầu hết các workload theo đơn nhân như tệp tĩnh nhỏ·lớn, middleware script và proxy phản hồi nhỏ
- Script eBPF được JIT compile thành mã native và sandbox trong không gian người dùng, với mục tiêu chi phí đủ thấp để chạy trên từng request
- Tác vụ mạng và đĩa được gửi qua
io_uring thông qua runtime monoio
- Hỗ trợ TLS 1.3, HTTP/2, Encrypted Client Hello, chọn chứng chỉ theo SNI, và fingerprinting JA4
- Toàn bộ website và dữ liệu TLS được phục vụ từ một tarball duy nhất, có thể hot reload bằng
SIGHUP
Mô hình cấu hình: chương trình chính là cấu hình
- zeroserve nhắm tới việc trở thành lựa chọn thay thế cho nginx và Caddy, và lựa chọn thiết kế cốt lõi nằm ở cách cấu hình
- nginx và Caddy cung cấp ngôn ngữ cấu hình khai báo như khối
location, quy tắc rewrite, chỉ thị map, try_files, rồi khi chạm giới hạn thì gắn thêm runtime script tùy chọn như Lua hoặc plugin Caddy
- Trong cấu trúc đó, hành vi bị chia thành một tầng chỉ thị có luồng điều khiển riêng và một tầng script chạy tại các điểm cụ thể trong vòng đời request
- zeroserve không có tệp cấu hình; một chương trình eBPF duy nhất nhìn thấy mọi request và quyết định routing, header, xác thực, giới hạn tốc độ và proxy
Phục vụ nguyên trạng một tarball duy nhất
- Toàn bộ website là một tệp
tar, và khi tải vào, zeroserve tạo map path -> byte-range rồi đọc theo phạm vi byte ngay trên chính tarball đó để phục vụ tệp
- Không có tệp nào được giải nén ra đĩa, nên website chỉ tồn tại trong một tệp duy nhất và không có document root nào có thể bị lộ do quy tắc
location sai
- Triển khai là thay thế nguyên tử một tệp duy nhất; để phát hành phiên bản mới, thay tarball rồi gửi
SIGHUP
- Lệnh đóng gói thư mục và chạy có dạng như sau
zeroserve --pack ./public > site.tar
zeroserve --addr 0.0.0.0:8080 site.tar
- Lệnh hot reload có dạng như sau
killall -SIGHUP zeroserve
- Việc reload sẽ hoán đổi website, script và dữ liệu TLS một cách nguyên tử trong cùng tiến trình, hoạt động mà không làm mất kết nối
- Mỗi instance là một event loop đơn luồng; đây là giới hạn nếu xét theo một tiến trình, nhưng phù hợp khi đơn vị mở rộng là “nhiều tiến trình hơn”
Script eBPF không gian người dùng
- Mọi tệp
.c đặt dưới .zeroserve/scripts/ sẽ được compile thành object eBPF bằng clang và llc tại thời điểm đóng gói, rồi chạy trên mọi request
- eBPF chạy trong không gian người dùng trong runtime async-ebpf bên trong một tiến trình thường không đặc quyền, không cần kernel BPF subsystem hay
CAP_BPF
- async-ebpf tích hợp uBPF để JIT compile bytecode thành mã máy x86-64 native
- Pointer cage sẽ mask mọi truy cập bộ nhớ của mã JIT vào một arena dành riêng cho chương trình, nhốt các truy cập sai phạm bên trong bộ nhớ của script
- Script chạy trực tiếp trên event loop đơn của zeroserve; để script chậm không chặn các kết nối khác, timer có thể ngắt mã native JIT đang chạy giữa chừng và trả quyền điều khiển về event loop
- Mô hình lập trình là một chuỗi script chạy theo thứ tự sắp xếp tên tệp, và các script chia sẻ một map metadata theo từng request
- Nếu script gọi
zs_respond hoặc zs_reverse_proxy thì chuỗi sẽ kết thúc sớm
- Các khóa dưới
zs.response.header.* sẽ trở thành header của mọi response; các khóa khác được dùng trong một bước template nhỏ để thay thế placeholder như <zs-meta>visitor</zs-meta> trong tệp HTML tại thời điểm xuất
- Bề mặt helper hỗ trợ đọc method·path·query·header·địa chỉ peer của request, rewrite URI, đặt·xóa header
- Các helper mã hóa và encoding cung cấp SHA-256, HMAC-SHA256, base64, hex,
getrandom
- Helper JSON hỗ trợ parse body request, tạo·sửa cây tài liệu, và phản hồi bằng
zs_json_respond
- Giới hạn tốc độ hỗ trợ token bucket dựa trên khóa tùy ý như IP của peer hoặc API key, và trạng thái vẫn được giữ sau hot reload
- Helper AWS SigV4 hỗ trợ header
Authorization có chữ ký và presigned URL để giao tiếp với S3 và các dịch vụ AWS khác
- Đăng nhập OIDC cung cấp luồng relying-party dựa trên Authorization Code + PKCE; toàn bộ session đăng nhập được đặt trong cookie XChaCha20-Poly1305 sealed để máy chủ vẫn stateless, cho phép đặt website tĩnh phía sau “Đăng nhập bằng Google”
- Endpoint động hoạt động theo cách script tự phản hồi trực tiếp tại một đường dẫn cụ thể; ví dụ trả về header
application/json và body {"status":"ok"} cho request /health
- Mỗi script chạy dưới giới hạn bộ nhớ mặc định 256KB; runtime sẽ chia thời gian thực thi cho các script chạy lâu và throttle các script mất kiểm soát
- Các script có thể gọi lẫn nhau bằng
zs_call, với độ sâu gọi bị giới hạn
- Script rơi vào vòng lặp vô hạn chỉ làm chậm request của chính nó; timer tiền nhiệm sẽ ngắt nó để máy chủ tiếp tục xử lý các request khác
- Tầng TLS chỉ hỗ trợ TLS 1.3 và được terminate bằng BoringSSL
- Encrypted Client Hello giúp SNI thực không xuất hiện dưới dạng plaintext, đồng thời cung cấp chọn chứng chỉ SNI theo thư mục và fingerprinting client JA4 được lộ cho script
- Chế độ transparent ECH relay chuyển nguyên trạng từng byte các handshake không thể giải mã tới upstream thực, cho phép tên được bảo vệ trộn phía sau tên công khai
Hiệu năng
-
Điều kiện benchmark
- So sánh zeroserve, nginx 1.26 và Caddy 2.11 khi phục vụ HTTPS với cùng nội dung và cùng chứng chỉ tự ký trên Ryzen 7 3700X 8 nhân
- Vì instance zeroserve về thiết kế là đơn luồng, tiêu chí so sánh là hiệu năng trên mỗi lõi
- Tất cả máy chủ đều bị ghim vào một CPU bằng
taskset; nginx dùng worker_processes 1, Caddy dùng GOMAXPROCS=1, còn zeroserve dùng cấu trúc đơn luồng sẵn có
- Tải được tạo từ lõi khác bằng
wrk -t4 -c100, lấy trung vị của 3 lần chạy, mỗi lần 10 giây
wrk dùng HTTP/1.1, nên các con số là HTTP/1.1 trên TLS 1.3, tức chi phí trạng thái ổn định của các kết nối HTTPS đã mở sẵn với keep-alive dài để phân bổ chi phí handshake
-
Tệp tĩnh nhỏ 174B
| Máy chủ |
req/s |
p99 |
| zeroserve |
36,681 |
5.4 ms |
| nginx |
31,226 |
7.8 ms |
| Caddy |
12,830 |
22 ms |
- zeroserve phục vụ tệp nhỏ trên một lõi nhanh hơn nginx khoảng 17%, đồng thời có độ trễ đuôi thấp hơn
- Các trường hợp cơ bản của website tĩnh như trang HTML, JSON nhỏ, CSS là mục tiêu tối ưu của zeroserve
-
Tệp tĩnh lớn 100KB
| Máy chủ |
req/s |
Thông lượng |
p99 |
| zeroserve |
8,000 |
782 MB/s |
22 ms |
| nginx |
7,600 |
773 MB/s |
28 ms |
| Caddy |
6,084 |
590 MB/s |
44 ms |
- Kết quả của ba máy chủ khá sát nhau, với zeroserve nhỉnh hơn một chút ở khoảng 780 MB/s trên một lõi
sendfile() vốn là thế mạnh của nginx với tệp lớn không được dùng dưới TLS, vì byte phải được mã hóa trong không gian người dùng, nên cả ba máy chủ đều bị ràng buộc bởi vòng lặp mã hóa và ghi
- Khi kernel TLS đều bị tắt trên cả ba, đường đọc·ghi
io_uring của zeroserve cho kết quả nhanh hơn đôi chút
eBPF vs Lua
- Đối tượng so sánh cho phần script là nginx + LuaJIT
ngx_http_lua_module, cách phổ biến để chạy mã nhanh bên trong máy chủ web
- Theo mặc định, zeroserve đặt timer tiền nhiệm script mỗi 2ms; khoảng thời gian càng mịn giúp throttle script có vấn đề nhanh hơn nhưng cũng tạo thêm chi phí cho script bình thường
- Ở mặc định 2ms, với phản hồi động hoàn toàn, eBPF đạt khoảng 32k req/s, thấp hơn 41k req/s của nginx Lua
- Khi tăng
--preempt-timer-interval-ms lên 10, thông lượng xử lý script phục hồi khoảng 40% và kết quả đảo chiều
-
Middleware chèn header theo từng request
| Engine |
req/s |
p99 |
| zeroserve eBPF 10ms |
43,709 |
5.1 ms |
| zeroserve eBPF 2ms mặc định |
31,334 |
6.7 ms |
nginx Lua header_filter |
28,653 |
8.4 ms |
- Trong trường hợp middleware nơi script chạy nhưng tệp tĩnh vẫn tiếp tục được phục vụ, eBPF 10ms cao hơn nginx Lua khoảng 50% và cũng có độ trễ đuôi thấp hơn
-
Phản hồi JSON động hoàn toàn
| Engine |
req/s |
p99 |
| zeroserve eBPF 10ms |
46,945 |
4.5 ms |
nginx Lua content_by_lua |
41,231 |
6.4 ms |
| zeroserve eBPF 2ms mặc định |
32,393 |
6.7 ms |
- eBPF được tinh chỉnh với khoảng 10ms đạt thông lượng cao hơn
content_by_lua của nginx ngay cả với phản hồi tổng hợp hoàn toàn
- Cả hai engine đều compile thành mã native; LuaJIT là tracing JIT, còn async-ebpf JIT compile eBPF thông qua uBPF
- Trong điều kiện mà mã hóa TLS là chi phí request chung, đường eBPF đã tinh chỉnh dẫn trước về thông lượng
- Với mặc định 2ms, eBPF vẫn giữ lợi thế ở middleware nhưng mất vị trí dẫn đầu ở phản hồi tổng hợp, nên khuyến nghị dùng 10ms cho script vận hành
Dùng làm reverse proxy
- zeroserve proxy tới backend bằng cách gọi
zs_reverse_proxy("http://127.0.0.1:9000") trong script
- Pool kết nối upstream hỗ trợ tối đa 128 kết nối cho mỗi backend và tái sử dụng kết nối rỗi trong 30 giây
- Để so sánh công bằng, do nginx mặc định đóng kết nối upstream sau mỗi request, nó được cấu hình rõ ràng với
keepalive 128, proxy_http_version 1.1, và header Connection để trống
- Caddy tái sử dụng kết nối theo hành vi mặc định
- Mỗi proxy terminate TLS trên một lõi rồi chuyển tiếp tới backend plaintext dùng chung; backend chạy trên máy chủ 2 lõi riêng và tự giữ được 100k req/s, nên chỉ đo overhead của proxy
-
Proxy phản hồi nhỏ 174B
| Proxy |
req/s |
p50 |
p99 |
| zeroserve |
26,486 |
3.3 ms |
8 ms |
| nginx |
21,761 |
4.2 ms |
10.5 ms |
| Caddy |
7,683 |
10.3 ms |
33 ms |
- Proxy
io_uring có pooling của zeroserve vượt nginx khoảng 22% và đạt thông lượng gấp khoảng 3.4 lần so với Caddy
- Với các workload proxy phổ biến như API call, JSON nhỏ, HTML từ app server, zeroserve thực hiện terminate TLS và chuyển tiếp backend nhanh hơn
-
Proxy phản hồi 100KB
| Proxy |
req/s |
Thông lượng |
| nginx |
5,882 |
585 MB/s |
| Caddy |
4,285 |
406 MB/s |
| zeroserve |
3,631 |
359 MB/s |
- Khi body phản hồi proxy lớn hơn, cơ chế buffering của nginx di chuyển byte hiệu quả hơn nên dẫn đầu, Caddy ở giữa, còn zeroserve tụt lại
- Khi phản hồi proxy lớn, nginx là công cụ tốt hơn; còn với phản hồi nhỏ nhưng số lượng nhiều, zeroserve nhanh hơn
Bộ nhớ
- Một instance zeroserve đơn ở trạng thái nhàn rỗi dùng khoảng 15MB PSS, cao hơn khoảng 6MB của nginx nhưng thấp hơn khoảng 60MB của Caddy
- Điều quan trọng là đơn vị thực thi là toàn bộ tiến trình; khi chạy một bản sao cho mỗi lõi, chúng ánh xạ cùng một binary và chia sẻ các trang mã
- Các tiến trình bổ sung chỉ thêm ít bộ nhớ ngoài working set riêng của chúng
Công bố
- zeroserve là một dự án mã nguồn mở được công bố trên GitHub
1 bình luận
Ý kiến trên Hacker News
Khi benchmark máy chủ web của TechEmpower biến mất, có vẻ như các dự án mới kiểu này có ít cơ hội hơn để tự chứng minh mình
Sửa: có lẽ tôi bị chậm thông tin, và thứ đang nổi lên gần đây hình như là https://www.http-arena.com/leaderboard/. Chúc may mắn
Chỉ là vốn dĩ họ cũng không chạy thường xuyên, nhìn lịch sử các vòng thì chưa đến một lần mỗi năm
Tôi thích việc thấy những thử nghiệm như thế này xuất hiện vì LLM đã giúp việc khám phá trở nên tương đối rẻ và nhanh
Tuy vậy, điều tôi rút ra ở đây là bản thân nginx khá ấn tượng. Điểm khác cũng đáng chú ý là dự án này tự mô tả là một lựa chọn thay thế cho nginx và Caddy, và đặt cược vào cách cấu hình
nginx và Caddy cung cấp ngôn ngữ cấu hình khai báo, và khi chạm tới giới hạn của chúng thì lại gắn thêm một runtime script như Lua hoặc plugin Caddy ở bên cạnh, nên hành vi bị chia thành hai tầng
Nhưng tôi nghĩ cách đặt cược đó là sai. Từ rất lâu rồi, mọi người đã thích cấu hình hơn là mã, và trong nhiều trường hợp chỉ cần tính năng tích hợp sẵn là đủ, không cần phải viết mã C
Mọi định dạng tệp cấu hình dường như ban đầu đều khá đơn giản. Ngay cả YAML lúc đầu cũng khá hợp lý, rồi mọi người bắt đầu muốn những thứ phức tạp hơn với anchor và alias
Ngay cả GitLab cũng có định dạng riêng kiểu như điều kiện và biến, và nó gần như là một mớ hack chỉ hoạt động ở những chỗ cụ thể. Apache cũng đi theo con đường tương tự với định dạng cấu hình dựa trên XML
Cuối cùng thì xuất hiện vô số ngôn ngữ lập trình tùy biến để quản lý cấu hình. Trong môi trường doanh nghiệp, người ta thậm chí không chỉnh trực tiếp mà viết script cho quy trình Ansible để thao tác từ xa
Nếu ngay từ đầu máy chủ được nhúng sẵn một trình thông dịch như Lua hay Python để quản lý cấu hình thì có lẽ đã bỏ qua được cả quá trình đó, và sẽ đơn giản hơn việc chỉnh các tệp cấu hình tùy biến bằng chương trình
Tất nhiên có thể nói những nỗ lực tùy biến đó được tối ưu cho mục đích cụ thể hơn ngôn ngữ phổ thông, nhưng lập luận đó ngay từ đầu chỉ đúng trong phạm vi hẹp của các ví dụ đồ chơi vốn chẳng cần đến cơ chế ấy
Còn nhớ file INI của Windows không. Đó là thời đẹp đẽ khi mã là mã và dữ liệu là dữ liệu
Hoặc đơn giản hơn, đọc toàn bộ manifest Ingress của một cụm Kubernetes rồi dựng lại pack từ đó
Ý chính là giao diện giữa công cụ và cấu hình cũng chỉ là một API nữa mà thôi, và quản trị viên hệ thống từ lâu đã mô tả trạng thái hệ thống bằng những cấu trúc ở cấp cao hơn, còn những byte cấu thành cấu hình chỉ là đầu ra của quá trình đó
Từ góc nhìn của AI thì cách đó có thể dễ xử lý hơn. AI làm được cả hai phía, nên có thể sẽ còn lâu trước khi một sự chuyển dịch như vậy thực sự được xem là ý tưởng rõ ràng tốt hơn
Tôi thích ý tưởng này
Chỉ là tôi sẽ yên tâm hơn nếu trong thư mục eBPF có thể đặt tệp
.rsthay vì.c. Đằng nào đây cũng đã là một dự án RustVà tôi cũng đã phần nào kỳ vọng vào một máy chủ web tăng tốc bằng kernel. Nếu có thể làm điều đó một cách an toàn bằng eBPF thì sẽ thực sự rất ấn tượng
Ngoài ra, chỉ một luồng thôi sao? Trên Linux, fork và chia sẻ hàng đợi kết nối đến là chuyện gần như tầm thường, và với Rust cũng chỉ mất vài dòng. Dùng SO_REUSEPORT thì phần còn lại kernel sẽ lo
Tiện nói luôn, nếu định theo đuổi io_uring thì tôi nghĩ cũng nên theo luôn kTLS. Nếu tránh được xử lý SSL trong không gian người dùng sau bắt tay thì thiết kế sẽ đơn giản hơn rất nhiều
Từ trước đến giờ tôi vẫn dùng nftables cho kiểu mục đích này nên chưa trực tiếp cần đến
Rất ngầu. Tôi tò mò liệu có thể kết hợp thứ này với chương trình XDP hay các loại chương trình BPF khác như chương trình gắn vào socket map, để tích hợp các tính năng HTTP lớp 7 xuống tầng thấp hơn không
Ý tưởng thì hay, nhưng tôi không chắc có nên tập trung vào tệp tĩnh hay không. Dạo này hiếm khi người ta dựng hẳn một máy chủ mới chỉ cho mục đích đó
Vì thế cái này có cảm giác như được làm riêng cho tôi, dù tôi cũng thừa nhận mình không phải người dùng điển hình
Trông khá ổn và tính năng cũng được. Nhưng có gì đó quá nhân tạo nên không khiến tôi thực sự bị thuyết phục
Không rõ các chỉ số có giả hay không, các hàm tiện ích có thực sự hoạt động hay không, hay đã có quá trình hardening tử tế chưa
Việc làm bằng vibe coding và cả README được tự động tạo ra thì tôi còn chấp nhận được. Nhưng đến cả bài blog công bố cũng do AI tạo, và hoàn toàn không có cơ sở nào để đánh giá liệu mức hiểu biết về chất lượng phần mềm của họ có giống tôi hay không
Đúng là một thế giới kỳ lạ. Nếu vài năm trước bài công bố được đăng mà không nói gì về AI thì có lẽ tôi đã chấp nhận không chút nghi ngờ, còn bây giờ cứ thấy một README bóng bẩy với các tham số dòng lệnh có vẻ hợp lý là tôi lập tức nghi README đang hallucinate, và thực ra có thể chẳng hề có các tùy chọn đó
Khi làm zeroserve thì tôi có dùng AI hỗ trợ khá nhiều, nhưng tôi tự kiểm tra đầu ra của AI và cũng chính tôi chịu trách nhiệm
zeroserve phục vụ tệp nhỏ trên một lõi đơn nhanh hơn nginx khoảng 17% và độ trễ đuôi cũng hẹp hơn. Đây là trường hợp zeroserve phát huy tốt với trang HTML, JSON nhỏ và CSS
Với tệp tĩnh lớn 100KB, zeroserve đạt 8,000 req/s, 782 MB/s, p99 22ms; nginx là 7,600 req/s, 773 MB/s, p99 28ms; còn Caddy là 6,084 req/s, 590 MB/s, p99 44ms
Dù vậy, tôi vẫn sẽ chọn một dự án lâu đời đã được audit, chứng minh trong thực tế và hardening kỹ lưỡng hơn là một dự án mới như thế này. Mức cải thiện không đủ lớn để đáng chấp nhận rủi ro
Tôi quyết định ở lại với thời đại cũ càng lâu càng tốt. Những người thông minh phát hành phần mềm, và những người thông minh bảo trì nó. Họ không cần AI. Đó là ngách của tôi
Có thể chúng tôi sẽ biến mất, nhưng tôi vẫn thấy như vậy tốt hơn. Chỉ là điều đó đi kèm giả định rằng những người thông minh đó có viết tài liệu. Cũng có nhiều người giỏi nhưng ghét viết tài liệu
Từ lâu tôi đã quyết rằng phần mềm không có tài liệu thì dù xuất sắc đến đâu cũng không đáng để tôi bỏ thời gian. Chủ yếu tôi nói về ứng dụng; tôi gần như không đọc tài liệu Linux, dù người khác bảo nó cũng không tệ lắm, nên tôi cũng không rõ
Đây là một khái niệm mới thú vị và tôi thích nó
Câu hỏi thực sự là mức độ cam kết của nhà phát triển và cộng đồng. Những người đứng sau Caddy và Nginx đã hỗ trợ sản phẩm một cách bền bỉ, và dự án này cũng sẽ cần rất nhiều sự tập trung và quan tâm
Tại sao lại là tarball?
Nó không giải nén gì ra đĩa. Vì toàn bộ site nằm trọn trong một tệp đó, nên sẽ không có document root nào để các quy tắc location sai lộ ra, và việc triển khai cũng trở thành một lần thay thế tệp duy nhất theo kiểu atomic
Tuy vậy, ngay cả lời giải thích đó cũng có thể là kiểu hợp thức hóa của LLM. Rải rác khắp bài có những cách diễn đạt như “the right shape” hay “the surface is broad”