만약 여러분이 컨테이너를 Nginx 또는 Traefik과 같은 TLS 종료 프록시 (로드 밸런서) 뒤에서 실행하고 있다면, --proxy-headers 옵션을 더하는 것이 좋습니다. 이 옵션은 Uvicorn에게 어플리케이션이 HTTPS 등의 뒤에서 실행되고 있으므로 프록시에서 전송된 헤더를 신뢰할 수 있다고 알립니다.
이 Dockerfile에는 중요한 트릭이 있는데, 처음에는 의존성이 있는 파일만 복사하고, 나머지 코드는 그대로 둡니다. 왜 이런 방법을 써야하는지 설명하겠습니다.
COPY./requirements.txt/code/requirements.txt
도커와 다른 도구들은 컨테이너 이미지를 증가하는 방식으로 빌드합니다. Dockerfile의 맨 윗 부분부터 시작해, 레이어 위에 새로운 레이어를 더하는 방식으로, Dockerfile의 각 지시 사항으로 부터 생성된 어떤 파일이든 더해갑니다.
도커 그리고 이와 유사한 도구들은 이미지 생성 시에 내부 캐시를 사용합니다. 만약 어떤 파일이 마지막으로 컨테이너 이미지를 빌드한 때로부터 바뀌지 않았다면, 파일을 다시 복사하여 새로운 레이어를 처음부터 생성하는 것이 아니라, 마지막에 생성했던 같은 레이어를 재사용합니다.
단지 파일 복사를 지양하는 것으로 효율이 많이 향상되는 것은 아니지만, 그 단계에서 캐시를 사용했기 때문에, 다음 단계에서도 마찬가지로 캐시를 사용할 수 있습니다. 예를 들어, 다음과 같은 의존성을 설치하는 지시 사항을 위한 캐시를 사용할 수 있습니다:
만약 여러분이 쿠버네티스와 머신 클러스터, 도커 스왐 모드, 노마드, 또는 다른 여러 머신 위에 분산 컨테이너를 관리하는 복잡한 시스템을 다루고 있다면, 여러분은 각 컨테이너에서 (워커와 함께 사용하는 Gunicorn 같은) 프로세스 매니저 대신 클러스터 레벨에서 복제를 다루고 싶을 것입니다.
쿠버네티스와 같은 분산 컨테이너 관리 시스템 중 일부는 일반적으로 들어오는 요청에 대한 로드 밸런싱을 지원하면서 컨테이너 복제를 다루는 통합된 방법을 가지고 있습니다. 모두 클러스터 레벨에서 말이죠.
이런 경우에, 여러분은 위에서 묘사된 것처럼 처음부터 도커 이미지를 빌드해서, 의존성을 설치하고, Uvicorn 워커를 관리하는 Gunicorn 대신 단일 Uvicorn 프로세스를 실행하고 싶을 것입니다.
컨테이너로 작업할 때, 여러분은 일반적으로 메인 포트의 상황을 감지하는 요소를 가지고 있을 것입니다. 이는 HTTPS를 다루는 TLS 종료 프록시와 같은 다른 컨테이너일 수도 있고, 유사한 다른 도구일 수도 있습니다.
이 요소가 요청들의 로드를 읽어들이고 각 워커에게 (바라건대) 균형적으로 분배한다면, 이 요소는 일반적으로 로드 밸런서라고 불립니다.
팁
HTTPS를 위해 사용된 TLS 종료 프록시 요소 또한 로드 밸런서가 될 수 있습니다.
또한 컨테이너로 작업할 때, 컨테이너를 시작하고 관리하기 위해 사용한 것과 동일한 시스템은 이미 해당 로드 밸런서로 부터 여러분의 앱에 해당하는 컨테이너로 네트워크 통신(예를 들어, HTTP 요청)을 전송하는 내부적인 도구를 가지고 있을 것입니다 (여기서도 로드 밸런서는 TLS 종료 프록시일 수 있습니다).
쿠버네티스나 또는 다른 분산 컨테이너 관리 시스템으로 작업할 때, 시스템 내부의 네트워킹 메커니즘을 이용함으로써 메인 포트를 감지하고 있는 단일 로드 밸런서는 여러분의 앱에서 실행되고 있는 여러개의 컨테이너에 통신(요청들)을 전송할 수 있게 됩니다.
여러분의 앱에서 실행되고 있는 각각의 컨테이너는 일반적으로 하나의 프로세스만 가질 것입니다 (예를 들어, FastAPI 어플리케이션에서 실행되는 하나의 Uvicorn 프로세스처럼). 이 컨테이너들은 모두 같은 것을 실행하는 점에서 동일한 컨테이너이지만, 프로세스, 메모리 등은 공유하지 않습니다. 이 방식으로 여러분은 CPU의 서로 다른 코어들 또는 서로 다른 머신들을 병렬화하는 이점을 얻을 수 있습니다.
또한 로드 밸런서가 있는 분산 컨테이너 시스템은 여러분의 앱에 있는 컨테이너 각각에 차례대로 요청을 분산시킬 것 입니다. 따라서 각 요청은 여러분의 앱에서 실행되는 여러개의 복제된 컨테이너들 중 하나에 의해 다루어질 것 입니다.
그리고 일반적으로 로드 밸런서는 여러분의 클러스터에 있는 다른 앱으로 가는 요청들도 다룰 수 있으며 (예를 들어, 다른 도메인으로 가거나 다른 URL 경로 접두사를 가지는 경우), 이 통신들을 클러스터에 있는 바로 그 다른 어플리케이션으로 제대로 전송할 수 있습니다.
이 시나리오의 경우, 여러분은 이미 클러스터 레벨에서 복제를 다루고 있을 것이므로 컨테이너 당 단일 (Uvicorn) 프로세스를 가지고자 할 것입니다.
따라서, 여러분은 Gunicorn 이나 Uvicorn 워커, 또는 Uvicorn 워커를 사용하는 Uvicorn 매니저와 같은 프로세스 매니저를 가지고 싶어하지 않을 것입니다. 여러분은 컨테이너 당 단일 Uvicorn 프로세스를 가지고 싶어할 것입니다 (그러나 아마도 다중 컨테이너를 가질 것입니다).
이미 여러분이 클러스터 시스템을 관리하고 있으므로, (Uvicorn 워커를 관리하는 Gunicorn 이나 Uvicorn 처럼) 컨테이너 내에 다른 프로세스 매니저를 가지는 것은 불필요한 복잡성만 더하게 될 것입니다.
당연한 말이지만, 여러분이 내부적으로 Uvicorn 워커 프로세스들를 시작하는 Gunicorn 프로세스 매니저를 가지는 단일 컨테이너를 원하는 특수한 경우도 있을 것입니다.
그런 경우에, 여러분들은 Gunicorn을 프로세스 매니저로 포함하는 공식 도커 이미지를 사용할 수 있습니다. 이 프로세스 매니저는 다중 Uvicorn 워커 프로세스들을 실행하며, 디폴트 세팅으로 현재 CPU 코어에 기반하여 자동으로 워커 개수를 조정합니다. 이 사항에 대해서는 아래의 Gunicorn과 함께하는 공식 도커 이미지 - Uvicorn에서 더 다루겠습니다.
만약 여러분의 어플리케이션이 충분히 단순해서 (적어도 아직은) 프로세스 개수를 파인-튠 할 필요가 없거나 클러스터가 아닌 단일 서버에서 실행하고 있다면, 여러분은 컨테이너 내에 프로세스 매니저를 사용하거나 (공식 도커 이미지에서) 자동으로 설정되는 디폴트 값을 사용할 수 있습니다.
여러분은 단일 프로세스를 가지는 다중 컨테이너 대신 다중 프로세스를 가지는 단일 컨테이너를 채택하는 다른 이유가 있을 수 있습니다.
예를 들어 (여러분의 장치 설정에 따라) Prometheus 익스포터와 같이 같은 컨테이너에 들어오는 각 요청에 대해 접근권한을 가지는 도구를 사용할 수 있습니다.
이 경우에 여러분이 여러개의 컨테이너들을 가지고 있다면, Prometheus가 메트릭을 읽어 들일 때, 디폴트로 매번 하나의 컨테이너(특정 리퀘스트를 관리하는 바로 그 컨테이너)로 부터 읽어들일 것입니다. 이는 모든 복제된 컨테이너에 대해 축적된 메트릭들을 읽어들이는 것과 대비됩니다.
그렇다면 이 경우에는 다중 프로세스를 가지는 하나의 컨테이너를 두어서 같은 컨테이너에서 모든 내부 프로세스에 대한 Prometheus 메트릭을 수집하는 로컬 도구(예를 들어 Prometheus 익스포터 같은)를 두어서 이 메그릭들을 하나의 컨테이너에 내에서 공유하는 방법이 더 단순할 것입니다.
요점은, 이 중의 어느것도 여러분들이 반드시 따라야하는 확정된 사실이 아니라는 것입니다. 여러분은 이 아이디어들을 여러분의 고유한 이용 사례를 평가하는데 사용하고, 여러분의 시스템에 가장 적합한 접근법이 어떤 것인지 결정하며, 다음의 개념들을 관리하는 방법을 확인할 수 있습니다:
만약 여러분이 컨테이너 당 단일 프로세스를 실행한다면, 여러분은 각 컨테이너(복제된 경우에는 여러개의 컨테이너들)에 대해 잘 정의되고, 안정적이며, 제한된 용량의 메모리 소비량을 가지고 있을 것입니다.
그러면 여러분의 컨테이너 관리 시스템(예를 들어 쿠버네티스) 설정에서 앞서 정의된 것과 같은 메모리 제한과 요구사항을 설정할 수 있습니다. 이런 방법으로 가용 머신이 필요로하는 메모리와 클러스터에 있는 가용 머신들을 염두에 두고 컨테이너를 복제할 수 있습니다.
만약 여러분의 어플리케이션이 단순하다면, 이것은 문제가 되지 않을 것이고, 고정된 메모리 제한을 구체화할 필요도 없을 것입니다. 하지만 여러분의 어플리케이션이 (예를 들어 머신 러닝 모델같이) 많은 메모리를 소요한다면, 어플리케이션이 얼마나 많은 양의 메모리를 사용하는지 확인하고 각 머신에서 사용하는 컨테이너의 수를 조정할 필요가 있습니다 (그리고 필요에 따라 여러분의 클러스터에 머신을 추가할 수 있습니다).
만약 여러분이 컨테이너 당 여러개의 프로세스를 실행한다면 (예를 들어 공식 도커 이미지 처럼), 여러분은 시작된 프로세스 개수가 가용한 것 보다 더 많은 메모리를 소비하지 않는지 확인해야 합니다.
만약 여러분이 여러개의 컨테이너를 가지고 있다면, 아마도 각각의 컨테이너는 하나의 프로세스를 가지고 있을 것입니다(예를 들어, 쿠버네티스 클러스터에서). 그러면 여러분은 복제된 워커 컨테이너를 실행하기 이전에, 하나의 컨테이너에 있는 이전의 단계들을 수행하는 단일 프로세스를 가지는 별도의 컨테이너들을 가지고 싶을 것입니다.
그러나 프로세스의 개수가 컨테이너가 실행되고 있는 CPU에 의존한다는 것은 또한 소요되는 메모리의 크기 또한 이에 의존한다는 것을 의미합니다.
그렇기 때문에, 만약 여러분의 어플리케이션이 많은 메모리를 요구하고 (예를 들어 머신러닝 모델처럼), 여러분의 서버가 CPU 코어 수는 많지만 적은 메모리를 가지고 있다면, 여러분의 컨테이너는 가용한 메모리보다 많은 메모리를 사용하려고 시도할 수 있으며, 결국 퍼포먼스를 크게 떨어뜨릴 수 있습니다(심지어 고장이 날 수도 있습니다). 🚨
여러분들이 쿠버네티스(또는 유사한 다른 도구) 사용하거나 클러스터 레벨에서 다중 컨테이너를 이용해 이미 사본을 설정하고 있다면, 공식 베이스 이미지(또는 유사한 다른 이미지)를 사용하지 않는 것 좋습니다. 그런 경우에 여러분은 다음에 설명된 것 처럼 처음부터 이미지를 빌드하는 것이 더 낫습니다: FastAPI를 위한 도커 이미지 빌드하기.
이 이미지는 위의 다중 프로세스를 가지는 컨테이너와 특수한 경우들에서 설명된 특수한 경우에 대해서만 주로 유용할 것입니다. 예를 들어, 만약 여러분의 어플리케이션이 충분히 단순해서 CPU에 기반한 디폴트 프로세스 개수를 설정하는 것이 잘 작동한다면, 클러스터 레벨에서 수동으로 사본을 설정할 필요가 없을 것이고, 여러분의 앱에서 하나 이상의 컨테이너를 실행하지도 않을 것입니다. 또는 만약에 여러분이 도커 컴포즈로 배포하거나, 단일 서버에서 실행하거나 하는 경우에도 마찬가지입니다.