77개 시뮬 시나리오와 4개 목표 빌더, 총 81개 시작점을 카테고리별로 정리했습니다. 카드를 누르면 즉시 로드됩니다.
돈이 흐르는 곳. ACID + idempotency + 정합성 우선. · row lock / double-charge 방지 / 외부 PG 호출 / cascading 차단
payment시작 →양방향 connection 많음. WebSocket / Reactive / Pub-Sub fanout. · connection 유지 비용 / message fanout / offline queue / 같은 방 broadcast
chat시작 →read 95% : write 5%. cache hit 극대화 + index 분리. · cache stampede / hot key / ES 인덱싱 지연 / read-after-write stale
search시작 →bandwidth heavy. CDN edge cache 90%+. origin 보호. · origin shielding / 첫 viewer cold cache / 메타데이터 vs 미디어 분리
video시작 →기본 1,000 RPS에서 Spring(Tomcat) CPU 63%, MySQL 11% — 3-tier에서 먼저 차는 건 앱이다. 슬라이더를 1,500(CPU 91%) → 1,700(99%, p99 291ms)으로 올리면 앱 한 대의 상한이 보이고, 2,000부터 accept queue가 넘쳐 queue-full 드롭이 시작된다(성공률 89.2%). MySQL은 같은 mix로 ≈8,800 RPS를 감당해서(앱 상한의 5배) 이 토폴로지에선 병목이 되지 않는다 — Spring을 6대로 늘려 10,000 RPS를 통과시켜야 비로소 MySQL CPU가 100%가 된다.
three-tier시작 →cached-read(70%) → Redis. hit 95%면 Redis가 즉답하고 miss 5%만 MySQL로 내려간다 — 2,000 RPS · 10초 실측: Redis 13,915건, MySQL은 6,685건(db-write 4,000 + db-read-light 2,000 + Redis miss ≈690)만 받는다. Redis cacheHitRate를 0.5로 내리면 miss가 10배가 되어 MySQL 부하가 그대로 따라 올라간다 — 캐시는 "DB 부하 = (1 − hit rate) × read"라는 곱셈식이다.
cached시작 →리포트 다운로드 API — 300 RPS인데 응답 버퍼가 512KB라 힙 회전이 CPU보다 빠르다. heap 64MB(parallel)에서 10초 동안 young GC 3회, Spring CPU는 32%(피크 53%)로 한가한데 p99 꼬리는 91.9ms까지 튄다. 같은 트래픽에 heap만 2,048MB로 되돌리면 GC 0회 · p99 51.2ms — 바뀐 건 힙 크기 하나뿐이다. "CPU에 여유가 있으니 괜찮다"가 왜 틀린 말인지 보여주는 프리셋.
gc-pressure시작 →병목은 한 번에 하나씩만 보인다 — 3단 실측(1,500 RPS · 10초). ① 풀 5 + HDD: 성공률 2.4%, 드롭 14,280건 중 queue-full 12,856 · pool-exhausted 1,424(acquireTimeout 1s 만료). 화면만 보면 "풀이 작아서"다. ② 풀만 50으로 늘리면 성공률 2.4% 그대로 — 진짜 상한은 디스크다: HDD 200 IOPS ÷ 요청당 5.7 IOPS(db-read-heavy 8 : db-write 2 가중) ≈ 35 RPS. ③ 디스크를 SSD(5,000 IOPS)로 바꾸면 24.6%로 뛰고, 이번엔 Spring CPU가 100%다(db-read-heavy 15.4ms/req ÷ 3.4코어 ≈ 220 RPS). 노브 하나를 고칠 때마다 다음 벽이 나온다.
pool-exhaustion시작 →Spring threads를 50 ↔ 200 ↔ 400으로 바꿔보세요. 동기 호출이라 thread가 응답 동안 점유되어 처리량이 정확히 비례합니다.
external-dep시작 →평균 3,000 msg/s인데 도착 패턴이 burst ×4 / 주기 4초 — 1초 동안 12,000 msg/s가 몰리고 3초는 0이다. Producer는 publish ack만 하므로 성공률 100%. 초과분은 Kafka lag으로 쌓였다가(100ms 샘플 기준 피크 7,897건) burst가 끝나고 2초 안에 0으로 빠진다 — 소비는 3 partition × 1,400 = 4,200 msg/s로 평탄하다. 즉 Consumer와 MySQL이 보는 건 12,000이 아니라 4,200이다. 큐는 처리량을 늘려주는 게 아니라 꼭대기를 깎아 시간으로 펴준다.
kafka-absorb시작 →HAProxy가 3개 Spring backend로 분배. 알고리즘 변경(roundrobin/leastconn/source)으로 분배 패턴 비교 가능.
lb-distribution시작 →외부 API가 30% 실패 + 500ms 추가 지연. 500 RPS × 홀드 0.56초(WAN 왕복 60ms + 500ms) = 280 동시 요청이 필요한데 Tomcat 스레드는 200뿐 — 10초 실측에서 HTTP 클라이언트 풀이 200/200으로 상시 만석이고 성공률 50.3%, 드롭은 queue-full 1,285 · downstream-error 1,056 · timeout 1,056이다. 외부는 동시 600까지 받아주는데(용량 문제 아님) 우리 스레드가 먼저 없어져서 나머지가 문 앞에서 죽는다 = cascading. 인스펙터에서 fault injection을 끄면 즉시 100%로 돌아온다.
external-failure시작 →1,000 RPS의 절반은 장애 난 외부 결제사(external-call, 30% 실패 + 500ms), 절반은 멀쩡한 내부 API(api-compute)다. CB를 끄면 외부 호출이 Tomcat 스레드 200개를 다 물고 늘어져 내부 트래픽까지 5,000건 중 3,584건(71%)만 처리되고 p50이 243ms로 늘어진다. CB를 켜면 외부는 circuit-open으로 즉시 포기하고 내부는 5,077건 전부 통과 · p50 60ms. 총 성공률은 오히려 60.9% → 51.7%로 내려간다 — CB는 성공률을 올리는 장치가 아니라 blast radius를 자르는 장치다. 인스펙터에서 CB 토글로 직접 비교.
cb-fast-fail시작 →외부 API가 25% transient 실패(timeout). retry 3회 · 100ms exponential backoff + 50ms jitter. 300 RPS · 10초 실측: 성공률 98.4%(최종 실패 이론치 0.25³ = 1.6%와 일치), 재시도 921회 = 요청당 0.31회(이론 0.25 + 0.0625 = 0.3125). 대신 꼬리를 지불한다 — p50 101.8ms인데 p95 485.8ms, p99 515.7ms가 backoff 대기다. 재시도는 성공률을 사고 지연으로 값을 치른다. RestTemplate 풀 200이라 대기 중 풀이 마르지는 않는다(실측 48/200).
retry-backoff-demo시작 →3,000 RPS · cached-read 50 : db-read-light 40 : db-write 10. Client → NGINX → Spring WebFlux → PostgreSQL 구성입니다. WebFlux의 ioWorkerCount=8, R2DBC 풀=30에서 시작해 한 번에 하나씩 바꿔 보세요. worker를 1로 줄이면 CPU 작업 병렬성이, R2DBC 풀을 1로 줄이면 DB 동시 호출이 제한됩니다. 비차단 I/O도 응답까지 커넥션·메모리·timeout을 유지합니다. 이 프리셋에는 Tomcat 비교 노드가 없습니다. 앱 프레임워크까지 바꾸는 별도 실험에서는 기본 CPU 비용과 JVM 설정도 달라지므로 CPU 차이를 컨텍스트 스위치 하나의 효과로 단정하면 안 됩니다.
reactive시작 →결제 시스템 v1 — LB → Spring 3종(각 2대) → 단일 MySQL. 세 step은 부하가 완전히 같다: 평균 7,000 RPS에 burst ×3 / 주기 4초(1.33초 동안 21,000 RPS, 나머지 2.67초는 0). 평균만 보면 MySQL 상한(≈12,500 RPS)에 여유가 있지만 꼭대기 21,000은 못 받는다 → 10초 실측 MySQL CPU 피크 100%, 앱 스레드가 DB를 기다리다 accept queue가 넘쳐 성공률 65.6%(드롭 29,120건 전부 queue-full). read·write·이벤트가 전부 한 DB로 몰리는 게 원인이다.
evolution-step1시작 →step1과 완전히 같은 부하(7,000 RPS, burst ×3 / 주기 4초)에 Redis + Primary + Replica×2만 붙였다. cached-read 30%는 Redis가, db-read 30%는 replica 2대가 가져가고 primary에는 write 10% + 이벤트 30%만 남는다 → 성공률 65.6% → 100%. 다만 이벤트가 아직 동기 write라 burst 꼭대기에 primary CPU가 90%까지 오르고 p99 꼬리가 62.9ms까지 튄다 — 그 남은 30%를 step3가 가져간다.
evolution-step2시작 →step2에서 이벤트 30%만 Kafka로 뺀 것. 같은 burst인데 primary가 보는 이벤트 쓰기는 꼭대기 6,300/s가 아니라 소비 상한 2,400/s(topic 4 partition × 600) 아래로 평탄해지고, 초과분은 lag으로 보존됐다가(피크 5,209건) 다음 burst 전에 0으로 빠진다 — 10초 실측 worker→primary 기록은 2,148/s로 이벤트 평균(7,000×30% = 2,100/s)과 같다. 즉 미룬 것이지 버린 게 아니다. 결과: primary CPU 피크 90% → 61%, p99 피크 62.9ms → 59.9ms, 성공률 100% 유지. 사용자 응답은 브로커 ack에서 끝나고 정산·알림은 worker가 자기 페이스로 뒤에서 처리한다.
evolution-step3시작 →결제 시스템 실무 패턴. 2,000 RPS · 10초 실측 — 결제 이벤트(event-publish 60%)는 payments topic으로 가고 3 consumer group(Accounting/Audit/Reward)이 각각 11,795건씩 받는다(같은 메시지를 셋 다 = pub-sub). 알림 발송 요청(api-compute 20%)은 notifications topic으로 가고 Notification Worker만 3,885건을 받는다. ★ publish 엣지를 워크로드로 갈라야 하는 이유: 두 topic 엣지를 같은 워크로드로 열면 엔진은 "택일"밖에 못 해서 결제 이벤트 절반이 알림 topic으로 샌다. topic 선택은 코드가 하는 일이지 로드밸런싱이 아니다.
kafka-payment-platform시작 →실제 K8s 계층 — Cluster > Pod > Container. production 클러스터(16c/64GB)에 web-pod(NGINX+Spring, sidecar) + db-pod(MySQL) 두 Pod. limits/replicas는 Pod-level.
k8s-pod-demo시작 →AMQP work queue 패턴 — Order Service가 RabbitMQ에 publish, 4개 Worker가 **같은 queue**를 구독해 경쟁 소비자(competing consumers)로 나눠 처리한다. 1,500 msg/s · 10초 실측: Worker 1~4가 3,730 / 3,760 / 3,751 / 3,749건 = 각자 **정확히 0.25**씩 가져간다. AMQP에는 partition이 없어 브로커가 **메시지마다** 다음 여유 consumer에게 넘기기 때문이다(prefetch로 한 consumer가 쥘 수 있는 미확인 메시지 수를 제한). Kafka와 갈리는 지점이 여기다 — Kafka는 partition 단위 배정이라 partition 3에 consumer 2면 2:1로 불균등해진다(multi-broker-production에서 실측 4,394 : 2,190). lag은 peak 80에서 0으로 회복 · 성공률 100%.
rabbitmq-work-queue시작 →같은 6,000 RPS를 셋으로 갈라 broker마다 2,000 msg/s를 보낸다. 다른 건 "소비 상한을 무엇이 정하느냐"다 — Kafka는 3 partition × 1,000 = 3,000/s(부하의 1.5배)라 10초 뒤 lag 98로 평온하고, Redis Streams는 2,000/s로 부하와 동률이라 lag 284에서 요동하고, RabbitMQ는 단일 큐 1,400/s라 초당 600건씩 밀려 lag 6,189가 된다(consumer 13,930건 = 1,393/s). 늘리는 방법도 각각이다: Kafka는 partition 추가, RabbitMQ는 consumer + prefetch, Streams는 메모리(maxlen) 한도 안에서.
broker-compare시작 →Topic을 first-class 노드로 — Kafka broker 옆에 payments/notifications 박스가 명시적. 3,000 RPS · 10초 실측: 주문 29,813건이 payments에 publish되고 같은 주문이 notifications에도 **복제** 발행되어(mirror 엣지) Kafka가 총 59,632건을 처리한다. payments를 구독하는 3 group(payments/audit/reward)과 notifications의 1 group이 **각각 29,677건**씩 받는다 — pub-sub이라 부하가 나뉘는 게 아니라 복제된다. 그 대가는 MySQL이 진다: 3개 worker가 같은 DB에 쓰므로 89,031건 · CPU 64%로 이 토폴로지에서 가장 뜨겁다. lag 278 · 성공률 100% · p50 42.2ms.
event-driven-arch시작 →실 production 시스템처럼 3 broker 동시 사용 — Kafka(영속 log) + RabbitMQ(work queue) + Redis Streams(realtime). 2,000 RPS · 10초 실측: API가 6,618 / 6,617 / 6,617로 셋에 고르게 발행하고 성공률 100%. ★ 같은 "consumer 2대"인데 분배가 다르다: Kafka는 partition 단위 sticky 배정이라 partition 3 ÷ consumer 2 = **4,394 : 2,190 (2:1)**로 기울고, RabbitMQ는 메시지 단위 경쟁 소비자라 **3,308 : 3,306 (1:1)**로 고르다. "Kafka consumer를 늘렸는데 왜 한쪽만 바쁘지?"의 답이 이 두 숫자다. Redis Streams는 단일 consumer가 6,583건. API Server는 6대 — 1대면 상한 ≈600 RPS의 3.3배라 CPU 100% + queue-full 4,008건으로 broker 비교가 아니라 API 포화가 화면을 덮었다.
multi-broker-production시작 →write→primary, read→replica 라우팅. 3,000 RPS · db-read-light 50 : db-read-heavy 20 : db-write 30 · 10초 실측: primary 9,105건(write 전량) · replica 10,618 / 10,616건(read를 정확히 반씩) — 세 DB CPU가 16 / 16 / 17%로 고르다. 성공률 100% · p50 44.2ms. 앱은 8대여야 한다: 인스턴스당 상한 ≈545 RPS라 2대(1,090 RPS)였을 때는 CPU 100% + queue-full 7,493건으로 "복제 분리"가 아니라 앱 포화가 화면을 덮었다. 복제 lag은 primary write rate에 비례해 커진다 — read-after-write 함정(방금 쓴 걸 replica에서 못 읽는다)이 여기서 나온다.
read-write-split시작 →결제 → 재고 → 배송 → 포인트 — Kafka 이벤트 chain (choreography). 중앙 coordinator 없이 각자 자기 step만 책임진다. 500 RPS · 10초 실측: Payment API가 **4,981건**을 발행하면 4개 서비스가 **각각 5,217건**을 받는다 — consumer group이 다르면 같은 메시지가 그룹 수만큼 복제되는 pub-sub이라, 4개를 합친 20,868건은 "부하 4배"가 아니라 "관점 4개"다. 발행량보다 236건 많은 건 Inventory의 재고 부족 5%(sagaFailureRate)가 compensate 이벤트를 같은 topic으로 되쏘기 때문이고, 그 보상 이벤트도 4개 서비스가 전부 받는다(Kafka 처리 5,245건). lag 28건 · e2e p50 42.3ms · p99 46.7ms · 성공률 100%.
payment-saga-choreography시작 →결제 시 hold (escrow_db에 PENDING 기록), 배송 완료/기간 만료 시 settle(SETTLED), 분쟁 시 refund(REFUNDED). 300 RPS · 10초 실측: hold 한 건이 PG 승인(왕복 100ms) → Escrow PENDING 기록을 **순서대로** 거쳐 e2e p50 145.9ms · p99 186.2ms, 성공률 100% · 드롭 0. escrow_db 2,995건 = 사용자 hold 2,694 + 정산 배치 301. 배치(api-compute 10% = 30/s)는 PENDING을 읽고 ledger_db에 이중 기입한다(301건). ★ Payment의 두 downstream을 택일(라운드로빈)로 두면 요청 절반은 PG만, 절반은 escrow만 거쳐 "돈은 잡혔는데 장부에 없는" 건이 생긴다 — 그래서 sequence 엣지다.
payment-escrow시작 →Order Service가 order_db + outbox table에 동시 INSERT (한 트랜잭션). Outbox Relay (Debezium/CDC)가 outbox 읽어 Kafka 발행. dual-write 함정 회피. (V8.5에서 outbox queue + relay lag 실모델)
outbox-pattern시작 →Command Service(write) → PostgreSQL + Kafka publish. Query Service x3(read) ← Redis cache + 읽기 모델 DB(denormalized view). 2,000 RPS · read 70 : write 20 : event 10 · 10초 실측: Command 6,002건 · Query 13,926건으로 읽기가 2.3배이고, Query 쪽만 3대로 늘려 CPU 34%를 유지한다(Command는 1대로 32%). 쓰기 경로는 PostgreSQL 3,976건 + Kafka 2,026건(lag 11)이고, Kafka가 읽기 모델 DB로 동기화해 4,020건 — 즉 읽기 모델은 쓰기 모델보다 **한 박자 늦다**(eventual consistency의 실체). 조회 11,922건은 Redis가 먼저 받아 미스만 읽기 모델로 내려간다. 성공률 100% · p50 44.5ms. API Gateway는 3대 — 1대면 CPU 100% + queue-full 1,422건으로 CQRS 교훈과 무관한 포화가 보였다.
cqrs-read-write-split시작 →요청의 데이터 키를 FNV-1a(key) mod 4로 라우팅한다. 기본 키는 request id mod 1000이며 같은 키는 같은 shard에 간다. 3,000 RPS · light read 60% / write 40%에서 네 shard의 처리량이 대체로 균등한지 확인한다. API는 6대로 앱 CPU 병목을 피했다. 키 분포가 치우치거나 같은 hot key가 반복되면 균등 분산은 보장되지 않는다. 이 전략은 modulo hash이며 consistent hashing·cross-shard query·실제 데이터 재배치는 모델링하지 않는다.
sharded-db시작 →Flash sale — 차감 트래픽 90%가 한 row(product_id=123)에 집중한다. 2,000 RPS burst ×4 · 10초 실측: 성공률 **39.9%** · p50 1,624.8ms이고, 통과량은 부하를 얼마로 올려도 **773 RPS**에서 멈춘다(1,000 RPS → 82.5%, 2,000 → 39.9%, 5,000 → 15.7%인데 처리 건수는 셋 다 7,7xx건으로 같다). 산수: hot row는 노드 안에서 직렬화되므로 상한 = 1 ÷ (1.36ms CPU + 3 IOPS ÷ 50,000) ≈ **700 req/s**이고 실측 MySQL 처리량이 정확히 7,003건/10초 = **700 RPS**다. ★ 그동안 MySQL CPU는 **14%**, 디스크 4%다 — 자원은 텅 비었는데 줄이 안 줄어든다. 그래서 서버를 늘려도(API x4 → x8) 안 낫는다. 해결: Redis INCR counter + 비동기 정산(잔량 조회 10%는 이미 Redis가 받는다) / 재고를 N개 sub-counter로 분산 / Kafka + serial consumer.
hot-key-contention시작 →Orchestrator가 saga state 기록 → Payment → Inventory → Shipping → Reward를 **순서대로 전부** 호출한다(sequence 엣지). 400 RPS · 10초 실측: 4개 서비스와 state DB가 각각 3,879~3,893건 = 요청 수와 1:1이고, 동기 chain이라 e2e p50 **77.7ms** · p99 100.6ms(choreography는 비동기라 42.3ms). orchestrator CPU 42%, 각 서비스 43~47%. ★ 이 엣지들을 택일(라운드로빈)로 두면 주문 하나가 5곳 중 **한 곳만** 호출하고 끝나 saga가 아예 성립하지 않는다(수정 전 실측: 각 서비스 783건 = 요청의 1/5). 흐름 가시성 ↑, SPOF 위험 — choreography(payment-saga-choreography)와 비교.
saga-orchestration시작 →CDN → primary (write+US users) / secondary (read+EU users). cross-region replication lag 80ms (us-east → eu-west). DR 가능, 글로벌 latency 감소 — 단 write는 항상 primary로 (지연 큰 cross-region 호출).
multi-region시작 →단일 진입점(Kong/AWS APIGW) — auth · rate limit(2,500 rps + burst 500) · routing. 3,000 RPS · 10초 실측: 게이트웨이가 25,446건만 통과시키고 **4,631건을 rate-limited(429)로 잘라** 성공률 84.6%다(= 2,500 ÷ 3,000 ≈ 83%). 통과분은 **전부** Auth를 거친 뒤(sequence 1) path에 따라 갈린다: Auth 25,434건(CPU 34%) · User 10,247 · Product 12,667 · Order 2,520. ★ 수정 전에는 auth 엣지가 cached-read만 받게 돼 있어 요청의 20%(5,120건)만 인증을 거쳤다 — 다이어그램은 "모든 요청 JWT 검증"인데 실제로는 아니었다. product_cache도 그때 0건이었다(지금 5,029건, hit 95%). 게이트웨이 자체는 CPU 3%로 한가하다 — 관문의 비용은 CPU가 아니라 "여기서 자르는 정책"이다.
api-gateway시작 →실시간 채팅 — 5,000 msg/s (10k user × 0.5 msg/s). 메시지 90%는 Redis Pub/Sub로 room broadcast, 10%는 chat_history(MySQL) batch flush다. 10초 실측: Redis 45,133건 · MySQL 4,920건 · WebFlux CPU 6%. Netty event-loop라 connection 수와 스레드 수가 분리된다 — Tomcat이면 connection 하나당 스레드 하나라 10k connection = 10k 스레드 = OOM.
websocket-chat시작 →live feed — 10,000 event/s. 80%는 subscriber로 나가는 push(sse-event), 20%는 내부 서비스가 올리는 원본 이벤트(event-publish → Event Producer → Kafka)다. SSE 서버는 그 topic을 consumer group으로 받아 다시 push한다 — 10초 실측: Producer 20,043건 발행, SSE 서버 99,771건 처리(push 79,900 + 소비 19,900), CPU 30%. ★ 발행자와 소비자를 같은 노드로 두면 소비한 메시지가 같은 워크로드로 publish 엣지에 다시 매칭돼 무한 증폭한다(실제 Kafka 파이프라인에서도 같은 함정) — 그래서 Producer를 분리했다.
sse-notification시작 →Service mesh — Order는 요청 하나마다 auth → (unary면 payment / stream이면 inventory) → own DB를 순서대로 전부 호출한다(sequence 엣지). 2,000 RPS · 10초 실측: Auth 19,963건(모든 호출에 붙는 interceptor라 CPU 45%로 가장 바쁘다), Payment 13,961(unary 70%), Inventory 6,005(stream 30%), order_db 19,964. ★ 이 엣지들을 "택일"로 두면 호출 하나가 auth 또는 payment 중 하나만 가고 order_db는 트래픽을 한 건도 못 받는다 — MSA 다이어그램이 거짓말하는 가장 흔한 형태다.
grpc-microservices시작 →대규모 service log aggregation. 5,000 evt/s · 10초 실측: App x3이 47,297건을 Kafka로 발행 → Logstash x2가 47,074건 transform → Elasticsearch x3가 **49,562건**(로그 47,074 + Kibana 대시보드 조회 2,488)을 색인한다. lag 224는 흐르는 양이다(전달률 3 partition × 5,000이 유입 4,730/s의 3배). CPU는 Logstash 37% > App 24% > ES 23% — 가장 비싼 건 저장소가 아니라 **파싱**이다(grok 정규식). 대시보드 조회 5%는 Kibana로 직행한다(수정 전에는 Kibana가 in-edge가 없는 죽은 노드였다). 성공률 100%.
log-pipeline시작 →광고 click stream 8,000 RPS(이벤트 90% + BI ad-hoc 8% + 대시보드 point query 2%). App → Kafka → Ingest가 batch insert → ClickHouse 4 shard, BI API는 같은 ClickHouse에 ad-hoc query. 10초 실측: 초당 640건뿐인 BI 쿼리가 BI API CPU를 73%까지 밀어올리는 동안, 이벤트 7,200/s를 다 받아낸 ClickHouse는 9%다. 컬럼나 OLAP에서 먼저 마르는 건 저장소가 아니라 쿼리를 들고 기다리는 API 쪽이다 — ad-hoc 동시성(동시 쿼리 상한 · 타임아웃 · 큐)이 진짜 운영 노브.
olap-analytics시작 →이미지/비디오 service. 10,000 req/s · 10초 실측: CDN이 99,309건을 받아 89,378건은 edge에서 응답하고 9,931건만 origin으로 나간다 — origin 몫 **0.100 = 1 − hit 0.9**가 그대로 곱셈으로 보인다. hit이면 e2e p50 30.1ms(edge 왕복 30ms), miss면 wan 왕복 60ms가 더 붙어 p95 90.4ms. 업로드 1%(100/s)는 CDN을 우회해 Transcoder로 가고(997건), 변환 job 하나가 S3 저장(1) → job_state 기록(2)을 순서대로 한다 — S3가 받는 10,892건 = CDN miss 9,931 + 변환 결과 961. 비용 감각: hit rate를 0.9 → 0.5로 내리면 origin으로 나가는 건수가 5배가 되고 egress 요금도 5배다.
media-cdn시작 →결제 정산 흐름의 구성 예시. Temporal 노드가 state 저장(CockroachDB) → Settlement activity(ledger 기록) → Notify activity(Email GW)를 순서대로 호출하고 각 응답을 기다린다. 50 RPS에서 모든 단계의 요청 수가 비슷한지 확인한다. 택일(라운드로빈)이면 정산·알림·state 저장 중 하나만 수행하므로 sequence가 필요하다. 이 엔진은 메모리 안의 요청·edge retry를 처리하며 Temporal의 영속 실행 이력이나 프로세스 재시작 후 장시간 workflow 복구는 재현하지 않는다. 응답 지연은 세 단계와 하위 호출을 모두 포함한다.
settlement-batch시작 →현대 e-commerce 검색 — CDN(결과 캐시) · Keycloak JWT auth · Elasticsearch(키워드) + pgvector(의미) · click 이벤트 Kafka → ClickHouse. 3,000 RPS · 10초 실측: 캐시 가능한 조회 60%는 CDN이 17,788건 받아 16,070건을 edge에서 끝내고 **1,718건(0.097 ≈ 1 − hit 0.9)**만 뒤로 넘긴다 — 그래서 Keycloak은 JWT 검증을 1,717건만 한다. 나머지 40%는 CDN을 우회해 API GW로 직행하고(질의 900/s · 클릭 300/s), 검색 한 건이 ES(8,974)와 pgvector(8,971)를 **둘 다** 거친다. 가장 뜨거운 노드는 pgvector CPU 27% — 벡터 유사도가 키워드 검색보다 비싸다는 게 카드에서 갈린다. 클릭 3,066건은 Kafka를 거쳐 ClickHouse 3,047건. 성공률 100% · e2e p50 40.3ms.
ecommerce-search시작 →결제 stream → Kafka → Flink(windowing + stateful, exactly-once)로 이상 패턴 감지. 5,000 event/s · 10초 실측: Payment API 49,834건 → Kafka 49,831건 → Flink x4 49,611건, 성공률 100% · p50 41.1ms. Kafka lag은 220건에서 머문다 — 전달률 3 partition × 5,000 = 15,000/s가 유입 4,983/s의 3배라 쌓일 이유가 없다(lag 220은 흐르는 중인 양이지 밀린 양이 아니다). Flink CPU는 2%뿐인데, 이건 "stateful 스트림이 싸다"가 아니라 이 모델의 Flink 비용 상수가 이벤트당 매우 작기 때문이다. ★ Flink는 이 모델에서 **단말**이다 — 받은 이벤트를 다음 노드로 넘기지 않으므로 뒤에 그려진 Alert Service · Cassandra · Prometheus는 트래픽을 받지 않는다(시각/문서용 배치). 실제 운영에서는 Flink가 alert를 발행하고 history를 쓰지만, 그 경로는 지금 엔진의 스트림 모델 밖이다.
fraud-detection시작 →App 10대 scale-out + DB 1대에 10,000 RPS. 10초 실측: App CPU 32%인데 MySQL CPU 88%(피크 93%) — 앱은 놀고 DB만 빨갛다. 산수도 그대로다: App 상한 = 34코어 ÷ 요청당 1.08ms ≈ 31,500 RPS, MySQL 상한 = 6.8코어 ÷ 0.60ms ≈ 11,300 RPS. 연결도 정확히 한계선이다 — App 10대 × Hikari 10 = 100 = max_connections. 여기서 App을 한 대만 더 붙이면 110 > 100이라 초과분이 연결 거절된다(scale-out-pool-exhaustion). 해결: read replica + cache + write sharding.
scale-out-single-db시작 →Black Friday — instance scale-out 직후 cache hit rate 30%(보통 95%). 5,000 RPS · 10초 실측: Redis가 49,465건 중 **34,696건을 miss**(hit 29.9%)로 흘려 MySQL이 34,691건 · CPU **25%**를 받는다. 같은 부하에 hit만 0.95로 되돌리면 DB 몫이 **2,454건**으로 떨어진다 — **14배 차이**다(0.70 ÷ 0.05, 반사실 `@warm`에서 실측). 성공률은 100%지만 그건 이 토폴로지의 DB가 아직 여유가 있어서고, 상한(≈11,000 RPS)의 3배를 넘기는 순간 무너진다 — cold cache가 무서운 건 배수가 크기 때문이다. 해결: cache warmup / pre-fill / 점진 rollout.
scale-out-cold-cache시작 →5 microservice chain, 홉당 편도 5ms. 1,000 RPS · 10초 실측: 각 서비스 CPU 53~56%로 전부 한가하고 드롭 0인데 e2e는 p50 127.7ms · p95 138.4ms · p99 142.1ms다. 산수: 인터넷 왕복 40ms + 사내 홉 6개 × 왕복 10ms = 60ms가 "가는 시간"이고, 나머지 ≈28ms가 5개 서비스의 처리 시간(요청당 5.7ms를 55% 점유 상태에서 나눠 쓰니 홉당 ≈5.5ms)이다. 즉 지연의 78%는 어느 노드 카드에도 안 찍힌다 — 그래서 "각자 빠른데 왜 느려?"가 된다. 해결: parallel call / batch / response aggregator로 홉 수를 줄인다.
scale-out-chain-latency시작 →App 3대가 Redis 외부 세션 저장소를 공유하는 구성 예시. 2,000 RPS 중 정적 자원 40%는 CDN, 세션 조회 30%는 Redis, 사용자 조회 30%는 MySQL로 흐른다. CDN miss만 앱에 전달되며 두 저장소가 실제 요청을 받는지 확인한다. 현재 엔진은 사용자 식별·로그인 상태·instance affinity를 모델링하지 않으므로 세션 유실이나 로그아웃, sticky session의 해결 효과는 시뮬레이션하지 않는다.
scale-out-session-sticky시작 →HikariCP는 minimumIdle = maximumPoolSize라 앱이 뜨는 순간 풀 크기만큼 연결을 전부 연다. App 20대 × 풀 50 = 1,000 vs MySQL max_connections 200 → 부하와 무관하게 800/1,000이 Too many connections다. 10초 실측: 성공률 정확히 20.0%(= 200 ÷ 1,000), 드롭 23,832건 전부 pool-exhausted, 노드 카드에 CONN 1000/200. App CPU 3% · DB CPU 3%로 자원은 텅 비었는데 연결만 없다 — 부하 그래프만 보면 절대 못 찾는 장애다. 고치는 법 셋(셋 다 실측 성공률 100%): ① 풀을 10으로 줄여 20×10 = 200 = max ② max_connections를 1,000으로 올림(연결당 메모리 비용) ③ PgBouncer/ProxySQL로 N:M 멀티플렉싱.
scale-out-pool-exhaustion시작 →External Gateway 30% 실패 + 500ms 지연. 1,500 RPS · 10초 실측: 성공률 **5.7%** · p50 7,481ms · 드롭 13,422건(queue-full 13,088 + downstream-error 334). 먼저 마르는 건 스레드 200이 아니라 그 앞의 **HTTP 클라이언트 풀 60**(App 3대 × 기본 20)이다 — 홀드가 0.5초 이상이라 통과 상한이 60 ÷ 0.5s ≈ 120 RPS로 잘린다(실측 완료 806건/10초 = 81 RPS). 그동안 App CPU는 0%다. 어느 쪽이든 교훈은 같다: **공용 아웃바운드 자원 하나가 마르면 그 노드로 가는 모든 트래픽이 같이 죽는다**. 해결: Circuit Breaker + Timeout + Bulkhead(= 자원을 호출 대상별로 쪼갠다).
scale-out-cascading시작 →short URL → 원본 URL. read 95% / write 5%. 10,000 RPS · 10초 실측으로 캐시 3단이 그대로 곱셈으로 보인다: CDN 94,771건 중 85,279건이 edge hit이고 **9,492건(0.100 = 1 − 0.9)**만 뒤로, 그중 Redis가 9,031건을 또 먹어 **460건(0.048 = 1 − 0.95)**만 replica까지 간다. 즉 조회 9.5만 건 중 DB가 보는 건 460건 = 0.5%다. 생성 5%(4,963건)는 CDN을 우회해 primary로 직행한다. Go 서비스 CPU 24% · 성공률 100% · p50 40.1ms. hash 충돌은 base62 + retry.
url-shortener시작 →10k user × 0.8 msg/s = 8000 msg/s. WebFlux WebSocket (non-blocking 10k connection) + Kafka fan-out + Cassandra (history + offline). offline user는 push notification.
chat-system시작 →50,000 req/s · 10초 실측: CDN이 488,938건을 받아 464,300건을 edge에서 끝내고 **24,638건이 miss → origin LB 24,529건(24,529 ÷ 488,938 = 0.050 = 1 − hit 0.95)**으로 나간다 — 5%가 곱셈으로 그대로 보인다. 그 5%가 origin에서는 여전히 2,450 RPS라 Origin Service를 16대나 써야 CPU 52%다(4대면 상한 ≈1,200 RPS로 2배 초과 — hit rate 1%p가 origin 용량 20%다). hit이면 p50 30.2ms, miss면 wan 왕복 80ms가 붙어 p99 115.4ms. origin 요청 하나는 metadata 조회(1) → S3 마스터 사본(2)을 순서대로 한다. 추천 2%(1,000 RPS)는 개인화라 캐시 대상이 아니어서 CDN을 우회한다(Recommendation x4 CPU 54% · pgvector 9,895건). CDN과 추천 API는 모두 CPU 약61%이며 캐시 miss 비율이 origin 부하를 결정한다. 성공률 100%. Adaptive Bitrate(HLS chunks) · Open Connect(자체 CDN).
video-streaming시작 →5,000 req/s — 호출 매칭 조회 40% + driver heartbeat 30% + trip 처리 20% + 수요 이벤트 10%. 10초 실측: Matching(Go geohash) 49,887건 · CPU 60%, Redis GEORADIUS 19,952건(hit 95%), Cassandra 15,172건, Trip Service x4 9,925건 · CPU 44%, CockroachDB 9,926건, Flink 4,817건 — 성공률 100% · p50 63.9ms. Matching은 16대여야 한다: go-http는 event-loop 1 loop라 인스턴스당 상한이 ≈417 RPS이고, 6대(2,500 RPS)였을 때는 절반이 **fd-exhausted 20,732건**으로 죽어 성공률 51.9%였다. ★ Flink(surge pricing)와 Cassandra는 이 모델에서 **단말**이다 — 받은 이벤트를 다음 노드로 넘기지 않으므로 그 뒤로 흐르는 트래픽은 없다(시각/문서용). 그래서 surge는 유입량 4,817건까지만 관측된다. 예전 배선은 Cassandra → Flink였는데 Cassandra도 단말이라 Flink가 한 건도 못 받았다 — 지금은 Matching이 수요 이벤트를 직접 흘린다.
ride-sharing시작 →8,000 req/s · read 80% / post 10% / write 10%. 10초 실측: 타임라인 조회는 CDN이 63,994건 받아 **6,461건(0.101 = 1 − hit 0.9)**만 Timeline Service로 넘긴다. 그 6,461건이 다시 9:1로 갈려 일반 user 5,815건은 미리 채워둔 Redis timeline에서(fan-out-on-write), 셀럽 646건만 읽는 시점에 Cassandra에서 모아온다(fan-out-on-read). 글쓰기 16,000건은 CDN을 우회해 Write Service(CPU 30%)로 가고, post 7,934건은 Kafka → Fanout Worker x8 → Redis에 미리 뿌린다(lag 32). 가장 뜨거운 노드는 Write Service 30% · user/post_db 18% — 읽기 8만 건이 아니라 쓰기 1.6만 건이 비싸다는 게 fan-out의 요점이다. 성공률 100%.
social-feed시작 →v1 stable 9대 + v2 canary 1대. 트래픽 비율은 인스턴스 수가 아니라 **route weight 9:1**로 정한다. 2,000 RPS · 10초 실측: v1 17,985건 · v2 1,880건 → v2 몫 **0.095**. v2에 5% error + 30ms 지연을 심었는데 전체 성공률은 **99.44%** — 사고 반경이 10% × 5% ≈ 0.5%로 잘린 것이 canary의 전부다. 두 버전 CPU도 32% : 35%로 인스턴스당 부하가 같다(v1 1,800 RPS ÷ 9대, v2 200 RPS ÷ 1대). ★ weight를 안 주면 엔진의 택일은 **엣지 단위** 라운드로빈이라 canary 1대가 50%를 받아 CPU 100% · 성공률 **79.9%**로 무너진다 — "9대 vs 1대"라는 인스턴스 수는 트래픽 비율을 정하지 않는다. 정상이면 weight를 9:1 → 3:1 → 1:1로 옮긴다.
v14b-canary-deploy시작 →Blue(현재 production) + Green(standby v2) 동시 운영. 3,000 RPS · 10초 실측: Blue x7이 29,882건 전량을 처리하며 CPU 70%, **Green x7은 0건 · CPU 0%**다 — 이 0이 blue-green의 비용이다(같은 크기를 놀리고 있어야 switch 후에도 같은 성능이 난다). 성공률 100% · p50 48.1ms. LB의 엣지를 green으로 옮기면(인스펙터에서 한 줄) 즉시 역전된다: Green 29,8xx건 · Blue 0건, 성공률 100% 유지 — 이것이 "rollback = LB 한 줄"의 실체다. 가장 위험한 것은 코드가 아니라 DB migration이다(두 버전이 같은 스키마를 동시에 쓴다).
v14b-blue-green시작 →4대 중 1대 deploying (cold JVM, 100ms 추가 지연). 트래픽은 route weight 3:1 = 라벨 그대로 75:25다. 1,200 RPS · 10초 실측: active 8,931건(몫 0.752) · deploying 2,946건(0.248), 인스턴스당 300 RPS로 CPU 50% : 48% — 부하는 공평한데 **p95가 149.6ms**로 튄다(active만이면 ≈49ms). 즉 rolling 중 느려지는 원인은 부하 불균형이 아니라 cold JVM 한 대다. 이 한 대를 readiness probe로 빼면(=weight 0) 남은 3대가 400 RPS씩 받아 CPU가 50% → **67%(+33%)**가 된다 — 이름의 "33% 증가"가 그것이고, 그래서 rolling에는 25% headroom이 필요하다. weight를 안 주면 엣지 라운드로빈이라 1대짜리 deploying이 50%를 받는다.
v14b-rolling-deploy시작 →NGINX mirror 모듈이 production 트래픽을 v2로 **복제**한다(택일이 아니다). 1,500 RPS · 10초 실측: v1이 15,004건 전량을 처리하고 v2가 사본 14,624건(97.5%)을 받는다 — 빠진 2.5%는 v2에 심어둔 2% error + 50ms 지연 때문에 v2 안에서 죽은 사본(timeout 307)이고, **사용자 성공률은 100%**다. 사본은 root 결과를 정산하지 않으므로 v2가 흔들려도 사용자는 모른다. 대신 값은 치른다: CPU가 v1 60% · v2 63%로 **거의 같다**(비용 2배). shadow_db가 v1의 MySQL과 같은 14,624 : 15,005를 받는 것이 side-effect 격리의 증거 — 이걸 안 나누면 v2의 write가 production 데이터를 오염시킨다.
v15b-shadow-deploy시작 →cookie/user_id hash로 50/50 split. 2,000 RPS · 10초 실측: Variant A 9,969 : B 9,953 = **0.500 : 0.500**이고 CPU도 29% : 28%로 같다. B에는 새 알고리즘을 흉내낸 지연을 심었는데 그게 p50 56.4ms에 섞여 나온다 — A/B의 관측 대상이 "성공률"이 아니라 "분포"라는 뜻이다. 실험 이벤트는 응답 경로가 아니라 **mirror 사본**이라 Kafka가 19,924건(A+B 처리량과 1:1) → ClickHouse 19,817건을 받는다(수정 전에는 워크로드 필터에 밀려 Kafka·ClickHouse가 0건이었다). 사본이라 metric 수집이 밀려도 사용자 지연에는 안 잡힌다. statistical significance 확인 후 rollout.
v15b-ab-test시작 →모든 instance에 v2 코드 배포 (deploy). 활성화는 flag로 (release). user_id % 100 < flagPercent → v2 path. 즉시 rollback = flag off. 3,000 RPS · 10초 실측: flag 평가 조회는 CDN/Redis 2단 캐시가 먹어 SDK 서버(go)는 **1,444건(CDN 14,912건의 0.097 = 1 − hit 0.9)**만 보고 CPU 3%다 — "매 요청 flag 평가"가 무섭지 않은 이유. 앱은 16,412건을 처리하며 CPU 43%. flag 평가 로그는 응답 경로가 아니라 **mirror 엣지**로 Kafka에 복제되므로(16,415건 = 앱 처리량과 1:1) 사용자 지연에 영향이 없고, ClickHouse가 16,336건을 소비해 lag은 79건에서 머문다. 성공률 100% · p50 45.0ms. LaunchDarkly / Unleash / GrowthBook.
v15b-feature-flag시작 →v1이 사용자 응답. 동시에 v1의 코드가 v2를 **복제 호출**한다(응답 사용 X) — shadow가 LB 수준의 mirror라면 dark launch는 앱 코드 수준이다. 1,000 RPS · 10초 실측: v1 9,910건 → MySQL 9,910건(전량)이고 v2도 사본 9,891건(99.8%)을 받아 Diff Service x8 → Prometheus까지 흐른다. 성공률 **100%** · p50 47.8ms — 사본은 root 결과를 정산하지 않아 사용자 지연·성공률에 안 잡힌다. 값은 CPU다: v1 58% · v2 58% · Diff 74%. ★ 이 엣지를 택일로 두면 요청 절반은 DB에 안 가고 v2로만 가서 데이터가 새는데(수정 전 실측 DB 1,895 · v2 1,835로 반반), 성공률만 보면 안 보인다.
v15b-dark-launch시작 →단일 Spring App x6에 4개 도메인(auth + order + inventory + reward) 통합. 도메인 간 호출이 함수 호출이라 네트워크 홉이 0이다. 3,000 RPS · 10초 실측: 29,787건 · App CPU 53% · MySQL 28% · **p50 45.9ms · p99 54.3ms** · 성공률 100%. 같은 부하·같은 mix를 4개 서비스로 쪼갠 v16-msa는 **p50 86.8ms · p99 105.0ms**로 41ms 느리다 — 그 41ms가 "MSA의 가격표"다(대신 팀 독립 배포·기술 선택·장애 격리를 산다). 비교: v16-msa.
v16-monolith시작 →같은 endpoint를 4개 service로 분리: Order가 Auth(Go)로 토큰을 검증하고, 자기 DB에 쓰고, Inventory → Reward로 이어진다. 각자 own DB · 홉당 편도 3ms. 3,000 RPS · 10초 실측: 4개 서비스와 4개 DB가 **전부 29,982~30,056건**(요청 30,153건과 사실상 1:1)을 받고(sequence 엣지) e2e **p50 86.8ms · p99 105.0ms** — 같은 부하·같은 mix의 monolith(45.9 / 54.3ms)보다 **41ms** 느리다. 그 41ms의 정체는 CPU가 아니다(어느 노드도 63%를 안 넘는다): 서비스 간 왕복 6ms × 4홉 + 각 서비스의 큐 대기다. 대신 얻는 것 — 팀별 독립 배포, 도메인별 기술(Auth는 Go: 인스턴스당 293 RPS라 16대), 장애 격리. ★ 엣지를 택일로 두면 체인이 홉마다 반토막 난다(수정 전 실측 Order 1,466 → Inventory 732 → Reward 366). 비교: v16-monolith.
v16-msa시작 →실 부하 테스트 시뮬 — 30s ramp 0→5k, 30s sustained 5k, 5s spike 5k→20k, 60s recovery 5k. loadProfile이 rps 슬라이더보다 우선한다. 첫 10초(ramp 구간) 실측: 도착이 0에서 1,667 RPS까지 올라 tick당 처리량이 평균의 **2.3배**까지 벌어지고, HPA가 붙은 App 3대가 각각 2,7xx건을 처리한다(CPU 23~36%). 성공률 100% — 아직 깨지지 않는 구간이다. 60초를 돌려 spike(20k)까지 보면 estimateCapacity가 추정한 상한을 어떻게 넘는지가 나온다. Redis 조회 20%를 넣은 이유: 그게 없으면 Redis가 트래픽을 한 건도 못 받는 죽은 노드였다.
load-test-ramp시작 →cached-read=idempotency check (25%). 같은 key 재호출 시 stored response 즉시 반환 — DB write X. 75%는 새 결제 → DB write. network retry / 더블 클릭 / PG webhook 재전송 흔함. Redis 없으면 같은 결제 2회 = 환불 사고.
idempotency-payment시작 →TTL 만료 순간 모든 요청이 cache miss → DB 폭증. Redis는 평소 hit 0.95지만 **keyTtlSec 2초**마다 200ms 동안 hit이 0으로 떨어진다(같은 TTL로 채운 키가 동시에 만료되는 순간). 3,000 RPS · burst ×3 / 주기 2초 · 10초 실측: miss 8,352 ÷ 조회 27,120 = **31%** — 만료 창은 시간의 10%(200ms ÷ 2s)뿐인데 그 창이 burst 꼭대기와 겹쳐서 미스가 3배로 부푼다. 결과가 DB의 몰림 배수다: MySQL은 tick당 처리량 최댓값이 평균의 **8.5배**(964 / 113)인데 그 앞의 API는 **3.2배**(도착 burst 그대로)다 — 그 차이 2.7배가 순수 stampede 몫이다. 성공률은 100%지만 DB CPU가 순간 39%까지 튄다. 해결: PER(Probabilistic Early Recompute) / single-flight lock / refresh-ahead / TTL에 지터.
thundering-herd시작 →숫자 키를 [0,333), [333,666), 나머지 영역으로 나눈다. 숫자가 아닌 hot-product-123도 마지막 shard로 간다. 범위 경계와 키 분포가 맞지 않으면 특정 shard에 부하가 집중된다. 세 프리셋은 모두 3,000 RPS, 동일한 hot write 60%·light read 40%, 동일한 앱 2대와 MySQL 3대다. 1,800 hot writes/s는 한 row의 약 700/s 직렬화 상한을 넘으므로 세 전략 모두 대기·실패가 발생한다. 샤딩 전략만으로 한 hot key를 해결한다는 비교가 아니다.
sharding-range시작 →FNV-1a(key) mod 3으로 목적지를 정한다. 서로 다른 키는 여러 shard에 분산될 수 있지만 같은 hot-product-123은 언제나 한 shard로 간다. modulo hash이며 consistent hashing은 아니다. shard 수가 바뀌면 많은 키가 이동할 수 있다. 세 프리셋은 모두 3,000 RPS, 동일한 hot write 60%·light read 40%, 동일한 앱 2대와 MySQL 3대다. 1,800 hot writes/s는 한 row의 약 700/s 직렬화 상한을 넘으므로 세 전략 모두 대기·실패가 발생한다. 샤딩 전략만으로 한 hot key를 해결한다는 비교가 아니다.
sharding-hash시작 →명시한 키→shard 표로 hot-product-123을 shard3에, 일반 조회 키를 shard1·2에 배치한다. 표에 없는 키는 modulo hash로 보낸다. 키 위치를 지정할 수 있어도 한 hot row의 쓰기 상한은 늘지 않는다. 이 표는 메모리 설정이며 메타 DB 조회 지연·장애·실제 데이터 이동은 모델링하지 않는다. 세 프리셋은 모두 3,000 RPS, 동일한 hot write 60%·light read 40%, 동일한 앱 2대와 MySQL 3대다. 1,800 hot writes/s는 한 row의 약 700/s 직렬화 상한을 넘으므로 세 전략 모두 대기·실패가 발생한다. 샤딩 전략만으로 한 hot key를 해결한다는 비교가 아니다.
sharding-directory시작 →서로 다른 60개 키(900~959)에 700 RPS 복잡 조회를 보냅니다. Range 경계 333·666 때문에 모두 shard3으로 갑니다. 각 DB는 1코어, 요청당 CPU 3ms라 한 샤드의 상한은 약 333 RPS입니다. 같은 조건의 Hot range / Hash와 비교하거나 인스펙터에서 전략만 hash로 바꿔 보세요. 한 hot key의 row lock과 달리 여러 키에 몰린 부하는 hash로 분산할 수 있습니다.
sharding-hot-range-range시작 →Range 예제와 동일한 60개 키(900~959), 700 RPS, 앱 2대와 1코어 DB 3대입니다. FNV-1a(key) mod 3으로 여러 키를 세 샤드에 분산합니다. DB 한 대의 CPU 상한은 약 333 RPS, 세 대 합계는 약 1,000 RPS입니다. 특정 키의 빈도가 아니라 연속된 키 범위에 부하가 몰린 경우를 비교합니다. 동일한 hot key 자체는 hash로 나뉘지 않습니다.
sharding-hot-range-hash시작 →같은 user balance row UPDATE 동시 발생 → SELECT FOR UPDATE 직렬화 → 대기 폭증. 2,000 RPS · burst ×3 / 주기 2초 · 10초 실측: 성공률 **38.0%** · p50 1,111ms · 드롭 12,511건 전부 queue-full. 통과량은 7,655건/10초 = **766 RPS**에서 멈추는데(hot row 상한 = 1 ÷ (1.36ms CPU + 3 IOPS ÷ 50,000) ≈ 700 req/s) 그동안 MySQL CPU는 **6%**, 디스크 2%다 — 자원이 아니라 **줄**이 문제다. Hikari를 40(×3 = 120)으로 열어 "풀 대기"라는 다른 원인을 배제했다. 해결: (a) Redis INCR + 비동기 DB 동기 (b) ledger 이벤트 소싱(append-only라 row lock이 없다) (c) optimistic lock + retry.
balance-race시작 →Monolith Spring x3 + MySQL 1대. auth + order + payment 한 코드. 단순 / 빠른 개발 / 강한 트랜잭션. 한계: 한 도메인 부하가 전체 영향.
strangler-step1시작 →Auth를 가장 먼저 분리 (가벼움 + 명확한 boundary). 나머지 order+payment는 monolith. 첫 step의 risk 최소. main 코드는 auth-service HTTP 호출로 변경.
strangler-step2시작 →Auth + Order + Payment 다 분리, 각자 own DB. 주문 쓰기는 Order → order DB → Payment → payment DB를 순서대로 타고, 서비스 간 홉 지연은 0.5ms가 아니라 실무 수준 3ms다. 1,000 RPS · 10초 실측: p50 51.4ms · p99 64.3ms — 모놀리스(strangler-step1: p50 44.8 · p99 50.7)보다 홉마다 왕복 지연이 그대로 붙는다. 강점은 도메인별 독립 배포/스케일, 대가는 이 누적 지연과 cascading 위험(CB/Retry 필수).
strangler-step3시작 →Elasticsearch refresh interval 노브 비교. 1s=fresh search but throughput ↓ / 30s=throughput ↑ but stale 30s. 카탈로그/추천=30s OK, 로그/실시간 alert=1s 필수.
es-refresh-compare시작 →결제 처리 chain — Kong → Auth → Payment Service → Ledger (append-only Postgres) + Kafka로 Fraud Detection (Flink) 실시간 분석. PCI-DSS / SOX 감사 추적 + 이상 거래 감지.
fintech-ledger시작 →사용자 query → Agent Orchestrator(LangGraph) → LLM Gateway(rate limit/cost) · Vector DB(RAG retrieval) · Tool Registry(function call) · Memory Fabric(대화 history 기록). 200 RPS · 10초 실측: Orchestrator 2,100건 · CPU 32%(피크 54%), LLM Gateway 1,074건 · CPU 28%, Vector DB 607건, Tool Registry 202건, Memory 213건 — 성공률 100% · p50 51.6ms · p99 156.3ms. RPS가 200뿐인데 CPU가 30%대인 것이 LLM 파이프라인의 성격이다(요청 하나가 비싸다 — throughput이 아니라 단가가 제약). ★ pgvector는 이 모델에서 **단말**이라 다음 노드로 안 넘긴다 — 그래서 retrieval을 Memory Fabric 뒤가 아니라 Orchestrator가 직접 Vector DB에 치도록 뒀다(예전 배선에서는 Vector DB 처리량이 0건이었다). cost tracking 핵심 — LLM call 당 $0.05~0.50.
ai-rag-pipeline시작 →이벤트 → Kafka → 알림 consumer 2대(WebSocket push / BullMQ Worker → SMTP). 3,000 RPS · burst ×3 / 주기 2초 · 10초 실측: 게이트웨이가 요청 하나를 인증(1)하고 Kafka로 발행(2)하므로 Auth와 Kafka가 **둘 다 29,998건** = 요청과 1:1이다(수정 전에는 auth 엣지가 mix에 없는 워크로드만 받게 돼 있어 인증이 6,032건 = 20%뿐이었다). Kafka lag은 burst 꼭대기에 **490**까지 찼다가 매 주기 0으로 빠진다 — 브로커가 burst를 흡수한다는 게 이 톱니다. ★ 같은 consumer group의 두 소비자는 partition 단위로 배정되므로 partition 3 ÷ consumer 2 = **19,857 : 10,141 (2:1)**로 기운다. consumer 수는 partition 수의 약수여야 한다. 성공률 100% · p50 40.0ms.
notification-arch시작 →관측 3 pillar — App → Jaeger(traces) + ELK(logs) + Prometheus(metrics). 세 엣지 모두 **mirror**다: 요청 하나가 셋 **다** 발행하고, 사본은 응답 경로 밖이라 사용자 지연에 안 잡힌다. 1,500 RPS · 10초 실측: App 15,032건에 대해 trace 15,029 · log 15,027 · metric 15,029로 **1:1:1**(수정 전에는 워크로드별 택일이라 4,470 / 4,469 / 6,016으로 요청마다 셋 중 하나만 나갔다). 값은 관측 쪽이 낸다: App CPU 3%인데 Jaeger는 **31%** — 트레이스 수집기가 애플리케이션보다 10배 뜨겁다는 게 "샘플링이 왜 필요한가"의 답이다. p50 41.1ms · 성공률 100%. high cardinality 주의(label 폭증).
observability-stack시작 →대규모 SaaS edge stack — DNS → WAF → Rate Limiter → API Gateway(Kong) → Service Discovery → microservices(Auth/Search/Analytics). 2,500 RPS · 10초 실측: edge 4단(DNS·WAF·Rate Limiter·Kong)이 각각 24,5xx건을 통과시키면서 CPU는 전부 **2% 이하**다 — edge layer는 비싸지 않다. 게이트웨이는 요청마다 Service Discovery에서 인스턴스 목록을 받은 뒤(24,550건, sequence 1) 라우팅한다(수정 전에는 SD 엣지가 unspecified라 0건이었다). 실제 비용은 뒤에 있다: Search 36% · Analytics 24% vs Auth 6%. 성공률 100% · p50 52.1ms(edge 4단을 지나느라 홉이 쌓인다).
enterprise-saas시작 →