대세가 apache에서 nginx로 넘어간 오늘날 backend 웹 서버로 또는 frontend edge reverse proxy 웹 서버로 nginx를 쓰는 사이트를 보기가 매우 쉬워졌습니다. nginx는 가장 빠르고 설정하기 쉬우며 많은 기능을 가진 웹 서버로 이름을 널리 알렸고 그 값어치는 수많은 벤치마크와 실제 운용 경험에 대한 여러 보고로 충분히 검증된 상태입니다.

그리고 오늘날, HTTP/2는 급격하게 성장하고 있는 웹 프로토콜입니다. HOL(head-of-line) blocking 문제 해결과 한 TCP 연결로 multiplexing을 지원하고 HTTP/1.1에서 소개된 pipelining, HPACK, 브라우저 차원 TLS통신 의무화 등 기존의 비효율적인 HTTP 통신 구조를 개선하고 TCP 자체에서 발생하는 문제를 최소화하며 네트워크 트래픽까지 줄일 수 있는 이 프로토콜은 다양한 글로벌 CDN의 지원, 웹 브라우저의 지원을 바탕으로 크게 인기를 끌고 있습니다. 참고: HTTP/1.1 파이프라이닝, HTTP/2의 이점으로 https는 http보다 더 빠른 통신속도를 보이고 있습니다.

nginx는 ngx_http_spdy_module을 시작으로 SPDY를 지원했었으며 1.9.5 버전부터는 ngx_http_v2_module을 통해 HTTP/2에 대한 지원을 하기 시작했습니다.

h2o

h2o는 HTTP/2에 최적화된 웹 서버입니다. HTTP/1.0, HTTP/1.1 fallback은 당연히 지원하고 cache-aware server push, TCP Fast Open(by default) 등 웹 서버 및 클라이언트 성능을 최적화하기 위한 여러 기능이 내장되어 있습니다. 외에 기본적으로 내장된 LibreSSL을 사용하기 때문에 암호화된 HTTP/2 통신에서 새로운 ciphersuite인 ChaCha20-Poly1305을 지원하는데 이는 OpenSSL 1.1.0부터 지원되는 기능이기도 합니다.

또한 자체적으로 밝힌 바, nginx보다 h2o가 더 빠르다는 벤치마크도 존재합니다.

다만 이런 벤치마크의 특성상 과연 내가 벤치마크해도 저런 결과가 나올까? 하는 의문이 들어 직접 설치해서 테스트해보기로 했습니다.

설치

ubuntu 16.04에 공식 홈페이지에 나와있는 설명대로 설치를 진행했습니다. LibreSSL이 OpenSSL보다 약간 느리기 때문에 벤치마크를 위해서라면 OpenSSL을 사용하라는 내용이 존재하지만 이를 무시하고 그냥 설치를 진행했습니다. 설치는 git 레포를 복제받아 진행했으며 대상 커밋은 ad1b607 입니다.

$ cmake -DWITH_BUNDLED_SSL=on .
...
$ make -j8
...
$ sudo make install
$ h2o -v
h2o version 2.2.0-DEV
OpenSSL: LibreSSL 2.4.4

테스트에 쓰인 nginx는 nginx 공식 레포에서 미리 컴파일된 패키지를 설치했습니다:

$ nginx -V
nginx version: nginx/1.10.3
built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.2)
built with OpenSSL 1.0.2g-fips  1 Mar 2016 (running with OpenSSL 1.0.2g  1 Mar 2016)
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-file-aio --with-threads --with-ipv6 --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_ssl_module --with-cc-opt='-g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' --with-ld-opt='-Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie'

설정

h2o는 다음과 같이 설정했습니다:

max-connections: 10240

hosts:
  "x.x.x.x:8081":
    listen:
      port: 8081
      ssl:
        certificate-file: examples/h2o/alternate.crt
        key-file: examples/h2o/alternate.key
      send-server-name: OFF
    paths:
      /:
        file.dir: examples/doc_root
    # access-log: /dev/null

설정 제일 아래 부분은 기본값으로 stdout에 로그가 출력되어 /dev/null로 로그를 보내다가 이렇게 하면 로그를 /dev/null에 던지는 코드가 계속 실행되는 부분을 발견하여 주석처리 하였습니다.

