20180209

Nginx HTTP/2 + Brotli 적용

nginx 1.13.9 mainline 빌드해서 HTTP/2 + Brotli 올린 기록. 패키지 버전(1.10 stable) 안 쓰고 소스 빌드로 간 이유: brotli 모듈은 아직 서드파티고, openssl도 1.1.0으로 올려야 TLS 1.3 draft 테스트가 가능하니까.

환경은 Ubuntu 16.04. 패키지 openssl은 1.0.2라 소스에서 1.1.0h로 함께 빌드. ngx_brotli는 구글 공식 레포(github.com/google/ngx_brotli)에서 pull.

./configure \
  --prefix=/etc/nginx \
  --sbin-path=/usr/sbin/nginx \
  --with-http_v2_module \
  --with-http_ssl_module \
  --with-http_realip_module \
  --with-http_stub_status_module \
  --with-file-aio \
  --with-threads \
  --add-module=../ngx_brotli \
  --with-openssl=../openssl-1.1.0h \
  --with-openssl-opt="enable-tls1_3"
make -j4 && sudo make install

nginx.conf 주요 블록.

listen 443 ssl http2;
listen [::]:443 ssl http2;

ssl_protocols TLSv1.2;  # 1.3은 아직 draft라 내부 스테이징만
ssl_ciphers  "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_prefer_server_ciphers on;
ssl_session_cache   shared:SSL:50m;
ssl_session_timeout 1d;
ssl_stapling on;
ssl_stapling_verify on;

# HTTP/2 기본 튜닝
http2_max_concurrent_streams 128;
http2_idle_timeout           3m;
http2_recv_timeout           30s;

# Brotli
brotli            on;
brotli_comp_level 5;
brotli_min_length 1024;
brotli_types
    text/plain text/css text/xml
    application/json application/javascript
    application/xml application/xml+rss
    image/svg+xml;

gzip on;   # br 미지원 클라(구형 사파리 등)용 fallback
gzip_comp_level 5;
gzip_types text/plain text/css application/json application/javascript application/xml;

왜 HTTP/2. 멀티플렉싱으로 한 TCP 커넥션에서 여러 요청을 병렬. 1.1의 head-of-line blocking을 스트림 단위로 분리. 모바일에선 연결 수 제약(iOS 4개 정도) 때문에 이 효과가 크다. 헤더 압축(HPACK)도 의외로 쏠쏠해서 헤더 많은 API 응답에선 체감됨. 단 TLS가 사실상 필수(h2c는 브라우저가 거부).

Brotli가 gzip보다 왜 나은지. 정적 사전(120KB)에 웹에서 자주 나오는 HTML/CSS/JS 토큰이 들어있어서 같은 비트레이트로 더 잘 찍어냄. 텍스트 자원(HTML, JSON, JS, CSS)에서 gzip 대비 추가 10~20% 감축. 대신 11단계까지 있는데 높은 레벨은 CPU가 오프라인 압축용이라 on-the-fly에선 금지. 5~6 근처가 gzip 6이랑 CPU/압축률 트레이드오프 스윗스팟.

실측. 메인 페이지 응답 본문(HTML+인라인 JSON) 기준.

  • 비압축 187KB
  • gzip 6: 43.2KB
  • brotli 5: 35.7KB (gzip 대비 17% 감소)
  • brotli 11 (offline): 31.9KB — 참고용

정적 JS 번들은 더 크게 벌어짐. main.bundle.js 512KB → gzip 142KB → brotli 118KB. 모바일 4G(실측 10Mbps 정도)에서 콜드 로드 체감은 200~400ms 짧아진다. 에어플레인모드 토글해서 cold start 5회 반복 평균으로 재봄.

함정 몇 개.

1. brotli_comp_level 11은 절대 금지. CPU 60% 꽃히고 TTFB 오히려 튐. 오프라인 precompress(brotli_static on)로 빌드 때 .br 파일 만들어 두는 방법이 있으니 정적 자원은 그쪽이 맞다.

2. HTTP/2 + keepalive 조합에서 특정 환경(iOS 11 초기 버전 기억) NAT timeout으로 좀비 커넥션이 쌓이는 이슈가 있었다. http2_idle_timeout 3m로 내리고 keepalive_timeout 65 유지하니 안정.

3. openssl 1.1.0 빌드 후 ldd /usr/sbin/nginx로 libssl 링킹 확인 필수. 시스템 libssl.so랑 섞이면 TLS 협상 에러가 랜덤하게 튄다. --with-ld-opt="-Wl,-rpath,/usr/local/openssl-1.1.0h/lib" 같이 rpath 박아서 해결.

4. ALPN 협상 실패로 h2가 h1.1로 떨어지는 경우 대부분 openssl 버전 문제. 1.0.2 이하에선 ALPN이 없거나 NPN으로 폴백되고 크롬 51부터는 NPN 완전히 뺐음. 1.0.2 이상 필수.

모니터링은 stub_status + access log에 $server_protocol, $ssl_protocol, $ssl_cipher, $http2 찍어서 grafana에 붙였다. h2 비율, brotli 히트율 대시보드로 본다. 이번 주 트래픽 기준 h2 92%, brotli 74%, gzip 20%, no-compress 6%. br이 상대적으로 낮은 건 브라우저가 Accept-Encoding에 br 넣는 조건이 HTTPS + 특정 버전 이상이라 그런 듯.

댓글 없음: