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

Chu Sang Yoon·2026년 4월 3일

lab

목록 보기
11/11
post-thumbnail

11편: Adaptive Load Balancer — AI Feedback Loop와 벤치마크 결과

GPT-4o가 벤치마크 데이터를 보고 rule을 자동으로 만들면, 과연 성능이 좋아질까?

8편에서 Runtime 설계를 끝냈다. 이번 편은 AI Feedback Loop가 실제로 어떻게 동작하는지, 그리고 결과가 어땠는지다.


1. AI Agent가 하는 일

Feedback Loop 전체 흐름:

1. K6로 4가지 시나리오 벤치마크 실행
       ↓
2. benchmark/results/ 에 JSON 저장
       ↓
3. AI Agent가 분석 → decision-rules.yml 새 버전 생성
       ↓
4. POST /alb/rules/reload → 재시작 없이 새 rule 적용
       ↓
5. 다시 K6 실행 → 고정 알고리즘 vs Adaptive 성능 비교

AI Agent 파일 구조:

ai-agent/
├── analyze.py            # 파이프라인 오케스트레이터
├── rule_generator.py     # 벤치마크 → GPT-4o → decision-rules.yml
├── pattern_classifier.py # 메트릭 시계열 → TrafficState 분류
├── post_analyzer.py      # 전환 로그 → 개선안 도출
├── rule_diff.py          # 버전 간 변경 이유 자동 문서화
└── rule_versions/        # v1.yml, v2.yml ... 버전 관리

2. GPT-4o 프롬프트 설계 원칙

temperature=0.3

temperature가 높으면 창의적이지만 일관성이 없다. rule 생성은 "같은 데이터면 비슷한 rule"이 나와야 한다. 0.3은 약간의 유연성을 허용하면서도 deterministic에 가까운 응답을 만든다.

pattern_classifier.py는 더 낮은 0.2를 쓴다. 패턴 분류는 재현성이 더 중요하기 때문이다.

System Prompt로 역할 고정

"You are a load balancer strategy expert. Respond only with valid JSON."

역할을 명확히 주면 도메인 지식이 활성화된다. "Respond only with valid JSON"은 response_format={"type": "json_object"}와 이중 보호다.

구체적인 수치 요구

"reason 필드에 왜 이 알고리즘인지 명확한 근거 포함 (성능 수치 포함)"

"좋아서" 같은 막연한 이유 대신 "latency 35% 개선" 같은 수치 기반 근거를 강제한다. 이 reason이 그대로 DecisionResult에 들어가서 Explainability를 달성한다.

토큰 절약

원본 MetricsSnapshot에는 서버별 상세 메트릭이 다 들어있다. GPT에 넘길 때는 핵심 수치만 압축해서 보낸다.

def _summarise_snapshots(snapshots: list) -> list:
    for s in snapshots:
        rps = s.get("rps", 0)
        change = ((rps - prev_rps) / prev_rps) if prev_rps else 0
        result.append({
            "rps": round(rps, 2),
            "rps_change_rate": round(change, 3),
            "avg_latency_ms": ...,
            # serverMetrics 상세는 제외 → 토큰 절약
        })

rps_change_rate를 직접 계산해서 포함시키는 게 포인트다. GPT가 raw 수치만 보는 게 아니라 변화율도 함께 볼 수 있게 한다.


3. Hot-Reload 메커니즘

AI Agent가 새 YAML을 만들면 서버 재시작 없이 즉시 적용된다.

# rule_generator.py
version_path.write_text(output)          # v9.yml 저장
_ACTIVE_RULES.write_text(output)         # decision-rules.yml 덮어쓰기
# POST /alb/rules/reload 호출
// RuleConfigLoader.java
public void reload() {
    load(); // YAML 재파싱
    log.info("Rules reloaded: {} rules loaded", rules.size());
}

rules 필드가 volatile이라 evaluate() 루프가 다음 사이클부터 새 rule을 읽는다.

rule_diff.py는 두 버전을 비교해서 변경 이유를 Markdown으로 자동 문서화한다.

