로드밸런싱 알고리즘 성능 비교 연구[10] -Adaptive Load Balancer

Chu Sang Yoon·2026년 4월 3일

lab

목록 보기
10/11

10편: Adaptive Load Balancer — 트래픽 상태에 따라 알고리즘을 스스로 바꾸는 로드밸런서

"최적 알고리즘은 하나가 아니다"는 걸 데이터로 확인했다. 그럼 상황에 따라 자동으로 바꾸면 되지 않을까?

지난 편에서 6가지 알고리즘을 실측 벤치마크했을 때 얻은 결론은 이거였다.

  • Steady Load → Least Connections (RPS 421, avg 136ms)
  • Server Failure → IP Hash (에러 3건 vs Round Robin의 345건)
  • Gradual Increase → Weighted Round Robin (서버 capacity 반영)

트래픽 패턴이 다르면 최적 알고리즘도 다르다. 근데 실제 서비스는 배포 시점에 알고리즘 하나를 고정한다. 이 한계를 해결하고 싶었다.


1. 핵심 아이디어

"런타임에 트래픽 상태를 감지하고, 그 상황에 맞는 알고리즘으로 자동 전환한다."

여기에 AI를 붙였다. 단, AI를 런타임 요청 경로에 직접 넣지 않았다.

[K6 벤치마크]
       ↓
[AI Agent (GPT-4o)] ← 오프라인 분석, 요청 경로 밖
       ↓
[decision-rules.yml 생성]
       ↓
[Runtime이 YAML 읽어서 deterministic하게 전환]

Runtime에는 절대 LLM을 호출하지 않는다. AI는 전략을 만드는 역할이고, 실행은 rule-based 엔진이 담당한다. 이유는 명확하다 — LLM 추론 지연은 수백ms~수초인데, 그게 요청 처리 경로에 끼어들면 안 된다.


2. 전체 아키텍처

[Client Requests]
       ↓
[Adaptive Load Balancer] ← [Rule Config (YAML)]
  ├─ MetricsCollector    (슬라이딩 윈도우 10s)
  ├─ PatternAnalyzer     (TrafficState 분류)
  ├─ DecisionEngine      (state → algorithm 매핑)
  └─ Algorithm Executor  (실제 분배)
       ↓
[Backend Servers × 4, port 5001~5004]

--- Offline Feedback Loop ---

[K6 Benchmark] → [Metrics Log (JSON)]
       ↓
[AI Agent (Python + GPT-4o)]
       ↓
[새 Rule 생성 → POST /alb/rules/reload]

기술 스택은 Java 21 + Spring Boot 3.4, 벤치마크는 K6, AI Agent는 Python + OpenAI GPT-4o다.


3. 핵심 설계 결정 3가지

3-1. 절댓값 threshold 대신 변화율(Derivative) 기반 감지

"RPS > 500이면 SPIKE"

이 규칙은 평소 RPS가 50인 소규모 서비스에는 너무 높고, 평소 RPS가 5000인 대규모 서비스에는 너무 낮다. threshold는 스케일에 의존한다.

변화율은 스케일에 독립적이다:

double rpsChangeRate = (currentRps - previousRps) / previousRps;
// +1.0 = 100% 증가 → SPIKE
// +0.3 = 30% 증가 → GRADUAL_INCREASE

서비스 규모가 어떻든 "이전 대비 100% 이상 급증"은 동일한 기준으로 감지된다.

latencySlope는 더 나아가서 최근 N개 윈도우에 선형 회귀를 돌린다. 단순히 "마지막 값이 올랐는지"만 보면 노이즈에 취약한데, 기울기(slope)를 구하면 일시적인 스파이크를 무시하고 진짜 추세를 잡을 수 있다.

// 최소자승법으로 latency 추세의 기울기 계산 (ms/window)
double slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);

3-2. Hysteresis(이력 현상)로 노이즈 필터링

에어컨의 온도 제어를 생각해보자.

26°C 설정

온도 > 27°C → 냉방 ON  (상향 임계값)
온도 < 25°C → 냉방 OFF (하향 임계값)

→ 25~27°C 구간에서는 현재 상태 유지

임계값을 딱 하나만 쓰면 26°C 주변에서 ON/OFF를 끊임없이 반복한다(채터링). Hysteresis는 이걸 막는 제어 이론의 개념이다.

이 프로젝트에서는 "새 TrafficState가 N개 연속 윈도우 동안 유지돼야 전환"으로 구현했다.

// 연속 상태 카운터
if (newState.equals(pendingState)) {
    consecutiveCount++;
} else {
    pendingState = newState;
    consecutiveCount = 1; // 다른 state 오면 리셋
}

// SPIKE: 2창 연속, 나머지: 3창 연속
int required = (newState == SPIKE) ? spikeSustainedWindows : sustainedWindows;
return consecutiveCount >= required;

수치 예시 (sustained_windows=3):

윈도우1: SPIKE 감지 → count=1 → 전환 안 함
윈도우2: HIGH_STABLE → count 리셋
윈도우3: SPIKE 감지 → count=1 → 전환 안 함 (노이즈)
윈도우4: SPIKE 감지 → count=2 → 전환 안 함
윈도우5: SPIKE 감지 → count=3 → 전환! ✅

