로드밸런싱 알고리즘 성능 비교 연구[4]

Chu Sang Yoon·2025년 7월 29일

lab

목록 보기
4/11
post-thumbnail

Docker로 4개 웹서버 환경 구축하기

들어가며

3-1편에서 Docker의 개념을 학습했으니, 이제 실제로 로드밸런싱 테스트를 위한 4개의 웹서버를 Docker로 구축해보겠습니다

각 서버는 서로 다른 성능 특성을 가지도록 설정하여 나중에 로드밸런싱 알고리즘들의 성능을 명확하게 비교할 수 있도록 만들 예정입니다.

1. 전체 아키텍처

[사용자 요청] 
     ↓
[Spring Boot 로드밸런서 :8080] ← 다음 편에서 구현
     ↓ 요청 분산
┌──────────────────────────────────────────────────────────────────────┐
│ [Docker 서버1] [Docker 서버2] [Docker 서버3] [Docker 서버4] 
│   :5001         :5002         :5003         :5004              │
└──────────────────────────────────────────────────────────────────────┘

2. Spring Boot

package com.loadbalancer;

import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.HashMap;

@RestController
public class TestController {
    
    @GetMapping("/")
    public Map<String, Object> home() {
        // 환경변수에서 서버 설정 읽기
        String serverId = System.getenv("SERVER_ID");
        String responseDelay = System.getenv("RESPONSE_DELAY");
        
        // 응답 지연 시뮬레이션
        if (responseDelay != null) {
            try {
                Thread.sleep(Integer.parseInt(responseDelay));
            } catch (Exception e) {
                // 무시
            }
        }
        
        Map<String, Object> response = new HashMap<>();
        response.put("server", serverId != null ? serverId : "unknown");
        response.put("timestamp", System.currentTimeMillis());
        response.put("responseDelay", responseDelay != null ? responseDelay + "ms" : "0ms");
        
        return response;
    }
    
    @GetMapping("/health")
    public Map<String, String> health() {
        return Map.of(
            "status", "healthy",
            "server", System.getenv("SERVER_ID")
        );
    }
	}
  • 메인 엔드 포인트 (/ )

    • 서버 식별을 위한 SERVER_ID 환경변수 읽기
    • 응답 지연 시뮬레이션을 위한 RESPONSE_DELAY 환경변수 처리
    • 현재 타임스탬프와 함께 서버 정보 반환
  • 헬스체크 엔드포인트(/health )

    • 서버상태와 ID를 간단히 반환
    • 로드밸런서의 헬스체크용으로 사용

3. Docker 설정

Dockerfile

FROM openjdk:17-jdk-slim
WORKDIR /app
COPY . .
RUN chmod +x gradlew && ./gradlew clean build -x test --no-daemon
RUN find build/libs -name "*.jar" ! -name "*plain.jar" -exec cp {} app.jar \;
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

베이스이미지

FROM openjdk:17-jdk-slim
  • OpenJDK 17 경량 버전 사용
  • slim 태그로 불필요한 패키지 제거하여 이미지 크기 최적화

작업 디렉토리

WORKDIR /app
  • 컨테이너 내부의 /app 디렉토리를 작업 공간으로 설정

빌드 과정 - 소스코드 복사

COPY . .
  • 현재 디렉토리의 모든 파일을 컨테이너의 /app 으로 복사
  • 프로젝트 전체(소스코드,Gradle 설정 등)이 포함

빌드과정 - Gradle 빌드

RUN chmod +x gradlew && ./gradlew clean build -x test --no-daemon
  • gradlew 실행 권한 부여
  • clean build 이전 빌드 결과 삭제 후 새로 빌드
  • -x test 테스트 실행 제외(빌드 시간 단축)
  • --no-deamon Gradle 데몬 사용 안함

JAR 파일 처리

RUN find build/libs -name "*.jar" ! -name "*plain.jar" -exec cp {} app.jar \;
  • build/libs 디렉토리에서 JAR 파일 검색
  • *plain.jar 제외 (Spring Boot의 실행 가능한 JAR 만 선택)
  • 찾은 JAR 파일을 app.jar 로 이름 변경

