원문 캡처 · github.com
원문 캡처 · github.com

Exponential Velocity는 기존 PHP 코드를 수정하지 않고 HTTP, 정적 파일, WebSocket, SSE를 한 프로세스에서 제공하도록 설계한 PHP 웹 서버다. 제작자는 copy-on-write(COW) 워커로 워커당 메모리를 줄여 nginx와 php-fpm 조합보다 더 많은 PHP 요청을 동시에 처리할 수 있다고 주장하지만, 성능 수치는 자체 벤치마크 결과이며 TLS 세션 재개 등 알려진 한계도 있다.

  • 배포 방식은 Linux·macOS용 바이너리, .deb·.rpm, Docker, PHP 8.0 이상에서 실행하는 .phar 등이다. 바이너리와 패키지는 빌드 도구가 지원하는 플랫폼에 한정된다.
  • 기본 실행은 php sbin/qbixserver.php이며, 기존 앱은 프레임워크별 --preset을 지정해 실행할 수 있다.
  • 기본 모드는 요청 사이에 워커를 재사용한다. 내부 정적 상태를 초기화할 수 없는 앱은 forkPerRequest: true를 설정할 수 있지만 지속 워커보다 느리다.
  • Windows에서는 pcntl_fork와 COW 대신 php-cgi 하위 프로세스를 사용하므로 COW 메모리 절감은 적용되지 않는다.

설치와 기존 앱 실행

저장소를 복제해 실행하거나, PHP가 포함된 자체 실행 바이너리를 내려받을 수 있다. 프로젝트의 빠른 시작 명령은 다음과 같다.

sh
git clone https://github.com/se7enxweb/exponential-velocity
cd exponential-velocity
php sbin/qbixserver.php

문서에 제시된 바이너리 다운로드 예시는 다음과 같다. Linux x86_64와 aarch64, Windows x64 명령이 안내돼 있지만 플랫폼 지원표에는 Windows 바이너리가 아직 빌드되지 않는다고 적혀 있다. macOS arm64 바이너리도 릴리스 지원 대상으로 기재돼 있다.

sh
# Linux x86_64
curl -LO https://github.com/se7enxweb/exponential-velocity/releases/latest/download/qbixserver-linux-x86_64
chmod +x qbixserver-linux-x86_64 && ./qbixserver-linux-x86_64

# Linux aarch64 -- also what a Raspberry Pi 4 or 5 runs
curl -LO https://github.com/se7enxweb/exponential-velocity/releases/latest/download/qbixserver-linux-aarch64
chmod +x qbixserver-linux-aarch64 && ./qbixserver-linux-aarch64

# Windows x64
curl -LO https://github.com/se7enxweb/exponential-velocity/releases/latest/download/qbixserver-windows-x64.exe
qbixserver-windows-x64.exe

PHP가 설치된 다른 환경에서는 .phar를 실행한다. 프로젝트 문서는 PHP 8.0 이상을 요구한다고 설명한다.

sh
curl -LO https://github.com/se7enxweb/exponential-velocity/releases/latest/download/qbixserver.phar
php qbixserver.phar --root=./web

기존 Laravel, Symfony, WordPress, Drupal, Exponential 앱은 루트 경로와 프리셋을 지정해 실행한다. 프리셋은 프런트 컨트롤러 경로, 업로드·메모리 제한, 세션 GC 설정의 기본값을 정하며 JSON 설정으로 덮어쓸 수 있다.

sh
cd my-laravel-app
php /path/to/sbin/qbixserver.php --root=public --preset=laravel --port=8080
sh
cd my-symfony-app
php /path/to/sbin/qbixserver.php --root=public --preset=symfony --port=8080
sh
cd my-wordpress-site
php /path/to/sbin/qbixserver.php --root=. --preset=wordpress --port=8080
sh
cd my-drupal-site
php /path/to/sbin/qbixserver.php --root=web --preset=drupal --port=8080
sh
cd my-exponential-site
php /path/to/sbin/qbixserver.php --root=. --preset=exponential --port=8080

