FastAPI 개발환경 셋팅으로 배우는 ngix와 웹서버의 관계

FastAPI 개발환경 셋팅으로 배우는 ngix와 웹서버의 관계

배경

9월부터 11월까지 간단한 사이드 프로젝트를 진행하기로 했다. 어렵지 않은 난이도에 평소에 접해보고자 했던 내용들을 접할 수 있어서 참여하기로 결정했다. 막 어제 개발 환경 셋팅을 끝냈다. 이번 기회에 내가 새로 배울 수 있는 내용은 1. 앱 개발자와 협업 (React Native) 2. Firebase Cloud Messaging를 기반으로 한 푸시 메시지 큐 시스템 활용 정도가 될 것 같다.

여기에 더불어 AWS ElasticBeanstalk으로 인스턴스를 띄우지 않고 EC2를 이용해 서버 인스턴스를 구성했다. 이번 기회에 Beanstalk 콘솔이 아닌 ubuntu-docker 환경에서 nginx같은 웹서버 소프트웨어를 직접 설정-운영 해보는 기회로 삼고자 했다.

개발 인프라 설명

위와 같은 구조로 서버를 구성했다.

  • 사이드 프로젝트이기 때문에 별도 DB서버를 둘 필요성을 느끼지 못해 서버 환경 내에 DB를 설치했다.
  • FastAPI 서버는 외부의 OAuth2 인증서버(네이버/카카오)와 통신하며, FCM에 메시지를 등록한다.
  • 클라이언트의 요청은 Nginx가 받아 FastAPI에 전달한다.
  • API 서버 환경(nginx + FastAPI (+ uvicorn)) 은 EC2 인스턴스 내 Docker 컨테이너로 감싸 운영한다.

nginx - FastAPI는 어떻게 상호작용하는가?

항상 Elastic Beanstalk의 콘솔 환경에서 서버를 운영하는 것에 익숙해져있었다. 하지만 내손으로 직점 nginx의 config 파일을 수정해가며 서버 설정을 하다보니, 자연스레 nginx와 FastAPI가 어떻게 상호작용 하는지에 대해 깊게 알아보고 싶어졌다.

FastAPI 까지 클라이언트의 요청이 전달되는 과정

  1. 외부(client)의 요청이 내 EC2 인스턴스의 80번 포트로 전달된다.
  2. 도커 컨테이너의 nginx는 호스트의 80번 포트와 연결되어있다. nginx는 80번 포트로 오는 외부의 요청을 받아, 컨테이너 내의 8080번 포트로 이를 전달한다.
  3. FastAPI의 ASGI인 Uvicorn은 컨테이너 내에서 8080번 포트를 점유하고 있으며, nginx로부터 전달받은 외부의 요청을 python(FastAPI)이 이해할 수 있도록 전환하여 python(FastAPI)의 로직을 실행시킨다.
  4. python(FastAPI) 코드가 비즈니스 로직에 따라 연산을 수행하며 결과값을 반환한다.
  5. 결과값을 받은 nginx는 내부 컨테이너의 python(FastAPI) 환경을 대신(reverse proxy)해 외부(client)에 응답을 전달한다.

nginx vs apache

nginx는 외부(클라이언트)의 요청을 듣고(listen), 이를 어떻게 처리해야하는지를 "다른 작업자에게 알려주기만 하는" 방식으로 작동된다. 이를 Event Driven 방식이라고 하며, 이벤트를 생성하고 소비하는 주체가 분리되는 Producer-Consumer Pattern 을 생각하면 쉽다.

반면 apache는 요청에 대한 작업을 apahce가 점유하는 프로세스나 스레드가 직접 수행한다.


쉽게말해 nginx는 비동기로 작업연산을 다른 worker에 "전달"만 하고 바로 다른 작업연산을 받는(nonblocking io) 하는 방식이라면, apache는 연산 작업을 직접 책임지고 수행(blocking io) 하는 방식이라고 볼 수 있다. 당연히 nginx식 처리는 더 많은 연산을 빠르게 처리할 수 있고, apache식 처리는 연산을 안정적이게 처리할 수 있게 된다.

nginx with WSGI(ASGI)

위의 과정을 거쳐 nginx는 외부(client)로 부터의 요청을 event loop에 등록에 등록한다. 이제 이 event loop에 등록된 작업을 수행할 작업자가 필요하다. python으로 작성된 웹서버에서 이를 수행하는 것이 바로 Web Server Gateway Interface 즉, WSGI라고 부르는 것이다. 이러한 WSGI에는 대표적으로 gunicorn이 있는데, FastAPI에서는 이를 Async 방식으로 튜닝한 uvicorn이라는 것을 ASGI(Asynchronous Server Gateway Interface)로 사용한다.

