본문 바로가기

AI/MLOps

[12/12] 아이펠 리서치 15기 TIL | Docker Swarm

반응형

이번엔 서버에 트래픽이 몰렸을 때 서버가 다운되는 것을 방지하기 위한 부하분산 방법에 대해 알아보자.

이번 포스팅도 아래의 깃허브를 바탕으로 진행된다.(./dockers/swarm)

https://github.com/KennethanCeyer/mlops-quicklab

 

GitHub - KennethanCeyer/mlops-quicklab: A repository for understanding MLOps pipeline.

A repository for understanding MLOps pipeline. Contribute to KennethanCeyer/mlops-quicklab development by creating an account on GitHub.

github.com

 


Docker swarm이란?

Docker에서 공식적으로 개발한 컨테이너 오케스트레이션(Container Orchestration) 도구이다.

Swarm은 여러 대의 도커 호스트를 하나의 클러스터로 묶어 마치 하나의 거대한 가상 도커 호스트처럼 사용할 수 있도록 한다.

 

이게 왜 필요한가?

 

아래의 상황을 보자.


웹 페이지 부하 확인

일단 이전에 여러 작업을 해두었는데, 간단히 정리하면

  • locust 설치 (여기 참고)
  • 구글 클라우드 콘솔 ->  compute engine -> VM 인스턴스 3개 만들기
  • 만들어진 인스턴스 3개 각각 SSH 누르고 docker install (여기 참고)
  • 도커 그룹으로 권한 설정 (여기 참고)

이 단계들을 완료한 다음

docker run -dit --name fastapi-app -p 8000:8000 kennethan/fastapi-app-prime

 

위 명령어를 실행하면 출력에 웹 페이지 링크가 나오고 들어가면 오류가 뜨는데,

  • https -> http
  • 끝에 :8000/docs 불이기
  • GCP 콘솔에서 방화벽 규칙 추가

를 하면 아래의 페이지가 나온다.

 

숫자 범위를 입력하고 그 안에서 소수를 찾는 간단한 프로그램이다.

 

이제 locust를 이용해 이 페이지에 부하를 줘보자.

 

아래 명령어를 로컬 터미널에서 실행한다. (경로는 /dockers/swarm)

locust -f locustfile.py


[2025-12-12 12:05:28,770] aiffel07-GE75-Raider-10SF/WARNING/locust.main: Python 3.8 support is deprecated and will be removed soon
[2025-12-12 12:05:28,770] aiffel07-GE75-Raider-10SF/INFO/locust.main: Starting web interface at http://0.0.0.0:8089
[2025-12-12 12:05:28,780] aiffel07-GE75-Raider-10SF/INFO/locust.main: Starting Locust 2.25.0

 

결과 두 번째 줄에 페이지 링크가 나오면 접속한다.

1. 총 유저 수,

2. 초당 몇 명이 접근할지,

3. 아까 띄웠던 Fastapi 주소

를 입력하고 start를 누르면

이렇게 차트를 볼 수 있다.

맨 위의 차트를 보면 RPS(초당 동시 접속자)가 점점 하락하며 Failures도 발생했다.

이 상황에서는 

 

새로고침을 해도 진행이 안 되는 모습이다. (패닉)

 

이런 부하 문제를 해결하기 위해 사용하는 것이 Swarm이다.


Docker Swarm 사용해보기

아까 GCP에서 만든 VM 인스턴스 터미널에 아래 명령어를 입력한다.

docker swarm init


Swarm initialized: current node (kirid8gz0mg12w1xzannn6du3) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token {토큰}

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

 

출력에 나온 docker swram join --token {토큰} 명령어를 나머지 VM 두 개에 붙여넣어주면

"This node joined a swarm as a worker." 라는 메시지가 나온다.

 

그리고 다시 매니저(1번 노드)에게 docker node ls 를 입력하면 다른 워커들의 정보를 볼 수 있다.

docker node ls


ID                            HOSTNAME                STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
kirid8gz0mg12w1xzannn6du3 *   docker-swarm-worker-1   Ready     Active         Leader           29.1.2
kyde3d5in0v1c9foje2z4t9xj     docker-swarm-worker-2   Ready     Active                          29.1.2
19ljlzhtcjcadflgtnuqpepdr     docker-swarm-worker-3   Ready     Active                          29.1.2

 

이제 서비스를 배포해보자.

같은 폴더에 있는 docker-compose.yml 파일의 내용 복사해서 worker 내부의 tmp 파일에 넣어주자.

(참고로 필자는 우분투 환경)

 

1. 로컬 터미널에서 복사하고,

cat docker-compose.yml | xclip -selection clipboard

 

2. worker 1 터미널에서 아래 명령어를 실행해서 yml 파일을 넣어주자. (+포트 8000:8000을 80:8000으로 수정)