프런트 컨트롤러가 있는 일반 PHP 앱은 --preset 없이 실행할 수 있다. 루트에 index.php가 있으면 깔끔한 URL을 자동으로 해당 파일에 연결하며, .htaccess가 있으면 RewriteRule과 RewriteCond를 적용한다고 프로젝트는 설명한다.

sh
php /path/to/sbin/qbixserver.php --root=public --port=8080

프로젝트는 소스 변환으로 header(), session_start(), setcookie(), ini_set() 등을 포함한 44개 PHP 함수를 가로채 요청 사이의 상태를 초기화한다고 밝힌다. 다만 define()으로 만든 상수와 stream_wrapper_register()는 지속 워커에서 요청 사이에 남을 수 있다. 마이그레이션·가져오기처럼 오래 실행되는 작업은 --workers=1 또는 CLI 실행을 권한다. 사용자 정의 PECL 확장처럼 C 수준 상태를 보유하는 확장도 초기화되지 않을 수 있으며, 문서는 PDO·curl·mbstring 등 표준 확장은 문제없다고 설명한다.

성능 주장과 측정 조건

프로젝트가 제시한 설계는 부모 프로세스가 프레임워크를 불러온 뒤 pcntl_fork()로 워커를 만드는 방식이다. 커널의 COW가 읽기 전용 메모리를 공유하므로 각 워커는 자체적으로 바꾼 페이지에 대해서만 메모리를 부담한다. 프로젝트 측정치로는 아무것도 불러오지 않은 워커가 사유 메모리(private memory) 약 1.3~1.9MB, 전체 CMS를 불러온 워커가 약 10MB다. RSS는 공유 페이지를 워커별로 중복 계산하므로 이 수치의 측정 기준으로 쓰지 않았다고 덧붙인다. 이벤트 루프의 워커 풀은 약 1,000개가 상한이며, stream_select를 쓰는 조건에서 4GB 서버로 수백 개의 CMS 워커 또는 약 2,000개의 소형 앱 워커를 실행할 수 있다는 설명이다.

기본 지속 워커 방식은 각 요청 뒤 Reflection 기반 스냅샷으로 정적 속성을 복구하고 전역 변수와 shim 함수 상태를 초기화한다. 제작자가 제시한 초기화 시간은 소형 앱 약 0.5ms, 클래스 약 600개인 CMS 4~5ms로, 일반 페이지 렌더링의 약 1.5%라고 한다. 이 모드에서 CPU 중심 작업은 2,294 req/s, I/O 작업은 1,060 req/s를 기록했다고 주장한다. 내부 정적 변수나 추적할 수 없는 전역 상태 때문에 지속 모드가 맞지 않으면 요청마다 새 fork를 만드는 forkPerRequest: true를 사용할 수 있다. 이 방식은 지속 모드보다 느리지만 워커당 메모리가 수십 MB 대신 수 MB 수준이므로 같은 메모리에서 php-fpm보다 많은 워커를 둘 수 있다는 것이 프로젝트의 설명이다.

프로젝트의 비교표는 다음 수치를 제시한다. 비교 대상과 조건은 각 항목의 표기대로이며, 전체 벤치마크 방법은 BENCHMARKS.md, 요청 사이 상태 복구 범위는 reset.md에 설명돼 있다.

항목nginx + php-fpmVelocity
워커 메모리30–60MB(복제)1.3–1.9 MB 기본, 전체 CMS 약 10 MB(COW, 사유 메모리 측정)
동시 PHP 처리(1GB)약 24개 워커약 530개 기본, 전체 CMS 약 90개(stream_select에서 풀당 약 1,000개 미만)
CPU 중심 처리량약 400 req/s(Swoole 4워커)2,294 req/s(100워커)
I/O 처리량(같은 RAM)78 req/s(fpm/Swoole 4워커)1,060 req/s(100워커)
WebSocket별도 서버 필요내장
설정 예시nginx와 fpm 풀·소켓php sbin/qbixserver.php

이는 프로젝트가 게시한 비교 수치이지 독립 검증 결과는 아니다. 자체 부하 도구는 폐쇄 루프(closed loop) 방식으로 동작한다. 요청이 끝날 때마다 새 요청을 보내므로 포화 상태에서 지연이 길어질 요청 일부는 애초에 보내지 않게 되는 coordinated omission 문제가 있다. 따라서 백분위 지연은 사용자가 경험할 지연이 아니라 서버가 받아 처리한 요청의 지연으로 읽어야 한다고 프로젝트는 경고한다.

