직접 잰 것
실측 기록
재현 조건을 만들고, 무너지는 순간을 계측하고, 고친 뒤 같은 조건에서 다시 잰 기록입니다. 설계 설명이 아니라 표가 결론이고, 각 저장소에 원본 결과와 재실행 명령이 있습니다. 실패한 가설과 측정 도구의 실수도 함께 적습니다.
저장소 4 · 실험 글 16
spring-ops-lab
저장소와 원본 결과Spring Boot 서버가 무너지는 방식
느린 업스트림 하나가 그것을 호출하지도 않는 엔드포인트를 죽이는가
무너지는 Spring 서버 1 - CPU는 놀고 있는데 API가 느리다: 업스트림 하나가 무관한 엔드포인트를 죽이는 과정을 thread dump 세 장으로 읽기`/api/fast` p50 2.9ms → 30초(클라이언트 타임아웃), 실패 66.7%. 요청 스레드 200개 전부가 소켓 읽기에 정지. 조치 후 같은 조건에서 p50 2.9ms·실패 0%
pause가 300배 짧아지면 p99는 얼마나 좋아지는가
G1과 ZGC가 p99에 남기는 차이 - 300배 짧은 pause가 꼬리의 11%를 가져갔다ZGC의 최악 pause는 0.07ms, G1은 24.13ms인데 피해자 p99는 13.62 → 12.07ms로 11%만 좋아졌다. 정작 이득은 할당하는 엔드포인트가 37%(17.95 → 11.26ms)로 더 컸고, 가장 느린 요청을 pause로 설명할 수 있는 수집기는 Parallel뿐이었다
핀닝은 얼마를 앗아가고 스레드 수가 상한이 아니면 무엇이 상한인가
가상 스레드를 켜서 37배 느려진 엔드포인트 - 핀닝과 상한 없는 admission을 재다경합이 전혀 없는 synchronized 하나가 가상 스레드에서 완료 14,398건을 392건으로 만들었다. 동시 처리는 100에서 1로, 캐리어 수와 같아졌다. 2부에서 세 조건의 성공 처리량은 모두 풀 용량이었고 달라진 것은 실패의 값이다(성공 p50 2,003 → 103ms)
data-ops-lab
저장소와 원본 결과데이터 계층이 틀리고 느려지는 방식
어느 이상 현상이 실제로 나고
격리 수준의 이상 현상을 직접 만들기 - 같은 REPEATABLE READ에서 PostgreSQL은 중단시키고 MySQL은 조용히 덮어썼다같은 REPEATABLE READ에서 lost update가 갈렸다. PostgreSQL 20/20 직렬화 실패, MySQL 20/20 조용히 덮어씀. SERIALIZABLE 비용은 22~36ms 대 3,051ms
커넥션 풀은 실제로 무엇을 아끼고 언제 키우면 손해인가
Postgres 커넥션 하나의 비용 - 풀을 키우면 대기가 사라지는 게 아니라 볼 수 없는 곳으로 옮겨간다커넥션 열기 5.48ms 대 질의 0.23ms, 유휴 커넥션 하나가 1.5MB. 처리량은 풀 10에서 정점이고 150에서 15% 낮은데, 그 구간에서 획득 대기는 89.3 → 38.8ms로 줄고 총 p50은 60.2 → 69.8ms로 늘었다
캐시가 만료되는 순간 몇 명이 원본까지 가고 방어는 무엇을 대가로 내는가
캐시 스탬피드와 single flight - 200명 중 125명이 원본까지 갔고, 예외를 삼키자 실패가 가장 빠른 요청이 됐다방어가 없으면 만료마다 200명 중 125.5명이 원본에 도달했다. 락 기반 single flight는 1.5회로 84배 줄이면서 처리량과 p99도 가장 좋았고, XFetch는 beta를 재계산 비용보다 키우면 오히려 나빠졌다
어떤 명령이 Redis의 단일 스레드를 얼마나 막는가
Redis 단일 스레드가 멈추는 순간 - SCAN이 3.7배 오래 걸리면서 아무도 막지 않고, 120ms 멈춤이 p99에 안 잡히는 이유`SCAN`은 `KEYS`보다 3.7배 오래 걸리면서 피해자 p99를 기준선 이하로 유지했다. 100만 원소 리스트의 `DEL`은 1.3ms지만 100만 멤버 Set은 121.4ms로 약 93배였고, 그 120ms 멈춤은 p99에 잡히지 않고 max에만 남았다
순서를 지키는 설계와 느린 것을 격리하는 설계는 어디에서 갈라지는가
순서와 격리는 같은 자원을 반대로 당긴다 - 핫 키가 워커 하나를 넘는 순간 세 설계가 갈라지는 지점핫 키가 워커 하나분을 넘기 전에는 세 설계가 같았다. 넘는 순간 파티션의 피해자 p99가 116ms → 22,894ms로 발산했고, 훔치기는 그것을 507ms로 45배 줄이면서 순서 위반 188건을 만들었다
옵티마이저가 인덱스를 안 쓰는 것인가 못 쓰는 것인가
인덱스가 선택되지 않는 순간 - 컬럼에 캐스팅 하나가 3,000배, 그리고 추정이 31.7배 틀려도 계획이 안 바뀌는 경우컬럼에 붙인 암묵적 형변환이 0.07ms를 208.7ms로, 추정은 실제의 417배. 낡은 통계는 31.7배 틀렸는데도 계획이 안 바뀌었다
ParityPay
저장소와 원본 결과장애 아래에서 금융 불변조건이 지켜지는가
같은 지갑 경합의 원인은 어디에 있는가
ParityPay로 검증하는 결제 정합성 2 - 잠금을 필요 이상으로 오래 쥐고 있었다: 추론을 측정으로 바꾼 기록잠금 보유 17ms 중 DB 실행은 2.31ms. 순서를 바꿔 1ms로 줄이자 처리량 1.75배, p95 274ms → 144ms
락 lease가 트랜잭션보다 먼저 끝나면 정말 정합성이 깨지는가
ParityPay로 검증하는 결제 정합성 10 - 락 lease가 트랜잭션보다 먼저 끝나면 정말 정합성이 깨지는가: SETNX 락, Watchdog, fencing token, 그리고 락 없는 구조의 실측락은 3회 모두 깨졌다(소유 겹침 1,697~1,714쌍, 초과 승인 340~350건). 락 없는 조건부 UPDATE는 여섯 번 전부 승인 정확히 240·drift 0, 처리량은 2배
Kafka에서 중복·유실·순서 역전을 직접 만들면 무엇이 막히는가
ParityPay로 검증하는 결제 정합성 8 - Kafka에서 중복·유실·순서 역전을 직접 만들어 보기: 멱등 소비자와 Outbox가 막는 것과 못 막는 것유실을 가른 것은 커밋 방식이 아니라 처리와 커밋의 순서였다. `acks=1·0`은 발행 배치 단위로 잃고 `acks=all`은 0건. poison 1건이 파티션을 1.3~1.6초 멈추고 조용히 버려졌다
느린 외부기관 앞에서 결제 서버를 무엇이 지키는가
ParityPay로 검증하는 결제 정합성 9 - 느린 기관 앞에서 결제 서버를 지키기: 타임아웃, 재시도, 차단기, 리미터, 벌크헤드가 각각 무엇을 막는가플랫폼 스레드 200개는 3회 모두 23.7~24.0초에 고갈. 재시도 3회는 기관 요청을 정확히 3.00배로 만들었고 지터는 봉우리를 낮추지 못했다. 벌크헤드 50은 무관한 API를 p95 10ms로 지켰다
부하·장애 실험이 문서와 코드가 못 찾은 것을 찾는가
ParityPay로 검증하는 결제 정합성 4 - 실험 27종이 찾아낸 결함 12건: 문서와 코드를 읽어서 나온 것은 하나도 없었다결함 12건 중 6건은 문서에 '대비되어 있다'고 적혀 있던 것이었고, 1건은 테스트가 있었는데 통과했다
monticker
저장소와 원본 결과실시간 시세 파이프라인의 순서와 지연
WebSocket과 폴링은 클라이언트 1만에서 무엇이 갈리는가
WebSocket push와 REST polling을 클라이언트 1만 개에서 재다 — 지연과 서버 비용, Kafka 정지 45초, 느린 소비자 25개WebSocket은 폴링의 CPU 1/10~1/20. 폴링은 5,300 req/s에서 천장을 쳐 요청의 95%를 버렸다. 느린 연결 25개가 모든 클라이언트를 p99 19초 멈췄고 발행 스레드 풀을 늘려 196ms로
키가 같은데도 순서가 깨지는 이유는 무엇인가
market.ticks 파티션 × 컨슈머 격자 — 키가 같아도 순서가 깨진 이유, 크래시 하나가 시세 전체를 멈춘 이유, 파티션 이웃이 인질이 되는 순간27회 중 9회에서 순서가 깨졌고 원인은 게이트웨이의 `acks=0`이었다. 고치자 45회 위반 0. 핫 종목이 스레드의 90%를 먹으면 같은 파티션 종목이 p99 13초로 함께 밀린다