cd tmp

vi docker-compose.yml

 

replicas: 4 이건

이 웹 서버를 4대의 컨테이너로 동시에 구동하겠다. 라는 의미이다.

 

그리고 나서 배포하라는 명령어를 입력한다.

docker stack deploy -c docker-compose.yml fastapi


Since --detach=false was not specified, tasks will be created in the background.
In a future release, --detach=false will become the default.
Creating network fastapi_default
Creating service fastapi_fastapi-app

 

그리고 docker swarm 클러스터에 배포되어 현재 실행 중인 모든 서비스의 목록과 상태를 출력해보면,

docker service ls


ID             NAME                  MODE         REPLICAS   IMAGE                                PORTS
n3z70htqkcez   fastapi_fastapi-app   replicated   4/4        kennethan/fastapi-app-prime:latest   *:80->8000/tcp

REPLICAS가 모두 다 잘 뜬 것을 확인할 수 있다. 즉, 4개의 컨테이너를 swarm 클러스터에 있는 3개의 노드에 각각 할당한다는 것이다. (이건 매니저 노드에서만 볼 수 있는 출력임)

위 명령어와 docker ps -a 둘 다 도커 환경의 상태를 보는 명령어지만, ps는 단일 노드(컨테이너) 상태를 확인하는 데 쓰이고, service는 docker swarm 클러스터 전체(serivce)를 보는데 쓰인다.

 

이제 각각의 노드(1,2,3번)에서 docker ps -a를 입력해서 컨테이너의 상태들을 확인해 보면,

1번 노드 (매니저)
docker ps -a
CONTAINER ID   IMAGE                                COMMAND                CREATED          STATUS          PORTS     NAMES
0219a3595f0d   kennethan/fastapi-app-prime:latest   "python app/main.py"   19 minutes ago   Up 19 minutes             fastapi_fastapi-app.2.vnfz6j3bgx5rep9jwlws7zplo


2번 노드
CONTAINER ID   IMAGE                                COMMAND                CREATED          STATUS          PORTS     NAMES
26199f9b0337   kennethan/fastapi-app-prime:latest   "python app/main.py"   19 minutes ago   Up 19 minutes             fastapi_fastapi-app.3.q432x6vjx4ciuduv3uffnnplk
f24d3b775bd4   kennethan/fastapi-app-prime:latest   "python app/main.py"   19 minutes ago   Up 19 minutes             fastapi_fastapi-app.4.q0ji2lvtd6oixjs0gjx0mw20q


3번 노드
CONTAINER ID   IMAGE                                COMMAND                CREATED          STATUS          PORTS     NAMES
a2be0b3f041f   kennethan/fastapi-app-prime:latest   "python app/main.py"   19 minutes ago   Up 19 minutes             fastapi_fastapi-app.1.nwkm8tdyzjnuh0ea74swccnvh

 

각 노드에 컨테이너가 각각 1,2,1개씩 할당되었다.

이제 다시 locust로 부하를 늘려서 확인해보자.

1. 로컬 터미널에서 locust 실행

locust -f locustfile.py

 

2. GCP 가서 매니저 노드의 외부 ip 클릭 후 https -> http, 끝에 docs 붙이기

3. locust에 아까와 동일하게 정보 입력 후 start

 

그리고 차트를 보면,

 

아까는 200 초반에서 비실대던 RPS가 지금은 600 초반을 유지중이다.

Failure이 좀 있긴 하지만 swarm을 하니 RPS가 굉장히 높아졌다.

 

(근데 사실 지금 생각해보니 replica 4대에 비해 2000, 200은 너무 높게 잡은 듯 하다. 실전에서는 RPS보다 failure 방지가 최우선이다.)

 

그렇다면, 이번엔 replica를 12로 대폭 늘려서 다시 실행해보자.

 

docker stack deploy -c docker-compose.yml fastapi


Since --detach=false was not specified, tasks will be created in the background.
In a future release, --detach=false will become the default.
Updating service fastapi_fastapi-app (id: n3z70htqkcezdtg088czb148w)
docker service ls

ID             NAME                  MODE         REPLICAS   IMAGE                                PORTS
n3z70htqkcez   fastapi_fastapi-app   replicated   12/12      kennethan/fastapi-app-prime:latest   *:80->8000/tcp



docker ps -a

