Resilience 엔지니어링 확장 섹션 20-24
TL;DR
- Python, Node.js, Rust 생태계의 Resilience 라이브러리를 비교해 언어별 선택 기준을 한 번에 파악할 수 있다.
- gRPC 내장 메커니즘, Kotlin Coroutine 통합 패턴, 최신 2025-2026 트렌드를 통해 설계부터 운영까지 연결된 관점을 제공한다.
- 모니터링 대시보드/PromQL/알림/런북까지 포함해 실무에서 바로 적용 가능한 운영 가이드를 제공한다.
1. 개념
이 문서는 Resilience 엔지니어링의 확장 주제(섹션 20~24)를 모아, 언어별 라이브러리 선택부터 gRPC·Kotlin 통합, 최신 산업 동향, 운영 모니터링 실전까지를 한 흐름으로 정리한 기술 레퍼런스다.
2. 배경
분산 시스템은 장애가 기본값이며, 단일 기술 스택만으로는 복원력 요구사항을 충족하기 어렵다. 특히 멀티 언어 아키텍처, gRPC 기반 마이크로서비스, 코루틴/비동기 실행 모델, 그리고 AI 서비스 확산으로 인해 기존의 단편적인 패턴 문서만으로는 현장 대응이 어려워졌다.
3. 이유
실무에서는 “어떤 라이브러리를 쓰는가”보다 “어떤 운영 결과를 만들 수 있는가”가 중요하다. 이 문서는 설계 원칙, 구현 패턴, 메트릭/알림/런북까지 한 번에 연결해 팀이 공통 기준으로 빠르게 의사결정하고 장애 대응 품질을 높이도록 돕는다.
4. 특징
- 언어별(Py/Node/Rust) 핵심 라이브러리 비교와 선택 포인트 제공
- gRPC 내장 Retry/Hedging/Health Checking 등 프로토콜 레벨 복원력 설명
- Kotlin Coroutine 환경에서의 Resilience4j 통합 시 주의사항 정리
- eBPF/Wasm/LLM/Platform Engineering 등 2025-2026 최신 트렌드 반영
- Grafana, PromQL, AlertManager, Tracing, Runbook까지 포함한 운영 중심 구성
5. 상세 내용
20. 다중 언어 생태계 Resilience 라이브러리
Java/Spring 생태계 밖에서도 Resilience 패턴은 필수이다. Python, Node.js, Rust 생태계 각각의 주요 라이브러리와 설계 철학, 사용법을 비교한다.
20.1 Python 생태계
pybreaker — Python Circuit Breaker
┌─────────────────────────────────────────────────────────────────┐
│ pybreaker (Daniel Fernandes Martins, v1.4.1) │
│ 이름 유래: "py" + "breaker" (Python용 CB) │
│ │
│ [핵심 설계] │
│ ├── CircuitBreaker 클래스 단일 진입점 │
│ ├── fail_max: 실패 임계값 (기본 5) │
│ ├── reset_timeout: OPEN→HALF_OPEN 전환 대기 시간 (기본 60초) │
│ ├── success_threshold: HALF_OPEN→CLOSED 전환 성공 횟수 │
│ ├── exclude: 실패로 간주하지 않을 예외 목록 │
│ └── Listener 패턴으로 상태 변경 이벤트 구독 │
│ │
│ [분산 상태 — Redis Storage] │
│ ├── CircuitRedisStorage로 다중 인스턴스 간 CB 상태 공유 │
│ ├── Redis Hash에 fail_counter, state, opened_at 저장 │
│ └── 마이크로서비스 환경에서 인스턴스 간 일관성 보장 │
│ │
└─────────────────────────────────────────────────────────────────┘
import pybreaker
import redis
# 기본 사용법
breaker = pybreaker.CircuitBreaker(
fail_max=5,
reset_timeout=30,
exclude=[ValueError], # ValueError는 실패로 간주하지 않음
)
@breaker
def call_external_service():
return requests.get("https://api.example.com/data", timeout=5)
# Listener 패턴 — 상태 변경 모니터링
class MetricsListener(pybreaker.CircuitBreakerListener):
def state_change(self, cb, old_state, new_state):
print(f"CB '{cb.name}': {old_state.name} → {new_state.name}")
def failure(self, cb, exc):
metrics.increment("cb.failure", tags={"breaker": cb.name})
breaker = pybreaker.CircuitBreaker(
fail_max=5,
reset_timeout=30,
listeners=[MetricsListener()],
)
# Redis 분산 상태 (마이크로서비스 환경)
redis_conn = redis.StrictRedis(host="redis", port=6379)
storage = pybreaker.CircuitRedisStorage(
pybreaker.STATE_CLOSED,
redis_conn,
namespace="payment-service",
)
breaker = pybreaker.CircuitBreaker(
fail_max=5,
reset_timeout=30,
state_storage=storage,
)
tenacity — 끈질긴 Retry 라이브러리
┌─────────────────────────────────────────────────────────────────┐
│ tenacity (Julien Danjou, v9.1.4) │
│ 이름 유래: "끈질김" — 포기하지 않고 재시도 │
│ retrying 라이브러리의 fork (유지보수 중단 대체) │
│ │
│ [핵심 설계 — 조합 가능한 Strategy 객체] │
│ ├── stop: 언제 멈출 것인가? │
│ │ ├── stop_after_attempt(N) │
│ │ ├── stop_after_delay(seconds) │
│ │ └── stop_after_attempt(5) | stop_after_delay(30) (OR) │
│ ├── wait: 얼마나 기다릴 것인가? │
│ │ ├── wait_fixed(seconds) │
│ │ ├── wait_exponential(multiplier, min, max) │
│ │ ├── wait_random(min, max) │
│ │ └── wait_exponential_jitter(initial, max, jitter) │
│ ├── retry: 어떤 조건에서 재시도할 것인가? │
│ │ ├── retry_if_exception_type(IOError) │
│ │ ├── retry_if_result(lambda r: r is None) │
│ │ └── retry_if_exception_type(A) | retry_if_result(B) (OR) │
│ └── asyncio/Trio 네이티브 지원 │
│ │
└─────────────────────────────────────────────────────────────────┘
from tenacity import (
retry, stop_after_attempt, stop_after_delay,
wait_exponential_jitter, retry_if_exception_type,
before_log, after_log,
)
import logging
logger = logging.getLogger(__name__)
# 조합 가능한 Strategy
@retry(
stop=stop_after_attempt(5) | stop_after_delay(30),
wait=wait_exponential_jitter(initial=1, max=60, jitter=2),
retry=retry_if_exception_type((ConnectionError, TimeoutError)),
before=before_log(logger, logging.WARNING),
after=after_log(logger, logging.WARNING),
reraise=True,
)
def fetch_data(url: str) -> dict:
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
# asyncio 지원
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential_jitter(initial=0.5, max=10),
)
async def async_fetch(session, url):
async with session.get(url) as resp:
return await resp.json()
stamina — 안전 최우선 Retry
┌─────────────────────────────────────────────────────────────────┐
│ stamina (Hynek Schlawack, v25.2.0) │
│ 이름 유래: "지구력" — 끝까지 버티기 │
│ tenacity의 opinionated wrapper │
│ │
│ [핵심 철학] │
│ ├── 안전한 기본값 최우선 — 잘못 쓰기 어렵게 설계 │
│ ├── Prometheus counter 내장 (stamina.retry_count) │
│ ├── structlog 통합 지원 │
│ ├── set_active(False) → 테스트 시 재시도 전체 비활성화 │
│ └── API 표면적 최소화 — 모범 사례만 노출 │
│ │
│ 비교: tenacity vs stamina │
│ ┌──────────────┬──────────────────┬──────────────────┐ │
│ │ 항목 │ tenacity │ stamina │ │
│ ├──────────────┼──────────────────┼──────────────────┤ │
│ │ API 크기 │ 많은 옵션 │ 최소한 │ │
│ │ 기본 전략 │ 없음 (직접 구성) │ exp backoff │ │
│ │ 모니터링 │ 콜백 직접 구현 │ Prometheus 내장 │ │
│ │ 테스트 │ mock 필요 │ set_active(False)│ │
│ │ 유연성 │ 매우 높음 │ 제한적 (의도적) │ │
│ └──────────────┴──────────────────┴──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
import stamina
# 간결한 API — 안전한 기본값 자동 적용
@stamina.retry(on=ConnectionError, attempts=5)
def call_api():
return httpx.get("https://api.example.com/data")
# 테스트 시 전체 재시도 비활성화
stamina.set_active(False) # 모든 @stamina.retry가 즉시 실행
# Prometheus 메트릭 자동 수집
# stamina_retries_total{callable="call_api", retry_num="1"} counter
HTTP 클라이언트 내장 Retry
┌─────────────────────────────────────────────────────────────────┐
│ Python HTTP 클라이언트 Retry 지원 │
│ │
│ [httpx — AsyncHTTPTransport] │
│ transport = httpx.AsyncHTTPTransport(retries=3) │
│ async with httpx.AsyncClient(transport=transport) as client: │
│ response = await client.get("https://api.example.com") │
│ → 연결 실패에만 재시도 (HTTP 오류 코드에는 재시도 안 함) │
│ │
│ [aiohttp] │
│ 기본 retry 없음 — 서드파티 라이브러리 필요 │
│ aiohttp-retry 패키지 사용: │
│ retry_options = ExponentialRetry(attempts=3) │
│ retry_client = RetryClient(retry_options=retry_options) │
│ │
│ [urllib3 — requests 내부] │
│ Retry(total=3, backoff_factor=0.3, │
│ status_forcelist=[500, 502, 503, 504]) │
│ → requests.Session()의 HTTPAdapter에 mount │
│ │
└─────────────────────────────────────────────────────────────────┘
20.2 Node.js 생태계
opossum — EventEmitter 기반 Circuit Breaker
┌─────────────────────────────────────────────────────────────────┐
│ opossum (Lance Ball / Red Hat, v8.1.3) │
│ 이름 유래: "주머니쥐 (Opossum)" │
│ — 위협받으면 죽은 척하는 동물 (CB가 OPEN이면 거부) │
│ │
│ [핵심 설계] │
│ ├── EventEmitter 기반 — Node.js 관용적 API │
│ ├── errorThresholdPercentage: 실패율 임계값 (기본 50%) │
│ ├── volumeThreshold: 최소 요청 수 (기본 5) │
│ ├── rollingCountTimeout: 통계 윈도우 (기본 10000ms) │
│ ├── timeout: 요청 타임아웃 (기본 10000ms) │
│ ├── AbortController 통합 — 타임아웃 시 요청 자동 취소 │
│ ├── capacity: Bulkhead (동시 실행 제한) │
│ └── fallback 함수 등록 │
│ │
│ [이벤트 목록] │
│ ├── fire — 함수 호출 시 │
│ ├── success — 성공 시 │
│ ├── failure — 실패 시 │
│ ├── timeout — 타임아웃 시 │
│ ├── reject — CB OPEN으로 거부 시 │
│ ├── open — CLOSED → OPEN 전환 │
│ ├── close — HALF_OPEN → CLOSED 전환 │
│ ├── halfOpen — OPEN → HALF_OPEN 전환 │
│ └── fallback — Fallback 실행 시 │
│ │
└─────────────────────────────────────────────────────────────────┘
import CircuitBreaker from 'opossum';
// 기본 사용법
const breaker = new CircuitBreaker(
async (url) => {
const controller = new AbortController();
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
{
timeout: 5000, // 5초 타임아웃
errorThresholdPercentage: 50, // 50% 실패율에서 OPEN
volumeThreshold: 10, // 최소 10개 요청 후 판단
rollingCountTimeout: 30000, // 30초 윈도우
rollingCountBuckets: 10, // 10개 버킷
resetTimeout: 15000, // 15초 후 HALF_OPEN
capacity: 20, // Bulkhead: 동시 20개 제한
}
);
// Fallback 등록
breaker.fallback((url) => ({
data: [],
source: 'cache',
message: 'Service temporarily unavailable',
}));
// 이벤트 기반 모니터링
breaker.on('open', () => console.warn('CB OPEN — 요청 차단 시작'));
breaker.on('halfOpen', () => console.info('CB HALF_OPEN — 시험 요청 허용'));
breaker.on('close', () => console.info('CB CLOSED — 정상 복귀'));
breaker.on('reject', () => metrics.increment('cb.rejected'));
// 실행
const data = await breaker.fire('https://api.example.com/users');
// Prometheus 메트릭 (opossum-prometheus 패키지)
import { PrometheusMetrics } from 'opossum-prometheus';
const prometheus = new PrometheusMetrics({ circuits: [breaker] });
cockatiel — Polly에서 영감받은 Policy 기반 Resilience
┌─────────────────────────────────────────────────────────────────┐
│ cockatiel (Connor Wills / MS VSCode팀, MIT) │
│ 이름 유래: "작은 앵무새 (Cockatiel)" │
│ — Polly(앵무새)에서 영감, .NET Polly의 Node.js 버전 │
│ │
│ [핵심 설계] │
│ ├── IPolicy 인터페이스 — 모든 패턴의 공통 계약 │
│ ├── wrap() 조합 — 여러 Policy를 하나로 합성 │
│ ├── Zero dependencies — 번들 크기 최소화 │
│ ├── SamplingBreaker — 통계 기반 CB │
│ ├── ConsecutiveBreaker — 연속 실패 기반 CB │
│ └── TypeScript 우선 설계 │
│ │
│ [Policy 종류] │
│ ├── RetryPolicy — 재시도 │
│ ├── CircuitBreakerPolicy — 차단기 │
│ │ ├── SamplingBreaker (비율 기반) │
│ │ └── ConsecutiveBreaker (연속 실패 기반) │
│ ├── TimeoutPolicy — 시간 제한 │
│ ├── BulkheadPolicy — 동시 실행 제한 │
│ └── FallbackPolicy — 대체 동작 │
│ │
└─────────────────────────────────────────────────────────────────┘
import {
CircuitBreakerPolicy, SamplingBreaker, ConsecutiveBreaker,
retry, handleAll, wrap, timeout, bulkhead, fallback, TimeoutStrategy,
} from 'cockatiel';
// SamplingBreaker — 통계 기반 CB
const circuitBreaker = new CircuitBreakerPolicy(
handleAll,
new SamplingBreaker({
threshold: 0.5, // 50% 실패율
duration: 30_000, // 30초 윈도우
minimumRps: 5, // 최소 5 req/s
})
);
// Policy 조합 (wrap)
const retryPolicy = retry(handleAll, {
maxAttempts: 3,
backoff: { type: 'exponential', initialDelay: 500 },
});
const timeoutPolicy = timeout(5_000, TimeoutStrategy.Aggressive);
const bulkheadPolicy = bulkhead(10, 20); // 동시 10개, 큐 20개
// 조합: Retry → CircuitBreaker → Timeout → Bulkhead (외→내)
const resilientPolicy = wrap(
retryPolicy, circuitBreaker, timeoutPolicy, bulkheadPolicy
);
const result = await resilientPolicy.execute(() =>
fetch('https://api.example.com/data').then(r => r.json())
);
p-retry — 미니멀 Retry
┌─────────────────────────────────────────────────────────────────┐
│ p-retry (Sindre Sorhus, v7.1.1) │
│ 이름 유래: "p" (Promise) + "retry" │
│ │
│ [핵심 설계] │
│ ├── ESM 전용 (CommonJS 미지원) │
│ ├── AbortError — 특정 실패에서 재시도 즉시 중단 │
│ ├── shouldRetry 콜백 — 조건부 재시도 │
│ ├── unref 옵션 — 이벤트 루프 보호 (타이머가 프로세스 종료 │
│ │ 를 막지 않도록) │
│ └── p-timeout과 함께 사용 권장 │
│ │
└─────────────────────────────────────────────────────────────────┘
import pRetry, { AbortError } from 'p-retry';
const result = await pRetry(
async () => {
const response = await fetch('https://api.example.com/data');
if (response.status === 404) {
throw new AbortError('Resource not found — 재시도 무의미');
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
},
{
retries: 5,
shouldRetry: (error) => error.message !== 'RATE_LIMITED',
onFailedAttempt: (error) => {
console.log(
`Attempt ${error.attemptNumber} failed. ` +
`${error.retriesLeft} retries left.`
);
},
unref: true, // 타이머가 Node.js 프로세스 종료를 막지 않음
}
);
20.3 Rust 생태계
tower — Service 추상화 계층
┌─────────────────────────────────────────────────────────────────┐
│ tower (tower-rs, v0.5.3) │
│ 이름 유래: "탑" — Layer를 쌓아 올리는 구조 │
│ │
│ [핵심 설계 — Service Trait] │
│ │
│ trait Service<Request> { │
│ type Response; │
│ type Error; │
│ type Future: Future<Output = Result<Response, Error>>; │
│ │
│ fn poll_ready(&mut self, cx: &mut Context) -> Poll<()>; │
│ fn call(&mut self, req: Request) -> Self::Future; │
│ } │
│ │
│ [ServiceBuilder 체이닝] │
│ ServiceBuilder::new() │
│ .timeout(Duration::from_secs(5)) │
│ .rate_limit(100, Duration::from_secs(1)) │
│ .concurrency_limit(50) // Bulkhead │
│ .retry(MyRetryPolicy) │
│ .service(MyService) │
│ │
│ [생태계 영향] │
│ ├── 144,000+ 의존 크레이트 (Rust 생태계 핵심) │
│ ├── Hyper, Tonic (gRPC), Axum 모두 tower 기반 │
│ ├── tower-http: HTTP 특화 미들웨어 │
│ └── tower-circuitbreaker: 별도 크레이트 (CB 전용) │
│ │
└─────────────────────────────────────────────────────────────────┘
use tower::{ServiceBuilder, ServiceExt, timeout::TimeoutLayer};
use tower::limit::{RateLimitLayer, ConcurrencyLimitLayer};
use std::time::Duration;
// ServiceBuilder를 통한 Layer 합성
let service = ServiceBuilder::new()
.layer(TimeoutLayer::new(Duration::from_secs(5)))
.layer(RateLimitLayer::new(100, Duration::from_secs(1)))
.layer(ConcurrencyLimitLayer::new(50))
.service(my_service);
// tower-circuitbreaker 별도 사용
use tower_circuitbreaker::{CircuitBreakerLayer, Config};
let cb_layer = CircuitBreakerLayer::new(
Config::new()
.failure_threshold(5)
.success_threshold(3)
.timeout(Duration::from_secs(30))
);
let service = ServiceBuilder::new()
.layer(cb_layer)
.service(my_service);
backon — Zero-cost Retry
┌─────────────────────────────────────────────────────────────────┐
│ backon (Xuanwo, v1.6.0) │
│ 이름 유래: "back on" + "backoff" │
│ backoff 크레이트 대체 — 더 현대적인 API │
│ │
│ [핵심 설계] │
│ ├── Iterator 기반 Backoff — 지연 시간 시퀀스 생성 │
│ ├── .retry() Trait Extension — 기존 함수에 체이닝 │
│ ├── Zero-cost abstraction — 런타임 오버헤드 없음 │
│ ├── async/sync 모두 지원 │
│ └── Tokio, async-std 런타임 무관 │
│ │
│ [Backoff 종류] │
│ ├── ExponentialBackoff { factor, min_delay, max_delay, │
│ │ max_times } │
│ ├── ConstantBackoff { delay, max_times } │
│ └── 커스텀 Iterator 구현 가능 │
│ │
└─────────────────────────────────────────────────────────────────┘
use backon::{ExponentialBuilder, Retryable};
use anyhow::Result;
use std::time::Duration;
async fn fetch_data() -> Result<String> {
let resp = reqwest::get("https://api.example.com/data").await?;
Ok(resp.text().await?)
}
// Trait Extension 패턴 — .retry()로 체이닝
let result = fetch_data
.retry(ExponentialBuilder::default()
.with_factor(2.0)
.with_min_delay(Duration::from_millis(100))
.with_max_delay(Duration::from_secs(10))
.with_max_times(5))
.when(|e| e.is::<reqwest::Error>()) // 조건부 재시도
.await?;
governor — GCRA Rate Limiter
┌─────────────────────────────────────────────────────────────────┐
│ governor (boinkor-net, v0.10.4) │
│ 이름 유래: "조속기 (Governor)" — 엔진 속도 조절 장치 │
│ │
│ [핵심 설계] │
│ ├── GCRA (Generic Cell Rate Algorithm) — ATM 네트워크 유래 │
│ ├── AtomicU64 단일 상태 — 극도로 낮은 메모리 사용 │
│ ├── lock-free 구현 — 높은 동시성 │
│ ├── Keyed Rate Limiter — IP별, 사용자별 독립 제한 │
│ └── tower-governor — tower Layer 통합 │
│ │
│ [tower-governor 통합] │
│ Axum / Tonic에서 미들웨어로 사용: │
│ let governor_layer = GovernorLayer::per_second(10); │
│ → 초당 10개 요청 제한 │
│ │
└─────────────────────────────────────────────────────────────────┘
use governor::{Quota, RateLimiter};
use std::num::NonZeroU32;
// 기본 Rate Limiter — 초당 50개 요청
let limiter = RateLimiter::direct(
Quota::per_second(NonZeroU32::new(50).unwrap())
);
// 요청 전 확인
if limiter.check().is_ok() {
handle_request().await;
} else {
return Err(Status::resource_exhausted("rate limited"));
}
// Keyed Rate Limiter — IP별 제한
let keyed_limiter = RateLimiter::keyed(
Quota::per_second(NonZeroU32::new(10).unwrap())
);
// IP별 독립 제한
keyed_limiter.check_key(&client_ip)?;
// tower-governor Axum 통합
use tower_governor::{GovernorLayer, GovernorConfigBuilder};
let config = GovernorConfigBuilder::default()
.per_second(10)
.burst_size(30)
.finish()
.unwrap();
let app = Router::new()
.route("/api", get(handler))
.layer(GovernorLayer { config });
20.4 언어별 라이브러리 비교표
| 항목 | Python pybreaker | Python tenacity | Python stamina | Node.js opossum | Node.js cockatiel | Rust tower | Rust backon |
|---|---|---|---|---|---|---|---|
| 패턴 | CB | Retry | Retry | CB+Bulkhead | CB+Retry+Timeout+Bulkhead | Timeout+RateLimit+Bulkhead | Retry |
| CB 지원 | O (핵심) | X | X | O (핵심) | O (SamplingBreaker) | 별도 크레이트 | X |
| Retry 지원 | X | O (핵심) | O (핵심) | X | O | O (Policy) | O (핵심) |
| 분산 상태 | Redis | X | X | X | X | X | X |
| 모니터링 | Listener | 콜백 | Prometheus 내장 | EventEmitter | 이벤트 | Metrics Layer | X |
| Async | X (동기) | O (asyncio) | O (asyncio) | O (Promise) | O (Promise) | O (Future) | O (Future) |
| 번들 크기 | 작음 | 중간 | 작음 | 중간 | Zero deps | Layer 별도 | 작음 |
| 철학 | 단순 CB | 유연한 조합 | 안전한 기본값 | Node 관용적 | .NET Polly 스타일 | 합성 가능한 Layer | Zero-cost |
21. gRPC 내장 Resilience 메커니즘
gRPC는 HTTP/2 기반 RPC 프레임워크로, 프로토콜 수준에서 다양한 Resilience 메커니즘을 내장하고 있다. 별도 라이브러리 없이도 상당한 수준의 복원력을 확보할 수 있다.
21.1 Retry Policy (gRFC A6)
┌─────────────────────────────────────────────────────────────────┐
│ gRPC Retry Policy — Service Config 기반 │
│ 참고: gRFC A6 "Client-Side Retry Design" │
│ │
│ [Service Config JSON 구조] │
│ │
│ { │
│ "methodConfig": [{ │
│ "name": [{ "service": "my.package.MyService" }], │
│ "retryPolicy": { │
│ "maxAttempts": 4, │
│ "initialBackoff": "0.1s", │
│ "maxBackoff": "1s", │
│ "backoffMultiplier": 2, │
│ "retryableStatusCodes": [ │
│ "UNAVAILABLE", "DEADLINE_EXCEEDED" │
│ ] │
│ } │
│ }] │
│ } │
│ │
│ [maxAttempts 제한] │
│ ├── 서버 설정: 최대 5 (하드 리밋) │
│ ├── 초과 값 지정 시 자동으로 5로 절삭 │
│ └── 첫 시도 포함 → maxAttempts=4이면 최대 3번 재시도 │
│ │
│ [Backoff 공식] │
│ random(0, min(initialBackoff * backoffMultiplier^(n-1), │
│ maxBackoff)) │
│ ├── +-20% jitter 자동 적용 │
│ └── n = 재시도 횟수 (1부터 시작) │
│ │
│ [retryThrottling — 토큰 기반 재시도 억제] │
│ "retryThrottling": { │
│ "maxTokens": 10, // 최대 토큰 수 │
│ "tokenRatio": 0.1 // 성공 시 회복 비율 │
│ } │
│ ├── 초기 토큰 = maxTokens │
│ ├── 재시도 시 1 토큰 소비 │
│ ├── 성공 시 tokenRatio만큼 회복 │
│ ├── 토큰 < maxTokens/2 이면 재시도 중단 │
│ └── 서버 과부하 시 자동으로 재시도 빈도 감소 │
│ │
│ [Server Pushback] │
│ ├── 서버가 "grpc-retry-pushback-ms" 메타데이터 반환 │
│ ├── 값 >= 0: 해당 시간 후 재시도 │
│ ├── 값 없음: 클라이언트 backoff 사용 │
│ └── 서버가 재시도 타이밍을 직접 제어 가능 │
│ │
└─────────────────────────────────────────────────────────────────┘
21.2 투명 재시도 vs 구성 가능 재시도
┌─────────────────────────────────────────────────────────────────┐
│ Transparent Retry vs Configurable Retry │
│ │
│ [Transparent Retry — 투명 재시도] │
│ ├── 조건: 요청이 서버 애플리케이션에 도달하지 못한 경우 │
│ │ ├── 연결 실패 (TCP 수준) │
│ │ ├── HTTP/2 REFUSED_STREAM │
│ │ └── RST_STREAM (NO_ERROR) │
│ ├── maxAttempts에 포함되지 않음 │
│ ├── 서버 애플리케이션은 요청 존재 자체를 모름 │
│ └── 항상 활성화 (비활성화 불가) │
│ │
│ [Configurable Retry — 구성 가능 재시도] │
│ ├── 조건: 서버가 retryableStatusCodes 중 하나를 반환 │
│ ├── maxAttempts에 포함됨 │
│ ├── Service Config에 명시적 설정 필요 │
│ ├── 서버 애플리케이션이 요청을 처리하고 실패 응답 │
│ └── 멱등성(Idempotency) 확인 필수 │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 재시도 판단 흐름 │ │
│ │ │ │
│ │ 요청 실패 │ │
│ │ ├── 서버 앱 미도달? │ │
│ │ │ ├── YES → Transparent Retry (자동) │ │
│ │ │ └── NO ↓ │ │
│ │ ├── retryPolicy 설정됨? │ │
│ │ │ ├── NO → 실패 반환 │ │
│ │ │ └── YES ↓ │ │
│ │ ├── Status Code가 retryableStatusCodes에 포함? │ │
│ │ │ ├── NO → 실패 반환 │ │
│ │ │ └── YES ↓ │ │
│ │ ├── maxAttempts 초과? │ │
│ │ │ ├── YES → 실패 반환 │ │
│ │ │ └── NO ↓ │ │
│ │ ├── retryThrottling 토큰 부족? │ │
│ │ │ ├── YES → 실패 반환 │ │
│ │ │ └── NO → Backoff 후 재시도 │ │
│ │ └── Server Pushback 있으면 해당 시간 대기 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
21.3 Hedging Policy
┌─────────────────────────────────────────────────────────────────┐
│ Hedging Policy — 투기적 실행 │
│ 참고: Jeff Dean "Tail at Scale" (2013) 구현 │
│ │
│ [개념] │
│ ├── 첫 요청의 응답이 hedgingDelay 내에 오지 않으면 │
│ │ 동일 요청을 추가 서버에 병렬 전송 │
│ ├── 가장 먼저 도착한 응답 사용, 나머지 취소 │
│ ├── tail latency (p99) 개선에 매우 효과적 │
│ └── retry와 동시 사용 불가 (methodConfig에서 택 1) │
│ │
│ [Service Config] │
│ { │
│ "methodConfig": [{ │
│ "name": [{ "service": "my.SearchService" }], │
│ "hedgingPolicy": { │
│ "maxAttempts": 3, │
│ "hedgingDelay": "0.5s", │
│ "nonFatalStatusCodes": ["UNAVAILABLE", "INTERNAL"] │
│ } │
│ }] │
│ } │
│ │
│ [동작 타임라인] │
│ │
│ t=0.0s 요청 #1 전송 ──────────────► Server A │
│ t=0.5s 응답 없음 → 요청 #2 전송 ──► Server B │
│ t=0.7s Server B 응답 도착 (사용) │
│ t=1.2s Server A 응답 도착 (취소/무시) │
│ │
│ → p99 latency: 1.2s → 0.7s로 개선 │
│ │
│ [주의사항] │
│ ├── 서버 부하 증가 (최대 maxAttempts배) │
│ ├── 멱등(Idempotent) 메서드에만 사용 │
│ ├── nonFatalStatusCodes: 해당 코드 수신 시 다음 hedging 허용 │
│ └── retryThrottling과 연동 (토큰 소비) │
│ │
└─────────────────────────────────────────────────────────────────┘
21.4 Health Checking (gRFC L5)
┌─────────────────────────────────────────────────────────────────┐
│ gRPC Health Checking Protocol │
│ 참고: gRFC L5 "gRPC Health Checking" │
│ │
│ [서비스 정의] │
│ package grpc.health.v1; │
│ │
│ service Health { │
│ rpc Check(HealthCheckRequest) │
│ returns (HealthCheckResponse); // Unary │
│ rpc Watch(HealthCheckRequest) │
│ returns (stream HealthCheckResponse); // Streaming │
│ } │
│ │
│ [상태 값] │
│ ├── SERVING — 정상, 요청 수락 가능 │
│ ├── NOT_SERVING — 비정상, 요청 거부 │
│ ├── UNKNOWN — 상태 미확인 (초기값) │
│ └── SERVICE_UNKNOWN — 해당 서비스 등록 안 됨 │
│ │
│ [Check vs Watch] │
│ ├── Check (Unary): 호출 시점의 상태 반환 │
│ │ └── 폴링 방식: 주기적으로 Check 호출 │
│ └── Watch (Streaming): 상태 변경 시 자동 Push │
│ └── 효율적: 변경 시에만 네트워크 사용 │
│ │
│ [K8s 1.24+ 네이티브 gRPC Probe] │
│ │
│ # Pod spec │
│ livenessProbe: │
│ grpc: │
│ port: 50051 │
│ service: "my.package.MyService" # 선택사항 │
│ initialDelaySeconds: 10 │
│ periodSeconds: 10 │
│ │
│ readinessProbe: │
│ grpc: │
│ port: 50051 │
│ initialDelaySeconds: 5 │
│ periodSeconds: 5 │
│ │
│ → K8s 1.24 이전: grpc-health-probe 바이너리 필요 │
│ → K8s 1.24+: 네이티브 지원 (바이너리 불필요) │
│ │
└─────────────────────────────────────────────────────────────────┘
21.5 Wait-for-Ready 시맨틱
┌─────────────────────────────────────────────────────────────────┐
│ Wait-for-Ready │
│ │
│ [개념] │
│ ├── 채널이 TRANSIENT_FAILURE 상태일 때 │
│ │ ├── 기본: 즉시 UNAVAILABLE 에러 반환 │
│ │ └── wait-for-ready: 채널이 READY가 될 때까지 큐잉 │
│ ├── deadline은 여전히 적용됨 │
│ │ └── deadline 초과 시 DEADLINE_EXCEEDED 반환 │
│ └── 서비스 시작 순서에 의존하지 않는 설계 가능 │
│ │
│ [사용 예시] │
│ │
│ // Java │
│ stub.withWaitForReady() │
│ .withDeadlineAfter(30, TimeUnit.SECONDS) │
│ .getData(request); │
│ │
│ // Go │
│ grpc.WaitForReady(true) │
│ │
│ [적합한 상황] │
│ ├── 서비스 시작 시 의존 서비스가 아직 준비 안 된 경우 │
│ ├── 일시적 네트워크 단절 후 자동 복구 기대 │
│ └── 배치 작업 등 즉시 응답이 필요하지 않은 경우 │
│ │
│ [부적합한 상황] │
│ ├── 사용자 대면 요청 (즉시 실패가 더 나은 UX) │
│ └── tight deadline이 있는 경우 │
│ │
└─────────────────────────────────────────────────────────────────┘
21.6 Keepalive + MAX_CONNECTION_AGE
┌─────────────────────────────────────────────────────────────────┐
│ gRPC Keepalive & Connection Management │
│ │
│ [문제: K8s에서 gRPC Long-Lived 연결] │
│ ├── gRPC는 HTTP/2 기반 → 하나의 TCP 연결에 다중 스트림 │
│ ├── 연결이 오래 유지됨 → K8s Service 로드밸런싱 무력화 │
│ ├── 새 Pod 추가 시 트래픽 분배 안 됨 │
│ └── 해결: MAX_CONNECTION_AGE로 주기적 연결 갱신 │
│ │
│ [서버 측 설정] │
│ │
│ GRPC_ARG_KEEPALIVE_TIME_MS = 7200000 // 2시간 │
│ GRPC_ARG_KEEPALIVE_TIMEOUT_MS = 20000 // 20초 │
│ GRPC_ARG_MAX_CONNECTION_AGE_MS = 3600000 // 1시간 │
│ GRPC_ARG_MAX_CONNECTION_AGE_GRACE_MS = 5000 // 5초 │
│ │
│ [Keepalive PING 메커니즘] │
│ ├── 클라이언트 → 서버: 주기적 HTTP/2 PING 프레임 │
│ ├── 서버 응답 없음 → 연결 끊김 감지 │
│ ├── KEEPALIVE_TIMEOUT 내 응답 없으면 연결 종료 │
│ └── NAT/방화벽 유휴 타임아웃보다 짧게 설정 │
│ │
│ [MAX_CONNECTION_AGE — +-10% Jitter 자동 적용] │
│ ├── 설정값: 3600초 → 실제: 3240~3960초 (랜덤) │
│ ├── Jitter 목적: 모든 연결이 동시에 끊기는 것 방지 │
│ ├── AGE 도달 → GOAWAY 프레임 전송 → 클라이언트 재연결 │
│ └── GRACE 기간 동안 진행 중 RPC 완료 허용 │
│ │
│ [K8s 환경 권장 설정] │
│ ├── MAX_CONNECTION_AGE: 30분~1시간 │
│ │ → Pod 스케일링 시 빠른 재분배 │
│ ├── KEEPALIVE_TIME: 30초 (K8s 기본 유휴 타임아웃 고려) │
│ └── Client-side LB (grpclb, xDS) 함께 사용 권장 │
│ │
└─────────────────────────────────────────────────────────────────┘
21.7 Resilience4j gRPC 통합
┌─────────────────────────────────────────────────────────────────┐
│ Resilience4j + gRPC 통합 패턴 │
│ │
│ [ClientInterceptor 기반 통합] │
│ │
│ gRPC Status Code → CB 실패 매핑: │
│ ┌──────────────────────┬──────────────┬──────────────────┐ │
│ │ gRPC Status Code │ CB 실패 여부 │ 이유 │ │
│ ├──────────────────────┼──────────────┼──────────────────┤ │
│ │ OK │ 성공 │ 정상 │ │
│ │ UNAVAILABLE │ 실패 │ 서비스 다운 │ │
│ │ DEADLINE_EXCEEDED │ 실패 │ 타임아웃 │ │
│ │ INTERNAL │ 실패 │ 서버 내부 오류 │ │
│ │ RESOURCE_EXHAUSTED │ 실패 │ 과부하 │ │
│ │ INVALID_ARGUMENT │ 성공 │ 클라이언트 오류 │ │
│ │ NOT_FOUND │ 성공 │ 비즈니스 로직 │ │
│ │ PERMISSION_DENIED │ 성공 │ 인증/인가 문제 │ │
│ │ UNAUTHENTICATED │ 성공 │ 인증/인가 문제 │ │
│ └──────────────────────┴──────────────┴──────────────────┘ │
│ │
│ 핵심: 서버 측 장애만 CB 실패로 간주, │
│ 클라이언트 오류(4xx 계열)는 CB 실패에서 제외 │
│ │
└─────────────────────────────────────────────────────────────────┘
// Resilience4j gRPC ClientInterceptor 구현
public class CircuitBreakerInterceptor implements ClientInterceptor {
private final CircuitBreaker circuitBreaker;
private static final Set<Status.Code> FAILURE_CODES = Set.of(
Status.Code.UNAVAILABLE,
Status.Code.DEADLINE_EXCEEDED,
Status.Code.INTERNAL,
Status.Code.RESOURCE_EXHAUSTED
);
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method,
CallOptions callOptions,
Channel next) {
// CB 상태 확인 — OPEN이면 즉시 거부
if (!circuitBreaker.tryAcquirePermission()) {
throw Status.UNAVAILABLE
.withDescription("CircuitBreaker is OPEN for "
+ method.getFullMethodName())
.asRuntimeException();
}
long startTime = System.nanoTime();
return new ForwardingClientCall
.SimpleForwardingClientCall<>(
next.newCall(method, callOptions)) {
@Override
public void start(
Listener<RespT> responseListener,
Metadata headers) {
super.start(
new ForwardingClientCallListener
.SimpleForwardingClientCallListener<>(
responseListener) {
@Override
public void onClose(
Status status,
Metadata trailers) {
long duration =
System.nanoTime() - startTime;
if (FAILURE_CODES.contains(
status.getCode())) {
circuitBreaker.onError(
duration,
TimeUnit.NANOSECONDS,
status.asRuntimeException()
);
} else {
circuitBreaker.onSuccess(
duration,
TimeUnit.NANOSECONDS
);
}
super.onClose(status, trailers);
}
}, headers);
}
};
}
}
// 사용
ManagedChannel channel = ManagedChannelBuilder
.forAddress("localhost", 50051)
.intercept(new CircuitBreakerInterceptor(circuitBreaker))
.usePlaintext()
.build();
21.8 gRPC Resilience 방식 비교
| 항목 | gRPC Built-in | Resilience4j | Istio/Envoy |
|---|---|---|---|
| Retry | Service Config JSON | RetryConfig | VirtualService retries |
| CB | 없음 (retryThrottling만) | CircuitBreakerConfig | outlierDetection |
| Hedging | hedgingPolicy | 없음 | 없음 (Mirror만) |
| Timeout | deadline | TimeLimiter | timeout |
| Rate Limit | 없음 | RateLimiterConfig | EnvoyFilter |
| Bulkhead | 없음 | BulkheadConfig | connectionPool |
| Health Check | grpc.health.v1 | HealthIndicator | DestinationRule |
| 설정 위치 | Service Config / 코드 | Java 코드 / YAML | K8s CRD |
| 장점 | 프로토콜 수준, 언어 무관 | 세밀한 제어, 풍부한 메트릭 | 코드 수정 없음, 인프라 수준 |
| 단점 | 제한적 (CB 없음) | Java/JVM 한정 | 운영 복잡성 |
| 권장 조합 | 기본으로 활성화 | 애플리케이션 레벨 보강 | 서비스 메시 전체 정책 |
22. Kotlin Coroutine 환경의 Resilience
Kotlin Coroutine은 비동기 프로그래밍의 새로운 패러다임을 제시한다. suspend 함수, Flow, 구조화된 동시성(Structured Concurrency)에서 Resilience 패턴을 올바르게 적용하려면 고유한 고려사항이 필요하다.
22.1 resilience4j-kotlin 모듈
┌─────────────────────────────────────────────────────────────────┐
│ resilience4j-kotlin — Coroutine 네이티브 확장 │
│ │
│ [핵심 확장 함수] │
│ ├── executeSuspendFunction — suspend 함수 실행 + 패턴 적용 │
│ ├── decorateSuspendFunction — suspend 함수 데코레이팅 │
│ └── Flow 확장 연산자 — circuitBreaker, retry, rateLimiter, │
│ timeLimiter 체이닝 │
│ │
│ [Cancellation 자동 처리] │
│ ├── CancellationException 발생 시 permission 자동 해제 │
│ ├── CB 실패 카운트에 포함되지 않음 │
│ └── Coroutine 취소 = 정상적인 제어 흐름 (예외가 아님) │
│ │
│ [주의사항] │
│ ├── TimeLimiter = 내부적으로 withTimeout 래핑 │
│ │ → Coroutine 환경에서는 직접 withTimeout 사용이 │
│ │ 더 자연스러움 │
│ ├── Bulkhead maxWaitTime > 0 → Dispatchers.IO에서 블로킹! │
│ │ → Semaphore 기반 구현 권장 │
│ └── @CircuitBreaker 어노테이션은 suspend 미지원 │
│ (Issue #2153) │
│ → AOP 프록시가 Coroutine Context 전파 못함 │
│ → 프로그래매틱 방식 사용 필수 │
│ │
└─────────────────────────────────────────────────────────────────┘
import io.github.resilience4j.kotlin.circuitbreaker.executeSuspendFunction
import io.github.resilience4j.kotlin.retry.executeSuspendFunction
import io.github.resilience4j.kotlin.timelimiter.executeSuspendFunction
import io.github.resilience4j.circuitbreaker.CircuitBreaker
import io.github.resilience4j.retry.Retry
import kotlinx.coroutines.flow.*
// suspend 함수 직접 실행
val circuitBreaker = CircuitBreaker.ofDefaults("paymentService")
val retry = Retry.ofDefaults("paymentRetry")
suspend fun callPaymentService(orderId: String): PaymentResult {
return circuitBreaker.executeSuspendFunction {
retry.executeSuspendFunction {
paymentClient.processPayment(orderId) // suspend 함수
}
}
}
// Flow 연산자 체이닝
fun observePayments(): Flow<Payment> {
return paymentStream()
.circuitBreaker(circuitBreaker) // CB 적용
.retry(retry) // Retry 적용
.rateLimiter(rateLimiter) // Rate Limit 적용
.catch { e ->
emit(Payment.fallback()) // Fallback
}
}
// decorateSuspendFunction — 재사용 가능한 데코레이팅
val decoratedCall = circuitBreaker.decorateSuspendFunction {
paymentClient.processPayment(it)
}
// 여러 곳에서 재사용
val result1 = decoratedCall("order-1")
val result2 = decoratedCall("order-2")
// 잘못된 사용: @CircuitBreaker 어노테이션 + suspend
// @CircuitBreaker(name = "payment") // suspend에서 동작 안 함!
// suspend fun processPayment() { ... }
//
// 올바른 사용: 프로그래매틱 방식
suspend fun processPayment(): PaymentResult {
return circuitBreaker.executeSuspendFunction {
paymentClient.process()
}
}
22.2 Ktor Client Resilience
┌─────────────────────────────────────────────────────────────────┐
│ Ktor Client — Built-in Resilience 플러그인 │
│ │
│ [HttpRequestRetry 플러그인 (built-in)] │
│ ├── retryOnServerErrors(maxRetries) │
│ ├── retryOnException(maxRetries, retryOnTimeout) │
│ ├── exponentialDelay(base, maxDelayMs) │
│ ├── constantDelay(millis) │
│ └── 커스텀 retryIf 조건 │
│ │
│ [HttpTimeout 플러그인 (built-in)] │
│ ├── requestTimeoutMillis │
│ ├── connectTimeoutMillis │
│ └── socketTimeoutMillis │
│ │
│ [CB 지원 — extra-ktor-plugins (Flaxoos)] │
│ ├── 서드파티 Ktor 플러그인 │
│ ├── CircuitBreaker 플러그인 제공 │
│ └── Resilience4j 대비 기능 제한적 │
│ │
└─────────────────────────────────────────────────────────────────┘
import io.ktor.client.*
import io.ktor.client.plugins.*
val client = HttpClient {
// Built-in Retry
install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 3)
retryOnException(maxRetries = 3, retryOnTimeout = true)
exponentialDelay(base = 2.0, maxDelayMs = 10_000)
// 커스텀 조건
retryIf { request, response ->
response.status.value == 429 // Too Many Requests
}
// 재시도 전 콜백
modifyRequest { request ->
request.headers.append(
"X-Retry-Count", retryCount.toString()
)
}
}
// Built-in Timeout
install(HttpTimeout) {
requestTimeoutMillis = 15_000
connectTimeoutMillis = 5_000
socketTimeoutMillis = 10_000
}
}
22.3 Spring WebFlux + Coroutine 통합
┌─────────────────────────────────────────────────────────────────┐
│ Spring WebFlux + Kotlin Coroutine Resilience │
│ │
│ [자동 변환] │
│ ├── Mono <-> suspend 함수 (awaitSingle, awaitSingleOrNull) │
│ ├── Flux <-> Flow (asFlow, asFlux) │
│ └── Spring이 내부적으로 Coroutine Bridge 제공 │
│ │
│ [ReactiveCircuitBreaker vs executeSuspendFunction] │
│ ┌────────────────────────┬─────────────────────────────┐ │
│ │ ReactiveCircuitBreaker │ executeSuspendFunction │ │
│ ├────────────────────────┼─────────────────────────────┤ │
│ │ Mono/Flux 기반 │ suspend 기반 │ │
│ │ Reactor 의존 │ kotlinx.coroutines 의존 │ │
│ │ Spring Cloud CB 통합 │ resilience4j-kotlin 직접 │ │
│ │ AOP 가능 │ AOP 불가 (suspend) │ │
│ │ 기존 WebFlux 코드 호환 │ Coroutine 우선 프로젝트 │ │
│ └────────────────────────┴─────────────────────────────┘ │
│ │
│ 권장: 신규 프로젝트는 executeSuspendFunction, │
│ 기존 WebFlux 마이그레이션은 ReactiveCircuitBreaker │
│ │
└─────────────────────────────────────────────────────────────────┘
import org.springframework.web.reactive.function.server.coRouter
import io.github.resilience4j.kotlin.circuitbreaker.executeSuspendFunction
// coRouter DSL — Coroutine 네이티브 라우팅
fun routes(handler: PaymentHandler) = coRouter {
"/api/payments".nest {
GET("/{id}", handler::getPayment)
POST("/", handler::createPayment)
}
}
// Handler — suspend 함수에서 Resilience4j 사용
@Component
class PaymentHandler(
private val circuitBreaker: CircuitBreaker,
private val paymentClient: PaymentClient,
) {
suspend fun getPayment(
request: ServerRequest
): ServerResponse {
val paymentId = request.pathVariable("id")
val payment = circuitBreaker.executeSuspendFunction {
paymentClient.getPayment(paymentId) // suspend 호출
}
return ServerResponse.ok().bodyValueAndAwait(payment)
}
}
// WebClient + Coroutine 변환
val result: Payment = webClient.get()
.uri("/api/payments/{id}", paymentId)
.retrieve()
.bodyToMono<Payment>()
.transform(CircuitBreakerOperator.of(circuitBreaker))
.awaitSingle() // Mono -> suspend 변환
22.4 Kotlin 특화 Resilience 패턴
sealed class로 ResilienceState 모델링
// Kotlin sealed class — CB 상태를 타입 안전하게 모델링
sealed class ResilienceState<out T> {
data class Success<T>(val data: T) : ResilienceState<T>()
data class Failure(
val error: Throwable
) : ResilienceState<Nothing>()
data class CircuitOpen(
val fallback: Any?
) : ResilienceState<Nothing>()
data class RateLimited(
val retryAfterMs: Long
) : ResilienceState<Nothing>()
data object Loading : ResilienceState<Nothing>()
}
// 패턴 매칭으로 처리
when (val state = fetchWithResilience()) {
is ResilienceState.Success -> render(state.data)
is ResilienceState.Failure -> showError(state.error)
is ResilienceState.CircuitOpen -> showFallback(state.fallback)
is ResilienceState.RateLimited -> retryAfter(state.retryAfterMs)
is ResilienceState.Loading -> showSpinner()
}
runCatching의 CancellationException 문제
┌─────────────────────────────────────────────────────────────────┐
│ runCatching + CancellationException 위험 │
│ │
│ [문제] │
│ ├── runCatching { ... } 은 모든 Throwable을 잡음 │
│ ├── CancellationException도 잡혀서 Result.failure로 래핑 │
│ ├── Coroutine 취소가 전파되지 않음 → 구조화된 동시성 파괴 │
│ └── 부모 Coroutine이 취소를 인지 못함 │
│ │
│ [해결: CancellationException 반드시 재던지기] │
│ │
│ // 위험: CancellationException 삼킴 │
│ val result = runCatching { suspendFunction() } │
│ │
│ // 안전: CancellationException 재던지기 │
│ suspend fun <T> runSuspendCatching( │
│ block: suspend () -> T │
│ ): Result<T> { │
│ return try { │
│ Result.success(block()) │
│ } catch (e: CancellationException) { │
│ throw e // 반드시 재던지기! │
│ } catch (e: Throwable) { │
│ Result.failure(e) │
│ } │
│ } │
│ │
└─────────────────────────────────────────────────────────────────┘
Channel 기반 Rate Limiting & supervisorScope
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.sync.Semaphore
// Channel 기반 Rate Limiter (Token Bucket)
class CoroutineRateLimiter(
private val permits: Int,
private val periodMs: Long,
) {
private val channel = Channel<Unit>(permits)
init {
CoroutineScope(Dispatchers.Default).launch {
while (isActive) {
repeat(permits) { channel.trySend(Unit) }
delay(periodMs)
}
}
}
suspend fun acquire() { channel.receive() }
}
// supervisorScope = Bulkhead (장애 격리)
// 하나의 자식 실패가 다른 자식에 영향 안 줌
suspend fun processOrders(orders: List<Order>) {
supervisorScope {
orders.map { order ->
async {
try {
processOrder(order)
} catch (e: Exception) {
logger.error("Order ${order.id} failed", e)
null // 개별 실패 -> 다른 주문은 계속 처리
}
}
}.awaitAll()
}
}
// Semaphore 기반 Bulkhead (권장)
// Resilience4j ThreadPool Bulkhead 대체
val bulkhead = Semaphore(permits = 20)
suspend fun callWithBulkhead(request: Request): Response {
bulkhead.withPermit {
return apiClient.call(request)
}
}
withTimeout vs TimeLimiter 비교
| 항목 | withTimeout (Coroutine) | TimeLimiter (Resilience4j) |
|---|---|---|
| 취소 방식 | CancellationException + 협력적 취소 | Future.cancel / withTimeout 래핑 |
| Coroutine 호환 | 네이티브 | 래핑 필요 |
| 메트릭 | 직접 구현 필요 | Prometheus/Micrometer 내장 |
| 설정 관리 | 코드 하드코딩 | YAML/동적 설정 |
| 권장 사용처 | 단순 타임아웃 | 모니터링 필요, 중앙 관리 |
22.5 KMP(Kotlin Multiplatform) Resilience 대안
┌─────────────────────────────────────────────────────────────────┐
│ KMP Resilience 라이브러리 비교 │
│ │
│ [Arrow Resilience] │
│ ├── Arrow FP 생태계의 Resilience 모듈 │
│ ├── Schedule API — 재시도 전략 조합 │
│ │ ├── Schedule.recurs(5) │
│ │ ├── Schedule.exponential(250.milliseconds) │
│ │ └── Schedule.recurs(5) and Schedule.exponential(...) │
│ ├── protectOrThrow — 성공 또는 예외 │
│ ├── protectEither — Either<Error, Success> 반환 │
│ └── KMP 지원 (JVM, Native, JS) │
│ │
│ [kmp-resilient] │
│ ├── 8가지 패턴: CB, Retry, Timeout, Bulkhead, │
│ │ Rate Limiter, Cache, Fallback, Hedge │
│ └── KMP 전용 설계 │
│ │
│ [Kresil] │
│ ├── Kotlin-first Resilience 라이브러리 │
│ ├── CB, Retry, Rate Limiter │
│ └── KMP 지원 │
│ │
│ ┌───────────────┬──────────────┬──────────────┬─────────────┐ │
│ │ 항목 │ Arrow │ kmp-resilient│ Kresil │ │
│ ├───────────────┼──────────────┼──────────────┼─────────────┤ │
│ │ FP 스타일 │ Either/결과 │ 콜백 │ 혼합 │ │
│ │ CB 지원 │ Schedule │ O │ O │ │
│ │ KMP 범위 │ 전체 │ 전체 │ JVM+Native │ │
│ │ 커뮤니티 │ 대규모 │ 소규모 │ 소규모 │ │
│ │ 성숙도 │ 높음 │ 중간 │ 초기 │ │
│ └───────────────┴──────────────┴──────────────┴─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
// Arrow Resilience — Schedule API
import arrow.resilience.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
// 스케줄 조합: 5회 재시도 AND 지수 백오프
val schedule = Schedule.recurs<Throwable>(5) and
Schedule.exponential<Throwable>(250.milliseconds, 2.0)
// retryOrElseRaise — 실패 시 예외
val result: String = schedule.retryOrElseRaise {
httpClient.get("https://api.example.com/data").body()
}
// Either 기반 안전한 처리
val either: Either<Throwable, String> = either {
schedule.retryRaise {
httpClient.get("https://api.example.com/data")
.body<String>()
}
}
22.6 테스팅
import kotlinx.coroutines.test.*
import io.github.resilience4j.circuitbreaker.CircuitBreaker
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig
import io.github.resilience4j.circuitbreaker.CallNotPermittedException
import app.cash.turbine.test
// runTest + TestDispatcher — Coroutine 시간 제어
@Test
fun `CB OPEN시 fallback 반환`() = runTest {
val cb = CircuitBreaker.ofDefaults("test")
// CB 강제 상태 전환 (테스트 헬퍼)
cb.transitionToOpenState()
val result = runCatching {
cb.executeSuspendFunction { apiClient.call() }
}
assertTrue(result.isFailure)
assertIs<CallNotPermittedException>(
result.exceptionOrNull()
)
}
// Turbine — Flow 테스트
@Test
fun `Flow에 CB 적용 시 상태 변화 검증`() = runTest {
val cb = CircuitBreaker.of(
"test",
CircuitBreakerConfig.custom()
.slidingWindowSize(3)
.failureRateThreshold(66.0f)
.build()
)
flowOf(1, 2, 3, 4, 5)
.map {
if (it <= 3) throw RuntimeException("fail")
else it
}
.circuitBreaker(cb)
.catch { /* 에러 무시 */ }
.test {
// CB가 OPEN되면 더 이상 emit 안 됨
cancelAndIgnoreRemainingEvents()
}
assertEquals(CircuitBreaker.State.OPEN, cb.state)
}
// CB 강제 상태 전환 헬퍼
fun CircuitBreaker.forceOpen() = transitionToOpenState()
fun CircuitBreaker.forceClose() = transitionToClosedState()
fun CircuitBreaker.forceHalfOpen() = transitionToHalfOpenState()
23. 2025-2026 최신 Resilience 동향
Resilience Engineering은 빠르게 진화하고 있다. eBPF, WebAssembly, AI/LLM, Platform Engineering 등 최신 기술과의 융합이 가속화되고 있다.
23.1 eBPF 기반 Resilience
┌─────────────────────────────────────────────────────────────────┐
│ eBPF + Resilience — 커널 수준 제어 │
│ │
│ [Cilium 1.16 — Gateway API 내장] │
│ ├── 2024년 AWS EKS 기본 CNI로 채택 │
│ ├── L4 = eBPF (커널 공간) — kube-proxy 대체 │
│ │ └── TCP/UDP 로드밸런싱, NAT 직접 처리 │
│ ├── L7 = Envoy (유저스페이스) — HTTP/gRPC 라우팅 │
│ │ └── CB, Retry, Rate Limit 등 L7 정책 │
│ ├── Gateway API 1.0 내장 — Ingress 대체 표준 │
│ └── kube-proxy 제거 → 네트워크 성능 ~30% 향상 │
│ │
│ [계층 분리 — L4 eBPF + L7 Envoy] │
│ │
│ 요청 흐름: │
│ Client → [eBPF L4] → [Envoy L7] → Service │
│ │ │ │
│ │ ├── Circuit Breaker │
│ │ ├── Retry Policy │
│ │ ├── Rate Limiting │
│ │ └── Health Checking │
│ │ │
│ ├── Connection Tracking │
│ ├── Load Balancing (DNAT) │
│ ├── Network Policy (필터링) │
│ └── 대부분의 L4 트래픽은 커널에서 처리 │
│ → Envoy 바이패스 가능 → 오버헤드 최소화 │
│ │
│ [Tetragon — 런타임 보안 + 옵저버빌리티] │
│ ├── eBPF 기반 런타임 보안 관찰 │
│ ├── 프로세스 실행, 파일 접근, 네트워크 이벤트 추적 │
│ ├── 보안 이벤트 → Resilience 판단에 활용 가능 │
│ └── "이 서비스가 공격받고 있는가?" 실시간 감지 │
│ │
│ [bpf_circuit_breaker — 사용자 정의 eBPF CB] │
│ ├── BPF Map에 실패 카운트, 상태 저장 │
│ ├── XDP/TC 훅에서 패킷 수준 CB 구현 │
│ ├── 유저스페이스 프록시 없이 커널에서 직접 차단 │
│ └── 연구/실험 단계 — 프로덕션 사례 제한적 │
│ │
└─────────────────────────────────────────────────────────────────┘
23.2 WebAssembly(Wasm) 기반 Resilience 확장
┌─────────────────────────────────────────────────────────────────┐
│ Proxy-Wasm — 프록시 확장 표준 │
│ │
│ [Proxy-Wasm v0.2.1 Specification] │
│ ├── 30+ 언어에서 Wasm으로 컴파일 가능 │
│ │ (Rust, Go, C/C++, AssemblyScript, Zig 등) │
│ ├── 샌드박스 격리 — 프록시 크래시 방지 │
│ ├── 핫 리로드 — 프록시 재시작 없이 필터 교체 │
│ └── Envoy, NGINX, Istio에서 지원 │
│ │
│ [Resilience 확장 사례] │
│ │
│ sentinel-go-envoy-proxy-wasm: │
│ ├── Alibaba Sentinel의 Go 구현을 Wasm으로 컴파일 │
│ ├── Envoy 사이드카에 로드 │
│ ├── 커스텀 Rate Limiting, Flow Control 구현 │
│ └── 프록시 수준에서 애플리케이션 로직 실행 │
│ │
│ [장점] │
│ ├── 언어 무관 — Rust로 작성, 모든 프록시에서 실행 │
│ ├── 안전 — 샌드박스 내 실행, 메모리 격리 │
│ ├── 성능 — 네이티브에 가까운 실행 속도 │
│ └── 유연 — Envoy 내장 필터로 불가능한 커스텀 로직 가능 │
│ │
│ [아키텍처] │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Envoy Proxy │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐│ │
│ │ │ HTTP │→│ Wasm │→│ Upstream ││ │
│ │ │ Listener │ │ Filter │ │ Cluster ││ │
│ │ │ │ │ (CB/RL) │ │ ││ │
│ │ └──────────┘ └──────────┘ └──────────────────┘│ │
│ │ ↑ Wasm VM │ │
│ │ │ (V8/Wasmtime) │ │
│ └────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
23.3 AI/LLM 서비스 Resilience
┌─────────────────────────────────────────────────────────────────┐
│ AI/LLM 서비스 Resilience — 2025-2026 핵심 과제 │
│ │
│ [복합 Rate Limit 체계] │
│ ├── RPM (Requests Per Minute) — 요청 횟수 제한 │
│ ├── TPM (Tokens Per Minute) — 총 토큰 제한 │
│ ├── ITPM (Input Tokens Per Minute) — 입력 토큰 제한 │
│ ├── OTPM (Output Tokens Per Minute) — 출력 토큰 제한 │
│ └── 4가지 제한 중 하나라도 초과하면 429 에러 │
│ │
│ 기존 HTTP API: 1차원 Rate Limit (RPM만) │
│ LLM API: 4차원 Rate Limit → 훨씬 복잡한 제어 필요 │
│ │
│ [Token Budget Management] │
│ ├── tiktoken 등으로 사전 토큰 수 계산 │
│ ├── 요청 전 토큰 예산 확인 → 초과 시 대기/분할 │
│ ├── Sliding Window로 분당 토큰 사용량 추적 │
│ └── 입력/출력 비율 예측 기반 예산 배분 │
│ │
│ [다중 제공업체 Circuit Breaker] │
│ │
│ 요청 → CB(OpenAI) ─── OPEN ──→ CB(Anthropic) ─── OPEN ──→ │
│ │ │ │
│ └── CLOSED → OpenAI └── CLOSED → Anthropic │
│ │
│ 모두 OPEN → 로컬 모델 (Ollama/vLLM) Fallback │
│ │
│ 가용성 비교: │
│ ├── 단일 제공업체: 99.7% uptime │
│ └── 다중 제공업체 CB: 99.99%+ uptime (이론적) │
│ 실측: 88.3% (단일) → 99.7% (다중 CB 적용) │
│ │
│ [Prompt Caching — GPU 과부하 완화] │
│ ├── 동일/유사 프롬프트의 KV Cache 재사용 │
│ ├── 시스템 프롬프트 캐싱 → 비용 50~90% 절감 │
│ ├── GPU 연산 부하 감소 → Rate Limit 여유 확보 │
│ └── Anthropic Prompt Caching, OpenAI Predicted Outputs │
│ │
│ [LiteLLM — 통합 LLM Gateway] │
│ ├── 470K+ 월간 다운로드 │
│ ├── 100+ LLM 단일 인터페이스 (OpenAI 호환 API) │
│ ├── 내장 Resilience: │
│ │ ├── Load Balancing (Round Robin, Least Connection) │
│ │ ├── Retry (provider 간 자동 전환) │
│ │ ├── Rate Limit 관리 (Redis 기반) │
│ │ └── Fallback Chain │
│ └── Budget Manager — 비용 제한 │
│ │
│ [GPU 서비스 레이어드 방어] │
│ │
│ Layer 1: API Gateway Rate Limit (RPM/TPM) │
│ ↓ │
│ Layer 2: Token Budget Manager (사전 토큰 검증) │
│ ↓ │
│ Layer 3: Circuit Breaker (제공업체별) │
│ ↓ │
│ Layer 4: Queue + Priority (요청 우선순위 관리) │
│ ↓ │
│ Layer 5: Prompt Caching (GPU 부하 감소) │
│ ↓ │
│ Layer 6: Fallback (다른 모델/제공업체/로컬) │
│ │
└─────────────────────────────────────────────────────────────────┘
23.4 Platform Engineering과 Resilience
┌─────────────────────────────────────────────────────────────────┐
│ Platform Engineering — Resilience 민주화 │
│ │
│ [Backstage Golden Path Templates] │
│ ├── 새 서비스 생성 시 Resilience 패턴 기본 포함: │
│ │ ├── Canary Deployment 설정 │
│ │ ├── OpenTelemetry 계측 │
│ │ ├── Circuit Breaker 기본 설정 │
│ │ ├── Retry + Timeout 기본값 │
│ │ ├── Health Check 엔드포인트 │
│ │ └── Grafana 대시보드 템플릿 │
│ ├── 개발자가 Resilience를 "선택"하는 게 아니라 │
│ │ "기본으로 제공"받는 구조 │
│ └── IDP (Internal Developer Platform) 표준화 │
│ │
│ [Golden Path 효과] │
│ ├── Before: 팀별 Resilience 구현 편차 → 장애 취약점 발생 │
│ └── After: 조직 전체 일관된 Resilience 수준 보장 │
│ │
│ [Self-Service Resilience Configuration] │
│ ├── 개발자 포털에서 CB 임계값 조정 │
│ ├── PR 기반 설정 변경 + 자동 검증 │
│ └── Guardrails: 위험한 설정 자동 차단 │
│ │
└─────────────────────────────────────────────────────────────────┘
23.5 ML 기반 프로액티브 Circuit Breaker
┌─────────────────────────────────────────────────────────────────┐
│ Proactive Circuit Breaker — ML 예측 기반 │
│ 참고: arXiv 2503.13195 (2025) │
│ │
│ [기존 CB의 한계] │
│ ├── Reactive: 장애 발생 후 감지 → 이미 일부 요청 실패 │
│ ├── Sliding Window 기반 → 과거 데이터로 현재 판단 │
│ └── 임계값 정적 → 트래픽 패턴 변화에 적응 못함 │
│ │
│ [ML 기반 Proactive CB] │
│ ├── 예측 기반 이상 감지 (Anomaly Detection) │
│ │ ├── 응답 시간 분포 변화 감지 (p50, p95, p99 추이) │
│ │ ├── 오류율 추세 분석 (미분값 기반) │
│ │ └── 장애 발생 전 CB OPEN (선제적 차단) │
│ │ │
│ ├── Ground Truth CB 패턴 │
│ │ ├── 서로게이트 모델: 경량 ML 모델이 서비스 상태 예측 │
│ │ ├── Health Score: 0.0~1.0 실수 │
│ │ │ (이진 OPEN/CLOSED 대신) │
│ │ └── 점진적 트래픽 조절 (0.7이면 30% 차단) │
│ │ │
│ └── LSTM 기반 실패 감지기 │
│ ├── 시계열 메트릭 입력 │
│ │ (latency, error_rate, cpu, memory) │
│ ├── 다음 N분의 장애 확률 예측 │
│ ├── 확률 > 임계값 → CB 사전 OPEN │
│ └── 학습 데이터: 과거 장애 이력 (Post-Mortem 기반) │
│ │
│ [Proactive CB 아키텍처] │
│ │
│ Metrics ──► Feature ──► ML Model ──► Health ──► CB │
│ (실시간) Pipeline (LSTM/IF) Score (점진적) │
│ │ (0.0~1.0) │
│ ├── latency_p99 │
│ ├── error_rate_5m │
│ ├── cpu_utilization │
│ ├── memory_pressure │
│ ├── queue_depth │
│ └── gc_pause_time │
│ │
└─────────────────────────────────────────────────────────────────┘
23.6 OpenTelemetry + Resilience4j 통합
┌─────────────────────────────────────────────────────────────────┐
│ OTel + Resilience4j — 메트릭과 트레이스 연결 │
│ │
│ [Exemplars — 메트릭 → 트레이스 연결] │
│ ├── 메트릭 데이터 포인트에 Trace ID 첨부 │
│ ├── "실패율이 50% 초과" → │
│ │ 해당 시점 실패 트레이스 즉시 확인 │
│ ├── Prometheus + Grafana Tempo/Jaeger 연동 │
│ └── 메트릭 대시보드 → 클릭 → 구체적 트레이스 │
│ │
│ [Issue #2283 — 메트릭 설명 충돌 문제] │
│ ├── Resilience4j 자체 메트릭 설명과 OTel 설명이 충돌 │
│ ├── MeterRegistry 등록 시 description 불일치 경고 │
│ └── 해결: OTel SDK 측 description 우선 또는 │
│ 커스텀 MeterFilter │
│ │
│ [통합 아키텍처] │
│ │
│ Application │
│ ├── Resilience4j (CB/Retry/Bulkhead) │
│ │ └── Micrometer Metrics │
│ │ └── OTel Meter Provider │
│ │ ├── Metrics → Prometheus (+ Exemplars) │
│ │ └── Traces → Jaeger/Tempo │
│ └── OTel Instrumentation │
│ └── Span 자동 생성 │
│ ├── CB state transition → Span Event │
│ ├── Retry attempt → Span Event │
│ └── Bulkhead rejection → Span Attribute │
│ │
└─────────────────────────────────────────────────────────────────┘
23.7 Serverless Resilience
┌─────────────────────────────────────────────────────────────────┐
│ Serverless Resilience — 2025-2026 │
│ │
│ [Lambda Powertools] │
│ ├── Idempotency — DynamoDB 기반 멱등성 보장 │
│ │ ├── @idempotent 데코레이터 (Python) │
│ │ ├── idempotency_key 자동 생성 │
│ │ └── TTL 기반 자동 만료 │
│ ├── Batch Processing — 부분 실패 처리 │
│ │ ├── SQS Batch에서 일부 실패 시 실패 건만 재처리 │
│ │ └── bisect_batch_on_function_error │
│ └── Parameters — SSM/Secrets Manager 캐싱 + 재시도 │
│ │
│ [Step Functions — 오케스트레이션 수준 Resilience] │
│ ├── Saga 패턴: 보상 트랜잭션 자동 실행 │
│ ├── Retry 내장: maxAttempts, backoffRate, interval │
│ ├── Catch: 특정 에러 타입별 분기 │
│ ├── Timeout: HeartbeatSeconds, TimeoutSeconds │
│ └── Circuit Breaker: Step Functions으로 구현 가능 │
│ (실패 횟수 DynamoDB 추적 → Choice State 분기) │
│ │
│ [SnapStart 2.0 — Cold Start 완화] │
│ ├── Java (GA since 2022) → Python, .NET 확장 (2025) │
│ ├── Firecracker microVM 스냅샷 기반 │
│ ├── 초기화 시간: ~5초 → ~200ms (Java Spring) │
│ ├── Resilience 영향: │
│ │ ├── Cold Start 감소 → Timeout 임계값 낮출 수 있음 │
│ │ ├── 스케일링 속도 향상 → Bulkhead 여유 증가 │
│ │ └── 주의: 스냅샷 후 연결 재수립 필요 (DB, Cache) │
│ └── Provisioned Concurrency 비용 절감 효과 │
│ │
└─────────────────────────────────────────────────────────────────┘
23.8 2025-2026 Resilience 연대표
┌─────────────────────────────────────────────────────────────────┐
│ 2025-2026 Resilience Engineering 연대표 │
│ │
│ 2024 Q4 │
│ ├── Cilium 1.16 릴리스 (Gateway API 내장) │
│ ├── AWS EKS Cilium CNI 기본 채택 │
│ └── Resilience4j 2.3.x 안정화 │
│ │
│ 2025 Q1 │
│ ├── Proxy-Wasm v0.2.1 스펙 안정화 │
│ ├── LiteLLM 1.x GA (LLM Gateway 표준화) │
│ ├── ML 기반 Proactive CB 논문 발표 (arXiv 2503.13195) │
│ └── Lambda SnapStart Python/.NET 지원 시작 │
│ │
│ 2025 Q2-Q3 │
│ ├── OTel + Resilience4j Exemplars 통합 안정화 │
│ ├── Backstage Golden Path에 Resilience 템플릿 표준화 │
│ ├── eBPF 기반 L4 CB 실험적 구현 등장 │
│ └── AI/LLM 다중 제공업체 CB 패턴 보편화 │
│ │
│ 2025 Q4 - 2026 Q1 │
│ ├── LSTM 기반 Proactive CB 프로덕션 사례 등장 │
│ ├── Wasm 기반 Resilience 필터 프로덕션 배포 증가 │
│ ├── Platform Engineering IDP에 Resilience 기본 통합 │
│ └── Kubernetes Gateway API + Resilience 정책 통합 │
│ │
│ [핵심 트렌드 요약] │
│ ├── 1. 인프라 수준 Resilience (eBPF, Wasm) │
│ │ ← 코드 수정 최소 │
│ ├── 2. AI/LLM 특화 패턴 │
│ │ (복합 Rate Limit, 다중 제공업체) │
│ ├── 3. 예측 기반 Proactive (ML/LSTM → 사전 차단) │
│ ├── 4. Platform Engineering │
│ │ (Golden Path → 기본 제공) │
│ └── 5. 관측 가능성 통합 │
│ (OTel + Resilience 메트릭 일체화) │
│ │
└─────────────────────────────────────────────────────────────────┘
24. 실전 모니터링 대시보드 구성 상세
Resilience 패턴의 효과를 극대화하려면 정밀한 모니터링이 필수이다. 이 섹션에서는 Resilience4j 메트릭 레퍼런스, Grafana 패널 구성, PromQL 쿼리, AlertManager 규칙, 분산 트레이싱 통합, Runbook까지 실전에서 필요한 모든 것을 다룬다.
24.1 Resilience4j 전체 메트릭 레퍼런스
┌─────────────────────────────────────────────────────────────────┐
│ Resilience4j 메트릭 전체 목록 │
│ │
│ [Circuit Breaker — 9개 메트릭] │
│ ┌──────────────────────────────────────────────┬──────────┐ │
│ │ 메트릭 이름 │ 타입 │ │
│ ├──────────────────────────────────────────────┼──────────┤ │
│ │ resilience4j_circuitbreaker_state │ Gauge │ │
│ │ └ state: closed=0, open=1, half_open=2, │ │ │
│ │ disabled=3, forced_open=4, metrics_only=5 │ │ │
│ │ resilience4j_circuitbreaker_calls_seconds │ Timer │ │
│ │ └ kind: successful, failed, ignored, │ │ │
│ │ not_permitted │ │ │
│ │ resilience4j_circuitbreaker_not_permitted │ │ │
│ │ _calls_total │ Counter │ │
│ │ resilience4j_circuitbreaker_failure_rate │ Gauge │ │
│ │ resilience4j_circuitbreaker_slow_call_rate │ Gauge │ │
│ │ resilience4j_circuitbreaker_buffered_calls │ Gauge │ │
│ │ resilience4j_circuitbreaker_slow_calls │ Gauge │ │
│ │ resilience4j_circuitbreaker_failed_calls │ Gauge │ │
│ │ resilience4j_circuitbreaker_successful_calls │ Gauge │ │
│ └──────────────────────────────────────────────┴──────────┘ │
│ │
│ [Retry — 4개 kind] │
│ ┌──────────────────────────────────────────────┬──────────┐ │
│ │ 메트릭 이름 │ 타입 │ │
│ ├──────────────────────────────────────────────┼──────────┤ │
│ │ resilience4j_retry_calls_total │ Counter │ │
│ │ └ kind: successful_without_retry, │ │ │
│ │ successful_with_retry, │ │ │
│ │ failed_without_retry, │ │ │
│ │ failed_with_retry │ │ │
│ └──────────────────────────────────────────────┴──────────┘ │
│ │
│ [Bulkhead — Semaphore / ThreadPool] │
│ ┌──────────────────────────────────────────────┬──────────┐ │
│ │ Semaphore Bulkhead │ 타입 │ │
│ ├──────────────────────────────────────────────┼──────────┤ │
│ │ resilience4j_bulkhead_available │ │ │
│ │ _concurrent_calls │ Gauge │ │
│ │ resilience4j_bulkhead_max_allowed │ │ │
│ │ _concurrent_calls │ Gauge │ │
│ ├──────────────────────────────────────────────┼──────────┤ │
│ │ ThreadPool Bulkhead │ 타입 │ │
│ ├──────────────────────────────────────────────┼──────────┤ │
│ │ resilience4j_bulkhead_queue_depth │ Gauge │ │
│ │ resilience4j_bulkhead_queue_capacity │ Gauge │ │
│ │ resilience4j_bulkhead_thread_pool_size │ Gauge │ │
│ │ resilience4j_bulkhead_core_thread_pool_size │ Gauge │ │
│ │ resilience4j_bulkhead_max_thread_pool_size │ Gauge │ │
│ │ resilience4j_bulkhead_active_thread_count │ Gauge │ │
│ └──────────────────────────────────────────────┴──────────┘ │
│ │
│ [RateLimiter] │
│ ┌──────────────────────────────────────────────┬──────────┐ │
│ │ 메트릭 이름 │ 타입 │ │
│ ├──────────────────────────────────────────────┼──────────┤ │
│ │ resilience4j_ratelimiter_available │ │ │
│ │ _permissions │ Gauge │ │
│ │ resilience4j_ratelimiter_waiting_threads │ Gauge │ │
│ └──────────────────────────────────────────────┴──────────┘ │
│ │
│ [Custom Tags 전략] │
│ ├── application: 서비스 이름 │
│ ├── environment: prod / staging / dev │
│ ├── region: ap-northeast-2 / us-east-1 │
│ └── 설정: MeterRegistryCustomizer에서 commonTags() 추가 │
│ │
└─────────────────────────────────────────────────────────────────┘
24.2 Grafana 패널 구성
┌─────────────────────────────────────────────────────────────────┐
│ Grafana 대시보드 패널 구성 │
│ │
│ [Row 1: Circuit Breaker 상태 Overview] │
│ ┌──────────────┬──────────────┬──────────────────────────────┐ │
│ │ State │ Failure Rate │ Calls per Second │ │
│ │ Timeline │ Gauge │ Time Series │ │
│ │ │ │ │ │
│ │ CLOSED │ ┌───┐ │ │ │
│ │ OPEN │ │47%│ │ ~~ successful ~~ │ │
│ │ HALF_OPEN │ └───┘ │ ~~ failed ~~ │ │
│ │ │ Threshold: │ ~~ not_permitted ~~ │ │
│ │ (서비스별 │ 50% 빨강 │ │ │
│ │ 색상 구분) │ 30% 노랑 │ │ │
│ └──────────────┴──────────────┴──────────────────────────────┘ │
│ │
│ [Row 2: Retry & Bulkhead] │
│ ┌──────────────────────────────┬────────────────────────────┐ │
│ │ Retry Outcomes │ Bulkhead Utilization │ │
│ │ Pie Chart │ Bar Gauge │ │
│ │ │ │ │
│ │ 72% no retry │ paymentPool ████████░░ │ │
│ │ 18% retry success │ inventoryPool ██████░░░░ │ │
│ │ 7% retry failed │ searchPool ████░░░░░░ │ │
│ │ 3% no retry fail │ │ │
│ │ │ 80% 노랑, 95% 빨강 임계값 │ │
│ └──────────────────────────────┴────────────────────────────┘ │
│ │
│ [Row 3: Rate Limiter & Response Time] │
│ ┌──────────────────────────────┬────────────────────────────┐ │
│ │ RateLimiter Permissions │ Average Response Time │ │
│ │ Time Series │ Time Series + Threshold │ │
│ │ │ │ │
│ │ — available_permissions │ — avg_response_ms │ │
│ │ — waiting_threads │ .. slow_call_threshold │ │
│ └──────────────────────────────┴────────────────────────────┘ │
│ │
│ [Template Variables 설정] │
│ ├── $application: label_values( │
│ │ resilience4j_circuitbreaker_state, application) │
│ ├── $environment: label_values(environment) │
│ ├── $cb_name: label_values( │
│ │ resilience4j_circuitbreaker_state{ │
│ │ application="$application"}, name) │
│ └── $interval: 1m, 5m, 15m, 1h │
│ │
└─────────────────────────────────────────────────────────────────┘
24.3 검증된 PromQL 쿼리 13개
┌─────────────────────────────────────────────────────────────────┐
│ PromQL 쿼리 레퍼런스 │
│ │
│ [1] CB 상태 인코딩 (State Timeline 패널용) │
│ │
│ resilience4j_circuitbreaker_state{ │
│ application="$application", │
│ name="$cb_name" │
│ } │
│ # 반환: 0=CLOSED, 1=OPEN, 2=HALF_OPEN, │
│ # 3=DISABLED, 4=FORCED_OPEN, 5=METRICS_ONLY │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ [2] 실패율 (Failure Rate) │
│ │
│ resilience4j_circuitbreaker_failure_rate{ │
│ application="$application", │
│ name="$cb_name" │
│ } │
│ # -1 = 최소 호출 수 미달 (아직 판단 불가) │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ [3] 성공 호출률 (rate) │
│ │
│ sum(rate( │
│ resilience4j_circuitbreaker_calls_seconds_count{ │
│ application="$application", │
│ name="$cb_name", │
│ kind="successful" │
│ }[$interval] │
│ )) │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ [4] 실패 호출률 (rate) │
│ │
│ sum(rate( │
│ resilience4j_circuitbreaker_calls_seconds_count{ │
│ application="$application", │
│ name="$cb_name", │
│ kind="failed" │
│ }[$interval] │
│ )) │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ [5] 무시된 호출률 │
│ (ignored — CB에 영향 안 주는 예외) │
│ │
│ sum(rate( │
│ resilience4j_circuitbreaker_calls_seconds_count{ │
│ application="$application", │
│ name="$cb_name", │
│ kind="ignored" │
│ }[$interval] │
│ )) │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ [6] 차단된 호출률 (not_permitted — CB OPEN) │
│ │
│ sum(rate( │
│ resilience4j_circuitbreaker_not_permitted_calls_total{ │
│ application="$application", │
│ name="$cb_name" │
│ }[$interval] │
│ )) │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ [7] Retry Storm 감지 │
│ │
│ sum(rate( │
│ resilience4j_retry_calls_total{ │
│ application="$application", │
│ kind=~"successful_with_retry|failed_with_retry" │
│ }[$interval] │
│ )) │
│ / │
│ sum(rate( │
│ resilience4j_retry_calls_total{ │
│ application="$application" │
│ }[$interval] │
│ )) │
│ # 결과 > 0.5 → Retry Storm 의심 │
│ # (50% 이상 재시도 발생) │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ [8] Bulkhead 사용률 │
│ │
│ 1 - ( │
│ resilience4j_bulkhead_available_concurrent_calls{ │
│ application="$application", │
│ name="$cb_name" │
│ } │
│ / │
│ resilience4j_bulkhead_max_allowed_concurrent_calls{ │
│ application="$application", │
│ name="$cb_name" │
│ } │
│ ) │
│ # 결과: 0.0 (여유) ~ 1.0 (포화) │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ [9] RateLimiter 잔여 Permission │
│ │
│ resilience4j_ratelimiter_available_permissions{ │
│ application="$application", │
│ name="$cb_name" │
│ } │
│ # 0에 가까우면 Rate Limit 임박 │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ [10] 평균 응답 시간 (ms) │
│ │
│ sum(rate( │
│ resilience4j_circuitbreaker_calls_seconds_sum{ │
│ application="$application", │
│ name="$cb_name", │
│ kind="successful" │
│ }[$interval] │
│ )) │
│ / │
│ sum(rate( │
│ resilience4j_circuitbreaker_calls_seconds_count{ │
│ application="$application", │
│ name="$cb_name", │
│ kind="successful" │
│ }[$interval] │
│ )) * 1000 │
│ # 초 → 밀리초 변환 │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ [11] Slow Call Rate │
│ │
│ resilience4j_circuitbreaker_slow_call_rate{ │
│ application="$application", │
│ name="$cb_name" │
│ } │
│ # slowCallDurationThreshold 초과 비율 │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ [12] Retry 소진율 (최대 재시도 후 최종 실패) │
│ │
│ sum(rate( │
│ resilience4j_retry_calls_total{ │
│ application="$application", │
│ kind="failed_with_retry" │
│ }[$interval] │
│ )) │
│ / │
│ sum(rate( │
│ resilience4j_retry_calls_total{ │
│ application="$application", │
│ kind=~".*_with_retry" │
│ }[$interval] │
│ )) │
│ # 결과 > 0.7 → 재시도가 도움이 안 되는 상황 │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ [13] RateLimiter 대기 스레드 추이 │
│ │
│ resilience4j_ratelimiter_waiting_threads{ │
│ application="$application", │
│ name="$cb_name" │
│ } │
│ # 지속적 증가 → Rate Limit 설정 검토 필요 │
│ │
└─────────────────────────────────────────────────────────────────┘
24.4 AlertManager 규칙 9개
# Resilience4j AlertManager Rules
groups:
- name: resilience4j_alerts
rules:
# [1] CB OPEN — Critical
- alert: CircuitBreakerOpen
expr: resilience4j_circuitbreaker_state == 1
for: 30s
labels:
severity: critical
annotations:
summary: >-
Circuit Breaker OPEN:
description: >-
서비스 의
CB 가
OPEN 상태입니다.
다운스트림 서비스 장애가 의심됩니다.
runbook_url: >-
https://wiki.example.com/runbook/cb-open
# [2] CB HALF_OPEN 장기 지속 — Warning
- alert: CircuitBreakerHalfOpenProlonged
expr: resilience4j_circuitbreaker_state == 2
for: 5m
labels:
severity: warning
annotations:
summary: >-
Circuit Breaker HALF_OPEN 5분 이상:
description: >-
HALF_OPEN 상태가 5분 이상 지속됩니다.
다운스트림이 불안정하거나
permittedNumberOfCallsInHalfOpenState가
너무 높게 설정되어 있을 수 있습니다.
# [3] 실패율 급증 (50%) — Warning
- alert: HighFailureRate
expr: >-
resilience4j_circuitbreaker_failure_rate > 50
for: 1m
labels:
severity: warning
annotations:
summary: >-
실패율 50% 초과:
(%)
# [4] 실패율 위험 (80%) — Critical
- alert: CriticalFailureRate
expr: >-
resilience4j_circuitbreaker_failure_rate > 80
for: 30s
labels:
severity: critical
annotations:
summary: >-
실패율 80% 초과:
(%)
# [5] Slow Call Rate 급증 — Warning
- alert: HighSlowCallRate
expr: >-
resilience4j_circuitbreaker_slow_call_rate > 50
for: 2m
labels:
severity: warning
annotations:
summary: >-
느린 호출 비율 50% 초과:
description: >-
다운스트림 응답 지연이 심해지고 있습니다.
Timeout 설정과 다운스트림 상태를 확인하세요.
# [6] Retry Storm 감지 — Warning
- alert: RetryStorm
expr: >-
sum(rate(
resilience4j_retry_calls_total{
kind=~".*_with_retry"
}[5m]
)) by (application)
/
sum(rate(
resilience4j_retry_calls_total[5m]
)) by (application)
> 0.5
for: 3m
labels:
severity: warning
annotations:
summary: >-
Retry Storm 감지:
description: >-
전체 요청의 50% 이상이 재시도되고 있습니다.
다운스트림 장애 시 재시도가 부하를
가중시킬 수 있습니다.
# [7] Retry 소진 — Critical
- alert: RetryExhaustion
expr: >-
sum(rate(
resilience4j_retry_calls_total{
kind="failed_with_retry"
}[5m]
)) by (application)
/
sum(rate(
resilience4j_retry_calls_total{
kind=~".*_with_retry"
}[5m]
)) by (application)
> 0.7
for: 5m
labels:
severity: critical
annotations:
summary: >-
Retry 소진:
description: >-
재시도 후에도 70% 이상 실패합니다.
재시도가 문제 해결에 도움이 되지 않는
상황입니다.
CB가 OPEN되어야 할 수 있습니다.
# [8] Bulkhead 임박 — Warning
- alert: BulkheadNearExhaustion
expr: >-
1 - (
resilience4j_bulkhead_available_concurrent_calls
/
resilience4j_bulkhead_max_allowed_concurrent_calls
) > 0.8
for: 2m
labels:
severity: warning
annotations:
summary: >-
Bulkhead 사용률 80% 초과:
# [9] RateLimiter 대기 스레드 증가 — Warning
- alert: RateLimiterWaitingThreads
expr: >-
resilience4j_ratelimiter_waiting_threads > 10
for: 1m
labels:
severity: warning
annotations:
summary: >-
RateLimiter 대기 스레드
개:
description: >-
Rate Limit에 걸린 대기 스레드가
증가하고 있습니다.
Rate Limit 설정 상향 또는
요청 패턴 검토가 필요합니다.
24.5 Distributed Tracing 통합
┌─────────────────────────────────────────────────────────────────┐
│ Spring Boot 3 + Micrometer Tracing + OTel │
│ │
│ [의존성] │
│ ├── micrometer-tracing-bridge-otel │
│ ├── opentelemetry-exporter-otlp │
│ └── resilience4j-micrometer │
│ │
│ [자동 계측] │
│ Spring Boot 3 + Micrometer Tracing 조합 시: │
│ ├── HTTP 요청 → 자동 Span 생성 │
│ ├── Resilience4j 메트릭 → 자동 수집 │
│ └── Trace ID가 로그에 자동 삽입 (MDC) │
│ │
│ [한계: Resilience4j 이벤트 → Span 연결은 수동] │
│ ├── CB 상태 전환 → Span Tag/Event 수동 추가 필요 │
│ ├── Retry 시도 횟수 → Span Event 수동 추가 필요 │
│ └── 커스텀 이벤트 리스너로 구현 │
│ │
└─────────────────────────────────────────────────────────────────┘
// 커스텀 이벤트 리스너 — CB 상태 변화를 Span Event로 기록
@Component
public class TracingCircuitBreakerListener
implements RegistryEventConsumer<CircuitBreaker> {
private final Tracer tracer;
@Override
public void onEntryAddedEvent(
EntryAddedEvent<CircuitBreaker> event) {
CircuitBreaker cb = event.getAddedEntry();
cb.getEventPublisher()
.onStateTransition(e -> {
Span currentSpan = tracer.currentSpan();
if (currentSpan != null) {
currentSpan.tag("cb.name", cb.getName());
currentSpan.tag("cb.state",
e.getStateTransition()
.getToState().name());
currentSpan.event("CB State: " +
e.getStateTransition().getFromState()
+ " -> " +
e.getStateTransition().getToState());
}
})
.onError(e -> {
Span currentSpan = tracer.currentSpan();
if (currentSpan != null) {
currentSpan.event("CB Error: "
+ e.getThrowable().getMessage());
}
});
}
}
// Retry 이벤트를 Span Event로 기록
@Component
public class TracingRetryListener
implements RegistryEventConsumer<Retry> {
private final Tracer tracer;
@Override
public void onEntryAddedEvent(
EntryAddedEvent<Retry> event) {
Retry retry = event.getAddedEntry();
retry.getEventPublisher()
.onRetry(e -> {
Span currentSpan = tracer.currentSpan();
if (currentSpan != null) {
currentSpan.event(String.format(
"Retry attempt #%d: %s",
e.getNumberOfRetryAttempts(),
e.getLastThrowable().getMessage()
));
currentSpan.tag("retry.attempts",
String.valueOf(
e.getNumberOfRetryAttempts()));
}
});
}
}
Jaeger/Tempo TraceQL 쿼리 예시
┌─────────────────────────────────────────────────────────────────┐
│ TraceQL 쿼리 예시 (Grafana Tempo) │
│ │
│ [1] CB OPEN 상태 전환이 발생한 트레이스 │
│ { span.cb.state = "OPEN" } │
│ │
│ [2] 3회 이상 Retry가 발생한 트레이스 │
│ { span.retry.attempts >= 3 } │
│ │
│ [3] 특정 서비스에서 에러가 발생한 트레이스 │
│ { resource.service.name = "payment-service" │
│ && status = error } │
│ │
│ [4] CB OPEN + 5초 이상 소요된 트레이스 │
│ { span.cb.state = "OPEN" && duration > 5s } │
│ │
│ [Jaeger 검색 쿼리] │
│ ├── Tag: cb.state=OPEN │
│ ├── Tag: retry.attempts>=3 │
│ ├── Min Duration: 5s │
│ └── Service: payment-service │
│ │
└─────────────────────────────────────────────────────────────────┘
24.6 Runbook 4종
Runbook 1: CB OPEN 대응 (5단계)
┌─────────────────────────────────────────────────────────────────┐
│ Runbook: Circuit Breaker OPEN 대응 │
│ │
│ [단계 1: 상황 파악 (2분 이내)] │
│ ├── Grafana 대시보드 → CB 상태 Timeline 확인 │
│ ├── 어떤 CB가 OPEN인지, 언제부터인지 확인 │
│ ├── 영향 범위: 해당 CB를 사용하는 API 엔드포인트 파악 │
│ └── 다른 CB도 OPEN인지 확인 (연쇄 장애 가능성) │
│ │
│ [단계 2: 다운스트림 확인 (3분 이내)] │
│ ├── 다운스트림 서비스 Health Check 직접 호출 │
│ ├── 다운스트림 서비스 메트릭 확인 │
│ │ (CPU, Memory, Error Rate) │
│ ├── 다운스트림 서비스 로그 확인 (최근 에러) │
│ └── 네트워크 이슈 여부 확인 (DNS, 방화벽, 인증서) │
│ │
│ [단계 3: 즉시 조치 (5분 이내)] │
│ ├── 다운스트림 장애 확인 시: │
│ │ ├── 다운스트림 팀에 알림 (Slack/PagerDuty) │
│ │ ├── Fallback이 정상 동작하는지 확인 │
│ │ └── 고객 영향도 파악 및 상태 페이지 업데이트 │
│ ├── 네트워크 이슈 시: │
│ │ ├── 인프라팀에 알림 │
│ │ └── 대안 경로 확인 (Multi-Region Failover) │
│ └── 원인 불명 시: │
│ ├── CB 설정 확인 (임계값이 너무 낮지 않은지) │
│ └── 최근 배포 이력 확인 (배포 후 발생?) │
│ │
│ [단계 4: 복구 확인] │
│ ├── 다운스트림 복구 후 │
│ │ CB가 HALF_OPEN → CLOSED 전환 확인 │
│ ├── Fallback 트래픽 → 정상 트래픽 전환 확인 │
│ ├── 메트릭 정상 범위 복귀 확인 (실패율, 응답 시간) │
│ └── Error Budget 소진량 확인 │
│ │
│ [단계 5: 포스트모텀] │
│ ├── 타임라인 정리 (감지 → 대응 → 복구) │
│ ├── 근본 원인 분석 (Root Cause Analysis) │
│ ├── CB 설정 튜닝 필요 여부 판단 │
│ ├── Fallback 정상 동작 여부 확인 │
│ └── 개선 Action Item 생성 │
│ │
└─────────────────────────────────────────────────────────────────┘
Runbook 2: Retry Storm 감지/대응
┌─────────────────────────────────────────────────────────────────┐
│ Runbook: Retry Storm 감지 및 대응 │
│ │
│ [감지 조건] │
│ 재시도 비율 > 50%이 3분 이상 지속 │
│ │
│ [대응 절차] │
│ 1. 다운스트림 상태 확인 │
│ ├── 다운스트림이 살아있지만 느린 경우: │
│ │ → Retry가 부하를 가중시키고 있을 가능성 │
│ │ → Retry maxAttempts 일시적 축소 검토 │
│ └── 다운스트림이 완전히 다운된 경우: │
│ → CB가 OPEN이어야 하는데 │
│ 왜 Retry가 발생하는지 확인 │
│ → CB 임계값 확인 │
│ (failureRateThreshold 너무 높은지) │
│ │
│ 2. Retry 설정 긴급 조정 (필요 시) │
│ ├── maxAttempts 축소 (5 → 2) │
│ ├── waitDuration 증가 (500ms → 2000ms) │
│ └── Spring Cloud Config / Feature Flag로 무중단 반영 │
│ │
│ 3. Retry Storm 해소 확인 │
│ ├── 재시도 비율 정상 범위(< 20%) 복귀 │
│ ├── 다운스트림 부하 감소 확인 │
│ └── 전체 시스템 TPS/응답시간 정상 확인 │
│ │
└─────────────────────────────────────────────────────────────────┘
Runbook 3: Bulkhead Exhaustion 대응
┌─────────────────────────────────────────────────────────────────┐
│ Runbook: Bulkhead Exhaustion 대응 │
│ │
│ [감지 조건] │
│ Bulkhead 사용률 > 80%이 2분 이상 지속 │
│ │
│ [대응 절차] │
│ 1. 원인 분석 │
│ ├── 다운스트림 응답 지연 → 슬롯이 오래 점유 │
│ ├── 트래픽 급증 → 정상적 용량 초과 │
│ └── 리소스 누수 → 슬롯이 반환되지 않음 │
│ │
│ 2. 즉시 조치 │
│ ├── 다운스트림 지연 시: │
│ │ ├── Timeout 확인 및 축소 검토 │
│ │ └── Timeout이 Bulkhead 점유를 │
│ │ 길게 만들 수 있음 │
│ ├── 트래픽 급증 시: │
│ │ ├── 오토스케일링 확인 │
│ │ └── Bulkhead maxConcurrentCalls │
│ │ 일시적 증가 검토 │
│ └── 리소스 누수 시: │
│ ├── Thread Dump / Heap Dump 수집 │
│ └── 비정상 스레드 상태 확인 │
│ │
│ 3. 포화 시: │
│ ├── BulkheadFullException 발생 빈도 확인 │
│ ├── Fallback 정상 동작 확인 │
│ └── 사용자 영향도 파악 │
│ │
└─────────────────────────────────────────────────────────────────┘
Runbook 4: 모니터링 → Chaos Engineering 피드백 루프
┌─────────────────────────────────────────────────────────────────┐
│ 모니터링 → Chaos Engineering 피드백 루프 │
│ │
│ [Observe → Hypothesize → Experiment → Learn] │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. OBSERVE (관찰) │ │
│ │ ├── 대시보드에서 │ │
│ │ │ "이 CB는 한 번도 OPEN된 적 없다" │ │
│ │ ├── "Bulkhead 사용률이 항상 10% 미만이다" │ │
│ │ └── "Retry가 정말 효과가 있는지 모르겠다" │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 2. HYPOTHESIZE (가설) │ │
│ │ ├── "CB 임계값이 너무 높아서 실제 장애 시 │ │
│ │ │ OPEN 안 될 수 있다" │ │
│ │ │ (failureRateThreshold=80% → 50%?) │ │
│ │ ├── "Bulkhead 설정이 너무 넉넉해서 │ │
│ │ │ 격리 효과가 없다" │ │
│ │ └── "Retry 3회 설정이 너무 많아서 │ │
│ │ Retry Storm 위험" │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 3. EXPERIMENT (실험 — Chaos Engineering) │ │
│ │ ├── Chaos Toolkit으로 다운스트림 500 에러 주입 │ │
│ │ ├── CB가 예상 시간 내 OPEN 되는지 확인 │ │
│ │ ├── Fallback이 정상 동작하는지 확인 │ │
│ │ ├── Retry Storm이 발생하지 않는지 확인 │ │
│ │ └── Bulkhead가 다른 서비스 호출을 │ │
│ │ 보호하는지 확인 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 4. LEARN (학습) │ │
│ │ ├── 실험 결과 → 설정 튜닝 │ │
│ │ │ ├── CB 임계값 조정 │ │
│ │ │ ├── Bulkhead 크기 조정 │ │
│ │ │ └── Retry 횟수/간격 조정 │ │
│ │ ├── 새로운 알람 규칙 추가 │ │
│ │ ├── 대시보드 개선 │ │
│ │ └── Runbook 업데이트 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──── 다시 1. OBSERVE로 ────┐ │ │
│ │ └───────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ [주기] │
│ ├── 일상: 대시보드 관찰 → 이상 징후 메모 │
│ ├── 월간: 관찰 결과 기반 가설 수립 │
│ ├── 분기: GameDay (Chaos Engineering 실험) │
│ └── 실험 후: 설정 튜닝 + 모니터링 개선 │
│ │
│ 핵심: 모니터링은 그 자체가 목적이 아니라, │
│ 시스템을 더 잘 이해하고 개선하기 위한 입력이다. │
│ │
└─────────────────────────────────────────────────────────────────┘