sh
php tests/bench-load.php https://localhost:8443/
php tests/bench-load.php http://127.0.0.1:8088/ --levels=1,4,16,64 --requests=500
php tests/bench-load.php https://host/ --http1 # compare protocols
php tests/bench-load.php https://host/ --json # for a pipeline

도구는 처리량이 더 늘지 않는 동시성 지점을 찾고 CSV를 작성하며, 코어 수·메모리·부하·워커 수·상주 메모리·열린 핸들 등 측정 당시의 시스템 정보도 출력한다. 프로젝트가 든 backlog 사례에서는 동시 요청 64개일 때 backlog 32의 처리량이 745 req/s, p99가 1067.0ms였고, backlog 1024에서는 2703 req/s, p99가 19.5ms였다. 동시 요청 128개에서는 각각 603 req/s와 1317.0ms, 2773 req/s와 37.9ms였다. 프로젝트는 기본 backlog 32에서 커널이 SYN을 버리고 클라이언트가 1초 뒤 재전송하는 상황이 수치에 영향을 줬다고 설명한다.

TLS는 별도의 성능 제약으로 제시된다. 같은 페이지에서 TLS를 쓸 때 최대 처리량은 1481 req/s, 쓰지 않을 때는 2961 req/s였다. 새 연결 6개의 시간 분해 예시는 DNS 7.99ms, TCP 0.32ms, TLS 20.96ms, 서버 1.46ms다. 프로젝트는 TLS 세션 재개가 아직 작동하지 않는다고 밝히며, openssl s_client -reconnect가 매번 New를 반환하고 Reused는 반환하지 않는다고 보고한다. 이는 프로젝트가 밝힌 가장 큰 미해결 항목이다. nginx와 비교할 때는 일반 HTTP도 함께 측정해야 하며, 그렇지 않으면 OpenSSL 성능을 주로 측정하게 된다는 설명이다.

기능, 설정과 운영

Velocity는 nginx·Apache·php-fpm 없이 한 프로세스에서 HTTP, WebSocket(Socket.IO 프로토콜 포함), SSE, 세션, 업로드, 정적 파일, .htaccess를 처리한다고 소개한다. Redis나 Node.js를 통한 pub/sub 구성도 필요 없다는 주장이다. 앱 코드·자산·SQLite 데이터베이스를 실행 파일 하나로 묶는 배포도 지원한다고 한다. SQLite 파일은 첫 실행 때 데이터 디렉터리로 복사되며 프레임워크 설정이 그 위치를 가리키도록 작성된다. Qbix, Laravel, Symfony, WordPress, Craft CMS, Drupal을 지원 대상으로 열거한다. 이 실행 파일은 기존 브라우저를 열어 앱을 표시하므로 Chromium을 포함하는 Electron보다 작다고 설명하며, 본문에는 약 5MB라는 수치와 다른 대목의 4.5MB 바이너리 표현이 함께 나온다.

관리 패널에서는 도메인·별칭·하위 도메인·문서 루트, HTTP→HTTPS 및 사용자 지정 리디렉션, HSTS, 오류 페이지, 인증서, 로그, 메트릭, 앱 검사를 다룬다. Let's Encrypt(ACME) 인증서 발급·갱신과 SNI에 따른 도메인별 인증서 선택도 내장 기능으로 제시한다. 패널은 기본적으로 꺼져 있는 TOTP 2단계 인증과 복구 코드를 지원한다. 대시보드·로그·메트릭 접근에는 인증이 필요하며, 기본 비밀번호 변경 강제와 로그인 잠금도 마련했다고 프로젝트는 설명한다. 허용 목록에 등록한 진입 스크립트와 정적 경로만 제공·실행하고 나머지는 프런트 컨트롤러로 보낸다.

설정은 JSON이며, 아래는 원문에 제시된 HTTPS 수동 인증서, HTTP/2, 정적 파일 캐시 기간, 응답 캐시, 사전 압축, backlog와 워커 설정 예시다.