Statev1 Algorithmv9 AlgorithmReason
HIGH_STABLEROUND_ROBINWEIGHTED_ROUND_ROBIN서버 가중치로 고RPS 효율 분산
SPIKEROUND_ROBINLEAST_CONNECTIONSLatency 35% 개선
OVERLOADED_NODEWLCWLC유지
GRADUAL_INCREASEWRRIP_HASH세션 일관성 유지하며 부하 분산

4. Rule 버전 히스토리 (v1 → v9)

버전HIGH_STABLESPIKEOVERLOADEDGRADUAL
v1ROUND_ROBINLCWLCWRR
v2LCWLCWRRRANDOM
v3LCWLCLCWRR
v4LCWLCWRRWRR
v5LCWLCWLCWRR
v6WRRLCWLCWRR
v7WRRLCWLCRANDOM
v8LCWLCWRRRANDOM
v9WRRLCWLCIP_HASH

LOW_TRAFFIC은 모든 버전에서 ROUND_ROBIN 유지. 저트래픽에서는 알고리즘 차이가 미미해서 GPT도 건드리지 않았다.


5. 벤치마크 결과 (stable 시나리오)

구분RPSAvg Latencyp95 LatencyError%
fixed-round_robin (기준)284.819.8ms24.7ms0%
adaptive-v3283.214.7ms23.9ms0%
adaptive-v4283.713.4ms17.0ms0% ✅
adaptive-v5269.516.4ms25.1ms0%
adaptive-v6~28412.5ms22.6ms0%

p95 기준으로 v4가 가장 낮은 17.0ms를 달성했다. 고정 Round Robin 대비 p95가 31% 개선됐다.

v6는 avg latency 12.5ms로 최저인데, 일부 사이클 데이터에 노이즈가 포함돼 있어서 단순 비교는 조심해야 한다.


6. 롤백 로직 — 새 rule이 오히려 나빠지면?

v1~v9를 순서대로 올리다 보면 가끔 이전 버전보다 성능이 나쁜 버전이 나온다. AI가 벤치마크 데이터를 분석해서 rule을 만들지만 완벽하지 않다. 그래서 새 rule을 적용한 뒤 성능이 악화되면 자동으로 이전 버전으로 롤백하는 로직을 추가했다.

동작 방식

새 rule 적용 (POST /alb/rules/reload)
       ↓
30초 대기 (메트릭 안정화)
       ↓
baseline(이전 버전) vs current(새 버전) 성능 비교
       ↓
아래 조건 중 하나라도 해당하면 자동 롤백:
  - p95 latency 15% 이상 악화
  - RPS 15% 이상 감소
  - error rate 2%p 이상 증가

핵심 구현

def evaluate_and_rollback(baseline_metrics: dict, current_metrics: dict,
                           previous_version: str) -> bool:

    p95_change = (current_metrics["p95"] - baseline_metrics["p95"]) / baseline_metrics["p95"]
    rps_change  = (current_metrics["rps"] - baseline_metrics["rps"])  / baseline_metrics["rps"]
    err_change  = current_metrics["error_rate"] - baseline_metrics["error_rate"]

    degraded = (
        p95_change  >  0.15 or   # p95 15% 이상 악화
        rps_change  < -0.15 or   # RPS 15% 이상 감소
        err_change  >  0.02      # error rate 2%p 이상 증가
    )

    if degraded:
        print(f"⚠ Performance degraded — rolling back to {previous_version}")
        _restore_version(previous_version)
        requests.post(f"{ALB_BASE}/alb/rules/reload")
        return True

    print("✓ New rules performing well")
    return False

왜 30초를 기다리나

rule이 바뀌어도 현재 진행 중인 요청들은 이전 알고리즘 상태로 완료된다. 슬라이딩 윈도우가 10초 단위라 최소 한 윈도우 이상이 새 rule로 채워져야 의미 있는 비교가 된다. 30초는 3개 윈도우가 쌓이는 시간이고, SwitchPolicy의 sustained_windows=3 설정과도 맞아 떨어진다.

실제로 롤백이 발생한 케이스

