API 서버 구조를 설계할 때, 백엔드 서버 앞단에 Nginx를 두는 것을 당연하게 생각하고 구축했던 적이 있다.

서버 앞단에 Nginx 같은 리버스 프록시를 두어야 더 안정적으로 많은 동시접속자 수를 제어할 수 있다는 말을 너무 많이 들어서 머리속에서 명확한 근거도 없이 그런가보다고 생각했던 것 같다.

이번 기회에 왜 Nginx를 리버스 프록시로 백엔드 서버 앞단에 두는 것이 더 안정적인지 알아보고, 내가 Nginx를 사용한 이유를 정리해보고자했다.

왜 리버스 프록시를 사용했는가?

Proxy Buffering

우리 서비스의 API 서버는 Uvicorn과 FastAPI 프레임워크를 사용하고 있다.

Uvicorn의 공식문서를 보면, 아래와 같은 글이 나와있다.

Using Nginx as a proxy in front of your Uvicorn processes may not be necessary, but is recommended for additional resilience. Nginx can deal with serving your static media and buffering slow requests, leaving your application servers free from load as much as possible.

serving your static media

우리 서비스는 정적인 파일을 CDN을 통해서 제공하고 있기 때문에, Nginx가 정적인 파일을 캐싱하여 제공할 수 있다는 점은 우리가 리버스 프록시로 Nginx를 사용하고 있는 것에 대한 이유가 되지 않았다. 

buffering slow requests

요청을 버퍼링한다는 것이 무슨 뜻일까?

버퍼와 버퍼링

버퍼란, 어떤 장치에서 다른 장치로 데이터를 송신할 때 일어나는 시간의 차이나 데이터 흐름의 속도 차이를 조정하기 위해 일시적으로 데이터를 저장하는 장치 또는 메모리를 의미한다.
버퍼링은, 일시적으로 데이터를 메모리(RAM 또는 HDD, SSD등)에 저장해놓는 행위를 의미한다.

nginx.conf 파일에서 proxy_buffering 라는 이름의 옵션으로 버퍼링의 활성화 여부를 결정하게 되는데, default 값은 on이다.

proxy_buffering은 Nginx가 백엔드 서버로부터 받은 응답을 내부 버퍼에 저장하고 전체 응답이 버퍼링될 때까지 클라이언트에 데이터 전송을 시작하지 않는다는 것을 의미한다.

그럼 클라이언트에게 더 늦게 응답이 가는 것 아닌가? 버퍼링은 왜 사용하는 거지?

 

맞다. proxy_buffering 옵션을 활성화하면 실제로 클라이언트에게 응답이 더 늦게 갈 수 있다.

proxy_buffering 옵션은, 클라이언트에게 빠른 응답을 하기 위한 옵션이 아니라, 백엔드 서버의 부하를 줄이기 위한 옵션이다.

버퍼링을 사용하지 않으면, Nginx는 백엔드 서버로부터 응답을 받고 동기적으로 클라이언트에게 응답을 전송하기 시작한다.

이로인해 백엔드 서버는 Nginx가 클라이언트에게 현재 응답 세그먼트를 전송하고 다음 응답 세그먼트를 받을 수 있을 때까지 대기해야 한다. 즉, 클라이언트에게 모든 응답이 전송될 때까지 백엔드 서버도 기다려야 한다는 뜻이다. 클라이언트가 응답을 느리게 받는 상황이면 백엔드 서버가 대기하는 시간은 더 길어지게 된다.

반면, 버퍼링을 사용하면, 백엔드 서버는 Nginx의 버퍼에 응답을 저장해놓고 바로 다른 요청을 처리할 수 있다. 백엔드 서버가 클라이언트에게 응답이 다 전송될 때까지 기다릴 필요가 없는 것이다.

왜 리버스 프록시로 Nginx를 사용했는가?

FastAPI 공식문서에서는 아래와 같은 리버스 프록시를 소개하고 있다.

  • Traefik
  • Caddy
  • Nginx
  • HAProxy

서버 구현 당시, 혼자 백엔드 전체 아키텍처를 설계하고 구현해야 했던 상황이었기 때문에, 리버스 프록시를 리스트업하고 각각의 특성 파악하는데에 시간을 쓰지 못했다. 레퍼런스가 가장 많고, 상업적으로 무료 이용이 가능한 리버스 프록시가 Nginx 였기 때문에 Nginx 선택 후, 빠른 구현에 신경썼었다. 

 

늦은 감이 있지만, 그래도 최소 FastAPI 공식문서에서 소개하고 있는 위 4가지의 리버스 프록시 정도는 한번 훑어볼 필요가 있다고 생각해서, 하나씩 각각의 특징을 찾아보려 한다. (공부 중)

Traefik

https://doc.traefik.io/traefik/

Caddy

https://caddyserver.com/docs/getting-started

HAProxy

https://www.haproxy.org/

nginx.conf 파일 구성에서 주의할 점

Upstream Keepalive

기본적으로 Nginx는 들어오는 모든 새로운 요청에 대해 업스트림(백엔드) 서버에 대한 새 연결을 생성한다.

즉, Nginx가 백엔드 서버와 매 요청마다 3-way handshake를 거쳐 연결을 생성하고, 4-way handshake를 거쳐 연결을 종료한다는 뜻이다. 이렇게 되면, 동시 접속자 수가 증가함에 따라, 시스템 리소스가 고갈된다는 문제가 있다.

위와 같은 문제는 Upstream과 Nginx와의 Keepalive를 설정하는 것으로 보완할 수 있다.

하나의 요청 후 연결을 바로 닫지 않고 연결을 유지하며, 추가 요청에 해당 연결을 사용하는 것이다.

 

아래와 같은 방식으로 Keepalive를 설정할 수 있다.

http {

    upstream backend {
        server api:3000;
        keepalive 2; // 각 worker process에서 업스트림 서버와 유지할 연결 수
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_set_header Host $host;
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header   "Connection" "";
        }
    }
}

기본적으로 Nginx는 업스트림 서버에 연결하기 위해 HTTP/1.0을 사용하므로, 서버에 Connection: close를 전달하는 헤더가 추가된다. 이렇게 되면, upstream{} 블록에 keepalive 설정을 해놓아도 요청이 완료되면 연결이 닫히게 되므로, 아래 지시문을 location{} 블록에 추가해서 프로토콜을 HTTP/1.1로 변경하고 Connection 헤더를 제거해야한다.

proxy_http_version 1.1;
proxy_set_header   "Connection" "";

 

참고

https://www.nginx.com/blog/avoiding-top-10-nginx-configuration-mistakes/

https://serverfault.com/questions/692577/whats-the-difference-between-proxy-buffer-and-proxy-cache-module-in-nginx-confi

https://12bme.tistory.com/367

복사했습니다!