json
{
 "Q": {
 "web": {
 "https": { "mode": "manual",
 "cert": "/etc/certs/fullchain.pem",
 "key": "/etc/certs/privkey.pem" },
 "http2": { "enabled": true },
 "static": { "maxAge": 31536000 },
 "cache": {
 "enabled": true,
 "dir": "files/cache/reverse",
 "defaultTtl": 0,
 "staleWhileRevalidate": 60,
 "revalidateLockSeconds": 30,
 "apcu": { "enabled": true, "maxSize": 65536 },
 "skip": { "cookies": ["PHPSESSID", "Q_sid"] }
 }
 },
 "webserver": {
 "backlog": 1024,
 "workers": 16,
 "precompress": { "enabled": true, "dir": "var/tmp/precompress" }
 }
 }
}

설정 문서의 주요 기본값은 Q.webserver.backlog 1024, 워커 수 자동 산정, 역방향 캐시 비활성화, staleWhileRevalidate 0, 정적 파일 maxAge 0이다. skip.cookies는 캐시를 우회시키는 쿠키 목록이다. 프로젝트는 앱의 세션 쿠키를 목록에 넣지 않으면 로그인 사용자에게 다른 사람의 페이지를 제공할 수 있고, 무관한 쿠키를 넣으면 방문자 모두 캐시를 우회할 수 있다고 경고한다. 한 설치 사례에서는 같은 호스트의 다른 앱이 남긴 쿠키가 기본 목록과 일치해 첫 페이지가 캐시 응답 74ms 대신 렌더링에 1300ms 걸렸다고 한다.

응답 캐시는 부모 프로세스에 있어 캐시 적중 시 워커에 요청을 보내거나 fork하지 않는다고 설명한다. 예시 수치는 캐시 적중 0.6ms, 전체 렌더링 1382ms다. 페이지 본문에서 만든 해시를 ETag로 사용해 내용이 바뀌지 않은 페이지는 다시 빌드해도 태그가 유지되게 하며, 압축 전후 표현에는 서로 다른 태그를 부여한다고 한다. 유효기간이 지난 뒤 staleWhileRevalidate를 쓰면 하나의 요청만 새 응답을 만들고 나머지는 기존 응답을 받는다. 8개 동시 요청 예시에서는 모두 새로 렌더링하던 상황 대신 한 요청이 1893ms에 새 응답을 만들고, 나머지 일곱 요청은 약 62ms에 기존 응답을 받았다. 오래된 응답에는 실제 경과 시간을 담은 Age와 X-Cache: STALE이 붙는다.

X-Cache-Refresh: 1은 저장된 사본을 건너뛰어 응답을 다시 만들고 저장한다. 프로젝트는 이를 Cache-Control: no-cache와 다르게 둔 이유로, 브라우저의 일반 새로고침이나 크롤러가 매번 렌더링을 강제하지 않도록 하기 위함이라고 설명한다.

sh
curl -H 'X-Cache-Refresh: 1' https://example.test/ # warm, don't just read

플랫폼별로 Linux·BSD·macOS에서는 pcntl_fork를 통한 COW 워커를 사용한다. PHP 8.6 이상에서는 Linux의 epoll, BSD·macOS의 kqueue를 제공하는 Io\Poll API를 감지해 사용하고, 이전 버전에서는 stream_select를 쓴다. 문서는 후자가 틱마다 O(n)으로 동작한다고 설명한다. Revolt는 설치돼 있으면 사용할 수 있지만, ev·event·uv 확장을 통한 epoll을 포함해 동시 연결 2,000개까지 측정했을 때 stream_select보다 빠르지 않았다고 한다. riscv64는 qemu-riscv64에서 PCRE JIT를 켠 순수 정규식 테스트가 코어 덤프를 내고 끄면 51개 테스트를 통과했다. 프로젝트는 이를 에뮬레이터 문제로 보고하며 실제 하드웨어에서는 시험하지 않았다고 명시한다.

지원 범위와 주의점