nginx가 event loop에 등록해놓은 요청들을 WSGI(ASGI)의 작업자들이 가져가 처리하고, 결과를 응답하면 nginx가 이 결과를 다시 클라이언트에 돌려주는 방식이다.

WSGI(ASGI) without nginx

쉽게 말해 nginx는 서버의 맨 앞에서, 외부(client)의 요청을 컨트롤하는 역할을 한다. 다만, 이러한 역할은 WSGI(ASGI) 자체적으로도 수행할 수 있다.

WSGI(ASGI) 만으로도 요청을 받을 수 있고, 파이썬이 필요한 연산을 하게 지시할 수 있기 때문이다.

간단히 예를 들자면, FastAPI 작업시 흔히 로컬에서 사용하는 uvicorn main:app 명령어로만 서버를 띄어도 외부의 요청을 처리할 수 있는 원리와 같다.

uvicorn 공식 문서에서도 nginx 뒤에서 배포하는 가이드와, nginx 없이 배포하는 가이드를 모두 설명한다.

실제로 동시 접속자수 700명 미만인 경우에 nginx 없이 WSGI (gunicorn) 만으로도 서버 운영이 가능하다고 한다.

하지만 우리는 왜 nginx(혹은 apache) 같은 웹서버 소프트웨어를 꼭 서버 맨 앞에 붙여 사용하는 걸까?

1. reverse proxy로의 이용

  • nginx는 reverse proxy 서버이다. 따라서 nginx가 웹서버의 실체를 가려준다. (통상적으로 클라이언트는 웹서버의 실체를 모르고, 웹서버는 클라이언트의 실체를 모르게 된다.) -> 이를 통해 보안상의 이점을 얻을 수있다고 한다.위의 로그에서 볼 수 있듯, api 서버는 로컬환경의 private ip(172.18.0.2)를 통해 요청을 받아온다. 실제 요청의 근원지를 모르는 상황이다. 그 요청을 전달해주는 nginx는 클라이언트의 실제 ip (61.43.54.2)를 알고 있다.위와 같이 api 서버가 nginx가 가린 프록시 정보(로컬 ip: 172.18.0.2)가 아닌, 실제 클라이언트의 정보(클라이언트 ip: 61.43.54.2)를 알기 위해서는, nginx가 클라이언트의 요청을 전달할 때 임의의 헤더에 원본 요청(클라이언트 요청)의 정보를 심어줘야 한다.

    #nginx.conf
    
      server {
          listen 80;
          server_name localhost;
          location / {
              proxy_pass         http://localhost:8080;
              proxy_redirect     off;
              proxy_set_header   Host $host;
              proxy_set_header   X-Real-IP $remote_addr;
              proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
          }
      }
    

    이렇게 nginx를 설정해주면, 클라이언트의 IP주소를 X-Real_IP라는 헤더에 심어 FastAPI에 전달하게 된다.

2. 기타 nginx가 제공하는 다양한 성능 튜닝 옵션

  • nginx를 앞에두고 별도의 Cache Server를 nginx에 연결시키거나 하는 등 아키텍쳐 측면에서 효율성을 극대화 할 수 있다.
  • 로드밸런서 성능 튜닝 옵션이나, 이를 기반으로한 다양한 로드(Load) 분배 알고리즘 등을 채택할 수 있다.
  • 그외 느린 클라이언트의 요청에 즉각 응답하지 않고, 웹서버의 처리가 완료되면 응답하여 timeout을 방지하는 등 proxy로서의 다양한 기능들을 활용할 수 있다.

즉, WSGI나 ASGI로도 서버를 돌릴 수 있지만, 이는 말그대로 요청을 전송하는 Gateway 역할에 불과하고, 다양한 보안-성능상의 이점을 얻으려면 nginx와 같은 웹서버 소프트웨어의 도움이 필수적이다.

정리

  1. nginx는 비동기적으로 외부세계의 요청을 처리할 수 있도록 event loop을 활용한다는 점에서, 요청부터 응답까지 프로세스나 스레드를 물고있는 apache와 다르다.
  2. 단순히 요청을 파이썬이 처리할 수 있도록 전달하고 응답한다는 개념에서 Web Server Gateway Interface (WSGI // ASGI) 로도 서버를 운영할 수 있다.
  3. 하지만 성능이나 보안상의 이유로 nginx를 사용하는 것이 좋다.
  4. 요청의 경우 : 클라이언트 --> nginx --[숨김]--> api 서버
  5. 응답의 경우 : api 서버 --> nginx --[숨김]--> 클라이언트

다음엔 nginx의 worker와 eventloop <> WSGI(ASGI)의 worker의 연산 처리 과정에 대해 연구해봐야겠다.