1번 노드 (매니저)
CONTAINER ID   IMAGE                                COMMAND                CREATED              STATUS              PORTS     NAMES
547662bf32c1   kennethan/fastapi-app-prime:latest   "python app/main.py"   About a minute ago   Up About a minute             fastapi_fastapi-app.9.jqhccxjmn23hvoj4mbnsc6416
4dc8828b0447   kennethan/fastapi-app-prime:latest   "python app/main.py"   About a minute ago   Up About a minute             fastapi_fastapi-app.5.sk5ve90e5ee47v9woflpt0u8x
dbebf5334e3a   kennethan/fastapi-app-prime:latest   "python app/main.py"   About a minute ago   Up About a minute             fastapi_fastapi-app.6.s5ye653mbtacugu4lnff089ki
0219a3595f0d   kennethan/fastapi-app-prime:latest   "python app/main.py"   47 minutes ago       Up 47 minutes                 fastapi_fastapi-app.2.vnfz6j3bgx5rep9jwlws7zplo

2번 노드
CONTAINER ID   IMAGE                                COMMAND                CREATED              STATUS              PORTS     NAMES
df1388f693e8   kennethan/fastapi-app-prime:latest   "python app/main.py"   About a minute ago   Up About a minute             fastapi_fastapi-app.7.qt75g7y0g4ytrpb0mipuxnkmh
6a80cad935b8   kennethan/fastapi-app-prime:latest   "python app/main.py"   About a minute ago   Up About a minute             fastapi_fastapi-app.10.tyb46mfctjs2zvb3ezxnx7tx0
26199f9b0337   kennethan/fastapi-app-prime:latest   "python app/main.py"   48 minutes ago       Up 47 minutes                 fastapi_fastapi-app.3.q432x6vjx4ciuduv3uffnnplk
f24d3b775bd4   kennethan/fastapi-app-prime:latest   "python app/main.py"   48 minutes ago       Up 47 minutes                 fastapi_fastapi-app.4.q0ji2lvtd6oixjs0gjx0mw20q

3번 노드
CONTAINER ID   IMAGE                                COMMAND                CREATED              STATUS              PORTS     NAMES
0948a8b9c6cf   kennethan/fastapi-app-prime:latest   "python app/main.py"   About a minute ago   Up About a minute             fastapi_fastapi-app.8.hzhhd35u9c90jxc6plssa8cit
4eb56c99b22a   kennethan/fastapi-app-prime:latest   "python app/main.py"   About a minute ago   Up About a minute             fastapi_fastapi-app.11.1vqk1kplu5gbmvnlxpyh1wzu4
a78654bf0e43   kennethan/fastapi-app-prime:latest   "python app/main.py"   About a minute ago   Up About a minute             fastapi_fastapi-app.12.mj4niv22t97rydbe08k7hsbd3
a2be0b3f041f   kennethan/fastapi-app-prime:latest   "python app/main.py"   48 minutes ago       Up 48 minutes                 fastapi_fastapi-app.1.nwkm8tdyzjnuh0ea74swccnvh

 

replica가 12로 잘 적용되었고, 각 노드들에 4개의 컨테이너씩 할당되었다.

 

이때, created 시간을 보면 알 수 있듯이 기존에 사용하고 있던 컨테이너까지 싹 다 없애고 다시 할당하는 것이 아니라, 필요한 만큼만 추가로 컨테이너를 할당하는 방식이다.(기존 컨테이너들을 죽이면 서버가 중단되기 때문)

 

이제 됐으니 다시 locust로 부하를 줘보자.(이전 방법과 동일)

 

흠,, 

쿨다운이 안 된 것인지 왜인지는 모르겠지만 아까와 RPS는 비슷해 보인다.

그래도 failure이 10 이상에서 4까지 감소하긴 했다.

 

이 구조를 그림으로 보면 아래와 같은 모양이다.

그런데, 위의 경우는 매니저 노드(load balancer)가 클러스터 내의 노드(또는 컨테이너)들 간의 트래픽을 분산하는 역할을 하는데, 외부에서 들어오는 최초의 진입점 역할을 못하며, 노드 장애 시 트래픽을 처리하지 못할 수 있다.

 

따라서 외부 트래픽을 제어할 수 있는 외부 LB(load balancer)가 필요한데 이것을 Externel LB 라고 한다.

 

이 ELB는 트래픽을 클러스터 내부의 매니저 노드들에게 분산시킨다.

이때 만약 특정 매니저 노드에 장애가 생기면 그 노드에게는 트래픽을 보내지 않아서 장애를 회피할 수 있다.


회고

locust로 직접 트래픽 환경을 실행해보니 swarm의 필요성을 느낄 수 있었다.

지금까지 EX, GD에서 했던 공부들과 아예 다른 것을 하니까 새롭기도 해서 좋긴 한데,

처음 접하는 용어들과 환경이 많아서 흐름을 이해하는데 좀 어렵기도 하고 오래 걸렸다.

블로그에 기록하면서 공부하다 보니 진도가 좀 밀렸는데, 주말까지 다 끝내야겠다.

반응형