v5 적용 시 GRADUAL_INCREASE에 WRR을 유지했는데, 실험 당시 트래픽 패턴과 맞지 않아서 avg latency가 16.4ms로 오히려 v4(13.4ms)보다 나빠졌다. 롤백 로직이 이를 잡아서 자동으로 v4로 복구됐다.

이 경험으로 "AI가 만든 rule도 검증이 필요하다" 는 걸 실증했다. AI Feedback Loop에 안전장치가 없으면 잘못된 rule이 그대로 운영에 적용될 수 있다.


7. AI-Assisted 개발 — 어떻게 활용했나

이 프로젝트는 AI를 "사용 도구"가 아니라 개발 파이프라인의 일부로 통합했다. 크게 두 가지 방식으로 활용했다.

6-1. Claude Code로 구현 속도 올리기

작업 흐름은 이렇게 잡았다.

1. 인터페이스/설계 의도 직접 정의 (사람이)
       ↓
2. CLAUDE.md에 컨텍스트 문서화
       ↓
3. Claude Code로 반복적인 코드 작성
       ↓
4. 생성된 코드 리뷰 + 동시성/엣지케이스 직접 검토

MetricsCollector, PatternAnalyzer, DecisionEngine 같은 클래스는 인터페이스와 설계 의도를 먼저 직접 잡고, 슬라이딩 윈도우 flush 로직, @Scheduled 타이밍 조합, p95 계산처럼 구조가 정해진 코드 작성에 Claude Code를 활용했다.

CLAUDE.md에 아키텍처 결정, 동시성 전략, 네이밍 컨벤션을 미리 정리해두는 게 핵심이다. 이 문서 없이 그냥 코드를 생성하면 스타일이 제각각이고 기존 설계 의도와 어긋난 코드가 나온다.

Python AI Agent 파이프라인도 같은 방식이었다. 각 모듈의 입력/출력 스펙과 GPT 프롬프트 전략을 먼저 설계하고, 실제 코드 작성 단계에서 Claude Code를 사용했다.

6-2. 단위 테스트 자동 생성

SwitchPolicy, PatternAnalyzer, IpHashBalancer 등 핵심 클래스에 대해 단위 테스트를 생성했다. 총 50개 이상의 테스트가 나왔고, 엣지 케이스(연속 카운터 overflow, hysteresis 경계값, cooldown 타이밍 race 등)도 커버됐다.

예상치 못한 이점이 있었다. AI가 테스트를 작성하는 과정에서 "이 케이스는 어떻게 처리하나요?"라는 형태로 빠진 엣지 케이스를 드러냈다. 구현 단계보다 테스트 생성 단계에서 설계 허점이 더 잘 보였다.


8. Multi-Agent 코드 리뷰 — 역할 분리가 핵심이다

"AI한테 코드 리뷰 받는다"고 하면 보통 이렇게 한다.

Claude에게: "이 코드 리뷰해줘"

이건 생각보다 효과가 제한적이다. 단일 에이전트는 Java 동시성 버그를 잡으면서 동시에 Python 파이프라인의 GPT 에러 처리까지 꼼꼼하게 보기 어렵다. 관점이 분산된다.

그래서 역할이 다른 3개의 에이전트를 병렬로 구성했다.

에이전트System Prompt 핵심담당 관점
Architect"전체 설계의 일관성과 레이어 간 의존성을 검토하라"인터페이스 설계, 확장성, 아키텍처 냄새
Java Reviewer"JVM 전문가로서 동시성 버그와 성능 이슈에 집중하라"AtomicInteger 사용 패턴, volatile 범위, overflow
Python+Config Reviewer"Python AI 파이프라인과 YAML 설정 파일을 집중 검토하라"GPT 응답 파싱, 에러 핸들링, 설정 schema

각 에이전트에게 전체 코드베이스를 넘기되 System Prompt로 역할을 고정하고, 결과를 합쳐서 심각도별로 분류했다.

단일 에이전트와 차이가 컸던 이유는 관점의 깊이다. Java Reviewer는 "Java 동시성 버그만" 생각하기 때문에 Math.abs(Integer.MIN_VALUE) 같은 JVM 특이사항까지 파고들었다. Python Reviewer는 GPT API 응답에서 scalar가 반환될 때의 크래시처럼 파이프라인 특화 버그를 잡아냈다. 한 에이전트가 두 역할을 동시에 맡았다면 이 깊이는 나오지 않았을 것이다.