nginx는 다음과 같이 설정했습니다. h2o 소스를 보지는 않았지만 기본적인 튜닝은 되어있을 것이라는 판단 하에 nginx 또한 최상의 성능을 끌어내기 위해 다양한 튜닝을 했습니다:

worker_processes  auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections 10240;
    multi_accept on;
    accept_mutex_delay 100ms;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    open_file_cache max=100 inactive=20s;
    tcp_nopush on;
    keepalive_timeout  65;

    server {
        listen       443 ssl http2 fastopen=256 default_server;
        server_name  _;

        location / {
            root   .../h2o/examples/doc_root;
            index  index.html index.htm;
        }

        ssl on;
        ssl_certificate .../h2o/examples/h2o/alternate.crt;
        ssl_certificate_key .../h2o/examples/h2o/alternate.key;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        # ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
        ssl_ciphers "ECDHE-RSA-AES128-GCM-SHA256";
        ssl_prefer_server_ciphers on;
        ssl_ecdh_curve secp384r1;
        ssl_session_cache shared:SSL:10m;
        ssl_session_tickets off;
        ssl_session_timeout 10m;

        access_log off;
    }
}
  • worker_processes: auto로 설정하여 자동으로 유효한 CPU 코어수로 지정합니다.
  • worker_connections: 한 worker가 받을 수 있는 connection 제한을 설정합니다.
  • multi_accept: 한 worekr가 동시에 여러 요청을 받을 수 있게 합니다. (이 옵션에 대해서는 여러 의견이 있지만 벤치마크를 돌려보고 on쪽이 성능이 더 좋아 켜두었습니다.)
  • accept_mutex_delay: accept()할 때 mutex 확보에 실패했을 때 기다릴 시간을 지정합니다. 기본값은 500ms인데 성능 향상을 위해 적당히 값을 줄입니다.
  • open_file_cache: file descriptor, size, update time, inode 등을 캐시할 방법을 지정합니다. (max: 최대 캐시, inactive: 캐시 만료시간)
  • tcp_nopush: nginx에서 사용하는 소켓에 TCP_CORK(리눅스), TCP_NOPUSH(BSD)를 지정합니다. 이를 지정하여 응답 헤더와 내용을 한번에 묶어 보내 패킷의 수를 최소화할 수 있습니다.
  • fastopen: TCP 3-way handshake시 payload에 데이터를 같이 보내 RTT를 줄입니다.
  • access_log: 로깅을 꺼서 성능을 향상시킵니다.

테스트에 사용된 index.html파일은 다음과 같습니다:

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to H2O</title>
  </head>
  <body>
    


<p>Welcome to H2O - an optimized HTTP server</p>



    


<p>It works!</p>



  </body>
</html>

벤치마크

다음 명령어로 벤치마크를 진행했습니다:

$ h2load -n 100000 -c 1000 https://x.x.x.x:xxx -t 16
...
$ echo "GET https://x.x.x.x:xxx" | vegeta attack -http2 -insecure -duration=10s -rate=1000 | tee result.nginx.bin | vegeta report -reporter="hist[0,.15ms,.3ms,.45ms,.6ms,.75ms,.9ms,1ms,2ms,5ms]"

우선 nginx의 결과입니다:

starting benchmark...
spawning thread #0: 63 total client(s). 6250 total requests
...
spawning thread #15: 62 total client(s). 6250 total requests
TLS Protocol: TLSv1.2
Cipher: ECDHE-RSA-AES128-GCM-SHA256
Server Temp Key: ECDH P-384 384 bits
Application protocol: h2
progress: 10% done
...
progress: 100% done

finished in 12.09s, 8272.15 req/s, 2.35MB/s
requests: 100000 total, 100000 started, 100000 done, 100000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 100000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 28.37MB (29749000) total, 9.73MB (10200000) headers (space savings 39.64%), 16.88MB (17700000) data
                     min         max         mean         sd        +/- sd
time for request:      516us       1.30s     74.59ms     59.11ms    85.52%
time for connect:   608.48ms       3.66s       2.70s    968.20ms    82.00%
time to 1st byte:   836.13ms       3.75s       3.19s       1.09s    82.00%
req/s           :       8.30       51.32       15.11       14.38    82.00%
Bucket           #     %       Histogram
[0s,     150µs]  0     0.00%
[150µs,  300µs]  0     0.00%
[300µs,  450µs]  342   3.42%   ##
[450µs,  600µs]  2617  26.17%  ###################
[600µs,  750µs]  6000  60.00%  #############################################
[750µs,  900µs]  207   2.07%   #
[900µs,  1ms]    19    0.19%
[1ms,    2ms]    257   2.57%   #
[2ms,    5ms]    36    0.36%
[5ms,    +Inf]   522   5.22%   ###