SPIKE만 2창(20초)으로 예외 처리한 이유: 3창(30초)을 기다리면 이미 서비스 장애가 발생했을 수 있다.


3-3. SwitchPolicy — 5중 안전장치

Hysteresis 외에도 전환을 막는 게이트가 4개 더 있다.

public synchronized boolean shouldSwitch(
        TrafficState newState, TrafficState currentState, double confidence) {

    if (newState == currentState) return false;                    // 1. 동일 상태
    if (confidence < minConfidence) return false;                  // 2. 신뢰도 부족
    if (isInCooldown()) return false;                              // 3. 쿨다운 중
    if (recentSwitchCount() >= maxSwitchesPerMinute) return false; // 4. 분당 횟수 초과
    if (consecutiveCount < required) return false;                 // 5. Hysteresis

    return true;
}

각 게이트의 역할:

  • Cooldown (10초): 전환 직후 메트릭이 안정화되기 전에 또 전환하는 걸 막는다
  • min_confidence (0.6): PatternAnalyzer가 확신하지 못하면 전환 안 함
  • max_switches_per_minute (3): 무한 전환 루프 방지
  • Hysteresis: 연속 N회 확인

4. TrafficState 5가지

State판단 기준
LOW_TRAFFICRPS < 5
HIGH_STABLERPS 높고 변화율 < 30%, latency 안정
SPIKERPS 변화율 > 100%
OVERLOADED_NODE특정 서버 error rate > 10% or avg latency > 500ms
GRADUAL_INCREASERPS 변화율 30~100% 구간 지속

PatternAnalyzer는 이 5가지를 우선순위 순서로 검사한다.

// 1순위: 특정 서버 이상 (가장 긴급)
if (findOverloadedServer(latest) != null) return OVERLOADED_NODE;

// 2순위: 저트래픽 (알고리즘 선택 의미 없음)
if (currentRps < lowRpsThreshold) return LOW_TRAFFIC;

// 3순위: 급증
if (rpsDerivative >= spikeThreshold) return SPIKE;

// 4순위: 점진적 증가
if (rpsDerivative >= gradualThreshold) return GRADUAL_INCREASE;

// 5순위: 안정 (기본값)
return HIGH_STABLE;

SPIKE가 오면서 특정 서버도 과부하가 걸리는 경우, 더 긴급한 OVERLOADED_NODE를 먼저 처리하는 방식이다.

confidence 계산은 임계값에서 얼마나 벗어났는지로 결정한다:

// SPIKE: rpsDerivative가 클수록 확신도 높음
double confidence = Math.min(0.99, 0.7 + rpsDerivative * 0.15);
// derivative=1.0 → confidence=0.85
// derivative=2.0 → confidence=0.99

5. 동시성 설계

알고리즘 전환이 런타임 요청 처리와 동시에 일어나는데, Lock을 최소화하면서 안전성을 보장하는 게 핵심이었다.

구조적용 위치이유
AtomicInteger CASRR 인덱스, LC 연결 수단일 카운터 원자적 연산
volatile알고리즘 참조, 서버 건강 상태즉시 가시성 보장
ConcurrentHashMapIP Hash 캐시키별 독립 락으로 경합 최소화
CopyOnWriteArrayList서버 목록읽기 빈도 >> 쓰기 빈도
ConcurrentLinkedQueue메트릭 이벤트 버퍼Non-blocking 요청 기록

volatile이 충분한 경우와 부족한 경우를 구분하는 게 중요하다.

// ServerPool: 알고리즘 전환
private volatile LoadBalancer currentBalancer;

// 전환: 단일 참조 교체 (write once) → volatile로 충분
public void switchAlgorithm(AlgorithmType type) {
    currentBalancer = balancers.get(type); // volatile write
}

// 요청: 기존 또는 새 인스턴스 중 하나만 보면 됨 → volatile read
public BackendServer selectServer(String clientInfo) {
    return currentBalancer.select(servers, clientInfo);
}

volatile이 부족한 경우: count++처럼 read-modify-write 복합 연산. 이건 AtomicInteger.compareAndSet()으로 처리해야 한다.


6. Explainability — 모든 결정에 "왜"를 기록

public record DecisionResult(
    TrafficState state,
    AlgorithmType selectedAlgorithm,
    double confidence,
    String reason,          // 핵심
    Instant timestamp,
    MetricsSnapshot snapshot
) {}

실제로 기록되는 reason 예시:

[Decision] state=SPIKE algorithm=LEAST_CONNECTIONS confidence=0.87
reason="RPS derivative=1.52 (threshold=1.0) → SPIKE detected.
LC selected: benchmark showed 35% lower p95 latency vs RR in SPIKE pattern.
Switch approved: cooldown=OK, sustained=2/2 windows."

이 로그가 쌓이면 AI Agent의 post_analyzer.py 입력으로 활용된다. "이 전환이 적절했는지" 사후 평가하는 데 쓰인다.


다음 편에서는 AI Feedback Loop 구체적인 구현, GPT-4o 프롬프트 설계, 자동 롤백 로직, 벤치마크 결과를 정리한다.

0개의 댓글