CRITICAL 버그들 (Java Reviewer 발견)

Math.abs(Integer.MIN_VALUE)가 음수를 반환한다

// 잘못된 코드 — RoundRobinBalancer
int idx = Math.abs(counter.getAndIncrement()) % servers.size();
// Integer.MIN_VALUE = -2147483648
// Math.abs(-2147483648) = -2147483648 (여전히 음수!)
// → ArrayIndexOutOfBoundsException

Java에서 Integer.MIN_VALUE의 절댓값은 int 범위를 벗어나서 음수가 된다. 21억 요청이 지나면 터진다.

// 수정
int idx = (counter.getAndIncrement() & Integer.MAX_VALUE) % servers.size();

IpHashBalancer IPv4 hash overflow

// 잘못된 코드
int hash = 0;
for (String part : parts) {
    hash = hash * 256 + Integer.parseInt(part);
    // 192.168.1.100 처리 중 overflow 발생
}

IPv4를 정수로 변환하는 과정에서 int 범위를 초과한다.

// 수정: long으로 계산 후 캐스팅
long hash = 0;
for (String part : parts) {
    hash = hash * 256 + Integer.parseInt(part);
}
return (int)(hash & 0x7FFFFFFF); // 부호 비트 제거

SwitchPolicy cooldown 시 hysteresis 카운터 미리셋

전환이 cooldown으로 막혔을 때 consecutiveCount를 이미 required까지 채워버리는 버그. 쿨다운이 끝나자마자 조건 검증 없이 즉시 전환되는 문제가 생겼다.

RuleConfigLoader JAR classpath hot-reload 불가

classpath:에서 파일을 읽으면 JAR 내부 리소스를 읽는 것이라 외부에서 덮어써도 반영이 안 된다. file: 경로로 외부 파일을 읽도록 수정해야 hot-reload가 동작한다.

WARNING 버그들 (Python+Config Reviewer 발견)

rule_generator.pyload_dotenv() 순서 오류

load_dotenv()를 OpenAI 클라이언트 초기화 이후에 호출하고 있었다. 환경변수가 로드되기 전에 클라이언트가 생성돼서 API 키를 못 읽는 케이스가 발생했다.

post_analyzer.py — GPT가 scalar 반환 시 crash

GPT 응답에서 리스트를 기대하는 필드에 단일 값이 오면 .items() 호출에서 AttributeError가 발생했다. 방어적 파싱이 빠져있었다.

Architect 관점에서 나온 설계 이슈

역할 특화 에이전트를 쓴 가장 큰 수혜가 여기였다. Java Reviewer는 코드 레벨 버그에 집중하고, Architect는 전체 레이어 흐름을 봤다.

주요 지적 사항:

  • RequestProxy의 SIMULATE 모드에서 error rate가 항상 0으로 고정돼 있어서 OVERLOADED_NODE 시나리오 테스트가 불가능했다
  • PrometheusMetricsPublisher에서 Gauge 람다 내부에 side-effect가 있어 Prometheus scrape 타이밍에 따라 메트릭이 부정확해질 수 있었다
  • DecisionEngineRuleConfigLoader를 직접 참조하는 구조라 rule 소스가 바뀔 때 Engine도 수정해야 하는 결합도 문제

9. 한계와 확장 가능성

한계

  • Feedback loop 타이밍과 K6 실행이 동기화 안 돼서 일부 사이클 데이터에 노이즈가 생겼다
  • TrafficState 분류가 단순 threshold 기반이라 경계 구간에서 오분류 가능성 있다
  • 벤치마크 시나리오가 synthetic이라 실제 트래픽 패턴과 차이가 있다

확장 가능성

  • State 분류를 ML 모델로 교체 (지금은 rule-based)
  • 두 알고리즘을 실시간 A/B 테스트로 병렬 운영
  • Multi-region 환경에서 region별 rule 적용

깃허브 : https://github.com/oOccasio/loadBalancing

0개의 댓글