다음 h2o의 결과입니다:

starting benchmark...
spawning thread #0: 63 total client(s). 6250 total requests
...
spawning thread #15: 62 total client(s). 6250 total requests
TLS Protocol: TLSv1.2
Cipher: ECDHE-RSA-AES128-GCM-SHA256
Server Temp Key: ECDH P-256 256 bits
Application protocol: h2
TLS Protocol: TLSv1.2
Cipher: ECDHE-RSA-AES128-GCM-SHA256
Server Temp Key: ECDH P-256 256 bits
Application protocol: h2
progress: 10% done
...
progress: 100% done

finished in 8.25s, 12126.65 req/s, 2.44MB/s
requests: 100000 total, 100000 started, 100000 done, 100000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 100000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 20.10MB (21074909) total, 1.47MB (1544909) headers (space savings 91.27%), 16.88MB (17700000) data
                     min         max         mean         sd        +/- sd
time for request:      276us    227.02ms     71.42ms     13.34ms    92.24%
time for connect:   349.15ms    991.57ms    732.81ms    157.30ms    63.60%
time to 1st byte:   405.79ms       1.06s    809.04ms    169.83ms    64.10%
req/s           :      12.02       19.16       12.62        0.79    90.70%
Bucket           #     %       Histogram
[0s,     150µs]  0     0.00%
[150µs,  300µs]  0     0.00%
[300µs,  450µs]  657   6.57%   ####
[450µs,  600µs]  5740  57.40%  ###########################################
[600µs,  750µs]  3318  33.18%  ########################
[750µs,  900µs]  63    0.63%
[900µs,  1ms]    7     0.07%
[1ms,    2ms]    148   1.48%   #
[2ms,    5ms]    9     0.09%
[5ms,    +Inf]   58    0.58%

결과

nginx는 8272.15rps, h2o는 12126.65rps로 h2o가 약 46%정도 더 빠른 부분을 확인할 수 있었습니다. 이 테스트는 TLS handshake가 끝난 후 시작되는 테스트이기 때문에 TLS 통신 과정에서 발생하는 변수는 매우 적으리라 생각하고 vegeta 테스트에서 h2o 쪽이 훨씬 안정적인 히스토그램을 보여주고 있기 때문에 h2o의 승이라고 볼 수 있겠습니다.

물론 nginx의 성능이 부족할 정도의 규모를 운영하는 사이트는 매우 드물고 가장 많이 쓰이는 reverse proxy 기능의 경우 nginx쪽이 h2o보다 더 많은 옵션을 제공하고 있으며, nginx가 훨씬 production ready에 가깝기 때문(검증)에 굳이 h2o로 넘어갈 이유는 없습니다. 오버엔지니어링(overengineering)이 될 수도 있겠고요.

하지만 정말 완전한 HTTP/2 기능을 원하고 조금이라도 더 빠른 것을 추구하는 변태라던가, 간단한 설정만으로 돌아가는 웹 서버를 찾는다면 h2o는 매우 훌륭한 선택이 될 것입니다. 시대를 따라가는 면에서 말이죠.

제 개인적으로는 nginx에 질려서 다양한 웹 서버를 사용해보고 있는데 최근에 Caddy라는 Go로 짜여진 웹 서버를 사용하고 있습니다. 자동으로 Let's Encrypt를 통해 HTTPS를 제공해주고 아무런 설정 없이 Qualys SSL labs 테스트에서 A등급 이상을 보장해주는 장점에 experimental feature(실험적 기능)로 제공되는 QUIC에 반해서 아예 정착하게 되었습니다. 버그를 찾아 PR을 보내 Contributor 뱃지를 얻기도 했고요. :)
참고: nginx는 아직 QUIC 지원에 대한 의사가 없어보입니다.

시간이 지날수록 nghttpx, h2o, caddy 등 다양한 웹 서버가 등장하고 있는 만큼 이러한 변화를 받아들이고 한 번 사용해보는 것도 나쁘지 않다고 봅니다. 특히 새로운 것을 좋아하는 사람이라면 말이죠 :P