티스토리 뷰

이번에는 Docker Compose를 사용해서 WAS를 Scale out하는 상황을 만들고, Load Balancing 까지 수행하는 Application을 만들어보려고 한다. 

Scale Out

Scale out이 필요한 Server Application을 굳이 복잡하게 만드는 것이 목적이 아니기 때문에.. 아래와 같은 간단한 코드를 작성해보자.

@RestController
@Slf4j
public class TestController {

    @GetMapping("/test")
    public String test() {
        log.info("하위");
        return "test";
    }
}

 

"/test" URL로 요청을 받는 Spring App을 만들었다. 이제 이 서버를 띄우기 위해 아래와 같은 docker-compose를 작성하자.

version: "3"
services:
  app:
    image: arm64v8/amazoncorretto:17-alpine-jdk
    volumes:
      - ./src:/app/src
      - ./gradle:/app/gradle
      - ./build.gradle:/app/build.gradle
      - ./gradlew:/app/gradlew
      - ./gradlew.bat:/app/gradlew.bat
      - ./settings.gradle:/app/settings.gradle
    working_dir: /app
    ports:
      - 8080:8080
    command: [ "./gradlew", "bootrun" ]
    restart: always

docker compose 파일을 모두 작성했다면, 아래의 명령어를 통해 spring app container를 실행시켜보자.

docker compose up

정상적으로 container가 실행된 것을 볼 수 있다. 그리고 실제로 http://localhost:8080/test에 접속해보면 아래와 같은 응답을 얻을 수 있다.

자 이제, 이 application의 성능을 확장해야 하는 상황이 왔다고 가정해보자. 그리고 우리는 수평적 확장을 하려고 한다.
Docker Compose의 명령어를 사용하면 간단하게 동일한 도커 이미지를 여러개의 도커 컨테이너로 띄울 수 있는데, 아래와 같다.

 docker compose up --scale app=2

이 명령어를 통해 scale out을 할 경우 자동으로 LB 까지 구현이 되는데 default 값은 round robin 방식이다.

다만 주의해야 할것이 있는데, docker-compose.yaml에 명시했던 외부 ports를 닫아야 한다. 하나의 외부 포트는 하나의 컨테이너만 사용할 수 있는데, 2개의 컨테이너가 같은 ports를 바라보면 안되기 때문이다. 만약 이 ports를 닫지 않으면 아래와 같은 에러를 마주치게 된다.

Error response from daemon: driver failed programming external connectivity on endpoint system-design-interview-app-2 (f5834c5fc0c9c6c23c007bf47bb6b4e13aee539c6a687de3eaabf77991adcfc2): Bind for 0.0.0.0:8080 failed: port is already allocated

그러니 명시한 ports 설정을 지우고 scale 명령어를 사용하면 정상적으로 2대의 container가 실행된 것을 볼 수 있다.

자 그럼, 이 컨테이너에 접속을 해보자.. 라기엔 포트가 닫혀있기 때문에 외부에서 접근할 수가 없다. 그러므로 외부에서 접근할 수 있도록 Web Server를 WAS 앞단에 두기로 하자.

Nginx 설정

Web Server는 Nginx를 사용할 것이다. 이유는 Nginx 말고 다른 WebServer를 사용해본 경험이 없기 때문이다.

먼저 nginx.conf 설정을 아래와 같이 작성해보자. 

events {
  worker_connections 1000;
}

http {
  upstream app {
    server app:8080;
  }

  server {
    listen 80;
    location / {
      proxy_pass http://app/;
     }
  }
}

nginx 설정은 아주 간단하게 구성을 했다. 우리가 만든 spring app을 등록하고, nginx의 80번 포트로 요청이 들어오면 우리가 만든 spring app으로 요청이 전달되도록 구성했다.

version: "3"
services:
  app:
    image: arm64v8/amazoncorretto:17-alpine-jdk
    volumes:
      - ./src:/app/src
      - ./gradle:/app/gradle
      - ./build.gradle:/app/build.gradle
      - ./gradlew:/app/gradlew
      - ./gradlew.bat:/app/gradlew.bat
      - ./settings.gradle:/app/settings.gradle
    working_dir: /app
    command: [ "./gradlew", "bootrun" ]
    restart: always

  nginx:
    image: nginx:latest
    volumes:
      - ./nginx/config/nginx.conf:/etc/nginx/nginx.conf
    restart: always
    ports:
      - "80:80"

그다음, docker compose에 nginx container를 등록했다. 이때 해당 컨테이너는 우리가 만든 nginx config를 사용하도록 volume을 묶어줬다. 이렇게 nginx 설정을 끝낸 후, 다시한번 docker compose를 실행시켜보자.

 docker compose up --scale app=2

docker ps -a 명령어를 통해 확인해보면, 정상적으로 nginx container, spring app container가 2개 띄워진 것을 확인해볼 수 있다.

http://localhost:80/test에 요청을 2번 보내보면, 동일한 응답을 이전과 같이 받는 것을 볼 수 있다.
차이가 없어보이지만, 로그를 확인해보면 분명히 다르다.

터미널에 찍힌 로그를 보면, app-1에 '하위'가 1번, app-2에 '하위'가 1번 찍힌 것을 볼 수 있다. 분명 같은 요청을 보냈지만 서로 다른 컨테이너로 요청이 분산되어 전달된 것이다.

이때 app-2 컨테이너를 종료하면 어떤일이 생길까?

그래서 app-2 container를 종료시키고, 다시 http://localhost:80/test로 요청을 보내봤다.

요청을 여러번 보내봤지만, 이전과 똑같은 응답을 받기 때문에 app-2 container가 종료되었는지 조차 알지 못한다.

대신 로그를 확인해보면, app-2 container가 종료된 이후의 모든 요청이 정상적으로 app-1 container로 전달되어 app-1에서 로그가 찍히는 것을 볼 수 있다. 즉, scale out한 컨테이너가 종료되더라도 정상적으로 동작하고 있는 container가 요청을 처리하고 있는 것이다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함