Repository navigation
Description
🔵 현재 상태 (2026-08-23) — 아래 본문은 2026-08-17 시점 기록이다
고쳤다. 방향 1(데드락 재시도)이
main에 있다 —ExerciseGrpcService.java:140-160, 상한 2(#280,4e1d64c). 상한은 고른 값이 아니라 실측이다(loadtest/results/r276-retry-2026-08-20/: 0회 37.8% · 1회 3.8% · 2회 0.0%). 지표pose_batch_deadlock_retries{outcome}· 테스트ExerciseGrpcServiceTest.java:94,115.조건도 닫혔다. 조건은 «다른 키» 가 아니라 «서로 다른 세션의 중복» 이고, 신규 키만 넣는 팔은 40배를 넣고도 0/960 이다(
r276-newkeys-aws-2026-08-23). 즉 정상 트래픽은 이 데드락을 안 만들고, 열리는 것은 재전송이 원본과 겹치는 순간 — 장애 중에 열리는 문이다.남은 것은 둘이다:
- 상한 2 의 동시성 의존 — 실측은 w=8 한 점인데 스윕은 w=16 에서
p=0.60이다. 그 구간에서도 2회로 0 인지는 안 쟀다. 읽을 것은outcome="exhausted"가 0 이 아니게 되는 동시성- 두 겹(Spring 2회 + AI 3회)이 다 소진된 뒤의 계약 — 지금은
INTERNAL이라 AI 가 «재시도해도 되는 실패» 와 «영구 실패» 를 구분 못 한다(서버 핸들러 4개 중 «1개만» 클라이언트 잘못을 INVALID_ARGUMENT 로 낸다 — 나머지 셋은 «내가 아프다» 로 보고한다 #209 와 같은 뿌리). 소진 카운터는 있는데 판단 기준선이 없다(slo-baseline.md미확정)아래 §고치는 방향 · §미검증 은 낡았다 — 남긴 이유는 판단의 이력이다.
2026-08-17 P5 게이트 박스(c7i.xlarge, i-0446bb89b9482934f)에서 실측으로 나왔다. rig 결함이 아니라 프로덕션 경로의 문제다.
무엇이 관측됐나
같은 (session_id, rep_number, timestamp_sec, created_at) 를 동시에 보내면 MySQL 이 데드락을 낸다.
c.s.s.Exercise.ExerciseGrpcService : 저장 실패: PreparedStatementCallback;
SQL [INSERT INTO pose_data (...) VALUES (?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE session_id = session_id];
Deadlock found when trying to get lock; try restarting transaction
실측 3팔 (500·500·300 요청, 대상 = 같은 박스의 Spring↔MySQL):
| 페이로드 | 동시성 | OK | 실패 | 저장된 행 |
|---|---|---|---|---|
| 요청마다 키가 다름 | c=10 | 500 | 0 | 2,500 |
| 키가 같음(중복) | c=10 | 326 | 174 (Internal · 데드락) | 500 |
| 키가 같음(중복) | c=1 | 300 | 0 | 500 |
c=1 에서는 조용히 삼킨다. c=10 에서는 34.8% 가 500 으로 죽는다. 즉 이 장치의 동작이 동시성에 따라 갈린다.
왜 문제인가 — 이 장치의 목적과 어긋난다
#188 은 재시도를 붙이기 위해 멱등을 넣었다
("재시도를 붙이는 순간 중복이 새로 생기고, 둘은 따로 고칠 수 없다" —
V6__add_pose_data_idempotency_key.sql 머리말). 그런데 실측은 그 반대를 보여준다:
- 재전송이 원본과 겹치는 순간(= 재시도가 실제로 하는 일) 데드락이 난다
- 데드락은
ON DUPLICATE KEY UPDATE가 삼키는 종류의 실패가 아니다 — 트랜잭션이 롤백되고 gRPC 가INTERNAL을 낸다 - 지금
PoseDataService.savePoseDataBatch에는 데드락 재시도가 없다. 즉 재시도가 실패하면 그 배치는 그냥 사라진다 — AI→Spring 데이터 유입 경로에 재시도가 없다 — SavePoseDataBatch 는 실패하면 rep 하나가 통째로 사라지고 로그만 남는다 #188 이 막으려던 «rep 하나가 통째로 사라진다» 와 같은 결과다
재현
- 페이로드: 같은
sessionId· 같은 프레임 배열(따라서rep_number·timestamp_sec동일) ghz --insecure --call ExerciseService.SavePoseDataBatch -c 10 -n 500Status code distribution에[Internal]이 나오고, 백엔드 로그에 위 데드락
왜 여태 안 보였나
uk_pose_event 가 2026-08-17 에 들어왔다. 그 전에는 중복이 그냥 다 INSERT 됐다.
그리고 #188(재전송)은 아직 구현 전이라
실사용 중복이 없다 — 지금 중복을 만들 수 있는 것은 부하 rig 뿐이었다
(#271).
즉 재시도를 붙이는 순간 이 문제가 실사용에서 열린다. 순서상 이것이 먼저다.
고치는 방향 (미결정) → 1 채택·구현 완료 (2026-08-20)
- 데드락 재시도 —
@Retryable/수동 루프. 멱등이라 재시도가 안전하다는 것이 전제인데, 그 전제를 이 데드락이 깨고 있으니 순환이 아니다(재시도하면 두 번째엔 중복으로 삼켜진다) - 배치 안 정렬 — 여러 행을 한 번에 넣을 때 키 순서를 고정하면 교차 대기가 줄어든다. 단 서로 다른 트랜잭션이 같은 키를 때리는 이 사례에 얼마나 듣는지는 미검증
INSERT IGNORE로 바꾼다 — 잠금 양상이 다른지 미검증이고, #219(IGNORE 가 FK 위반·NOT NULL 위반까지 삼킨다)와 얽힌다
미검증 → 셋 중 둘이 닫혔다 (2026-08-20 기제 · 2026-08-23 조건). 남은 것은 위 상태 블록에
- 데드락의 정확한 기제(중복 키 검사 중 잡는 잠금 vs 배치 내 행 순서) —
SHOW ENGINE INNODB STATUS를 안 봤다 - 실사용 재전송이 정말 동시에 도착하는지 — 재시도 간격이 있으면 c=1 에 가까워 조용히 삼켜진다
- 34.8% 라는 비율은 이 부하·이 박스의 값이다. 일반화하지 말 것
Activity
measure(loadtest): 게이트 라운드를 회수한다 — 수정본은 100% 저장하고, 「조용하다」는 절반만 맞았다
docs(decisions): 재고표에 오늘 밤 결과를 반영한다 — P5 는 풀렸고, 새로 둘이 열렸다
feat(loadtest): 從 둘을 더 얹는다 — rewriteBatchedStatements 기여분(#221)과 데드락 기제(
Khyojae commentedon Aug 17, 2026
기제를 잡았다 — 유니크 인덱스가 아니라 PRIMARY 의 파티션 끝이다
2026-08-18 P5 라운드에서
SHOW ENGINE INNODB STATUS를 회수했다(#276 의 미검증 항목).재현 조건이 먼저 갈렸다
단일 세션 중복으로는 데드락이 안 난다. 모든 요청이 같은 행을 같은 순서로 잠그면 순환이 안 생긴다 — 그건 데드락이 아니라 그냥 락 대기다. 아침 게이트가 34.8%를 낸 조건도 다세션이었다.
즉 「중복이 있으면 데드락」이 아니라 「서로 다른 세션의 중복이 동시에 있으면 데드락」 이다.
덤프
읽으면
uk_pose_event가 아니라index PRIMARY다. 중복 키 검출이 방아쇠일 뿐, 실제로 다투는 자리는 PK 쪽이다supremum— 인덱스의 «끝». PK 가 AUTO_INCREMENT 라 삽입이 항상 끝에 몰린다created_at이 세션 시작 시각이라(멱등의 근거) 같은 달 세션이 전부p2026_05에 들어간다. 즉 파티셔닝이 분산에 기여하지 않는 자리다고치는 방향에 미치는 영향
이슈 본문이 세 방향(재시도 · 배치 내 키 정렬 · INSERT IGNORE)을 적었는데, 기제가 잡히니 ② 배치 내 정렬은 효과가 의심스럽다 — 다투는 자리가 배치 안 행 순서가 아니라 페이지 끝의 insert intention 이라서다. ①(데드락 재시도)이 남는 쪽이고, 멱등이 있으므로 재시도는 안전하다.
미검증
fix(loadtest): 從 셋을 회수하고 R4 의 워밍업 함정을 두 겹으로 고친다 (#221, #276, Q5)
Khyojae commentedon Aug 20, 2026
재현 완료 — 조건은 셋이고, 하나만 빠져도 0% 다
로컬(2물리코어)에서 격리 테이블
pose_data_r276(CREATE TABLE ... LIKE pose_data— 14파티션·PK·uk_pose_event전부 복제)에 팔 넷 × 4판 = 16판. 워커 8 · 워커당 INSERT 문 40 · 문당 행 25 · 그 외 에러 0.created_at같은 날created_at을 워커마다 다른 달로uk_pose_event없음세 조건이 «동시에» 성립할 때만 난다 — ①
uk_pose_event존재 ② 서로 다른 키 ③ 같은 파티션 동시 삽입.1. 중복만으로는 안 난다
single_session은 8워커가 100% 중복을 때리는데 960시도에 0 이다. 중복은ON DUPLICATE KEY UPDATE로 접혀 새 행이 안 들어가니 삽입 자리를 다툴 일이 없다. 본문의 "c=1 에서는 조용히 삼킨다" 가 c=8 에서도 그대로다 — 가르는 것은 동시성이 아니라 «새 키인가» 다.2. 🔴 「멱등 키가 원인인가」 — 필요조건이 맞다. 제목이 자리를 맞게 짚었다
no_uk는 페이로드가 글자 그대로 같고 키만 없는데 0 이다. 그리고 키가 없으면 중복이 안 걸려 행이 실제로 쌓여 8,000행을 넣었다 — 같은 파티션 끝에 40배 더 밀어넣고도 0 이다.그래서 재는 중에 세워뒀던 기제(다투는 자리는
PRIMARY의 supremum,AUTO_INCREMENT라 삽입이 끝에 몰려서)는 반증됐다. 그게 맞다면no_uk가 더 심했어야 한다. 자리는uk_pose_event쪽이다.3. 🔴 본문 §「고치는 방향」의 전제 하나를 정정해야 한다
rig 주석과 본문이 "실사용은
created_at= 세션 시작 시각이라 날짜별로 흩어진다" 를 완화 근거로 깔고 있는데, 흩어지는 것은 날짜이고 파티션은 월 단위(p2026_01…p2026_12)다. 동시에 운동 중인 사람은 정의상 같은 순간에 있으니 같은 달, 따라서 같은 파티션이다.즉 실사용 조건은
diff_partition이 아니라same_partition팔이다.diff_partition의 0% 가 답한 것은 「기제가 파티션 지역성이다」이지 「실사용은 안전하다」가 아니다.4. 재시도 횟수 유도 (임의값을 안 넣으려고)
p = 0.3750, 시도 독립 가정 아래 n회 재시도 후 잔여 실패율 p^(n+1):
여전히 미검증
SHOW ENGINE INNODB STATUS를 이 판에서도 안 읽었다. 말할 수 있는 건 「자리는uk_pose_event쪽」까지고, 그 안에서 무엇이 무엇을 기다리는지는 아니다INSERT IGNORE(고치는 방향 3) · 재시도 간격 효과 미측정 (이 rig 은 간격 0)rig 과 전체 판정:
loadtest/measure_r276_deadlock.sh·loadtest/results/r276-deadlock-2026-08-20/README.md(커밋 eb8bafa)Khyojae commentedon Aug 20, 2026
🔴 정정 — 앞 코멘트의 「자리는
uk_pose_event쪽」이 틀렸다SHOW ENGINE INNODB STATUS를 읽었다. 두 트랜잭션이 대칭이다:(2) 도 같은 자리에 대칭.
uk_pose_event는 잠금 목록에 한 줄도 안 나온다.즉 다투는 자리는
PRIMARY의 supremum(파티션p2026_05)이고, 둘 다 거기 X 를 쥔 채 서로의 insert intention 을 기다린다. insert intention 은 다른 트랜잭션의 X 와 충돌하므로 그대로 순환이다.앞 코멘트에서 뭘 잘못 이었나
no_uk팔의 0% 를 보고 「키가 필요조건」 → 「그러니 다투는 자리도 키」로 이었는데, 필요조건과 다투는 자리는 같은 것이 아니다. 두 실측을 같이 놓아야 맞다:no_uk0% (삽입 40배)index PRIMARYsupremumuk_pose_event의 중복 검사가PRIMARYsupremum 에 X 락을 쥐게 만들고, 그 X 락이 서로의 삽입을 막는다. 키가 없으면 순수 삽입이라 insert intention 끼리는 서로 충돌하지 않아 0% 다.앞 코멘트의 나머지(조건 셋 ·
no_uk0% · 파티션 완화가 성립하지 않는다는 §3 · p=0.3750 유도)는 그대로 유효하다.여전히 미검증
innodb_print_all_deadlocks는 켜뒀지만 전수 대조는 안 했다rig:
loadtest/measure_r276_innodb_status.sh· 원문:loadtest/results/r276-deadlock-2026-08-20/innodb-status.md(커밋 17c391a)measure(db): #276 의 조건을 셋으로 좁힌다 — 키를 뺀 팔이 삽입 40배인데 0 이다
measure(db): #276 의 잠금 원문을 읽는다 — 자리는 PRIMARY supremum, 키는 그 자리를 만드는 조건
measure(db): #276 의 p 는 상수가 아니었다 — 워커 2 에서 1.2%, 16 에서 59.5%
Khyojae commentedon Aug 20, 2026
후속 — p 는 상수가 아니다. 재시도 횟수를 하나의 p 로 정하면 안 된다
앞 코멘트의 p = 0.3750 은 워커 8 에서의 값 하나였다. 레벨 2·4·8·16 × 4블록(첫 블록 버림) = 16판, 라틴 방격으로 레벨과 판 순서를 분리해서 다시 쟀다. 그 외 조건은 같다(같은 파티션 ·
uk_pose_event있음 · 워커당 문 40).워커 2 에서는 사실상 사라진다(240시도에 2건). 2→4 가 23배이고 그 위로는 완만하다(1.47배 · 1.41배).
🔴 앞 코멘트의 재시도 표를 깎는다
p 를 상수로 놓고 p^(n+1) 로 유도했는데, p 가 동시성의 함수라 레벨마다 다시 해야 한다:
w=16 에서는 3회 재시도해도 12.6% 가 남는다 — 앞 표의 「3회면 2.0%」와 6배 차이다.
그리고 되먹임이 불리한 쪽으로 있다. 재시도는 실패한 요청을 다시 던지는 일이라 그 순간의 동시성을 올린다 — 재시도를 붙이면 p 가 오른쪽 열로 이동한다. 앞 코멘트가 "두 힘이 반대 방향이고 어느 쪽이 이기는지는 안 쟀다" 라고 적은 자리인데, 이 판이 그중 불리한 쪽에만 크기를 붙였다(유리한 쪽 — 중복으로 접히는 경로 — 은 여전히 미측정).
구현에 주는 함의: 재시도 횟수를 「p 하나」로 정하면 실효 동시성이 높은 구간에서 낙관 쪽으로 틀린다. 동시성을 낮추는 쪽(배치 크기·요청 병합·백오프 간격)이 재시도 횟수보다 레버가 크다 — 워커 2 에서 1.2% 다.
판 자체의 재현 폭
워커 8 이 두 라운드에서 37.5% ↔ 42.2%, 약 5pp 벌어진다. 소수점 아래는 인용하면 안 된다. 판 하나(w=8 블록1)가 18.8% 로 원인 미상 튀었고, 중앙값이라 결론은 견뎠다(평균이면 35.1%).
미검증
SQLSTATE '40001'핸들러 저장 프로시저 rig 이 따로 필요하고, 아직 안 돌렸다rig:
loadtest/measure_r276_worker_sweep.sh· 판정:loadtest/results/r276-worker-sweep-2026-08-20/README.md(커밋 b1dc3d2)measure(db): #276 의 23배는 계단이 아니라 비탈이었다 — 대부분이 2→3 한 칸에 있다
Khyojae commentedon Aug 20, 2026
후속 — 2→4 의 23배를 좁혔다. 계단이 아니라 비탈이다
앞 코멘트가 2→4 에서 23배를 남기고 그 사이를 안 쟀다. 레벨 3 을 넣어 12판(라틴 방격, 첫 블록 버림, 결함 0):
끊어지는 임계점은 없다. 23배의 대부분이 2→3 한 칸에 몰려 있고 그 뒤로는 완만하다.
🔶 뛰는 자리가 이 박스의 물리 코어 수(2)를 넘는 첫 칸과 겹치는데, 해석이고 대조가 없다. 코어 수를 바꿔 확인한 판이 없고, 본문의 AWS 게이트 박스(c=10)도 코어를 넘은 조건이라 가르는 데 못 쓴다.
덤 — 판 자체의 재현이 확인됐다
앞 스윕과 겹치는 레벨 2·4 가 중앙값까지 같게 나왔다(1.2% · 28.7%). 워커 8 이 두 라운드에서 37.5% ↔ 42.2% 로 5pp 흔들렸던 것과 대비된다. 🔶 「낮은 동시성 구간이 더 안정적」은 레벨당 3판이라 정황까지다.
구현에 주는 함의 (앞 코멘트에서 이어서)
동시 요청을 2~3 구간으로 눌러두는 것이 재시도 횟수를 늘리는 것보다 레버가 크다는 게 더 분명해졌다 — 3 에서 이미 16.7% 다. 다만 그 구간이 코어 수와 함께 움직인다면 배포 박스의 코어 수에 따라 안전선도 달라진다는 뜻이고, 그건 아직 안 쟀다.
rig:
loadtest/measure_r276_worker_sweep.sh(LEVELS="2 3 4") · 판정:loadtest/results/r276-worker-sweep-fine-2026-08-20/README.md(커밋 5596599)measure(db): #276 의 재시도를 실측한다 — 유도가 4배 비관이었다
45 remaining items
feat(ai): AI 를 스크레이프 타깃으로 세운다 — 관측이 셋 중 둘에서 셋이 된다 (#151) (#473)
docs(slo): 판정선을 처음으로 확정한다 — §4 전부 · §5 셋 (2026-08-23 사용자 confirm) (#475)
fix(rig): 편집이 셸 텍스트를 두 번 망가뜨렸다 — 블록을 통째로 다시 쓴다
fix(rig): 편집이 셸 텍스트를 두 번 망가뜨렸다 — 블록을 통째로 다시 쓴다
measure(uk): 유니크 키의 대가는 «체제» 가 정한다 — 메모리 −1.5% · 흩어진 디스크 삽입 −46% (#503)
feat(pose): 데드락 재시도 상한을 5로 올린다 (2026-08-26 사용자 confirm) (#276 ②) (#553)
Khyojae commentedon Aug 26, 2026
2026-08-26:
deadlockMaxRetries3 → 5로 재확정(#553, 사용자 confirm). 근거는loadtest/results/r276-ceiling-rank-aws-2026-08-26/— 팔당 판을 3→6으로 늘려 재측정한 결과 잔여 실패율 중앙값 3(8.0%) > 4(5.9%) > 5(2.5%)로 단조 감소, 같은 블록 내 짝비교로 5가 4를 6/6·3을 5/6에서 이김.남은 두 항목(상한의 동시성 의존 · 두 겹 소진 후 계약)은 그대로 안 닫힌 채 남는다 — 이번 라운드가 답한 건 "상한을 얼마로 할지"뿐이고, 그 두 구조적 질문은 안 건드렸다.
fix(compose): prod에 AI 워커 3개 스티키 라우팅 누락 보강 + 데드락 재시도 기본값 드리프트 정정 (#552)…
measure(pattern-analysis): #276 ③ exhausted 첫 관측 — AI 재시도 계층 통과 0/1500
Khyojae commentedon Aug 30, 2026
AI 재시도 계층까지 실제로 통과시켜 첫 관측: 동시성16·중복키100%·상한5(배포기본값) 조건에서 exhausted = 0/1,800. Spring의 내부 재시도 소진(ABORTED)은 AI의 다음 시도에서 매번 즉시 회수됐고(AI retried == Spring exhausted, 정확히 일치), DEADLINE_EXCEEDED는 한 번도 안 났다.
r276-retry-latency-2026-08-27이 우려했던 "Spring 재시도 지연(p99 3.989s) vs AI 5s 데드라인" 충돌은 이 조건에서 실현되지 않음.단일 조건 실측이라 일반화하지 않음 — 상세:
loadtest/results/r276-ai-layer-aws-2026-08-30/README.md(하네스:loadtest/measure_r276_ai_layer.py, 브랜치measure/r276-ai-layer-exhausted, 아직 main 미병합)test(pose): main 의 Exercise 임계값 필드 개편에 맞춰 재전송 데드락 테스트 시드 정리 (#276)
fix(pose): 재전송 데드락의 근본 처방 — savePoseDataBatch 를 READ COMMITTED 로 (#276)…
docs(decisions): #276 ㄷ(자연키 PK) 비용 측정 착수 — 설계 §5, 박스·규모·범위 확정 (#841)
fix(pose): binlog_format=STATEMENT 면 기동 거부 — RC 저장이 오류 1665 로 전부 막히는 …
Khyojae commentedon Nov 9, 2026
ㄷ(멱등 키를 PK 로) 비용 측정 결과 — PR #851
09-24 에 ㄴ(RC)을 채택하면서 ㄷ 은 «치르는 값 미측정» 으로 보류했다. 그 값을 EC2 3라운드(11-08 ~ 11-09)로 쟀다.
설계
docs/decisions/r276-lock-root-cause-fix.md§5-6 · 원본loadtest/results/r276-natpk-aws-2026-11-08/README.md평소 쓰기·읽기 비용은 거의 없거나 오히려 이득이고, 비용은 PK 를 한 번 바꾸는 작업에 몰려 있다.
함정 하나: 네이티브 INPLACE 는 기본
innodb_online_alter_log_max_size(128MiB)면 약 2시간 돈 뒤 ERROR 1799 로 매번 실패하고 롤백한다. 2GiB 로 올려 통과했다(최소 필요값은 안 쟀다).미확인으로 남긴 것: 네이티브가 pt-osc 보다 약 6배 느린 이유 · 온라인 로그 최소 필요값 · 메모리 체제에서 p99 가 더 큰 역전.
결정은 안 했다 — 설계 §5-1 의 «ㄷ 이 싸면 ㄴ 을 다시 연다» 조건이 걸렸고, 다시 열지는 따로 정한다. 그때까지 RC 그대로다.