diff --git a/docs/decisions/ai-receive-path-scaling.md b/docs/decisions/ai-receive-path-scaling.md index f4f9acfc..154333b6 100644 --- a/docs/decisions/ai-receive-path-scaling.md +++ b/docs/decisions/ai-receive-path-scaling.md @@ -1,10 +1,13 @@ # AI 수신 경로 — 「16코어 중 9.5만 쓰는」 마지막 후보를 어떻게 확인하고, 고칠 것인가 -작성일: 2026-08-17 -상태: 🟢 **잤다 — 從 R10-a 완료 (2026-08-23).** 「16 중 9.5」의 답은 **«한 프로세스라서»** 다 -([결과](../../loadtest/results/frame-path-r10a-2026-08-23/README.md)) — 프로세스를 둘로 쪼개니 CPU 9.5 → **14.5 vCPU**, 처리량 **1.30배**. -🔴 **그 안에서 원인 셋이 아직 안 갈렸다** — GIL · 단일 이벤트 루프 · 프로세스 내부 락. -프로세스 분리는 셋을 **한꺼번에** 우회한다. §10 이 그 자리다. +작성일: 2026-08-17 · 갱신: 2026-09-02 (R10-b — handler_concurrency 도 반증) +상태: 🟢 **잤다 — 從 R10-a·R10-b 완료.** 「16 중 9.5」의 답은 **«한 프로세스라서»** 다 +([R10-a 결과](../../loadtest/results/frame-path-r10a-2026-08-23/README.md)) — 프로세스를 둘로 쪼개니 CPU 9.5 → **14.5 vCPU**, 처리량 **1.30배**. +🟢 **R10-b(2026-09-02)가 `GRPC_MAX_WORKERS`(핸들러 동시성, 기본 10)도 반증했다** — 5·20으로 +흔들어도 처리량·지연이 2% 안에서 그대로다([결과](../../loadtest/results/frame-path-r10b-2026-09-02/README.md)). +🔴 **그래도 그 안에서 원인 셋이 아직 안 갈렸다** — GIL · 단일 이벤트 루프 · 프로세스 내부 락. +프로세스 분리는 셋을 **한꺼번에** 우회하고, 핸들러 동시성 반증도 이 셋을 못 가른다(그 슬롯 +수가 문제가 아니라는 것만 확인). §10 이 그 자리다. 대상: ~~후보 일곱을 지우고 하나 남은 것의 확인 방법~~ → **확인은 끝났다.** 남은 것은 **원인 분리**와 **선택지**다 연관: [`../../loadtest/results/ai-scaling-aws-2026-08-17/README.md`](../../loadtest/results/ai-scaling-aws-2026-08-17/README.md)(R6·프로파일·부하기 재확인) · diff --git a/docs/decisions/experiment-inventory.md b/docs/decisions/experiment-inventory.md index 21504fa4..8587cc17 100644 --- a/docs/decisions/experiment-inventory.md +++ b/docs/decisions/experiment-inventory.md @@ -1,7 +1,18 @@ # 실험 재고 — Spring · MySQL · AI 세 파트에서 남은 측정 작성일: 2026-08-17 -갱신: 2026-08-27(2) — **MySQL 축 3번이 닫힌 날.** R4 의 정확한 워크로드(gRPC 배치·conn=1·c=100)로 +갱신: 2026-09-01 — **AI 축에 빠져 있던 행 하나를 채운 날(정정이 아니라 누락 추가).** +사용자가 "실측 남은 것 확인"에 이어 "재고표 드리프트 정정"을 요청해 처음엔 §3 3번(「팔마다 +CPU 다른 이유」)이 08-30 커밋 셋(#593→#615→#616)으로 닫혔다고 짚었는데 **틀렸다** — 그 +항목의 근거 문서(`per-process-ceiling-variance.md`)를 다시 열어 보니 08-28 설계 이후 +**여전히 «착수 미결정»**(R/N/J 응답모드 × GIL 프로브 교차)이고, 08-30에 실제로 돈 것은 +`max_workers=10` 매직넘버라는 **완전히 다른 질문**이었다. **진짜 드리프트는 "답이 났는데 +표가 모른다"가 아니라 "질문 자체가 표에 없었다"** — 새 행으로 추가했다(아래 §3). 내용: +로컬 스윕(#601)만으론 원 질문 미확정, 대신 EC2 60판이 **진짜 천장 원인**(콜백 스레드 +폭증 — GIL 아님)을 부수로 잡아 새 이슈 [#614](https://github.com/Shadowfit/init/issues/614)를 +열었고, 실제 Spring 연결판(#616)이 프로덕션 조건에서는 그 위험을 반증(스레드 안 쌓임)했다. +단 «진짜 성공 경로(DB 커밋)의 지연»은 그 반증판도 안 쟀다 — 잔여 측정으로 남았다. +이전 갱신: 2026-08-27(2) — **MySQL 축 3번이 닫힌 날.** R4 의 정확한 워크로드(gRPC 배치·conn=1·c=100)로 4팔×4반복 AWS 재현 — 역전이 **재현 안 됨**(A↔B RPS 차 +0.03%, R4 원본은 −15.5%). H1(그룹커밋)도 다시 반증. 「① 워크로드 특이성 vs ② R4 가 1판짜리 잡음」 중 **②가 이겼다**. [결과](../../loadtest/results/durability-inversion-r4-repro-2026-08-27/README.md) @@ -80,7 +91,7 @@ DBA 지원축의 결손 3개(백업 · 복제 · 무중단 DDL)는 **셋 다 실 | 우선 | 실험 | 상태 | 왜 지금 | |---|---|---|---| -| ~~1?~~ | ~~**P4 — 복제 지연 · 반동기 대가 (Q1·Q2)**~~ | ✅ **완료 (측정 2026-08-22 · 회수 08-23)** — [결과](../../loadtest/results/replication-aws-2026-08-22/README.md) · [정본](./replication-lag-and-semisync.md) | **EC2 2대**(`c7i.2xlarge` · 같은 AZ) · 1,000만 행 · **10판 전부 유효 · 결함 0**(산포 A 0.6% / B 0.2%). **반동기의 대가 = 처리량 −5.9% · 커밋 p95 +13.0%** 이고 🔴 **H3 이 조건을 만들었다** — 같은 대가가 부하 모양에 따라 **−5.9% ↔ −10.3%** 로 두 배가 된다. 지연은 팔 A 약 60초 · 팔 B 약 57초. 🟢 **급소였던 G3 이 최대 소득** — `lag_net` 이 세 번 통과했고 굵기 **+1.017~1.018초가 무대 100배에도 안 변한다.** 그래서 08-17 이 Q3 을 잃은 원인이 «현상» 이 아니라 **«계측»** 이었음이 사후 확인됐다. 🔴 **인용 전 조건 넷** — 같은 AZ(**다른 AZ 미측정**) · 앱 경로가 아닌 SQL writer · 계측 바닥 ~1초 · `main` 이 아닌 커밋(`e2fbe6c`, G2 결함 수정 [#380](https://github.com/Shadowfit/init/pull/380) 이 08-23 에 머지되며 해소). ⭐ **스모크 선행이 값을 했다** — 1회차가 G2 에서 막혔고, 1,000만 행 시딩을 마친 뒤에야 만났을 결함이었다. **남은 것은 «안 잰 것» 하나 — 다른 AZ** 이고 그건 새 라운드다 | +| ~~1?~~ | ~~**P4 — 복제 지연 · 반동기 대가 (Q1·Q2)**~~ | ✅ **완료 (측정 2026-08-22 · 회수 08-23)** — [결과](../../loadtest/results/replication-aws-2026-08-22/README.md) · [정본](./replication-lag-and-semisync.md) | **EC2 2대**(`c7i.2xlarge` · 같은 AZ) · 1,000만 행 · **10판 전부 유효 · 결함 0**(산포 A 0.6% / B 0.2%). **반동기의 대가 = 처리량 −5.9% · 커밋 p95 +13.0%** 이고 🔴 **H3 이 조건을 만들었다** — 같은 대가가 부하 모양에 따라 **−5.9% ↔ −10.3%** 로 두 배가 된다. 지연은 팔 A 약 60초 · 팔 B 약 57초. 🟢 **급소였던 G3 이 최대 소득** — `lag_net` 이 세 번 통과했고 굵기 **+1.017~1.018초가 무대 100배에도 안 변한다.** 그래서 08-17 이 Q3 을 잃은 원인이 «현상» 이 아니라 **«계측»** 이었음이 사후 확인됐다. 🔴 **인용 전 조건 넷** — 같은 AZ(**다른 AZ 미측정**) · 앱 경로가 아닌 SQL writer · 계측 바닥 ~1초 · `main` 이 아닌 커밋(`e2fbe6c`, G2 결함 수정 [#380](https://github.com/Shadowfit/init/pull/380) 이 08-23 에 머지되며 해소). ⭐ **스모크 선행이 값을 했다** — 1회차가 G2 에서 막혔고, 1,000만 행 시딩을 마친 뒤에야 만났을 결함이었다. **남은 것은 «안 잰 것» 하나 — 다른 AZ** 이고 그건 새 라운드다. 🆕 **2026-09-01 설계 · 09-02 판 설계 확정**(사용자 confirm) — [정본 §0-4](./replication-lag-and-semisync.md#0-4-🆕-설계--다른-az-대조-라운드-2026-09-01-설계--2026-09-02-판-설계-확정--실행-미착수), rig 변경 불필요(AZ 는 인스턴스 배치 문제) 확인. 확정: 소스 2a·리플리카 2c · 옵션 C(버림1+본판4) · 다세션만(H3 제외) · 비용 승인. **실행(EC2 기동)은 별도 확인 필요, 아직 안 함** | | ~~#204~~ | ~~**[#204](https://github.com/Shadowfit/init/issues/204) 리포트 핵심 쿼리**~~ | ✅ **완료 (2026-08-19~20)** — [결과](../../loadtest/results/report-query-explain-2026-08-19/README.md) | 세 주장 중 **둘은 확인, 하나는 열쇠가 이미 있었다.** ㄱ 프루닝 — 세 팔 전부 **14파티션 전탐색**이고 인덱스로는 못 고친다(`created_at` 을 같이 넘긴 판만 1개). ㄴ 비커버링 — 논리 페이지 **3,777.5 ↔ 779(4.85배)**, 행당 4페이지. ㄷ filesort — A 에서 확인되지만 **[#188](https://github.com/Shadowfit/init/issues/188) 의 `uk_pose_event` 가 정렬 순서를 그대로 가지고 있어** B 에서 사라진다. 🔴 단 V6 은 `origin/main` 에 없다 — 지금 배포본엔 ㄷ 가 살아 있고, **#188 이 머지되면 손 안 대고 닫힌다** | | ~~#205 B·C~~ | ~~**[#205](https://github.com/Shadowfit/init/issues/205) 카드 B·C**~~ | ✅ **완료 (2026-08-20)** — [카드 C](../../loadtest/results/card-c-overlap-2026-08-20/README.md) · [카드 B](../../loadtest/results/card-b-avg-2026-08-20/README.md) | **C**: N=8,000 에서 **422배**(22.2초 ↔ 52.6ms), 배가별 4.2~4.4배로 2차식 확인. 🔴 단 근거는 «읽은 행 수» 가 아니었다 — 계획이 `Inner hash join (no condition)` 이라 **읽기는 O(N)·비교가 N²** 다. **B**: 불균일 200/200 갈리고 균일 **0/2,002** — 기제가 예측한 자리에서만. 곡선 쿼리는 커버링으로 **6.7배**인데 **핸들러 동일**(읽는 폭의 차이). 🔴 **두 카드가 «읽은 행 수는 비용의 대리 지표가 아니다» 를 반대 방향으로 보였다** | | ~~카드 A~~ | ~~**커버링 인덱스의 쓰기 대가**~~ | ✅ **완료 (2026-08-22)** — [결과](../../loadtest/results/card-a-write-cost-2026-08-22/README.md) · [change buffer 후속](../../loadtest/results/card-a-ibuf-armE-2026-08-22/summary.md) · [처리량(ms) 재설계](./covering-index-write-throughput.md) | **A÷B = 1.245**(논리 페이지 수정) · `log_write_req` 1.242 로 독립 두 카운터가 0.3% 안에서 일치. 행당으로 풀면 **+1.046 페이지 수정/행** — 세컨더리 엔트리 하나가 리프 페이지 수정 1회라는 **산수가 그대로 맞는다**. 팔 간 **겹침 0**(A 최소 106,254 > B 최대 85,556). 🔴 **답을 낸 것은 무대가 아니라 지표다** — 같은 팔이 `ms` 로는 **3.8배**(71.8초 ↔ 272.3초) 벌어지는데 카운터는 0.33% 안이다. ✅ **2026-09-06 처리량(ms) 실측 완료** — [결과](../../loadtest/results/card-a-write-throughput-2026-09-06/README.md): 조용한 EC2 박스(`c7i.2xlarge`)에서 같은 팔을 16판 재현, `bp_write_req` A÷B=1.244로 카드 A와 정합. **그런데 처리량(RPS) A÷B는 0.976·문당 p50은 완전히 동일(1.000)·p99만 1.047** — 논리 대가 +24.4%가 실제 처리량으로는 ~2%로 준다(문당 fsync가 인덱스의 추가 비용을 덮음, `TXN_STMTS=1` 조건 한정). ✅ **2026-09-06(2) 배치 커밋 후속으로 그 한정 조건을 직접 검증** — [결과 §7](../../loadtest/results/card-a-write-throughput-2026-09-06/README.md#7-배치-커밋-후속--가설-확인-2026-09-06-txn_stmts20): `TXN_STMTS=20`(fsync 20분의 1)으로 재현하니 RPS A÷B 0.976→**0.901**(−2.4%→−9.9%)·p99 A÷B 1.047→**2.948**(+4.7%→+195%)로 대가가 그대로 드러난다(`bp_write_req`는 1.244→1.252로 안 변함 — 일 자체는 그대로, fsync 가리개만 걷힘). 즉 "+24.4%→~2%" 축소는 **`TXN_STMTS=1`이라는 조건에 종속**된 결과였다. 채택 여부는 여전히 사용자 결정. ⚠️ **change buffer 는 안 탔다** — 세 페이로드 모양(집중·새 id 분산·**기존 id 분산**) 전부 0이고, 팔 E 는 `bp_reads` 221로 **디스크를 실제로 쳤는데도** 0이라 「페이로드 탓」이 아니다(원인은 가설로 남김) | @@ -146,6 +157,7 @@ DBA 지원축의 결손 3개(백업 · 복제 · 무중단 DDL)는 **셋 다 실 | ~~1~~ | ~~🔴 **ㄱ(GIL) — «루프가 느린 이유»**~~ | ✅ **완료 (2026-08-24) · 답은 GIL** — [§8 결과](../../loadtest/results/ceiling-cause-stage2-2026-08-24/README.md) · [설계](./per-process-ceiling-cause.md) §8 · [정본](./ai-process-ceiling-cause.md) | 🔴 **이 줄은 2026-08-27 까지 낡아 있었다** — 08-23(5) 문구(«계측 넷 붙었고 한 판도 안 돌았다»)가 08-24 라운드 뒤에도 안 고쳐졌다(PR [#534](https://github.com/Shadowfit/init/pull/534), 정본에는 이미 반영돼 있었다). `c7i.4xlarge` 1대·팔 F(현행+프로브)/Z(널핸들러+프로브)/C(프로브끔) 13판. **답: GIL 총 점유율 `rho`≥0.939**(F=0.939, Z=0.363로 계산을 빼면 사실상 사라진다) + **순수 파이썬 경로가 정확히 1.04 vCPU 에서 막힌다**(널 핸들러로 fps 2배 올려도 rps·CPU 둘 다 안 움직임). 「16 중 9.5」= 파이썬 ~1코어(GIL 직렬화) + MediaPipe ~8.5코어(C++ 안에서 GIL 놓아 병렬). 프로브 자체는 공짜(F↔C −0.86%, 산포 안). 🟢 **후속 N스윕(§9)도 같은 날 닫혔다** — [결과](../../loadtest/results/proc-count-sweep-2026-08-24/README.md)(PR [#539](https://github.com/Shadowfit/init/pull/539)): **N=3 이 답**(3→4 는 +1.24%, 산포 6.36% 안), 두 번째 천장(14.5 vCPU)의 정체는 **박스 포화**(N=3 에 98%, N=4 에 100% — 더 큰 박스로 풀린다). 🔴 **여전히 안 잰 것**: 스티키 라우팅 비용([`ai-sticky-routing.md`](./ai-sticky-routing.md) 몫) · RAM/N 비례 · N>4 · 더 큰 박스에서의 재현 | | ~~2~~ | ~~🔴 **ASGI «앞» — 130개가 어디 있나**~~ | 🟢 **결과 완료 (2026-08-24)** — [설계 §12-7](./ai-process-ceiling-cause.md) · [결과](../../loadtest/results/pre-asgi-transport-2026-08-24/README.md) | 🔴 **이 행이 「미실행」으로 남아 있던 것 자체가 문서 드리프트였다** — §12-7 에 결과가 이미 있다(2026-08-27 발견, [[project_doc_drift_pattern]]). `c7i.4xlarge` 1대 · U N K K N U U N K K N U 13판 · 12판 유효 · 에러 0. **연결 수립은 병목이 아니다**(U↔N −0.24%, N↔K +0.86%, 둘 다 판정선 5.10% 안) — **「밖의 130개」는 커널 accept 큐**이고 이는 §9(2026-08-17)의 재현이다. **큐는 병목의 결과지 원인이 아니다** — 천장의 진짜 원인(ㄱ GIL 이냐 ㄹ 인가)은 그대로 남았다, 그건 위 1번(§8, F/Z/C) 몫 | | 🆕 | 🔴 **AI 워커 부하-중 자발적 장애 빈도** | 🆕 **시도 2회, 둘 다 답을 못 냄 (2026-08-28)** — [설계](./ai-worker-load-soak-experiment.md) · [결과](../../loadtest/results/ai-worker-load-soak-2026-08-28/README.md) | 유휴 24시간 soak(0회, [`ai-channel-pool-hardening.md`](./ai-channel-pool-hardening.md) §6)의 다음 단계로 가속 스트레스(동접 3배≈203세션)를 두 번 돌렸다. **둘 다 `shadowfit-ai`가 아니라 대상 EC2 박스 전체가 부하 시작 90초~10분 만에 응답 불능**이 돼 원래 판정 채널(`RestartCount`/`OOMKilled`)을 한 번도 못 봤다. 1차가 의심한 MySQL 내부 락 정체는 2차(캡+실시간 계측)의 직접 증거로 **반박**됐고(사고 30~40초 전 MySQL 완전 idle), 대신 호스트 독립 채널 셋이 같은 30초 창에서 동시에 멎는 패턴이 새로 나와 EBS I/O 크레딧 소진을 후보로 연 이슈 [#603](https://github.com/Shadowfit/init/issues/603)이 미검증으로 열려 있다. **인프라 안정성 문제(#603)를 먼저 닫아야 원래 질문을 다시 물을 수 있다** | +| 🆕 | 🔴 **`max_workers=10` 매직넘버 — 원 질문 미확정, 부수로 콜백 스레드 폭증 발견** | 🆕 **원 질문 판정 불가 · 부수 발견은 별도 코드결함 이슈로 분리 (2026-08-30)** — [#593](https://github.com/Shadowfit/init/issues/593)(CLOSED) · [로컬 스윕 결과(#601)](../../loadtest/results/grpc-threadpool-sizing-593/README.md) · [EC2 원인규명 결과(#615)](../../loadtest/results/grpc-reattach-thread-cpu-ec2-2026-08-30/README.md) · [실Spring 검증 결과(#616)](../../loadtest/results/grpc-reattach-real-spring-ec2-2026-08-30-r2/README.md) | 원 질문(동시성 10에서 지연이 꺾이는가)은 **로컬 스윕만으론 못 가른다** — 동시성 1부터 이미 거의 선형이라 GIL·`DetectorPool._guard`와 미분리(README §4에 명시). 대신 동시성 100·60판(pidstat -t + py-spy)이 **진짜 천장 원인을 우연히 잡았다** — GIL이 아니라 **완료 콜백 스레드 폭증**: `StopAnalysis` 호출마다 Spring 보고용 `threading.Thread`를 상한 없이 새로 만드는데(`exercise_servicer.py:393`), 이 프로브 무대엔 Spring이 없어 각 스레드가 재시도 끝까지(최대 15초) 못 끝나고 90초 만에 450개 넘게 쌓여 logging 전역 락을 경합시킨다(py-spy 스택 5개 중 4개가 `logging/__init__.py:973 acquire`). **실제 Spring 연결판(#616)으로는 재현 안 됨** — Spring이 빠르게 응답하면(p95 213ms) 스레드가 안 쌓인다(60판 내내 1~61 사이 진동, 증가 추세 없음). 🔴 단 콜백이 전부 `NOT_FOUND` 즉시 거절이었고 **진짜 성공 경로(DB 커밋)의 지연은 안 쟀다** — 남은 측정. 코드 자체(상한 없는 Thread 생성)는 여전히 위험 요소로 [#614](https://github.com/Shadowfit/init/issues/614)(OPEN)에 남아 있다 — **§4(구현·계약) 성격**이라 신뢰성 축에도 함께 걸어둔다 | | **3** | 🔴 **팔마다 끌어오는 CPU 가 왜 [다른가]()** | 🆕 **새로 열림 (2026-08-23)** · [근거](../../loadtest/results/post-loop-breakdown-2026-08-23/README.md) §3-1 · [재설계](./per-process-ceiling-variance.md) | 응답 생성 팔 셋의 **rps/vCPU 가 1.72% 안에 모인다**(rps 산포는 9.1%) — 팔은 «요청당 CPU 비용» 을 하나도 못 바꿨고 바뀐 건 **프로세스가 끌어오는 CPU** 뿐이다(8.91 ↔ 9.94 vCPU). 🔑 **«프로세스당 천장» 은 고정값이 아니라 루프/스레드풀 분배에 따라 11% 움직인다.** 왜인지는 안 답했다 — **1번과 같은 판이 물을 수 있다** | | ~~1~~ | ~~🔴 **「346 RPS 천장에서 서버가 9.5 vCPU 만 쓰는 이유」**~~ | 🟢 **답이 나왔다 — «한 프로세스라서» (2026-08-23)** — [從 R10-a 결과](../../loadtest/results/frame-path-r10a-2026-08-23/README.md) | 1 프로세스 270.7 rps / **9.52 vCPU** ↔ 2 프로세스 352.6 rps / **14.50 vCPU** — **1.30배**. 못 닿던 6.5 vCPU 중 5 를 회수해 박스의 91% 까지 갔다. 부수로 **`lease` = 0.01ms 라 후보 2순위(검출기 획득)가 지워졌고**, **GIL 스위치 «간격» 은 레버가 아니다**(100배를 흔들어도 A 팔 산포 2.29% 안). 🔴 **남은 것 둘** — ① **원인이 셋 중 무엇인가**(GIL · 단일 이벤트 루프 · 프로세스 내부 락. 프로세스 분리가 셋을 한꺼번에 우회한다) ② **두 번째 천장 14.5 vCPU** 의 정체. ⚠️ 조건 — 부하가 **앱 레이트의 2배**(6fps, 앱은 3fps)이고 proc-split 반복이 **n=2** 다 🔴 **초판 해석 정정**: 회수 당일 「지연의 82% 가 ASGI 앞에 있다」로 읽었는데 **틀렸다** — 리틀의 법칙(W=L/λ)이 두 박스에서 **0.4%·0.8% 오차**로 그 차이를 전부 설명한다(160/274.7=582 ↔ 실측 580 · 160/318.2=503 ↔ 499). **닫힌 루프에서 지연은 포화의 결과지 병목의 위치가 아니다.** ⚠️ **R10-b 의 전제도 약해졌다** — 부하기 동거 비용이 [무대 §3](./r10-loadgen-topology.md) 의 추정 **0.5~1.4 vCPU** 가 아니라 **실측 0.24**(설명하려는 6.5 의 약 **4%**)였다. | | ~~R6~~ | ~~**GIL 이냐 캐시냐**~~ | ✅ **완료 (2026-08-17) · GIL 반증** — [결과 §1](../../loadtest/results/ai-scaling-aws-2026-08-17/README.md) | 프로세스/스레드가 전 구간 **0.96~1.06x**(판정선은 1.7~2배였다). 스레드도 16워커에서 **7.08배** 확장하고, 순수 추론은 이 박스에서 **15.2 vCPU** 를 쓴다 — 즉 «8.69 vCPU» 는 추론 경로의 한계가 아니다 | @@ -166,7 +178,7 @@ DBA 지원축의 결손 3개(백업 · 복제 · 무중단 DDL)는 **셋 다 실 |---|---| | 🔴 **보안 — 측정보다 먼저** | ~~[#187](https://github.com/Shadowfit/init/issues/187) AI HTTP 세션 소유권 검증 부재~~ · ~~[#185](https://github.com/Shadowfit/init/issues/185) refresh token 평문 저장~~ — 🔄 **정정 (2026-08-29)**: 둘 다 2026-08-22에 COMPLETED로 닫혔다(문서 드리프트, [[project_issues_go_stale_after_fix]]). #187은 세션별 nonce 발급(PR [#304](https://github.com/Shadowfit/init/pull/304)·[#323](https://github.com/Shadowfit/init/pull/323)·[#326](https://github.com/Shadowfit/init/pull/326), main에 병합)로 막혔다 — 단 평문 구간 도청이면 무력화된다는 축([#148](https://github.com/Shadowfit/init/issues/148))은 별도로 열려 있고, "남의 세션에 꽂았을 때 어디까지 오염되나"의 동적 재현은 여전히 미실측이라고 닫는 코멘트가 직접 남겨뒀다 | | 🔴 **신뢰성 — 재전송의 선행** | 🆕 [#276](https://github.com/Shadowfit/init/issues/276) **동시 삽입이 데드락**(c=10 에서 34.8% 실패, 실측) · [남은 미검증 재설계](./r276-retry-followup.md) — **재고표 정정 필요**: 그 재설계 문서가 밝힌 바로는 아래 "남은 미검증" 중 ①(w=16 재시도 2회)은 이미 닫혔고(2026-08-23·26 라운드에서 재시도 상한 5로 확정) ③(왕복 비용)도 절반 닫혔다(#416 버그 수정) — 실제 남은 건 ②(백오프 간격)와 ③ 잔여뿐이다. ✅ **재현·조건 판별 완료 (2026-08-20, 로컬 팔 넷 16판)** — [결과](../../loadtest/results/r276-deadlock-2026-08-20/README.md): **세 조건이 «동시에» 성립할 때만 난다** — ① `uk_pose_event` 존재 ② 서로 **다른** 키 ③ **같은 파티션** 동시 삽입. 하나만 빠져도 0% 다(100% 중복 단일세션 **0%** · 파티션 가름 **0%** · **키 뺀 팔 0%**, 같은 파티션 **37.5%**). 🔴 **키를 뺀 팔은 삽입이 40배인데도 0** 이라 **키가 필요조건**이다(= 이슈 제목이 맞다). ⚠️ **단 「그러니 다투는 자리도 키」로 이어가면 틀린다** — 그 서술을 한 번 올렸다가 잠금 원문으로 정정했다(바로 아래). 🔴 **이슈가 기대한 완화는 성립하지 않는다** — 파티션이 월 단위라 동시 사용자는 전부 같은 파티션이다. ✅ **잠금 자리**: [원문](../../loadtest/results/r276-deadlock-2026-08-20/innodb-status.md) 이 `index PRIMARY` 의 **supremum** 을 가리킨다 — 키가 그 자리에 X 락을 만들고 그 X 가 서로의 insert intention 을 막는다. ✅ **동시성 곡선**: p 는 상수가 아니다 — 워커 2·3·4·8·16 에서 **0.0125 · 0.1667 · 0.2875 · 0.4219 · 0.5953**([스윕](../../loadtest/results/r276-worker-sweep-2026-08-20/README.md) · [미세](../../loadtest/results/r276-worker-sweep-fine-2026-08-20/README.md)). 23배가 **2→3 한 칸**에 몰린 비탈이고 임계점은 없다. ✅ **재시도**: 실측 **1회 3.8% · 2회 0.0%**([결과](../../loadtest/results/r276-retry-2026-08-20/README.md)) — 🔴 `p^(n+1)` 유도가 **4배 비관**이었다(두 번째 시도는 대개 중복으로 접힌다, 조건부 실패율 약 10%). 남은 미검증: 높은 동시성(w=16)에서도 2회로 0 인가 · 백오프 간격 · 앱 쪽 재시도의 왕복 비용 · 중복 검사의 «어느 단계» 가 X 를 잡는가. 재시도를 안전하게 만들려고 넣은 장치가 정작 재시도에서 500 을 낸다 — **[#188](https://github.com/Shadowfit/init/issues/188) 을 구현하기 전에 풀어야 한다** | -| 신뢰성 | [#188](https://github.com/Shadowfit/init/issues/188) 재시도 부재(rep 하나가 통째로 사라진다) · [#206](https://github.com/Shadowfit/init/issues/206) gRPC 예산 미전파 · [#208](https://github.com/Shadowfit/init/issues/208) 종료 정책 3갈래 | +| 신뢰성 | [#188](https://github.com/Shadowfit/init/issues/188) 재시도 부재(rep 하나가 통째로 사라진다) · [#206](https://github.com/Shadowfit/init/issues/206) gRPC 예산 미전파 · [#208](https://github.com/Shadowfit/init/issues/208) 종료 정책 3갈래 · 🆕 [#614](https://github.com/Shadowfit/init/issues/614) `StopAnalysis` 콜백 `threading.Thread` 상한 없음(Spring 저하·재배포 시 스레드 폭증 위험, §3 AI축 새 행에서 발견) | | 계약 | [#209](https://github.com/Shadowfit/init/issues/209) 핸들러 4개 중 1개만 클라이언트 잘못을 `INVALID_ARGUMENT` 로 낸다 · [#218](https://github.com/Shadowfit/init/issues/218) 국면 이름표와 rep 판정이 다른 자를 쓴다 | | 기능 부재 | [#193](https://github.com/Shadowfit/init/issues/193) · [#228](https://github.com/Shadowfit/init/issues/228) 자세 문제 유형 감지기가 **통째로 없다** | | 구조 | [#176](https://github.com/Shadowfit/init/issues/176) · [#175](https://github.com/Shadowfit/init/issues/175) · [#174](https://github.com/Shadowfit/init/issues/174) · [#173](https://github.com/Shadowfit/init/issues/173) | @@ -221,6 +233,17 @@ DBA 지원축의 결손 3개(백업 · 복제 · 무중단 DDL)는 **셋 다 실 ## 결정 로그 +- **2026-09-01: 처음 지목한 드리프트가 오진이었다 — AI 축 3번(팔마다 CPU 다른 이유)은 + 08-30에 안 닫혔다.** 사용자가 "실측 남은 것"을 물었을 때 08-30 커밋 셋(#593→#615→#616)이 + §3 3번 행을 닫았다고 짚었는데, 근거 문서(`per-process-ceiling-variance.md`)를 다시 열어 + 보니 그 항목은 **여전히 «착수 미결정»**(08-28 설계 이후 R/N/J 응답모드 × GIL 프로브 교차를 + 한 판도 안 돌렸다)이었다. 08-30에 실제로 돈 것은 `max_workers=10` 매직넘버라는 **완전히 + 다른 질문**([#593](https://github.com/Shadowfit/init/issues/593))이고, 그 질문은 재고표에 + **행 자체가 없었다.** **진짜 드리프트는 "답이 났는데 표가 모른다"가 아니라 "질문이 표에 + 없었다"** — 새 행으로 추가했다(§3). 정정 계기: 커밋 메시지의 이슈 번호만으로 관련 항목을 + 추정하고 대상 설계 문서를 직접 안 열어 본 것 — 다음엔 후보 항목의 원본 설계 문서를 + 먼저 열어 «착수/완료» 상태부터 확인할 것. + - **2026-08-27: 재고표 1번(HTTP p99) 행이 낡아 있던 것을 정정.** 사용자가 그 행을 짚어 "실측하자"고 했는데, 확인해보니 이미 답이 나와 있었다 — 표가 "🆕 쓰기축 rig이 #510으로 열려 있다(미머지·미실행)" 로 적어둔 것과 달리 **#510은 2026-08-24에 이미 병합·실행 완료**(從 R13, EC2 2대, 가정 피크 ×180 diff --git a/docs/decisions/r10-loadgen-topology.md b/docs/decisions/r10-loadgen-topology.md index 0e5ff65c..d6d2cf2e 100644 --- a/docs/decisions/r10-loadgen-topology.md +++ b/docs/decisions/r10-loadgen-topology.md @@ -1,9 +1,12 @@ # R10 의 무대 — 부하기를 대상 박스에 둘 것인가 -작성일: 2026-08-23 -상태: 🟢 **결정 완료 (2026-08-23 사용자 confirm) — 쪼갠다. R10-a(1대 동거)를 먼저 돌린다.** -R10-b(2대)는 `handler_concurrency` 를 기준 조건에서 재는 판으로 미룬다 — 착수는 R10-a 를 -보고 다시 정한다. 상세는 §7. +작성일: 2026-08-23 · 갱신: 2026-09-02 (R10-b 회수 — §9) +상태: ✅ **R10-a·R10-b 둘 다 완료.** R10-a(2026-08-23)가 GIL 스위치 간격을 반증하고 +「16 중 9.5」를 프로세스당 천장으로 답했다. R10-b(2026-09-02)는 남은 후보 +`handler_concurrency`(`GRPC_MAX_WORKERS`)도 반증했다 — 5·10·20 을 흔들어도 처리량·지연이 +2% 안에서 그대로다. 두 후보가 다 지워져서 **천장의 원인은 프로세스당(GIL 또는 이벤트루프) +쪽으로 좁혀진 채 남는다** — «어느 쪽인지」는 여전히 미확정([`ai-receive-path-scaling.md`](ai-receive-path-scaling.md) +자체의 캐비어트). 상세는 §7·§9. 연관: [`ai-receive-path-scaling.md`](ai-receive-path-scaling.md) §10·§12 · [`AWS-RIDE-ALONG.md`](../../loadtest/AWS-RIDE-ALONG.md) §1 從 R10 · [rig](../../loadtest/results/frame-path-overhead-2026-08-23/README.md) §7-2 · @@ -204,6 +207,41 @@ R10-b 의 격자가 줄어든다.** 2대 배선을 만들기 전에 그 답이 --- +## 9. ✅ R10-b 회수 (2026-09-02) — `handler_concurrency` 도 반증됐다 + +원본: [`../../loadtest/results/frame-path-r10b-2026-09-02/`](../../loadtest/results/frame-path-r10b-2026-09-02/README.md) +무대: 대상 `c7i.4xlarge` + 부하기(러너) `c7i.xlarge` · 160세션·90초·풀201 · 버림1+본판9(라틴방격) + +| | 결과 | +|---|---| +| `GRPC_MAX_WORKERS`(5·10·20)가 천장을 움직이나 | 🔴 **반증** — 팔간 차이 2% 안, R10-a의 자체 산포와 같은 급 | +| §2의 「부하기 오염」 우려(0.5~1.4 vCPU) | ✅ **이번엔 원리적으로 안 걸린다** — 2대 진짜 분리라 부하기가 대상 CPU를 안 먹는다 | +| 원격 CPU 계측 | 🔴 **전 판 실패**([#647](https://github.com/Shadowfit/init/issues/647)) — 처리량·지연 판정에는 영향 없음 | + +**최소 격자로 좁힌 것이 값을 했다.** 원래 §7 추천(ㄱ. 2대 완전 분리)의 배선을 그대로 쓰되, +A/B(계측 on/off) 대조는 R10-a가 이미 닫아서 빼고 `GRPC_MAX_WORKERS` 하나만 흔들었다 — 판 +10개로 끝났다(§7 원 견적 대비 훨씬 쌈). + +**부수 소득 — 2대 배선 버그 셋을 실전에서 처음 걸렀다**(§8의 "2대 배선의 소요를 안 재봤다"가 +실제로 그 자리였다): `bootstrap.sh`의 `.env` heredoc 오염, SSH `nohup` 응답 미분리, `--bind`가 +원격 모드에서도 로컬 기본값을 안 바꾸던 것. 전부 로컬 문법 검증(컴파일·`--help`·단위테스트) +으로는 못 잡는 종류였다 — **실제로 SSH 왕복을 태워봐야만 나오는 결함**이었다는 뜻이고, §8이 +"소요를 안 재봤다"로 남겨둔 리드타임 위험이 실제로 발생했다(리허설 재시도 1회, 코드 수정 후 +재개). + +**부수 사고** — 리허설도 `run_all.sh`를 그대로 태워 AUTO_SHUTDOWN 대상이 됐는데, 취소용 +root SSH가 러너(부하기)에는 안 열려 있었다(P4류 절차는 대상에만 열길 가정했었다). `stop`으로 +세이브·재기동해 회수는 했지만 부하기 가동시간 기록이 두 세그먼트로 쪼개졌다 +([#648](https://github.com/Shadowfit/init/issues/648)). + +### 남는 것 + +「16 중 9.5」를 만드는 것이 GIL 인지 단일 이벤트 루프인지 내부 락인지는 **R10-a도 R10-b도 +안 갈랐다** — 둘 다 프로세스 분리가 셋을 한꺼번에 우회하기 때문이다(R10-a §6 자체 캐비어트). +이 구분은 이번 라운드의 범위 밖으로 남긴다 — 필요해지면 별도 설계가 있어야 한다. + +--- + ## 결정 로그 - 2026-08-23: 문서 초안 — 갈래 셋(ㄱ 2대 / ㄴ 1대 동거 / ㄷ 코어 격리) · 판정선 넷에 diff --git a/docs/decisions/replication-lag-and-semisync.md b/docs/decisions/replication-lag-and-semisync.md index 480b5337..e60ee11f 100644 --- a/docs/decisions/replication-lag-and-semisync.md +++ b/docs/decisions/replication-lag-and-semisync.md @@ -1,14 +1,16 @@ # 복제 — 「한 대가 죽어도 된다」를 지연과 대가로 바꾼다 -작성일: 2026-08-13 · 갱신: 2026-08-23 (2대 라운드 회수 — §0-3) -상태: ✅ **2대 라운드 완료 (2026-08-22 측정 · 2026-08-23 회수).** Q1·Q2 에 값이 붙었다 — -**반동기의 대가는 처리량 −5.9% · 커밋 p95 +13.0%** 이고, 부하가 단일 핫세션으로 몰리면 -**−10.3% 로 거의 두 배**가 된다(H3). 리플리카는 이 부하(초당 약 5만 행)에서 **분 단위로 -밀린다**(약 60초). Q4·Q5 는 로컬 라운드(2026-08-17)가 닫았고, **Q3 은 「이 계측으로는 못 +작성일: 2026-08-13 · 갱신: 2026-09-02 (cross-AZ 대조 라운드 회수 — §0-5) +상태: ✅ **같은 AZ·다른 AZ 두 라운드 모두 완료.** Q1·Q2 에 값이 붙었다 — +**같은 AZ(08-22)** 는 반동기의 대가 처리량 −5.9% · 커밋 p95 +13.0%, 단일 핫세션이면 +−10.3% 로 거의 두 배(H3). **다른 AZ(09-02, §0-5)** 는 같은 대가가 **처리량 −17.2% · +커밋 p50 +23.1%(약 3~4배)로 커진다** — 단 자릿수까지는 안 벌어졌다(§9-1 ②의 "자릿수 +단위" 예견은 반증됨). 리플리카는 이 부하(초당 약 5만 행)에서 **분 단위로 밀린다**(약 60초, +AZ 무관). Q4·Q5 는 로컬 라운드(2026-08-17)가 닫았고, **Q3 은 「이 계측으로는 못 잰다」로 닫힌 채**다 — 값이 아니라 한계가 답이었다. -🔴 **인용 전에 §0-3 의 조건 넷을 볼 것** — 같은 AZ · 앱 경로가 아닌 SQL writer · 계측 바닥 -~1초 · `main` 이 아닌 커밋. 특히 **다른 AZ 는 안 쟀다.** §9-1 ② 가 그 선택을 「Q2 의 답을 -자릿수 단위로 바꾼다」고 적어뒀으므로 «반동기의 대가는 N» 을 조건 없이 인용하면 안 된다. +🔴 **인용 전에 §0-3 의 조건을 볼 것** — 앱 경로가 아닌 SQL writer · 계측 바닥 ~1초 · +같은 AZ 조건은 이제 §0-5 로 좁혀졌으니 **"반동기의 대가는 N"을 인용할 땐 AZ 조건을 반드시 +같이 적을 것.** 대상: DBA 축 결손 3개 중 **3번, 마지막**([[user_career_target]]). 백엔드 축에서는 「읽기를 어디로 보낼 것인가」의 전제가 된다. 연관: [`./backup-restore-rto-rpo.md`](./backup-restore-rto-rpo.md), [`./online-ddl-vs-blocking-alter.md`](./online-ddl-vs-blocking-alter.md), [`./commit-count-and-mysql-metrics.md`](./commit-count-and-mysql-metrics.md), [`./report-read-path.md`](./report-read-path.md), [`../../loadtest/AWS-RIDE-ALONG.md`](../../loadtest/AWS-RIDE-ALONG.md) 主-P4 @@ -163,6 +165,127 @@ Q3 의 창 길이, Q5 의 지연 크기 모두 계측 오버헤드와 하트비 --- +## 0-4. 🆕 설계 — 다른 AZ 대조 라운드 (2026-09-01 설계 · 2026-09-02 판 설계 확정 · 실행 미착수) + +**목적**: §0-3 조건 1(「같은 AZ 에서만 성립한다」)을 좁힌다. §9-1 ②가 예견한 대로 반동기의 +대가가 다른 AZ 에서 **자릿수 단위로** 달라지는지 실측한다. + +**무대 — 새로 만들 것이 없다.** `repl2_rig.sh`/`repl2_probe.sh`/`repl2_sweep.sh`는 `REPL_AZ_MODE`를 +이미 **라벨로만** 받는다(§10-2-1 예시에 `"cross-az(2a→2c)"`가 이미 있었다) — AZ 배치는 rig 이 +정하는 게 아니라 **인스턴스를 어느 서브넷에 띄우는지**가 정한다. rig 코드 변경 없음. + +**인프라 확인 (읽기전용, 2026-09-01 aws cli 조회)**: + +| 확인 | 결과 | +|---|---| +| 서브넷 | 기본 VPC(`vpc-0cf624ff85806b815`)에 **4개 AZ(2a·2b·2c·2d) 전부 기본 서브넷이 이미 있다** — 새 서브넷/VPC 작업 불필요 | +| 보안그룹 | `shadowfit-measure-sg`(`sg-0382132d2f8b5f6ce`)가 **자기 참조 규칙**(전체 포트, 소스=자기 자신)이라 AZ 와 무관하게 그대로 재사용된다 — 08-22 라운드가 쓴 SG 그대로 두 대에 물리면 된다 | + +**소스는 관례대로 `ap-northeast-2a`, 리플리카만 다른 AZ 서브넷에 띄운다.** + +### 결정 — 넷은 확정, 착수는 별도 (2026-09-02 사용자 confirm) + +- [x] **AZ 쌍** → 🟢 **소스 `2a` · 리플리카 `2c`**(08-22 라운드와 그대로 비교 가능한 쪽 채택) +- [x] **판 설계** → 🟢 **옵션 C** — 버림 1 + 본판 4, `REPS=2`(`repl2_sweep.sh`의 `gen_arm_order`가 + 블록마다 팔 순서를 뒤집어 **`A B B A`**를 만든다 — REPS=3의 `A B B A A B`와 같은 규칙, + 위치 합이 맞는 배열이다). 완전반복(옵션 B, `REPS=3`) 비용은 안 치른다 — 「자릿수 단위로 + 달라지는가」는 2판씩으로도 방향이 잡힌다는 판단. 🔴 **정정(09-02 코드 확인)**: 애초 + 적었던 「A A B B」는 틀린 표기였다 — 그 배열은 팔과 판 순서가 교락돼 이 라운드가 + 막으려는 문제([[feedback_measure_design_needs_repeats]])를 그대로 재현했을 것이다. + `REPS` 손잡이가 실제로 이미 위치균형 배열을 만들어주므로 그대로 쓴다 +- [x] **H3(핫세션) 대조** → 🟢 **이번 라운드는 다세션만.** 핫세션 교차는 범위 밖, 필요하면 후속 +- [x] **비용** → 🟢 **승인.** 2대(`c7i.2xlarge`) + 이번엔 **AZ 간 데이터 전송 요금**이 실제로 + 붙는다(08-22 는 같은 AZ 라 0 이었다). 옵션 C 기준 예상 가동시간은 08-22 라운드의 절반 + 남짓(러너 대략 0.7h) — **추정이지 실측 아니다**, 실행 후 `MANIFEST.txt` 에 실제값을 채운다 +- [x] **착수 여부(실행)** → 🟢 **실행됨 (2026-09-01~02).** 단 🔴 **이 확인은 사후 재구성이다** + — 실행이 이 설계 확인과 별도 경로로 일어났고(§0-5 머리말), 실행 당시 누가 "지금 + 띄우자"를 확정했는지의 대화 로그는 없다. 산출물(§0-5)로 「이 설계대로 돌았다」는 + 확인되지만, 「실행 confirm 이 실제로 있었는가」는 이 문서로는 답할 수 없다. + +### 실행 절차 (확정 — §10-2-1 과 동일 구조, AZ·판 설계만 다르다) + +```bash +# 소스: 2a 기본 서브넷(subnet-0f22e466008d27262), 리플리카: 2c 기본 서브넷(subnet-0d7f507333c66b23d) +# 둘 다 sg-0382132d2f8b5f6ce 물림 — 자기참조 SG 라 서브넷이 달라도 규칙 변경 불필요, +# 실행 직전 두 인스턴스 SG 재확인만 할 것 + +export REPLICA_HOST=<리플리카 사설 IP — 2c 서브넷> +export REPLICA_SSH="ssh -i /root/.ssh/measure.pem -o StrictHostKeyChecking=no root@$REPLICA_HOST" +export REPL_AZ_MODE="cross-az(2a→2c)" +export REPS=2 # 옵션 C — 본판 4, gen_arm_order(2) = "A B B A" (repl2_sweep.sh 로 확인됨, 09-02) +export HOT_ARMS="" # 확정: 이번 라운드는 H3(핫세션) 제외 — 비우면 for 루프가 안 돈다(repl2_sweep.sh:187) + +PHASES="repl_preflight repl_gate repl ridealong collect" \ + nohup bash loadtest/aws/run_all.sh > /root/run_all.log 2>&1 & +``` + +✅ **`REPS=2` 가 「A B B A」(위치균형 4판)를 만드는 것을 `repl2_sweep.sh:37-44`(`gen_arm_order`) +로 확인했다** (2026-09-02) — 검증된 명령이다. + +--- + +## 0-5. ✅ cross-AZ(2a→2c) 라운드 회수 (2026-09-01~02 측정) — §0-3 조건 1이 좁혀졌다 + +원본: [`../../loadtest/results/replication-aws-cross-az-2026-09-02/`](../../loadtest/results/replication-aws-cross-az-2026-09-02/README.md) +(`MANIFEST.txt` · `phases.tsv` · `repl/`) +무대: **`c7i.2xlarge` 2대** · 소스 `ap-northeast-2a` · 리플리카 `ap-northeast-2c` · +`pose_data_scale` 1,000만 행 · 옵션 C(버림2 + 본판4, `REPS=2`) · **6판 전부 유효 · 결함 0** + +> ⚠️ **실행 경위가 불완전하다.** 이 라운드는 §0-4 설계·착수 확인을 마친 뒤, 이 저장소에 +> 세션이 둘 이상 동시에 붙는 알려진 조건([[project_concurrent_sessions]])에서 **사용자가 +> 이 대화에서 확인한 세션이 아닌 다른 경로로 실행됐다.** 재기동을 시도하던 세션이 S3 에서 +> 이미 완주된 결과(`ec2-20260901-184855`)를 발견해 회수했다 — 실행 당시의 의사결정 로그는 +> 없고, 있는 것은 산출물뿐이다. 상세: 결과 README 머리말. + +| | 결과 | +|---|---| +| **Q1** 몇 초 뒤처지는가 (다른 AZ) | ✅ **팔 A 약 60초 · 팔 B 약 52초** — 같은 AZ(60.5s·57.5s)와 **거의 같거나 더 작다.** 🔴 예상과 반대 방향 | +| **Q2** 반동기가 무는 값 (다른 AZ) | ✅ **처리량 −17.2% · 커밋 p50 +23.1% · p95 +18.1%** — 같은 AZ 대비 **최대 약 3.8배** | +| **§9-1 ② 의 "자릿수 단위" 예견** | 🔴 **반증됐다** — 자릿수(10배 이상)까지는 안 벌어졌다. "여전히 한 자릿수 %대이되 방향이 뚜렷한, 최대 3~4배" 가 실측값이다 | +| H3(핫세션) | ⬜ **이 라운드는 범위 밖** — 다세션만 쟀다(설계 §0-4 결정) | + +### 조건 — 08-22 라운드와 같이 인용할 때 볼 것 + +같은 AZ 라운드의 조건 넷(§0-3) 중 **1번(같은 AZ)만 이 라운드로 갈린다.** 나머지 셋 +(앱 경로 아닌 SQL writer · 계측 바닥 ~1초 · `main` 커밋)은 이 라운드도 그대로 적용된다. +추가로: 🔴 **REPS=2(4판)라 08-22(REPS=3, 6판)보다 반복이 적다** — 산포 자체는 비슷한 +자릿수(A 0.27%·B 0.65%)로 나왔지만 표본이 더 적다는 점은 남는다. + +### 🔴 Q1 이 예상과 반대로 갈렸다 — 반동기 쪽이 오히려 덜 밀린다 + +cross-AZ 에서 팔 B(반동기)의 지연이 같은 AZ 대비 **9.3% 더 작게** 나왔다. RTT 가 커지면 +지연도 커질 것이라는 순진한 예상과 반대다. 읽는 방식은 08-22 라운드 §2-1 과 같은 결이다 — +**반동기가 지연을 줄이는 게 아니라, cross-AZ 에서 팔 B 의 처리량이 더 크게 깎이니 +(Q2) 리플리카가 따라잡을 절대량 자체가 줄어든다.** ⚠️ 이 인과는 08-22·09-02 두 라운드 +연속으로 관측만 됐고 **아직 한 번도 직접 검증되지 않았다** — 소스를 인위로 같은 만큼 +늦춘 비동기 대조판이 있어야 갈린다. + +### Q2 — 방향은 맞았고 크기는 예상보다 작았다 + +§9-1 ②가 "자릿수 단위로 달라진다"고 예견했던 것은 **틀렸다.** 실측은 처리량 기준 +2.9배(−5.9%→−17.2%), 커밋 p50 기준 3.8배(+6.1%→+23.1%) — **방향은 예견대로(더 커진다) +지만 크기는 자릿수가 아니라 배수 몇 개 선에서 멈췄다.** 왕복(RTT) p50 이 1.3배만 늘고 +p95·max 가 3.6배 늘어난 것(§1-1)과 겹쳐 보면, **커밋 p50 대가가 왕복 p50 상승을 거의 +그대로 반영**하고 **커밋 p99 대가(1.2배)는 왕복 꼬리(3.6배)만큼은 안 벌어진** 모양이다 — +같은 AZ 라운드가 "대가가 평균보다 꼬리에 크다"고 적었던 것과 **반대 패턴**이다. +⚠️ 이것도 관측된 패턴 기술이지 원인 검증이 아니다. + +### 새로 열린 것 (버그 2건) + +이 라운드를 발견하는 과정에서, 같은 실험을 모르고 재기동하려던 시도가 실패하며 rig 자체의 +결함 둘을 걸렀다: + +- **`run_all.sh` 의 `FINAL_OK` 판정 결함** — `repl_preflight` 가 FAIL 이어도 S3 업로드 + 자체는 성공하므로 `FINAL_OK=1` 이 되고, `AUTO_SHUTDOWN=1` 이 두 인스턴스를 곧바로 + 꺼버린다. 진단 로그가 볼륨과 함께 사라진다. [#641](https://github.com/Shadowfit/init/issues/641) +- **P4 절차 문서 공백** — AL2023 AMI 는 기본 root SSH 가 막혀 있는데, `aws/README.md` 의 + P4 절차는 `REPLICA_SSH` 가 root 로그인을 전제한다. 이 단계가 문서 어디에도 없다. + [#642](https://github.com/Shadowfit/init/issues/642) + +상세: [결과 README §4·§5](../../loadtest/results/replication-aws-cross-az-2026-09-02/README.md) + +--- + ## 0. 이 문서가 여는 것 경력기술서 §6「비어 있는 것」의 마지막 칸이다. @@ -698,6 +821,20 @@ Q1 은 통째로 인위 지연이다. ## 결정 로그 +- 2026-09-02: **§0-4 의 판단 넷을 사용자가 그대로 확정** — AZ 쌍(소스 2a·리플리카 2c) · + 판 설계(옵션 C, 버림 1 + 본판 4) · H3 미포함(다세션만) · 비용 승인. 실행 절차에 AZ 쌍을 + 구체 서브넷 ID 로 못박았다. 🔴 **`REPS` 가 옵션 C 의 배열을 실제로 만드는지는 아직 안 봤다** + — 실행 직전 `repl2_rig.sh` 로 재검증 필요, 문서엔 「의도」로만 남긴다. **착수(EC2 기동) 자체는 + 이번 confirm 의 범위 밖**이고 여전히 별도 확인이 필요하다. + +- 2026-09-01: **다른 AZ 대조 라운드 설계 착수** (사용자 결정 — 재고표 후보 중 P4 를 확정하고 + 설계를 요청). rig 은 이미 AZ 와 무관하게 완비돼 있음을 재확인했다 — `REPL_AZ_MODE` 는 + 라벨일 뿐이고 `cross-az(2a→2c)` 예시가 §10-2-1 에 이미 있었다. 인프라도 읽기전용 aws cli + 조회로 확인 — 기본 VPC 에 4개 AZ 전부 서브넷이 있고 보안그룹이 자기참조 규칙이라 **새 + 프로비저닝이 필요 없다.** 남은 것은 판단 넷(AZ 쌍 · 판 설계 · H3 포함 여부 · 비용 승인)이고 + §0-4 에 트레이드오프와 추천을 남겼다. **착수(실행)는 아직 안 함** — 설계 confirm 과 실행 + confirm 을 분리한다([[feedback_user_decides_not_claude]]). + - 2026-08-13: 설계 초안. 질문 5개(**Q3·Q4 가 급소**) · 팔 3개(C 는 양성 대조군) · read-after-write 시나리오 · 가설 6개와 반증 조건. 무대는 DDL·백업 rig 부품 재사용이고, **리플리카 초기화는 08-13 백업 라운드에서 G5 로 검증된 XtraBackup 사본을 쓴다.** diff --git a/loadtest/aws/README.md b/loadtest/aws/README.md index a7a69898..113316de 100644 --- a/loadtest/aws/README.md +++ b/loadtest/aws/README.md @@ -418,7 +418,7 @@ nohup python3 loadtest/results/frame-path-overhead-2026-08-23/run_arms.py \ | 팔 | A(기준) · B(구간·`lease` + 앵커) · `A@0.0005`·`A@0.05`(GIL **양방향**) · `B@0.0005`(계측×GIL **상호작용**, 2×2 를 닫는 칸) 🔴 **정본은 [설계 §13](../../docs/decisions/ai-receive-path-scaling.md) 이고 위 판 문자열은 거기서 옮긴 것이다** — 바꾸려면 설계부터 고칠 것 | | 배열 | **5×5 라틴 방격 + 버림 1 = 26판.** 각 팔이 **각 위치에 정확히 한 번** 온다 — 재기동이 −24%, 판 순서가 −6.4% 를 흔들기 때문(설계 §9-4) | | ⏱ 소요 | 판당 120초면 **52분**, 160초면 **69분** — `setup`(160세션 여는 시간)이 미측정이라 **버림판이 처음 준다.** 인스턴스 1.5시간 | -| 🔴 **안 읽는 값** | `handler_concurrency` — 부하기가 동거해서 **절대값이 다친다.** R10-b(2대)의 몫이다(§7) | +| 🔴 **안 읽는 값** | `handler_concurrency` — 부하기가 동거해서 **절대값이 다친다.** R10-b(2대, 아래)의 몫이다(§7) | | 러너 | 🟢 **`PHASES="framepath collect"` 로 돈다** — 아래 「무인으로 돌리려면」. 위 손 명령도 그대로 유효하다(회수만 손으로) | ⚠️ **이 절차로 실제로 띄워 본 적은 없다.** ROLE 은 `bash -n` 과 역할 분기 검증만 거쳤고, diff --git a/loadtest/aws/bootstrap.sh b/loadtest/aws/bootstrap.sh index 23d8e82b..ca808df1 100644 --- a/loadtest/aws/bootstrap.sh +++ b/loadtest/aws/bootstrap.sh @@ -295,10 +295,16 @@ DB_PASSWORD=$PW # **새로 늘면서** ROLE=db 가 다시 깨졌다 — 즉 이건 «한 번 고치면 끝» 이 아니라 # **compose 에 `:?` 가 늘 때마다 여기가 따라와야 하는** 자리다. # -# 값은 아무 문자열이어도 된다. 이 역할은 AI 를 안 띄우므로 아무도 이 토큰으로 인증하지 않는다. -# ⚠️ p6-target 은 아래에서 **진짜 값으로 덮어쓴다**(양쪽이 같아야 하는 자리라 거기선 생성한다). -AI_PUBLIC_TOKEN=${AI_PUBLIC_TOKEN:-unused-in-this-role} -INTERNAL_API_TOKEN=${INTERNAL_API_TOKEN:-unused-in-this-role} +# 이 역할은 AI 를 안 띄우므로 값 자체는 아무도 인증에 안 쓴다. 하지만 두 토큰이 같으면 +# `shadowfit-ai` 가 기동 시 `INTERNAL_API_TOKEN 과 AI_PUBLIC_TOKEN 이 같다` 로 죽는다(#230) — +# ROLE=db 뒤 (의도된 시나리오는 아니지만) 손으로 전체 스택을 올리면 재현된다(#578). +# 그래서 고정 문자열 대신 **항상 독립적으로 랜덤 생성**해(heredoc 바로 위) 둘이 우연히도 같아질 +# 구조를 없앤다. +# ⚠️ p6-target 은 이 값을 그대로 쓴다 — 거기서 다시 만들면 아래 준비 완료 echo 와 값이 어긋난다. +# (#708 이 이 자리를 `unused-in-this-role` 고정값 + 옛 주석으로 되돌렸었다 — 위의 랜덤 생성이 +# 먼저 돌아서 동작은 안 깨졌지만 주석이 거꾸로 말하고 있었다. #813 에서 복구) +AI_PUBLIC_TOKEN=$AI_PUBLIC_TOKEN +INTERNAL_API_TOKEN=$INTERNAL_API_TOKEN EOF echo " MYSQL_ROOT_PASSWORD 를 rig 기본값과 맞췄다 (+ 해석만 되면 되는 변수들)" @@ -312,9 +318,7 @@ fi echo " compose 해석 확인 ✅ (mysql 만 띄워도 파일 전체가 해석된다)" if [ "$ROLE" = "p6-target" ]; then - # 토큰은 없으면 만든다 — 값 자체는 아무 문자열이어도 되지만 **양쪽이 같아야** 한다. - [ -n "$AI_PUBLIC_TOKEN" ] || AI_PUBLIC_TOKEN=$(head -c 24 /dev/urandom | base64 | tr -d '/+=') - [ -n "$INTERNAL_API_TOKEN" ] || INTERNAL_API_TOKEN=$(head -c 24 /dev/urandom | base64 | tr -d '/+=') + # 토큰은 위 .env 블록에서 이미 만들었다(없으면 랜덤 생성) — 여기서는 그 값을 그대로 쓴다. cat >> "$WORKDIR/.env" <