자체 실행 바이너리는 Linux x86_64·aarch64와 macOS arm64용이 배포된다고 적혀 있다. Linux aarch64 바이너리는 Raspberry Pi 4·5에서 쓸 수 있다고 안내한다. Windows x64 바이너리는 아직 빌드되지 않았으며, Windows에서 .phar를 통한 실행도 테스트되지 않았다. CI에서 부팅과 페이지 제공을 확인했다고 기재한 플랫폼은 Linux glibc의 x86_64, aarch64, armv5, armv7, i386, ppc64le, riscv64, s390x, Alpine/musl의 x86_64·aarch64·i386, FreeBSD 14, DragonFly BSD, OmniOS다. 다만 이 테스트는 지원을 보장한다는 뜻이 아니라 프로젝트가 시험한 플랫폼이라고 설명한다. AIX, IBM i, Solaris 11, Haiku는 PHP 8 이상이 있으나 CI 실행기가 없거나 시험이 끝나지 않아 ‘예상되지만 검증되지 않음’으로 분류한다. OpenBSD와 NetBSD도 작업 중이라고 적었다.

기본 파일 경로는 0.0.4.41부터 UNIX·Linux 관례에 맞춰 정리했다. 서버와 관리 명령은 sbin/, 일반 사용자 명령은 bin/에 두며, 이전 경로도 같은 프로그램·인수·출력·종료 상태로 동작하는 전달 경로로 남겼다고 한다. 패키지는 qbixserver, qbixctl, qbixconsole을 /usr/sbin에 설치하고 /usr/bin에는 링크를 둔다. systemd는 /usr/sbin/qbixserver를 실행한다. 0.0.4.42부터 셸도 설치하며, uwebserver의 기본 수신 주소는 127.0.0.1:8000으로 바뀌었다. 이전에는 모든 인터페이스의 8080 포트에서 수신했다. --user 또는 --allow-root 없이 root로 실행하지 않으며 알 수 없는 옵션을 받으면 실행을 시작하지 않는다. 변경 사항과 업그레이드 안내는 저장소 문서에 있다.

보안 설명에는 경로 탈출 검사, CR/LF가 든 헤더 차단, 실제 전송 본문에 따른 Content-Length 계산, HPACK 입력 범위 검사, HTTP/2 리소스 제한이 포함된다. HTTP/2 제한은 동시 스트림, 헤더 목록 크기, CONTINUATION 프레임(CVE-2024-27316), 스트림 생성·초기화 속도(CVE-2023-44487) 등을 다루며, 과부하에는 ENHANCE_YOUR_CALM으로 응답한다고 한다. 취약점은 공개 이슈로 신고하지 말고 SECURITY.md가 있으면 그 지침을 따르거나 유지관리자에게 비공개로 연락하라고 안내한다.

테스트 명령은 다음과 같다. 단위 테스트는 네트워크나 인증서 없이 PHP만으로 실행할 수 있고, 전체 셸 테스트는 실제 소켓과 TLS 테스트도 수행한다고 프로젝트는 설명한다.

sh
php tests/run-unit.php # everything self-contained, ~3s
php tests/run-unit.php hpack # only tests whose name matches
bash tests/run.sh # the above, then real sockets and TLS

저장소에는 SQLite CRUD·REST API·정적 HTML을 다루는 todo, SQLite 저장과 GET/POST를 다루는 counter, WebSocket 방·Socket.IO를 다루는 chat, SSE와 AI 토큰 스트리밍을 다루는 stream, 이벤트와 클러스터 복제를 다루는 swarm, 협업 편집을 다루는 collab 예제가 있다. todo 실행 예시는 다음과 같다.

sh
php sbin/qbixserver.php --root=examples/todo/web --port=8080

설치·기능·이전 안내와 벤치마크는 Exponential Velocity 저장소에서 확인할 수 있다. 소스는 MIT 라이선스이며, 저작권 표기는 원래 웹 서버 엔진의 Qbix, Inc.에 2024–2026년, Exponential Velocity의 7x에 2025–2026년으로 기재돼 있다.

부록: 원문 예시

본문의 "기능, 설정과 운영" 대목에 딸린 원문 예시다.

text
rebuild 1  etag="ae368d…-gzip"  last-modified=20:48:29 rebuild 2  etag="ae368d…-gzip"  last-modified=20:48:30 rebuild 3  etag="ae368d…-gzip"  last-modified=20:48:31