포트 노출 및 애플리케이션 실행

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
  • 컨테이너의 8080포트를 외부에 노출
  • 컨테이너 시작 시 JAR 파일 실행

DockerFile → 멀티 스테이지 빌드 개선

멀티스테이지 빌드

  • 한 개의 Dockerfile 안에서 여러 개의 FROM 을 사용해 단계별로 이미지를 만드는 방식
  • 사용이유?
    • 무거운 빌드 환경에서 컴파일
    • 가벼운 런타임 환경에서 실행파일만 복사
    • 최종 이미지에는 실행에 필요한 것만 포함

코드

# 빌드 스테이지
FROM openjdk:17-jdk-slim AS builder

WORKDIR /app

# Gradle 래퍼와 빌드 스크립트만 먼저 복사 (의존성 캐싱을 위해)
COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .

# 실행 권한 부여 및 의존성 다운로드
RUN chmod +x gradlew
RUN ./gradlew dependencies --no-daemon

# 소스 코드 복사 및 빌드
COPY src src
RUN ./gradlew clean build -x test --no-daemon

# 런타임 스테이지 - Eclipse Temurin 사용 (OpenJDK 기반)
FROM eclipse-temurin:17-jre

WORKDIR /app

# 빌드 스테이지에서 JAR 파일만 복사
COPY --from=builder /app/build/libs/*.jar app.jar

# 애플리케이션용 사용자 생성 (보안 강화)
RUN addgroup --system spring && adduser --system spring --ingroup spring
USER spring:spring

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

개선점

  • 이미지 크기 대폭 감소 기존: JDK + Gradle + 소스코드 + 빌드 결과물 모두 포함 (500~600 MB) 새버전: 런타임에는 JRE + JAR 파일만 포함 (200~250 MB)
  • 빌드 캐시 최적화 기존: COPY . . → 소스코드 1줄만 바껴도 전체 재빌드 새버전: 의존성 파일들을 먼저 복사 → 의존성은 캐시되고, 소스코드 변경시에만 해당 레이어만 재빌드
  • 보안향상 기존: root 사용자로 실행 새버전: spring 전용 사용자 생성하여 실행
  • 운영환경 최적화 기존: JDK(개발+컴파일+실행도구 모두 포함) 새버전: JRE(실행만 가능, 컴파일 도구 제거)

Docker-compose.yml

services:
  # 서버 1: 빠르고 안정적
  web-server-1:
    build: .
    container_name: web-server-1
    ports:
      - "5001:8080"
    environment:
      - SERVER_ID=server-1
      - RESPONSE_DELAY=50    # 50ms 지연

  # 서버 2: 보통 성능
  web-server-2:
    build: .
    container_name: web-server-2
    ports:
      - "5002:8080"
    environment:
      - SERVER_ID=server-2
      - RESPONSE_DELAY=150   # 150ms 지연

  # 서버 3: 느린 서버
  web-server-3:
    build: .
    container_name: web-server-3
    ports:
      - "5003:8080"
    environment:
      - SERVER_ID=server-3
      - RESPONSE_DELAY=300   # 300ms 지연

  # 서버 4: 매우 느리고 불안정
  web-server-4:
    build: .
    container_name: web-server-4
    ports:
      - "5004:8080"
    environment:
      - SERVER_ID=server-4
      - RESPONSE_DELAY=500   # 500ms 지연

각각 서버 특성

image.png

  • 동일한 코드, 다른 성능 같은 Dockerfile을 사용하지만 RESPONSE_DELAY 가 달라 성능이 다름
  • 개별 포트 할당

    • 각 서버를 독립적으로 테스트 가능
    • curl localhost:5001 vs curl localhost:5004 비교 가능
  • 환경 변수로 설정 제어

    • SERVER_ID : 어느 서버가 응답했는지 식별
    • RESPONSE_DELAY : 인위적 지연으로 서버 성능 차이 시뮬레이션 가능

4. 실행결과

server-1

server-2

server-3

server-4

응답 시간 측정

# 각 서버별 응답시간 측정
for port in 5001 5002 5003 5004; do
  echo "Testing server on port $port:"
  time curl -s http://localhost:$port/ > /dev/null
  echo ""
done
  • time

    • 명령어 실행 시간을 측정
    • real (실제 경과 시간), user (CPU 사용시간), sys (시스템 호출 시간) 출력
  • curl -s

    • -s : silent 모드 (진행률 표시 숨김)
    • /d /dev/null : 응답 내용은 버리고 시간만 측정

결과 분석

  • 설정이 올바르게 작동하는 것을 볼 수 있음

    • 각 서버별로 명확한 응답 시간 차이 확인 가능
    • 순서대로 점점 느려지는 패턴 확인
  • 추가 오버헤드의 원인들?

    • 어플리케이션 시작 시간: Spring Boot 초기화
    • 네트워크 지연: Docker 컨테이너 간 통신
    • HTTP 처리 시간: 요청/응답 파싱
    • 시스템 오버헤드: 컨테이너 내부 처리
  • 일관된 오버헤드

    • 모든 서버에서 비슷한 +150~160ms 추가 시간

헬스체크

5. 트러블 슈팅

Docker Desktop 문제

증상: pipe/dockerDesktopLinuxEngine 에러

error during connect: Get 
"http://%2F%2F.%2Fpipe%2FdockerDesktopLinuxEngine/v1.49/containers/json": 
open //./pipe/dockerDesktopLinuxEngine: The system cannot find the file specified.
  • Docker Desktop이 WSL2와 제대로 연결되지 않음

WSL2 설치 확인

# PowerShell 관리자 권한으로 실행
wsl --list --verbose

WSL2 재시작

# PowerShell 관리자 권한으로
wsl --shutdown

# 30초 대기 후 확인
wsl --list --verbose

정상상태

NAME                   STATE           VERSION
* Ubuntu-22.04           Running         2
  docker-desktop         Running         2
  docker-desktop-data    Running         2

Docker 종료 후 재실행

JAR 파일 복사 에러

증상: cp: target 'app.jar' is not a directory

여러 JAR 파일 생성으로 인한 충돌

해결: find 명령어로 plain.jar 제외

# 문제가 있던 방식
RUN cp build/libs/*.jar app.jar

# 해결된 방식  
RUN find build/libs -name "*.jar" ! -name "*plain.jar" -exec cp {} app.jar \;

마무리

4개의 서로 다른 성능 특성을 가진 웹서버를 Docker로 성공적으로 구축했습니다.

각 서버별로 50ms부터 500ms까지 응답시간 차이를 확인할 수 있었고,

이제 로드밸런싱 테스트를 위한 기반이 완성되었습니다.

다음 편에서는 6가지 로드밸런싱 알고리즘의 동작 원리와 특징을 살펴보겠습니다.

Phase 1: 계획 및 이론

  • 📋 1편: 목적 및 계획 - 연구 동기와 전체 로드맵
  • 🔍 2편: 로드밸런싱 정복 - 개념, 필요성, 종류

Phase 2: 환경 구축 및 설계

  • 🐳 3-1편: Docker 개념 학습하기 - 개념, 특징, 명령어
  • 🐳 3-2편: Docker 실습 - 4개 서버 환경 구축하기
  • ⚙️ 4편: 6가지 알고리즘 탐구 - 각각의 원리와 특징

Phase 3: 구현 및 테스트

  • 💻 5편: Spring Boot로 구현하기 - 실제 코딩 과정
  • 📊 6편: 성능 테스트 & 결과 분석 - 21회 테스트 데이터

Phase 4: ML 확장 및 마무리

  • 🧠 7편: ML로 똑똑하게 만들기 - Flask + 머신러닝
  • 🎯 8편: 최종 결과 및 후기 - 성과와 아쉬운 점

0개의 댓글