Zeroserve: eBPF로 스크립팅할 수 있는 무설정 웹 서버
요약
Zeroserve는 eBPF 프로그램을 사용하여 설정 파일 없이 요청을 처리하는 무설정 HTTPS 서버입니다. 단일 tarball 내에 정적 파일과 스크립트를 포함하며, io_uring 기반의 고성능 네트워크 처리를 지원합니다.
핵심 포인트
- eBPF를 활용한 사용자 공간 샌드박스 미들웨어 구현
- 설정 파일 대신 프로그램 코드로 라우팅 및 인증 처리
- 단일 tarball 배포 및 SIGHUP을 통한 원자적 핫 리로드
- io_uring 및 monoio 런타임을 통한 고성능 네트워크 I/O
- 작고 빠른
HTTPS 서버인 zeroserve는 웹사이트 tarball을 받아 HTTP/2와 TLS 1.3으로 제공하고, tarball 안의 eBPF 프로그램을 사용자 공간 샌드박스 미들웨어로 요청마다 실행함 - 구성 파일 없이 eBPF 프로그램이 요청별 라우팅, 헤더, 인증, 속도 제한, 프록시를 결정해 nginx·Caddy의 선언형 설정과 별도 스크립팅 계층을 하나로 합침
- 사이트는 단일
tar 파일로 인덱싱되고 디스크에 풀리지 않으며, tarball 교체와SIGHUP
으로 사이트·스크립트·TLS 자료를 연결 손실 없이 원자적으로 교체함
- 단일 코어 HTTPS 벤치마크에서 zeroserve는 소형 정적 파일 36,681 req/s, 10ms eBPF 동적 JSON 46,945 req/s, 소형 프록시 26,486 req/s를 기록했지만,
100KB 프록시는 nginx가 5,882 req/s로 우위임 - zeroserve는 nginx와 Caddy의 대안을 목표로 단일 tarball 배포, 프로그램형 설정, 사용자 공간 eBPF, 현대적 TLS를 결합하지만, 큰 프록시 응답에는 nginx가 더 적합함
개요
- zeroserve는 웹사이트 tarball 하나를 HTTP/2와 TLS 1.3으로 제공하는 작고 빠른 무설정 HTTPS 서버임
- tarball 안에 넣은 eBPF 프로그램은 모든 요청에서 사용자 공간 샌드박스 미들웨어로 실행되며, 요청 재작성, 인증, 속도 제한, 백엔드 리버스 프록시 처리 가능
- 단일 코어 기준으로 소형·대형 정적 파일, 스크립트 미들웨어, 소형 응답 프록시 대부분의 워크로드에서 nginx보다 높은 성능을 보이는 목표의 서버임
- eBPF 스크립트는 네이티브 코드로 JIT 컴파일되고 사용자 공간에서 샌드박싱되며, 요청마다 실행할 만큼 낮은 비용을 목표로 함
- 네트워크와 디스크 작업은 monoio 런타임을 통해
io_uring
으로 제출됨
- TLS 1.3, HTTP/2, Encrypted Client Hello, SNI 인증서 선택, JA4 핑거프린팅 지원
- 전체 사이트와 TLS 자료는 tarball 하나에서 제공되며,
SIGHUP
으로 핫 리로드 가능
구성 모델: 프로그램이 곧 설정
- zeroserve는 nginx와 Caddy의 대안을 목표로 하며, 핵심 설계 선택은 구성 방식임
- nginx와 Caddy는
location
블록, rewrite
규칙, map
지시어, try_files
같은 선언형 설정 언어를 제공하고, 한계에 도달하면 Lua나 Caddy 플러그인 같은 선택적 스크립팅 런타임을 옆에 붙이는 구조임
- 그 구조에서는 동작이 자체 제어 흐름을 가진 지시어 계층과 요청 생명주기의 특정 지점에서 실행되는 스크립트 계층으로 나뉨
- zeroserve에는 구성 파일이 없으며, eBPF 프로그램 하나가 모든 요청을 보고 라우팅, 헤더, 인증, 속도 제한, 프록시를 결정함
단일 tarball을 그대로 제공
- 전체 사이트는 하나의
tar
파일이며, zeroserve는 로드 시 path -> byte-range
맵을 만들고 tarball 자체에 바이트 범위 읽기를 수행해 파일 제공
- 어떤 파일도 디스크에 풀리지 않기 때문에 사이트는 단일 파일 안에만 존재하며, 잘못된
location
규칙이 노출할 문서 루트가 없음
- 배포는 단일 파일의 원자적 교체 방식이며, 새 버전 배포는 tarball 교체 뒤
SIGHUP
전송
- 디렉터리 패키징과 실행 명령은 다음 형식임
zeroserve --pack ./public > site.tar
zeroserve --addr 0.0.0.0:8080 site.tar
killall -SIGHUP zeroserve
- 리로드는 같은 프로세스 안에서 사이트, 스크립트, TLS 자료를 원자적으로 교체하고 연결 손실 없이 동작함
- 각 인스턴스는 단일 스레드 이벤트 루프이며, 프로세스 하나 기준으로는 제한이지만 확장 단위가 “더 많은 프로세스”일 때 맞는 형태로 제시됨
사용자 공간 eBPF 스크립팅
.zeroserve/scripts/
아래에 둔 모든 .c
파일은 패키징 시점에 clang
과 llc
로 eBPF 오브젝트로 컴파일되고 모든 요청에서 실행됨
- eBPF는 커널 BPF 서브시스템이나
CAP_BPF
없이 일반 비권한 프로세스 안의 async-ebpf 런타임에서 사용자 공간으로 실행됨
- async-ebpf는 uBPF를 내장해 바이트코드를 네이티브 x86-64 머신 코드로 JIT 컴파일함
- 포인터 케이지(pointer cage)는 JIT 컴파일된 코드의 모든 메모리 접근을 프로그램 전용 아레나로 마스킹해 잘못된 접근을 스크립트 메모리 안에 가둠
- 스크립트는 zeroserve의 단일 이벤트 루프에서 직접 실행되며, 느린 스크립트가 다른 연결을 멈추지 않도록 타이머가 JIT 컴파일된 네이티브 코드를 실행 중간에 인터럽트하고 제어를 이벤트 루프로 돌려줄 수 있음
- 프로그래밍 모델은 파일명 정렬 순서로 실행되는 스크립트 체인이며, 스크립트들은 요청별 메타데이터 맵을 공유함
- 스크립트가
zs_respond
나 zs_reverse_proxy
를 호출하면 체인은 단락 종료됨
zs.response.header.*
아래 키는 모든 응답의 헤더가 되며, 다른 키는 HTML 파일의 <zs-meta>visitor</zs-meta>
같은 플레이스홀더를 출력 시점에 치환하는 작은 템플릿 패스에 사용됨
- 헬퍼 표면은 요청 메서드·경로·쿼리·헤더·피어 주소 읽기, URI 재작성, 헤더 설정·삭제를 지원함
- 암호화와 인코딩 헬퍼는 SHA-256, HMAC-SHA256, base64, hex,
getrandom
제공
- JSON 헬퍼는 요청 본문 파싱, 문서 트리 생성·수정,
zs_json_respond
응답 지원
- 속도 제한은 피어 IP나 API 키 같은 임의 키 기반의 토큰 버킷을 지원하며, 상태는 핫 리로드 뒤에도 유지됨
- AWS SigV4 헬퍼는 S3와 기타 AWS 서비스 통신용 서명
Authorization
헤더와 presigned URL 지원
- OIDC 로그인은 Authorization Code + PKCE 기반 relying-party 흐름을 제공하며, 전체 로그인 세션을 sealed XChaCha20-Poly1305 쿠키에 담아 서버를 상태 비저장으로 유지한 채 정적 사이트를 “Google로 로그인” 뒤에 둘 수 있음
- 동적 엔드포인트는 특정 경로에서 스크립트가 직접 응답하는 방식이며, 예시에서는
/health
요청에 application/json
헤더와 {"status":"ok"}
본문을 반환함
- 각 스크립트는 기본 256KB 메모리 상한 아래 실행되며, 런타임은 오래 실행되는 스크립트를 실행기에서 시간 분할하고 폭주 스크립트를 스로틀링함
- 스크립트는
zs_call
로 서로 호출할 수 있으며, 호출 깊이는 제한됨
- 무한 루프에 빠진 스크립트는 자기 요청만 지연시키며, 선점 타이머가 이를 인터럽트해 서버가 다른 요청을 계속 처리함
- TLS 계층은 TLS 1.3 전용이며 BoringSSL로 종료됨
- Encrypted Client Hello는 실제 SNI가 평문으로 나타나지 않게 하며, 디렉터리 기반 SNI 인증서 선택과 스크립트에 노출되는 JA4 클라이언트 핑거프린팅 제공
- 투명 ECH 릴레이 모드는 복호화할 수 없는 핸드셰이크를 실제 업스트림으로 바이트 단위 그대로 전달해 보호된 이름이 공개 이름 뒤에 섞일 수 있게 함
성능
벤치마크 조건
- zeroserve, nginx 1.26, Caddy 2.11을 8코어 Ryzen 7 3700X에서 같은 콘텐츠와 같은 자체 서명 인증서로 HTTPS 제공 비교
- zeroserve 인스턴스가 설계상 단일 스레드이므로 비교 기준은 코어당 성능임
- 모든 서버는
taskset
으로 CPU 하나에 고정됐고, nginx는 worker_processes 1
, Caddy는 GOMAXPROCS=1
, zeroserve는 기존 단일 스레드 구조 사용
- 부하는 다른 코어에서
wrk -t4 -c100
으로 생성했고, 10초 실행 3회의 중앙값 사용
wrk
는 HTTP/1.1을 사용하므로 수치는 TLS 1.3 위의 HTTP/1.1이며, 긴 keep-alive 연결로 핸드셰이크 비용을 분산한 이미 열린 HTTPS 연결의 정상 상태 비용임
소형 정적 파일 174B
-
| 서버 | req/s | p99 |
-
| --- | ---: | ---: |
-
| zeroserve |
36,681 | 5.4 ms | -
| nginx | 31,226 | 7.8 ms |
-
| Caddy | 12,830 | 22 ms |
-
zeroserve는 단일 코어에서 nginx보다 약 17% 빠르게 소형 파일을 제공했고, 꼬리 지연도 더 낮음
-
HTML 페이지, 소형 JSON, CSS 같은 정적 사이트 기본 사례가 zeroserve 튜닝 대상임
대형 정적 파일 100KB
- | 서버 | req/s | 처리량 | 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 |
- 세 서버의 결과는 가까웠고, zeroserve가 단일 코어에서 약 780 MB/s로 약간 앞섬
- nginx의 대형 파일 강점인
sendfile()
은 TLS 아래에서 사용되지 않으며, 바이트를 사용자 공간에서 암호화해야 하므로 세 서버 모두 암호화와 쓰기 루프에 묶임
- kernel TLS를 세 서버 모두 끈 상태에서 zeroserve의
io_uring
읽기·쓰기 경로가 약간 더 빠른 결과임
eBPF vs Lua
- 스크립팅 비교 대상은 웹 서버 안에서 빠른 코드를 실행하는 일반적 방식인 nginx + LuaJIT
ngx_http_lua_module
임
- zeroserve는 기본값으로 스크립트 선점 타이머를 2ms마다 설정하며, 세밀한 간격은 문제 스크립트를 빠르게 스로틀링하지만 정상 스크립트에도 비용을 줌
- 기본 2ms에서는 완전 동적 응답 기준 eBPF가 약 32k req/s로 nginx Lua의 41k req/s보다 낮음
--preempt-timer-interval-ms
를 10으로 올리면 스크립팅 처리량이 약 40% 회복되고 결과가 뒤집힘
요청별 헤더 주입 미들웨어
- | 엔진 | req/s | p99 |
- | --- | ---: | ---: |
- | zeroserve eBPF 10ms |
43,709 | 5.1 ms | - | zeroserve eBPF 2ms 기본값 | 31,334 | 6.7 ms |
- | nginx Lua
header_filter
| 28,653 | 8.4 ms |
-
스크립트가 실행되지만 정적 파일은 계속 제공되는 미들웨어 사례에서 10ms eBPF가 nginx Lua보다 약 50% 높고 꼬리 지연도 더 낮음
완전 동적 JSON 응답
- | 엔진 | req/s | p99 |
- | --- | ---: | ---: |
- | zeroserve eBPF 10ms |
46,945 | 4.5 ms | - | nginx Lua
content_by_lua
| 41,231 | 6.4 ms |
- | zeroserve eBPF 2ms 기본값 | 32,393 | 6.7 ms |
- 10ms 간격의 조정된 eBPF는 완전 합성 응답에서도 nginx의
content_by_lua
보다 높은 처리량을 기록함
- 두 엔진 모두 네이티브 코드로 컴파일되며, LuaJIT는 트레이싱 JIT이고 async-ebpf는 uBPF를 통해 eBPF를 JIT 컴파일함
- TLS 암호화가 공통 요청 비용인 조건에서 조정된 eBPF 경로가 처리량에서 앞섬
- 2ms 기본값에서는 eBPF가 미들웨어 우위는 유지하지만 합성 응답 선두는 내주므로, 운영 스크립트에는 10ms 사용 권장
리버스 프록시로 사용
- zeroserve는
zs_reverse_proxy("http://127.0.0.1:9000";)
를 스크립트에서 호출해 백엔드로 프록시함
- 업스트림 연결 풀은 백엔드당 최대 128개 연결과 30초 유휴 재사용을 지원함
- 공정 비교를 위해 nginx는 기본적으로 요청마다 업스트림 연결을 닫는 특성을 고려해
keepalive 128
, proxy_http_version 1.1
, 비운 Connection
헤더를 명시 사용함
- Caddy는 기본 동작대로 연결 재사용
- 각 프록시는 단일 코어에서 TLS를 종료하고 공유 평문 백엔드로 전달했으며, 백엔드는 별도 2코어 서버로 자체 100k req/s를 유지해 프록시 오버헤드만 측정함
소형 174B 응답 프록시
- | 프록시 | 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 |
- zeroserve의 풀링된
io_uring
프록시는 nginx보다 약 22% 앞섰고 Caddy 대비 약 3.4배 처리량을 기록함
-
API 호출, 소형 JSON, 앱 서버 HTML 같은 일반적 프록시 워크로드에서 zeroserve가 TLS 종료와 백엔드 전달을 더 빠르게 수행함
100KB 응답 프록시
- | 프록시 | req/s | 처리량 |
- | --- | ---: | ---: |
- | nginx |
5,882 | 585 MB/s | - | Caddy | 4,285 | 406 MB/s |
- | zeroserve | 3,631 | 359 MB/s |
- 프록시 본문이 커지면 nginx의 버퍼링이 바이트를 더 효율적으로 이동해 앞서고, Caddy가 중간, zeroserve가 뒤처짐
- 프록시 응답이 크면 nginx가 더 나은 도구이며, 작고 많은 응답이면 zeroserve가 더 빠름
메모리
- 유휴 상태의 단일 zeroserve 인스턴스는 약 15MB PSS를 사용하며, nginx의 약 6MB보다 많고 Caddy의 약 60MB보다 적음
- 실행 단위가 전체 프로세스라는 점이 중요하며, 코어마다 복사본을 실행할 때 같은 바이너리를 매핑해 코드 페이지를 공유함
- 추가 프로세스는 자체 워킹셋 외에는 적은 메모리를 더함
공개
- zeroserve는 GitHub에서 오픈소스로 공개된 프로젝트임
AI 자동 생성 콘텐츠
본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기