diff --git a/docs/decisions/rpo-rto-targets.md b/docs/decisions/rpo-rto-targets.md index b14cad95..5f51d60a 100644 --- a/docs/decisions/rpo-rto-targets.md +++ b/docs/decisions/rpo-rto-targets.md @@ -1,6 +1,6 @@ # RPO·RTO 목표 — «얼마나 잃어도 되고, 얼마 안에 돌아와야 하나» 를 정하는 분기점 -작성일: 2026-11-07 · 갱신: 2026-11-08 (§8-1 읽기 : 쓰기 비율 가정 추가 · §6-2 측정 둘 — 재생 시간·호스트 상실 RPO) +작성일: 2026-11-07 · 갱신: 2026-11-08 (§8-1 읽기 : 쓰기 비율 가정 추가 · §6-2 측정 둘 — 재생 시간·호스트 상실 RPO · 측정 셋 — 새 서버 준비·반출 업로드·리플리카 승격) 상태: 🟡 **방향 확정 · 숫자 미정.** 구성 2(백업 + binlog 서버 밖 반출) 채택, 리플리카 보류, 반동기 미채택 (2026-11-07 사용자 confirm). **반출 주기·백업 주기·보존 기간 숫자는 아직 없다** — 추천안이 숫자를 정하지 않았고, 근거 없이 채우지 않는다(§6 Q4·Q6). @@ -102,7 +102,7 @@ Flyway 마이그레이션의 테이블 기준. «재생성» 칸은 코드·문 | **프로세스 죽음** | OOM kill, mysqld 크래시 | 디스크에 남음 | **0** — 커밋된 건 디스크에 있다(`innodb_flush_log_at_trx_commit=1`·`sync_binlog=1`) | ⬜ **미측정** — 재기동 + redo 복구 시간 | | **호스트 재부팅** | OS 패치, 인스턴스 재시작 | 남음 | 0 | ⬜ 미측정 | | **논리 사고** | 실수로 `DROP TABLE`·대량 `DELETE` | 남음 (사고도 binlog 에 기록) | **사고 직전** — PITR 실측 확인(백업 문서 Q5) | 백업 붓기(물리 약 174초 · 논리 약 21분, 1억 행) **+ binlog 재생 — 1GB 당 약 2분 15초(표준), 병렬이면 약 절반** ([재생 실측](../../loadtest/results/binlog-replay-time-2026-11-07/README.md)) | -| **호스트·디스크 상실** | EBS 고장, 인스턴스 삭제, `compose down -v`, 랜섬웨어 | 🔴 **binlog 도 같이 사라짐** | 🔴 **정의 안 됨** — «마지막 백업 시점» 인데 백업 주기·위치가 없다 | 새 서버 준비(⬜) + 백업 붓기 — **백업이 서버 밖에 없으면 복구 불가** | +| **호스트·디스크 상실** | EBS 고장, 인스턴스 삭제, `compose down -v`, 랜섬웨어 | 🔴 **binlog 도 같이 사라짐** | 🔴 **정의 안 됨** — «마지막 백업 시점» 인데 백업 주기·위치가 없다 | 새 서버 준비(**약 140초** — 빈 EC2 → MySQL 쿼리 받음, [실측](../../loadtest/results/server-prep-and-ship-cost-2026-11-08/README.md)) + 백업 붓기 — **백업이 서버 밖에 없으면 복구 불가** | ⚠️ 복구 시간 실측은 m6i.xlarge · gp3 · 1억 행(합성) 조건이고 «운영에서 N분» 으로 인용 금지(백업 문서 §5). ⚠️ 운영 구성(`docker-compose.prod.yml`)에는 백업 설정이 **없다** — 볼륨 `mysql_data` 하나에 데이터·binlog 가 같이 있다(`:53`·`:226`). @@ -120,12 +120,12 @@ Flyway 마이그레이션의 테이블 기준. «재생성» 칸은 코드·문 |---|---|---|---|---|---| | **0** | 지금 (백업 없음, binlog 같은 디스크) | 🔴 전부 | 🔴 복구 불가 | 없음 | 없음 | | **1** | 주기 백업 → 서버 밖(S3 등) | **백업 주기** (예: 하루면 최대 24시간) | 새 서버 + 백업 붓기 (물리 약 174초 · 논리 약 21분 실측, 1억 행) | 없음 (백업 중 정지 42~126ms 실측) | 저장 요금 · 백업 잡 | -| **2** | 1 + binlog 를 주기적으로 서버 밖에 (회전 때 업로드) | **binlog 업로드 주기** — 실측 최악 ≈ 주기(60초 주기 6~51초 · 300초 주기 109~300초, [호스트 상실 실측](../../loadtest/results/hostloss-rpo-2026-11-07/README.md)) | 1 + **binlog 재생 — 1GB 당 약 2분 15초**(DAU 1,000 가정 하루치 ≈ 0.6GB ≈ 1분 20~35초, [재생 실측](../../loadtest/results/binlog-replay-time-2026-11-07/README.md)) | 없음 | 2 와 비슷 · 업로드 잡 · 주기적 `FLUSH BINARY LOGS` | +| **2** | 1 + binlog 를 주기적으로 서버 밖에 (회전 때 업로드) | **binlog 업로드 주기** — 실측 최악 ≈ 주기(60초 주기 6~51초 · 300초 주기 109~300초, [호스트 상실 실측](../../loadtest/results/hostloss-rpo-2026-11-07/README.md)) | 1 + **binlog 재생 — 1GB 당 약 2분 15초**(DAU 1,000 가정 하루치 ≈ 0.6GB ≈ 1분 20~35초, [재생 실측](../../loadtest/results/binlog-replay-time-2026-11-07/README.md)). 새 서버 약 140초 + 붓기 174초 + 하루치 재생을 더하면 **약 6.5~7분**(서로 다른 실험의 합, 사람 시간 제외 — [합산](../../loadtest/results/server-prep-and-ship-cost-2026-11-08/README.md#4-구성-2-의-복구-시간에-대입하면)) | 없음 | 2 와 비슷 · 업로드 잡 · 주기적 `FLUSH BINARY LOGS` — 가정 피크에서 반출 1번 **업로드 0.48~0.71초 · FLUSH 52~56ms**([실측](../../loadtest/results/server-prep-and-ship-cost-2026-11-08/README.md)) | | **2'** | 1 + binlog 실시간 스트리밍 (`mysqlbinlog --stop-never` 를 다른 서버에서) | 수 초 | 2 와 같음 | 없음 | 받는 서버 1대 상시 | -| **3** | 2 + **비동기 리플리카** | 리플리카가 못 받은 양 — 쓰기 부하에서 **약 60초 밀림** 실측(같은 AZ·SQL writer) | **승격 시간** — 밀린 양 적용 + 앱 연결 전환 (⬜ 미측정, 수동이면 사람 시간 추가) | 없음 | 서버 1대 상시 · 복제 감시 | +| **3** | 2 + **비동기 리플리카** | 리플리카가 못 받은 양 — 쓰기 부하에서 **약 60초 밀림** 실측(같은 AZ·SQL writer) | **승격 시간** — DB 쪽은 밀림 없으면 **약 0.25초**, 약 21초 밀리면 **약 7.8초**(거의 전부 밀린 것 적용, 한 박스 · [실측](../../loadtest/results/replica-promotion-2026-11-08/README.md)). 앱 연결 전환·감지·사람 시간은 ⬜ 미측정 | 없음 | 서버 1대 상시 · 복제 감시 | | **4** | 3 + **반동기** | 거의 0 — 🔴 단 리플리카가 끊기면 **조용히 비동기로 강등**(실측) | 3 과 같음 | **처리량 −5.9%(같은 AZ) ~ −17.2%(다른 AZ)** · 몰리면 −10.3% (실측) | 3 + 강등 알람 · 리플리카 재기동 절차(시작 옵션으로 켤 것 — [11-07 결과](../../loadtest/results/replication-semisync-recovery-2026-11-07/README.md)) | -미측정 칸(⬜)은 «0» 이 아니다. 특히 **binlog 재생 시간**은 2·2' 의 RTO 를 정하는 값인데 아직 없다. +미측정 칸(⬜)은 «0» 이 아니다. 2·2' 의 RTO 를 정하는 binlog 재생 시간·새 서버 준비 시간과 3 의 승격 시간은 11-08 에 쟀다(§6-2). --- @@ -141,7 +141,8 @@ Flyway 마이그레이션의 테이블 기준. «재생성» 칸은 코드·문 - [x] **Q3. RPO 를 하나로** → 🟢 **하나로 둔다.** 저장소를 나누지 않는다(§7) - [~] **Q4. 호스트 상실 RPO** → 🟢 **구성 2 채택** — 백업 + binlog 를 주기적으로 서버 밖에. RPO = binlog 반출 주기. 🟡 **반출 주기 숫자는 미정** — 추천안이 정하지 않았다. 고르는 근거는 «주기를 줄일수록 늘어나는 것» - (업로드 횟수·`FLUSH BINARY LOGS` 빈도·작은 binlog 파일 수)이고 그건 아직 안 쟀다 + (업로드 횟수·`FLUSH BINARY LOGS` 빈도·작은 binlog 파일 수) — 11-08 에 쟀다: 가정 피크에서 반출 1번에 업로드 + 0.48~0.71초·FLUSH 52~56ms, 하루 횟수는 60초 주기 1,440번 · 300초 주기 288번(§6-2). FLUSH 가 쓰기를 막는 시간과 요금은 안 쟀다 - [~] **Q5. 호스트 상실 RTO** → 🟢 **리플리카(3) 보류.** 🟡 목표 숫자는 미정 — 구성 2 의 실제 RTO(백업 붓기 + **binlog 재생**)를 먼저 재고, 그 값이 받아들일 수 없을 때 3 을 다시 연다 - [~] **Q6. 백업 주기·보관 위치·보존 기간** → 🟢 **위치는 서버 밖**(구성 2 의 정의). 🟡 **주기·보존 기간은 미정** — @@ -164,6 +165,9 @@ Flyway 마이그레이션의 테이블 기준. «재생성» 칸은 코드·문 |---|---|---| | ~~binlog 재생 시간 (백업 이후 N시간치)~~ ✅ 11-08 | 구성 2 의 RTO 를 정한다 — Q5 의 근거 | [재생 실측](../../loadtest/results/binlog-replay-time-2026-11-07/README.md) | | ~~호스트 상실 → S3 만으로 복구~~ ✅ 11-08 | 구성 2 가 실제로 되는지 + 잃은 양 | [호스트 상실 실측](../../loadtest/results/hostloss-rpo-2026-11-07/README.md) | +| ~~새 서버 준비 시간~~ ✅ 11-08 | 구성 2 RTO 의 첫 단계(§4 의 빈칸) | [준비·업로드 실측](../../loadtest/results/server-prep-and-ship-cost-2026-11-08/README.md) | +| ~~반출 업로드 비용~~ ✅ 11-08 | Q4 반출 주기의 대가 · «최대 손실 = 주기 + 업로드 시간» 의 업로드 시간 | 같은 실측 §2 | +| ~~리플리카 승격 시간~~ ✅ 11-08 | 구성 3 이 구성 2 보다 실제로 얼마나 짧은가 — Q5 · R1 | [승격 실측](../../loadtest/results/replica-promotion-2026-11-08/README.md) | | 프로세스 재기동 RTO | §4 첫 줄의 빈칸 | 로컬로 충분 (메커니즘) | --- @@ -260,3 +264,12 @@ DAU 와 비교해 판단을 닫지 않는다는 원칙과도 어긋난다([[feed 🔴 300초의 최악이 299.5초로 주기에 붙었다 — «최대 손실 = 주기» 로 약속하려면 업로드 시간을 더해 잡아야 한다. 쓰기 중 XtraBackup 은 EC2 7판 모두 1회 성공(로컬에선 redo 경합으로 실패한 판 있음). 다음에 잴 것: 복구 중에만 디스크 기록을 늦춘 재생(다른 세션 제안) · 기준 1억 행 · 새 서버 준비 시간. +- 2026-11-08: **§6-2 측정 셋 — 새 서버 준비 · 반출 업로드 비용 · 리플리카 승격** (EC2 m6i.xlarge · 8.0.46 · 커밋 `8200b521`·`96677a25`). + 숫자 결정(Q4·Q5·Q6)은 **여전히 사용자 몫**이고 이 항목은 근거만 붙인다. + ① [준비 실측](../../loadtest/results/server-prep-and-ship-cost-2026-11-08/README.md): 빈 EC2 → 복구 대상 MySQL 이 쿼리를 받기까지 **약 140초**(3대, 139.5~142.9초) — + 그중 bootstrap 약 107~110초. 미리 구운 AMI 경로는 안 쟀다. 구성 2 의 복구 시간을 다른 실험 값과 더하면 **약 6.5~7분**(사람 시간 제외). + ② 같은 실측 §2: 가정 피크(초당 45행)에서 반출 1번의 **업로드 0.48~0.51초(60초 주기, 약 5.8MB) · 0.70~0.71초(300초 주기, 약 29MB)**, FLUSH 52~56ms. + → 호스트 상실 실험이 남긴 «최대 손실 = 주기 + 업로드 시간» 의 업로드 시간은 이 조건에서 1초 미만. 최대 속도로 쓰면 1GB 파일이 2.3~12초. + ③ [승격 실측](../../loadtest/results/replica-promotion-2026-11-08/README.md): DB 쪽 승격 **밀림 없으면 약 0.25초 · 약 21초 밀리면 약 7.8초**(본판 8, 잃은 트랜잭션 0). + 한 박스 동거라 «잃은 0» 은 다른 박스에선 성립하지 않고, 감지·결정·앱 연결 전환 시간은 빠져 있다. + → Q5 를 다시 열지는 «구성 2 의 약 7분이 목표 RTO 안에 드나» 로 가른다 — 목표 숫자는 아직 없다. diff --git a/loadtest/results/binlog-replay-time-2026-11-07/README.md b/loadtest/results/binlog-replay-time-2026-11-07/README.md index ecd0c10b..37cf1bef 100644 --- a/loadtest/results/binlog-replay-time-2026-11-07/README.md +++ b/loadtest/results/binlog-replay-time-2026-11-07/README.md @@ -40,7 +40,7 @@ RUN_ID `replay-ec2-20261107-173709` · 커밋 `7c5e4900` · 19판 전부 검증 | 백업 붓기 | 물리(XtraBackup) 약 174초 · 논리 약 21분 | 08-13 백업 라운드 — **1억 행**(합성), m6i.xlarge | | 백업 붓기 (작은 무대) | 내려받기 2.4~2.6초 + prepare·copy-back 53~57초 | 짝 실험(호스트 상실) — **1,000만 행** | | binlog 재생 | 위 표 — 1GB 당 약 2분 15초(M1) | 이 실험 | -| 새 서버 준비 | ⬜ 안 쟀다 | — | +| 새 서버 준비 | 약 140초 (빈 EC2 → MySQL 쿼리 받음, 3대) | 11-08 [준비 실측](../server-prep-and-ship-cost-2026-11-08/README.md) — 미리 구운 AMI 경로는 안 쟀다 | 예) DAU 1,000 가정에서 **백업을 하루 한 번** 뜨면, 최악(백업 직전 상실)의 재생 몫은 하루치(약 0.6GB) ≈ **약 1분 20초~1분 35초(M1)** (잰 칸은 1.1일치 93~95초), **일주일에 한 번**이면 약 7일치 ≈ 4.1GB ≈ **약 9~10분(M1, 환산)** 이다. 여기에 백업 붓기(1억 행이면 약 3분)와 새 서버 @@ -76,7 +76,7 @@ EC2 에서 몇 시간 돌린 뒤에야 만났을 것들 — 커밋 `7c5e4900` ① 재생 중 서버가 죽으면 처음부터 다시 해야 한다 ② 끝나고 설정을 되돌려 운영으로 넘기는 시간은 이 값에 안 들어간다. M1 이 트랜잭션당 약 7.6ms(31만 행 ÷ 25행 ÷ 94초)라 «커밋마다 fsync» 만으로 설명되는지는 이 데이터로 갈리지 않는다 - 기준 1억 행에서 같은 재생 — 조건 2 -- 새 서버 준비 시간 — §2 의 빈칸 +- ~~새 서버 준비 시간 — §2 의 빈칸~~ → 11-08 약 140초([준비 실측](../server-prep-and-ship-cost-2026-11-08/README.md)) ## 6. 재현 diff --git a/loadtest/results/hostloss-rpo-2026-11-07/README.md b/loadtest/results/hostloss-rpo-2026-11-07/README.md index 521a1786..1ef278e1 100644 --- a/loadtest/results/hostloss-rpo-2026-11-07/README.md +++ b/loadtest/results/hostloss-rpo-2026-11-07/README.md @@ -26,7 +26,7 @@ N초였다** — 60초 주기에서 6~51초, 300초 주기에서 109~300초. 언 - **잃은 시간 ≈ «주기 안에서 잃은 시점» × 주기** — 주기 끝 무렵에 잃을수록 많이 잃는다. 최악은 주기 하나만큼이다 - 🔴 **300초 주기의 최악(299.5초)은 주기에 바짝 붙었다.** 반출은 «닫고 → 올리기» 라서 올리는 동안 쌓인 몫이 더해질 수 있다 — - 주기를 «최대 손실» 로 약속하려면 **주기 + 업로드 시간** 으로 잡아야 한다(이 무대의 업로드 시간은 따로 안 쟀다) + 주기를 «최대 손실» 로 약속하려면 **주기 + 업로드 시간** 으로 잡아야 한다(이 무대의 업로드 시간은 따로 안 쟀다 → 11-08 가정 피크에서 반출 1번 0.48~0.71초, [업로드 실측](../server-prep-and-ship-cost-2026-11-08/README.md) §2) - 잃은 «시간» 은 쓰기 속도와 무관하지만, 잃은 «행 수» 는 쓰기 속도에 비례한다 — 이 실험의 writer 는 최대 속도(15개 연결, 초당 수천 행)라 행 수는 DAU 1,000 가정보다 훨씬 크다. **인용은 «초» 로 한다** - 「커밋 성공 받은」 = writer 클라이언트(소스 밖 컨테이너)가 COMMIT 응답을 받은 것. 서버가 사라져도 이 기록은 남는다 diff --git a/loadtest/results/replica-promotion-2026-11-08/README.md b/loadtest/results/replica-promotion-2026-11-08/README.md new file mode 100644 index 00000000..d0d4aaba --- /dev/null +++ b/loadtest/results/replica-promotion-2026-11-08/README.md @@ -0,0 +1,64 @@ +# 리플리카 승격 시간 — 소스가 죽은 순간부터 리플리카가 새 쓰기를 받기까지 (2026-11-08, EC2) + +설계 문서: [`../../../docs/decisions/rpo-rto-targets.md`](../../../docs/decisions/rpo-rto-targets.md) §5 구성 3 · §8 R1 +짝 실험: [`../server-prep-and-ship-cost-2026-11-08/`](../server-prep-and-ship-cost-2026-11-08/README.md) §4 (구성 2 의 복구 시간 약 6.5~7분) +무대: **EC2 `m6i.xlarge` 1대** · gp3 200GB · `ap-northeast-2d` · MySQL **8.0.46**(`mysql:8.0`) · GTID 복제(비동기) · +`flush=1`·`sync_binlog=1` · 버퍼풀 1GB · RUN_ID `promote-ec2-20261108-034706` · 커밋 `96677a25` · 10판(버림 2 + 본판 8) + +질문: 리플리카(구성 3)를 붙이면 호스트 상실 RTO 가 구성 2 의 약 7분보다 실제로 짧아지나? 문서는 그렇게 가정만 했고 승격 시간을 잰 적이 없다. + +## 0. 한 줄 + +**DB 쪽 승격은 밀린 게 없으면 약 0.25초, 약 21초 밀려 있으면 약 7.8초였다 — 승격 시간은 거의 전부 «밀린 것 따라잡기» 다.** +구성 2 의 약 7분보다 두 자릿수 짧다. 단 장애를 알아채고 · 승격을 결정하고 · 앱 연결을 바꾸는 시간은 **빠져 있다.** + +## 1. 결과 (본판) + +| 팔 | 판 | 죽일 때 밀림 | 밀린 트랜잭션 | 받기 중지 | **적용** | 전환 | 첫 쓰기 | **승격 합** | 잃은 트랜잭션 | +|---|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| paced | 4 | 0초 | 0 | 48 | 89 | 64 | 49 | **250ms** | 0 | +| paced | 5 | 0초 | 0 | 47 | 92 | 66 | 49 | **254ms** | 0 | +| paced | 8 | 0초 | 0 | 48 | 89 | 68 | 50 | **255ms** | 0 | +| paced | 9 | 0초 | 0 | 45 | 91 | 57 | 47 | **240ms** | 0 | +| stress | 3 | 22초 | 3,440 | 51 | 7,868 | 74 | 52 | **8,045ms** | 0 | +| stress | 6 | 22초 | 3,440 | 52 | 7,657 | 76 | 51 | **7,836ms** | 0 | +| stress | 7 | 20초 | 3,258 | 52 | 7,627 | 75 | 52 | **7,806ms** | 0 | +| stress | 10 | 21초 | 3,380 | 49 | 7,635 | 89 | 49 | **7,822ms** | 0 | + +(버림판: paced 254ms · stress 21초 밀림 → 7,822ms. 단위 없는 칸은 ms) + +- **paced(가정 피크 초당 45행)는 밀림이 0 이고 승격이 240~255ms** 다. 이 중 대부분은 rig 의 고정 비용이다 — 구간마다 + `docker exec` 로 mysql 클라이언트를 새로 띄우는데 한 번에 43~44ms(`exec_ms`)이고, 승격 경로에서 5번 띄운다(약 220ms) +- **stress 는 승격의 97% 이상이 «적용»(받아 둔 릴레이 로그를 끝까지 적용)** 이다. 밀린 3,258~3,440건을 7.6~7.9초에 적용 — + 초당 약 430건(건당 25행이라 초당 약 1만 행) +- 🟡 죽이기 직전 리플리카는 약 21초 밀려 있었는데 따라잡는 데는 약 7.8초였다. 추정: 소스와 리플리카가 한 박스라 부하 중엔 + CPU 를 나눠 썼고, 소스가 죽은 뒤엔 리플리카가 혼자 썼다(§3-3). 확인하지 않았다 +- 잃은 트랜잭션은 10판 모두 0 — 밀린 것은 «받았지만 아직 적용 안 한» 것이었고, 받는 쪽은 소스를 따라가 있었다(§3-2) + +## 2. 구성 2 와 나란히 + +| | 구성 2 (백업 + binlog 반출) | 구성 3 (+ 비동기 리플리카) | +|---|---|---| +| DB 가 다시 쓰기를 받기까지 | **약 6.5~7분** (새 서버 140초 + 붓기 174초 + 하루치 재생 80~95초, [합산](../server-prep-and-ship-cost-2026-11-08/README.md#4-구성-2-의-복구-시간에-대입하면)) | **0.25~8초** (이 실험) | +| 빠진 것 | 사람 시간 · S3 내려받기 | 사람 시간 · 앱 연결 전환 | + +→ 사람 시간을 같은 크기로 더해도 구성 3 이 몇 분 짧다. 그 몇 분이 **목표 RTO 안에 들어가야 하는지**가 Q5 의 결정이다(이 실험은 정하지 않는다). + +## 3. 조건 — 인용할 때 떼면 안 되는 것 + +1. **사람·앱 시간 제외.** 장애 감지 · 승격 결정 · 앱 연결(DataSource URL·DNS) 전환은 안 쟀다. 이 값은 «스크립트로 자동화했을 때의 하한» 이다 +2. **비동기 복제인데 잃은 게 0 인 건 한 박스라서다.** 소스와 리플리카가 같은 박스의 컨테이너라 전송이 거의 즉시였다. 실제로 다른 박스·다른 + AZ 에 두면 «아직 못 받은» 몫이 생기고 그건 잃는다 — 그 양은 [다른 AZ 복제 실험](../replication-aws-cross-az-2026-09-02/README.md)을 본다 +3. **한 박스 동거.** 부하 중 CPU 를 나눠 써서 밀림이 두 박스일 때와 다를 수 있고, 소스가 죽은 뒤엔 리플리카가 CPU 를 다 쓴다. 두 박스의 승격 시간은 안 쟀다 +4. «죽음» 은 `docker kill` 이다 — 프로세스·네트워크가 한꺼번에 사라지는 호스트 상실의 대리 +5. 리플리카 적용 스레드 수는 기본값(8.0.46 은 `replica_parallel_workers` 기본 4)이고 판마다 기록하지는 않았다 +6. paced 는 문서 §8-1 **가정** 피크(측정 아님), stress 는 4개 연결이 240초간 최대 속도 — 상한 확인용이지 운영 값이 아니다 + +## 4. 재현 + +```bash +bash launch.sh <커밋 SHA> # EC2 1대. 결과는 S3 의 RUN_ID 아래 +OUT=$PWD/_out STRESS_SECS=60 PACED_SECS=30 bash promote.sh # 로컬 스모크 +``` + +산출물: `ec2/trials.tsv`(판 1줄) · `ec2/CONDITIONS.txt` · `ec2/run.log` · `ec2/box.txt` · `ec2/round.log`. diff --git a/loadtest/results/replica-promotion-2026-11-08/ec2/CONDITIONS.txt b/loadtest/results/replica-promotion-2026-11-08/ec2/CONDITIONS.txt new file mode 100644 index 00000000..51920c52 --- /dev/null +++ b/loadtest/results/replica-promotion-2026-11-08/ec2/CONDITIONS.txt @@ -0,0 +1,7 @@ +# 승격 실험 조건 (2026-11-08T03:49:25+00:00) +mysql image : mysql:8.0 +server opts : --log-bin=binlog --binlog-format=ROW --gtid-mode=ON --enforce-gtid-consistency=ON --innodb-flush-log-at-trx-commit=1 --sync-binlog=1 --innodb-buffer-pool-size=1G --innodb-redo-log-capacity=2G +replica workers : (판마다 시작 후 기록하지 않음 — 기본값, 8.0.46 은 replica_parallel_workers 기본 4) +trials : paced stress stress paced paced stress stress paced paced stress (앞 paced·stress 각 첫 판은 버림) +paced : 45 행/초(문서 §8-1 가정) 120s · stress: 4 연결 240s +제외 : 감지·판단·앱 연결 전환 시간 diff --git a/loadtest/results/replica-promotion-2026-11-08/ec2/box.txt b/loadtest/results/replica-promotion-2026-11-08/ec2/box.txt new file mode 100644 index 00000000..155c7ec8 --- /dev/null +++ b/loadtest/results/replica-promotion-2026-11-08/ec2/box.txt @@ -0,0 +1,7 @@ +instance_type=m6i.xlarge +4 + total used free shared buff/cache available +Mem: 15 0 10 0 4 14 +Swap: 0 0 0 +Filesystem Size Used Avail Use% Mounted on +/dev/nvme0n1p1 200G 6.8G 194G 4% / diff --git a/loadtest/results/replica-promotion-2026-11-08/ec2/round.log b/loadtest/results/replica-promotion-2026-11-08/ec2/round.log new file mode 100644 index 00000000..526b3740 --- /dev/null +++ b/loadtest/results/replica-promotion-2026-11-08/ec2/round.log @@ -0,0 +1,205 @@ +== start 2026-11-08T03:47:24+00:00 SHA=96677a257986ccc3f53204f23c0d274ca557db5e RUN_ID=promote-ec2-20261108-034706 CAP_MIN=150 + +──── root SSH (다른 박스가 이 박스로 붙는 라운드용, #642) ──── + ec2-user 의 authorized_keys 를 root 로 복사(강제명령 줄 제거), PermitRootLogin prohibit-password + +──── 패키지 ──── + buildx 0.12.2 → 0.19.3 으로 올린다 (compose build 는 0.17+ 를 요구한다) + +──── 저장소 — https://github.com/Shadowfit/init.git @ 96677a257986ccc3f53204f23c0d274ca557db5e ──── + 커밋: 96677a25 + +──── .env ──── +bootstrap.sh: line 281: compose: command not found +bootstrap.sh: line 281: shadowfit-ai: command not found +bootstrap.sh: line 281: INTERNAL_API_TOKEN:?: command not found +bootstrap.sh: line 281: MYSQL_EXPORTER_PASSWORD: command not found +bootstrap.sh: line 281: :?: command not found +bootstrap.sh: line 281: shadowfit-ai: command not found +bootstrap.sh: line 281: INTERNAL_API_TOKEN: command not found +bootstrap.sh: line 281: unused-in-this-role: command not found + MYSQL_ROOT_PASSWORD 를 rig 기본값과 맞췄다 (+ 해석만 되면 되는 변수들) + compose 해석 확인 ✅ (mysql 만 띄워도 파일 전체가 해석된다) + +──── MySQL 컨테이너 ──── +time="2026-11-08T03:48:21Z" level=warning msg="/root/init/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion" + Image mysql:8.0 Pulling + edf85873f64e Pulling fs layer 0B + 6ef6c7b50a93 Pulling fs layer 0B + e3e5d1ac74c1 Pulling fs layer 0B + 0d74d296605b Pulling fs layer 0B + 297d04cfe470 Pulling fs layer 0B + 4c8a3e0d4e4b Pulling fs layer 0B + a63160a5eda1 Pulling fs layer 0B + 7534d1db9f8d Pulling fs layer 0B + 49ec2dab01d9 Pulling fs layer 0B + ab24264a27e9 Pulling fs layer 0B + 96d30d9fbee8 Pulling fs layer 0B + a63160a5eda1 Waiting 0B + 7534d1db9f8d Waiting 0B + 49ec2dab01d9 Waiting 0B + ab24264a27e9 Waiting 0B + 96d30d9fbee8 Waiting 0B + 297d04cfe470 Waiting 0B + 4c8a3e0d4e4b Waiting 0B + 0d74d296605b Waiting 0B + e3e5d1ac74c1 Downloading 15.75kB + e3e5d1ac74c1 Download complete 0B + edf85873f64e Downloading 474.5kB + 6ef6c7b50a93 Downloading 884B + 6ef6c7b50a93 Verifying Checksum 0B + 6ef6c7b50a93 Download complete 0B + edf85873f64e Downloading 35.16MB + edf85873f64e Verifying Checksum 0B + edf85873f64e Download complete 0B + edf85873f64e Extracting 491.5kB + edf85873f64e Extracting 5.407MB + edf85873f64e Extracting 8.356MB + edf85873f64e Extracting 17.2MB + 0d74d296605b Downloading 64.9kB + 297d04cfe470 Downloading 2.607kB + 297d04cfe470 Verifying Checksum 0B + 297d04cfe470 Download complete 0B + edf85873f64e Extracting 26.05MB + 0d74d296605b Verifying Checksum 0B + 0d74d296605b Download complete 0B + edf85873f64e Extracting 32.44MB + 4c8a3e0d4e4b Downloading 335B + 4c8a3e0d4e4b Verifying Checksum 0B + 4c8a3e0d4e4b Download complete 0B + edf85873f64e Extracting 41.78MB + edf85873f64e Extracting 44.24MB + edf85873f64e Extracting 47.31MB + edf85873f64e Pull complete 0B + 6ef6c7b50a93 Extracting 884B + 6ef6c7b50a93 Extracting 884B + a63160a5eda1 Downloading 507.3kB + 6ef6c7b50a93 Pull complete 0B + e3e5d1ac74c1 Extracting 32.77kB + e3e5d1ac74c1 Extracting 783.6kB + e3e5d1ac74c1 Extracting 783.6kB + 7534d1db9f8d Downloading 316B + 7534d1db9f8d Verifying Checksum 0B + 7534d1db9f8d Download complete 0B + e3e5d1ac74c1 Pull complete 0B + 0d74d296605b Extracting 65.54kB + a63160a5eda1 Downloading 39.62MB + a63160a5eda1 Verifying Checksum 0B + a63160a5eda1 Download complete 0B + 0d74d296605b Extracting 2.621MB + 49ec2dab01d9 Downloading 529.9kB + 0d74d296605b Extracting 6.173MB + 0d74d296605b Pull complete 0B + 297d04cfe470 Extracting 2.607kB + 297d04cfe470 Extracting 2.607kB + 297d04cfe470 Pull complete 0B + 4c8a3e0d4e4b Extracting 335B + 4c8a3e0d4e4b Extracting 335B + 4c8a3e0d4e4b Pull complete 0B + a63160a5eda1 Extracting 524.3kB + 49ec2dab01d9 Downloading 30.68MB + 49ec2dab01d9 Downloading 68.8MB + a63160a5eda1 Extracting 7.864MB + 49ec2dab01d9 Downloading 97.92MB + a63160a5eda1 Extracting 14.16MB + 49ec2dab01d9 Download complete 0B + a63160a5eda1 Extracting 22.02MB + ab24264a27e9 Downloading 3.502kB + ab24264a27e9 Downloading 5.327kB + ab24264a27e9 Verifying Checksum 0B + ab24264a27e9 Download complete 0B + a63160a5eda1 Extracting 30.93MB + 96d30d9fbee8 Downloading 120B + 96d30d9fbee8 Verifying Checksum 0B + 96d30d9fbee8 Download complete 0B + a63160a5eda1 Extracting 38.8MB + a63160a5eda1 Extracting 47.19MB + a63160a5eda1 Extracting 49.93MB + a63160a5eda1 Pull complete 0B + 7534d1db9f8d Extracting 316B + 7534d1db9f8d Extracting 316B + 7534d1db9f8d Pull complete 0B + 49ec2dab01d9 Extracting 557.1kB + 49ec2dab01d9 Extracting 9.47MB + 49ec2dab01d9 Extracting 17.83MB + 49ec2dab01d9 Extracting 22.28MB + 49ec2dab01d9 Extracting 28.41MB + 49ec2dab01d9 Extracting 31.75MB + 49ec2dab01d9 Extracting 32.31MB + 49ec2dab01d9 Extracting 32.87MB + 49ec2dab01d9 Extracting 33.42MB + 49ec2dab01d9 Extracting 33.98MB + 49ec2dab01d9 Extracting 34.54MB + 49ec2dab01d9 Extracting 35.65MB + 49ec2dab01d9 Extracting 36.21MB + 49ec2dab01d9 Extracting 37.32MB + 49ec2dab01d9 Extracting 37.88MB + 49ec2dab01d9 Extracting 38.44MB + 49ec2dab01d9 Extracting 38.99MB + 49ec2dab01d9 Extracting 39.55MB + 49ec2dab01d9 Extracting 40.11MB + 49ec2dab01d9 Extracting 41.22MB + 49ec2dab01d9 Extracting 42.34MB + 49ec2dab01d9 Extracting 42.89MB + 49ec2dab01d9 Extracting 43.45MB + 49ec2dab01d9 Extracting 44.56MB + 49ec2dab01d9 Extracting 45.12MB + 49ec2dab01d9 Extracting 45.68MB + 49ec2dab01d9 Extracting 46.24MB + 49ec2dab01d9 Extracting 47.91MB + 49ec2dab01d9 Extracting 49.58MB + 49ec2dab01d9 Extracting 52.36MB + 49ec2dab01d9 Extracting 60.72MB + 49ec2dab01d9 Extracting 69.07MB + 49ec2dab01d9 Extracting 77.43MB + 49ec2dab01d9 Extracting 86.34MB + 49ec2dab01d9 Extracting 95.81MB + 49ec2dab01d9 Extracting 106.4MB + 49ec2dab01d9 Extracting 117MB + 49ec2dab01d9 Extracting 127MB + 49ec2dab01d9 Extracting 129.4MB + 49ec2dab01d9 Pull complete 0B + ab24264a27e9 Extracting 5.327kB + ab24264a27e9 Extracting 5.327kB + ab24264a27e9 Pull complete 0B + 96d30d9fbee8 Extracting 120B + 96d30d9fbee8 Extracting 120B + 96d30d9fbee8 Pull complete 0B + Image mysql:8.0 Pulled + Network init_shadowfit-net Creating + Network init_shadowfit-net Creating + Volume init_mysql_data Creating + Volume init_mysql_data Creating + Volume init_mysql_data Created + Volume init_mysql_data Created + Network init_shadowfit-net Created + Network init_shadowfit-net Created + Container shadowfit-mysql Creating + Container shadowfit-mysql Created + Container shadowfit-mysql Starting + Container shadowfit-mysql Started + 준비 대기... — 떴다 (TCP 인증 질의 연속 2회) + +──── percona-toolkit 이미지 ──── +docker.io/percona/percona-toolkit:latest + +──── xtrabackup 이미지 (P3 백업 · P4 복제) — percona/percona-xtrabackup:8.0 ──── +docker.io/percona/percona-xtrabackup:8.0 + +──── 스키마 — Flyway 마이그레이션 ──── + SKIP_FLYWAY=1 — 건너뛴다. 從 R1·R3 은 «측정 대상 부재» 로 찍힌다 + +──── 준비됨 ──── + 작업 디렉터리 : /root/init + 커밋 : 96677a25 (96677a257986ccc3f53204f23c0d274ca557db5e) + 디스크 여유 : 193G + MySQL : 8.0.46 + +다음: + cd /root/init + S3_BASE=s3://<버킷>/<프리픽스> nohup bash loadtest/aws/run_all.sh > /root/run_all.log 2>&1 & + + ⚠️ nohup 없이 & 만 붙이면 SSH 가 끊길 때 같이 죽는다. +shadowfit-mysql +== rig exit 0 +== finish: DONE rc=0 (2026-11-08T04:22:29+00:00) diff --git a/loadtest/results/replica-promotion-2026-11-08/ec2/run.log b/loadtest/results/replica-promotion-2026-11-08/ec2/run.log new file mode 100644 index 00000000..45966b02 --- /dev/null +++ b/loadtest/results/replica-promotion-2026-11-08/ec2/run.log @@ -0,0 +1,21 @@ +[promote] ── 판 1 (버림) 팔=paced +[promote] 판 1: 밀림 0s·0tx → 승격 254ms (적용 89) · 잃은 트랜잭션 0 +[promote] ── 판 2 (버림) 팔=stress +[promote] 판 2: 밀림 21s·3376tx → 승격 7822ms (적용 7647) · 잃은 트랜잭션 0 +[promote] ── 판 3 (본판) 팔=stress +[promote] 판 3: 밀림 22s·3440tx → 승격 8045ms (적용 7868) · 잃은 트랜잭션 0 +[promote] ── 판 4 (본판) 팔=paced +[promote] 판 4: 밀림 0s·0tx → 승격 250ms (적용 89) · 잃은 트랜잭션 0 +[promote] ── 판 5 (본판) 팔=paced +[promote] 판 5: 밀림 0s·0tx → 승격 254ms (적용 92) · 잃은 트랜잭션 0 +[promote] ── 판 6 (본판) 팔=stress +[promote] 판 6: 밀림 22s·3440tx → 승격 7836ms (적용 7657) · 잃은 트랜잭션 0 +[promote] ── 판 7 (본판) 팔=stress +[promote] 판 7: 밀림 20s·3258tx → 승격 7806ms (적용 7627) · 잃은 트랜잭션 0 +[promote] ── 판 8 (본판) 팔=paced +[promote] 판 8: 밀림 0s·0tx → 승격 255ms (적용 89) · 잃은 트랜잭션 0 +[promote] ── 판 9 (본판) 팔=paced +[promote] 판 9: 밀림 0s·0tx → 승격 240ms (적용 91) · 잃은 트랜잭션 0 +[promote] ── 판 10 (본판) 팔=stress +[promote] 판 10: 밀림 21s·3380tx → 승격 7822ms (적용 7635) · 잃은 트랜잭션 0 +[promote] 완료 → /root/rig_out/trials.tsv diff --git a/loadtest/results/replica-promotion-2026-11-08/ec2/trials.tsv b/loadtest/results/replica-promotion-2026-11-08/ec2/trials.tsv new file mode 100644 index 00000000..21f91fc6 --- /dev/null +++ b/loadtest/results/replica-promotion-2026-11-08/ec2/trials.tsv @@ -0,0 +1,11 @@ +trial kind arm lag_s_at_kill backlog_tx io_stop_ms apply_ms switch_ms write_ms promote_ms acked lost_tx exec_ms +1 버림 paced 0 0 47 89 69 49 254 213 0 41 +2 버림 stress 21 3376 50 7647 75 50 7822 42380 0 49 +3 본판 stress 22 3440 51 7868 74 52 8045 42412 0 51 +4 본판 paced 0 0 48 89 64 49 250 213 0 44 +5 본판 paced 0 0 47 92 66 49 254 213 0 43 +6 본판 stress 22 3440 52 7657 76 51 7836 42354 0 45 +7 본판 stress 20 3258 52 7627 75 52 7806 42311 0 48 +8 본판 paced 0 0 48 89 68 50 255 213 0 44 +9 본판 paced 0 0 45 91 57 47 240 213 0 44 +10 본판 stress 21 3380 49 7635 89 49 7822 42391 0 44 diff --git a/loadtest/results/replica-promotion-2026-11-08/ec2_userdata.sh b/loadtest/results/replica-promotion-2026-11-08/ec2_userdata.sh new file mode 100644 index 00000000..b4506148 --- /dev/null +++ b/loadtest/results/replica-promotion-2026-11-08/ec2_userdata.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# EC2 무인 라운드 — 리플리카 승격 시간 (RPO·RTO 분기점 R1 후속) +# 규칙은 server-prep-and-ship-cost-2026-11-08/ec2_userdata.sh 와 같다 (SHA 고정·grep 단언·감시견·terminate). +# 대문자 자리표시자(골뱅이 두 쌍으로 감싼 것)는 launch.sh 가 띄우기 직전에 채운다. +set -uo pipefail +SHA=@@SHA@@ +RUN_ID=@@RUN_ID@@ +S3=s3://shadowfit-measure-055447613012/shadowfit/$RUN_ID +CAP_MIN=@@CAP_MIN@@ +RIG=/root/init/loadtest/results/replica-promotion-2026-11-08 +OUT=/root/rig_out +LOG=/root/round.log +exec > >(tee -a "$LOG") 2>&1 + +up() { aws s3 cp "$LOG" "$S3/round.log" --only-show-errors || true; [ -d "$OUT" ] && aws s3 sync "$OUT" "$S3/out" --only-show-errors || true; } +finish() { echo "== finish: $1 ($(date -Is))"; up; aws s3 cp - "$S3/FINISHED" <<< "$1 $(date -Is)" || true; shutdown -h now; } + +echo "== start $(date -Is) SHA=$SHA RUN_ID=$RUN_ID CAP_MIN=$CAP_MIN" +( sleep $((CAP_MIN * 60)); echo "🔴 상한 ${CAP_MIN}분 도달 — 끈다"; finish CAP ) & +( while sleep 300; do up; done ) & + +cd /root +curl -fsSL "https://raw.githubusercontent.com/Shadowfit/init/$SHA/loadtest/aws/bootstrap.sh" -o bootstrap.sh || finish BOOTSTRAP_DOWNLOAD +grep -q 'ROLE=${ROLE:-db}' bootstrap.sh || finish BOOTSTRAP_ASSERT +ROLE=db REF=$SHA SKIP_FLYWAY=1 bash bootstrap.sh || finish BOOTSTRAP_FAIL + +[ -f "$RIG/promote.sh" ] || finish RIG_MISSING +grep -q 'WAIT_FOR_EXECUTED_GTID_SET' "$RIG/promote.sh" || finish RIG_ASSERT +grep -q '_tpl' /root/init/loadtest/results/binlog-replay-time-2026-11-07/_writer_common.sh || finish WRITER_ASSERT +docker ps --format '{{.Names}}' | grep -E '^shadowfit-' | xargs -r docker stop + +mkdir -p "$OUT" +{ echo "instance_type=$(curl -s -H "X-aws-ec2-metadata-token: $(curl -s -X PUT http://169.254.169.254/latest/api/token -H 'X-aws-ec2-metadata-token-ttl-seconds: 60')" http://169.254.169.254/latest/meta-data/instance-type)"; nproc; free -g; df -h /; } > "$OUT/box.txt" +env OUT=$OUT WORK=/root/rig_work PACED_SECS=120 STRESS_SECS=240 bash "$RIG/promote.sh" > "$OUT/run.log" 2>&1 +rc=$? +echo "== rig exit $rc" +finish "DONE rc=$rc" diff --git a/loadtest/results/replica-promotion-2026-11-08/launch.sh b/loadtest/results/replica-promotion-2026-11-08/launch.sh new file mode 100644 index 00000000..61ba9fd0 --- /dev/null +++ b/loadtest/results/replica-promotion-2026-11-08/launch.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# 리플리카 승격 라운드 EC2 1대(m6i.xlarge) — 사용: bash launch.sh (DRY=1 이면 --dry-run) +set -euo pipefail +export MSYS_NO_PATHCONV=1 +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SHA=${1:?SHA 가 필요하다} +git ls-remote origin | grep -q "^$SHA" || { echo "🔴 SHA $SHA 가 원격에 없다 — 먼저 푸시"; exit 1; } +RUN_ID=promote-ec2-$(date -u +%Y%m%d-%H%M%S) +REGION=ap-northeast-2 +AMI=$(aws ec2 describe-images --region $REGION --owners amazon \ + --filters "Name=name,Values=al2023-ami-2023*-kernel-6.12-x86_64" "Name=state,Values=available" \ + --query "reverse(sort_by(Images,&CreationDate))[:1].ImageId" --output text) +ud=$(mktemp) +sed -e "s|@@SHA@@|$SHA|" -e "s|@@RUN_ID@@|$RUN_ID|" -e "s|@@CAP_MIN@@|150|" "$HERE/ec2_userdata.sh" > "$ud" +grep -qE '@@[A-Z_]+@@' "$ud" && { echo "🔴 채우지 않은 자리표시자가 있다"; exit 1; } +t_api=$(date +%s%3N) +id=$(aws ec2 run-instances --region $REGION --image-id "$AMI" --instance-type m6i.xlarge \ + --key-name shadowfit-measure --security-group-ids sg-0382132d2f8b5f6ce --subnet-id subnet-0f9ea5dacb7260d34 \ + --iam-instance-profile Name=shadowfit-measure \ + --instance-initiated-shutdown-behavior terminate \ + --block-device-mappings '[{"DeviceName":"/dev/xvda","Ebs":{"VolumeSize":200,"VolumeType":"gp3","DeleteOnTermination":true}}]' \ + --tag-specifications "ResourceType=instance,Tags=[{Key=Project,Value=shadowfit-measure},{Key=RunId,Value=$RUN_ID}]" \ + "ResourceType=volume,Tags=[{Key=Project,Value=shadowfit-measure}]" \ + --user-data "$(cat "$ud")" ${DRY:+--dry-run} --query 'Instances[0].InstanceId' --output text 2>&1) || { echo "$id"; [ -n "${DRY:-}" ] && exit 0; exit 1; } +echo "RUN_ID=$RUN_ID id=$id t_api=$t_api" diff --git a/loadtest/results/replica-promotion-2026-11-08/promote.sh b/loadtest/results/replica-promotion-2026-11-08/promote.sh new file mode 100644 index 00000000..8745cb91 --- /dev/null +++ b/loadtest/results/replica-promotion-2026-11-08/promote.sh @@ -0,0 +1,209 @@ +#!/bin/bash +# 리플리카 승격 시간 — 소스가 죽은 순간부터 리플리카가 새 쓰기를 받기까지 (RPO·RTO 분기점 R1 후속) +# +# 질문: 리플리카(구성 3)를 붙이면 호스트 상실 RTO 가 «구성 2 의 약 7분» 보다 실제로 짧아지나? +# 문서는 «리플리카를 붙이면 RTO 가 짧아진다» 고 가정했지 승격 시간을 잰 적이 없다. +# +# 재는 구간 (소스를 죽인 시각 t_kill 기준, 스크립트로 잴 수 있는 것만): +# io_stop_ms STOP REPLICA IO_THREAD +# apply_ms 받아 둔 릴레이 로그를 끝까지 적용 (WAIT_FOR_EXECUTED_GTID_SET) ← 밀린 양에 비례할 것으로 예상 +# switch_ms STOP REPLICA · RESET REPLICA ALL · read_only 해제 +# write_ms 첫 쓰기 성공 + 읽기 확인 +# promote_ms 위 넷의 합 = 승격 시간 +# +# 🔴 제외한 것(사람/앱 시간): 소스가 죽은 것을 알아채는 시간 · 승격을 결정하는 시간 · 앱 연결을 새 서버로 바꾸는 시간. +# 실제 RTO 는 promote_ms 보다 이만큼 크다. 이 값은 «스크립트로 자동화했을 때의 하한» 이다. +# 🔴 비동기 복제(반동기 아님). 소스가 죽을 때 리플리카가 아직 못 받은 트랜잭션은 사라진다 → lost_tx 로 같이 남긴다. +# 🔴 소스·리플리카가 같은 박스의 컨테이너다. 부하 중엔 CPU 를 나눠 쓰고(밀림이 두 박스일 때와 다를 수 있다), +# 소스가 죽은 뒤엔 리플리카가 CPU 를 다 쓴다. 두 박스 구성의 승격 시간은 따로 안 쟀다. +# +# 팔 (밀림 조건) — 쓰기 속도: +# paced 문서 §8-1 가정 피크 45행/초 — 밀림이 거의 없을 것으로 예상 +# stress 연결 여럿이 쉬지 않고 쓴다 — 밀림이 쌓인다. 상한 확인용. 운영 값이 아니다 +# 밀림은 조작하는 변수가 아니라 «죽이는 순간 관측한 값» 이다 — backlog_tx 를 같이 남겨 승격 시간을 그것으로 본다. + +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="${OUT:-$HERE/_out}" +WORK="${WORK:-$HERE/_work}" +mkdir -p "$OUT" "$WORK" + +IMG=${MYSQL_IMAGE:-mysql:8.0} +PW=repl +SRC=pr_src; REP=pr_rep; NET=pr_net +SRC_VOL=pr_src_data; REP_VOL=pr_rep_data +# 시행 순서 — 앞의 둘은 버림판(팔마다 하나). 이후는 ABBA 로 팔과 순서가 섞이지 않게 +TRIALS=${TRIALS:-"paced stress stress paced paced stress stress paced paced stress"} +PACED_ROWS_PER_S=${PACED_ROWS_PER_S:-45} +ROWS_PER_TX=${ROWS_PER_TX:-25} +STRESS_CONNS=${STRESS_CONNS:-4} +PACED_SECS=${PACED_SECS:-120} +STRESS_SECS=${STRESS_SECS:-300} +SERVER_OPTS=(--log-bin=binlog --binlog-format=ROW --gtid-mode=ON --enforce-gtid-consistency=ON + --innodb-flush-log-at-trx-commit=1 --sync-binlog=1 + --innodb-buffer-pool-size=${BUFFER_POOL:-1G} --innodb-redo-log-capacity=${REDO_CAPACITY:-2G}) + +log() { printf '[promote] %s\n' "$*" >&2; } +die() { log "🔴 $*"; exit 1; } +q() { docker exec -i "$1" mysql -uroot -p"$PW" -N -s -e "$2" 2>/dev/null; } +now_ms() { date +%s%3N; } +source "$HERE/../binlog-replay-time-2026-11-07/_writer_common.sh" + +wait_ready() { + local i + for i in $(seq 1 300); do [ "$(q "$1" "SELECT @@skip_networking;")" = 0 ] && return 0; sleep 1; done + die "$1 가 5분 안에 쿼리를 안 받는다" +} + +# GTID 집합 문자열 → 트랜잭션 수. "uuid:1-5:8-9" → 5 + 2 = 7. UUID 가 여럿이어도 합친다. +gtid_count() { echo "$1" | tr -d ' ' | tr ',' '\n' | awk -F: 'NF>=2{for(i=2;i<=NF;i++){n=split($i,a,"-"); s+=(n==2)?(a[2]-a[1]+1):1}} END{print s+0}'; } + +make_paced_proc() { + cat > "$WORK/_paced.sql" <<'SQL' +DELIMITER // +DROP PROCEDURE IF EXISTS lab.paced_ack // +CREATE PROCEDURE lab.paced_ack(IN p_session BIGINT, IN p_rows INT, IN p_secs INT, IN p_gap DOUBLE) +BEGIN + DECLARE v_end DATETIME(6); + DECLARE v_off INT DEFAULT 0; + DECLARE v_ver BIGINT; + SET v_end = SYSDATE(6) + INTERVAL p_secs SECOND; + WHILE SYSDATE(6) < v_end DO + START TRANSACTION; + INSERT INTO lab.pose_data_scale (session_id, timestamp_sec, joint_coordinates, sync_rate, is_correct, feedback_message) + SELECT p_session, ts, jc, 75.0, 1, fb FROM lab._tpl WHERE n >= v_off AND n < v_off + p_rows; + UPDATE lab.session_probe SET version = version + 1, last_active_at = SYSDATE(6) WHERE id = p_session; + SELECT version INTO v_ver FROM lab.session_probe WHERE id = p_session; + COMMIT; + SELECT p_session, v_ver, ROUND(UNIX_TIMESTAMP(SYSDATE(6)) * 1000000); + SET v_off = (v_off + p_rows) % (750 - p_rows); + DO SLEEP(p_gap); + END WHILE; +END // +DELIMITER ; +SQL + docker exec -i "$SRC" mysql -uroot -p"$PW" < "$WORK/_paced.sql" 2>/dev/null || die "paced 프로시저 생성 실패" +} + +start_pair() { + docker rm -f "$SRC" "$REP" >/dev/null 2>&1 || true + docker network create "$NET" >/dev/null 2>&1 || true + docker volume rm -f "$SRC_VOL" "$REP_VOL" >/dev/null 2>&1 || true + docker volume create "$SRC_VOL" >/dev/null; docker volume create "$REP_VOL" >/dev/null + docker run -d --name "$SRC" --network "$NET" -e MYSQL_ROOT_PASSWORD="$PW" -v "$SRC_VOL:/var/lib/mysql" \ + "$IMG" --server-id=1 "${SERVER_OPTS[@]}" >/dev/null + docker run -d --name "$REP" --network "$NET" -e MYSQL_ROOT_PASSWORD="$PW" -v "$REP_VOL:/var/lib/mysql" \ + "$IMG" --server-id=2 "${SERVER_OPTS[@]}" >/dev/null + wait_ready "$SRC"; wait_ready "$REP" + # 읽기 전용은 기동 «뒤에» 건다 — 옵션으로 주면 이미지의 초기화(root 계정 설정)가 쓰기를 못 해 서버가 안 뜬다 + q "$REP" "SET GLOBAL read_only=1; SET GLOBAL super_read_only=1;" || die "읽기 전용 설정 실패" + q "$REP" "CHANGE REPLICATION SOURCE TO SOURCE_HOST='$SRC', SOURCE_USER='root', SOURCE_PASSWORD='$PW', + SOURCE_AUTO_POSITION=1, GET_SOURCE_PUBLIC_KEY=1; START REPLICA;" || die "복제 연결 실패" + q "$SRC" " + CREATE DATABASE lab; + CREATE TABLE lab.pose_data_scale ( + id bigint NOT NULL AUTO_INCREMENT, session_id bigint NOT NULL, timestamp_sec double NOT NULL, + joint_coordinates text COLLATE utf8mb4_unicode_ci NOT NULL, sync_rate double DEFAULT NULL, + is_correct tinyint(1) DEFAULT 1, feedback_message varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id, created_at) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + CREATE TABLE lab.session_probe (id BIGINT PRIMARY KEY, version BIGINT NOT NULL DEFAULT 0, + last_active_at DATETIME(6) NULL) ENGINE=InnoDB; + CREATE TABLE lab.promo_probe (id INT PRIMARY KEY AUTO_INCREMENT, note VARCHAR(40)) ENGINE=InnoDB;" || die "스키마 실패" + local i vals="" + for i in $(seq 1 "$STRESS_CONNS"); do vals="$vals${vals:+,}($((100000 + i)),0,NULL)"; done + q "$SRC" "INSERT INTO lab.session_probe VALUES $vals;" + setup_writer "$SRC" + make_paced_proc + # 시작 시점엔 리플리카가 따라잡은 상태여야 한다 + local g; g=$(q "$SRC" "SELECT @@gtid_executed;") + [ "$(q "$REP" "SELECT WAIT_FOR_EXECUTED_GTID_SET('$(echo "$g" | tr -d '\n')', 120);")" = 0 ] || die "리플리카가 초기 상태를 못 따라잡았다" +} + +trial() { # $1 번호 $2 팔 $3 kind(버림|본판) + local idx=$1 arm=$2 kind=$3 tdir="$WORK/t$1" i pids=() + mkdir -p "$tdir" + log "── 판 $idx ($kind) 팔=$arm" + start_pair + local secs; [ "$arm" = paced ] && secs=$PACED_SECS || secs=$STRESS_SECS + if [ "$arm" = paced ]; then + local gap; gap=$(awk -v r="$ROWS_PER_TX" -v p="$PACED_ROWS_PER_S" 'BEGIN{printf "%.3f", r/p}') + docker run --rm --network "$NET" "$IMG" mysql -h "$SRC" -uroot -p"$PW" --unbuffered -N -B \ + -e "CALL lab.paced_ack(100001, $ROWS_PER_TX, 36000, $gap);" > "$tdir/w_1.tsv" 2>/dev/null & + pids+=($!) + else + for i in $(seq 1 "$STRESS_CONNS"); do + docker run --rm --network "$NET" "$IMG" mysql -h "$SRC" -uroot -p"$PW" --unbuffered -N -B \ + -e "CALL lab.load_run($((100000 + i)), $ROWS_PER_TX, 36000);" > "$tdir/w_$i.tsv" 2>/dev/null & + pids+=($!) + done + fi + sleep "$secs" + + # 죽이기 직전의 상태 — 밀림은 «관측값» 이다 + local src_g rep_g lag + src_g=$(q "$SRC" "SELECT @@gtid_executed;") + rep_g=$(q "$REP" "SELECT @@gtid_executed;") + lag=$(docker exec "$REP" mysql -uroot -p"$PW" -e "SHOW REPLICA STATUS\G" 2>/dev/null | awk '/Seconds_Behind_Source/{print $2}') + # 리플리카가 아직 적용 못 한 트랜잭션 수 = (소스 집합 − 리플리카 집합). 죽이기 직전 값이라 몇 건 오차가 있다 + local backlog_tx behind + behind=$(q "$REP" "SELECT GTID_SUBTRACT('$(echo "$src_g" | tr -d '\n ')', '$(echo "$rep_g" | tr -d '\n ')');" | tr -d '\n ') + backlog_tx=$(gtid_count "$behind") + # docker exec 한 번의 고정 비용 — 승격 구간마다 이만큼이 껴 있다 (mysql 클라이언트를 매번 새로 띄운다) + local o0 o1 exec_ms + o0=$(now_ms); q "$REP" "SELECT 1;" >/dev/null; o1=$(now_ms); exec_ms=$((o1 - o0)) + + local t_kill; t_kill=$(now_ms) + docker kill "$SRC" >/dev/null # 호스트 상실의 대리 — 프로세스와 네트워크가 한꺼번에 사라진다 + for i in "${pids[@]}"; do wait "$i" 2>/dev/null || true; done + + # ── 승격 ───────────────────────────────────────────────────────────── + local t0 t1 t2 t3 t4 retrieved w + t0=$(now_ms) + q "$REP" "STOP REPLICA IO_THREAD;" >/dev/null + t1=$(now_ms) + retrieved=$(q "$REP" "SELECT RECEIVED_TRANSACTION_SET FROM performance_schema.replication_connection_status;" | tr -d '\n ') + if [ -n "$retrieved" ]; then + w=$(q "$REP" "SELECT WAIT_FOR_EXECUTED_GTID_SET('$retrieved', 1800);") + [ "$w" = 0 ] || log "⚠️ 적용 대기가 상한을 넘겼거나 실패 (w=$w)" + fi + t2=$(now_ms) + q "$REP" "STOP REPLICA; RESET REPLICA ALL; SET GLOBAL super_read_only=0; SET GLOBAL read_only=0;" >/dev/null || log "⚠️ 전환 실패" + t3=$(now_ms) + q "$REP" "INSERT INTO lab.promo_probe (note) VALUES ('first-write'); SELECT COUNT(*) FROM lab.promo_probe;" >/dev/null || log "⚠️ 첫 쓰기 실패" + t4=$(now_ms) + + # ── 잃은 양 — 클라이언트가 «커밋 성공» 으로 받은 마지막 버전 합 vs 승격된 리플리카의 합 ── + local sum_acked sum_rest lost + sum_acked=$(for f in "$tdir"/w_*.tsv; do awk 'NF>=3{v=$2} END{print v+0}' "$f"; done | awk '{s+=$1} END{print s+0}') + sum_rest=$(q "$REP" "SELECT COALESCE(SUM(version),0) FROM lab.session_probe;") + lost=$((sum_acked - sum_rest)) + + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$idx" "$kind" "$arm" "${lag:-NULL}" "$backlog_tx" "$((t1 - t0))" "$((t2 - t1))" "$((t3 - t2))" "$((t4 - t3))" \ + "$((t4 - t0))" "$sum_acked" "$lost" "$exec_ms" >> "$OUT/trials.tsv" + log "판 $idx: 밀림 ${lag:-?}s·${backlog_tx}tx → 승격 $((t4 - t0))ms (적용 $((t2 - t1))) · 잃은 트랜잭션 $lost" + docker rm -f "$SRC" "$REP" >/dev/null 2>&1 || true +} + +{ + printf 'trial\tkind\tarm\tlag_s_at_kill\tbacklog_tx\tio_stop_ms\tapply_ms\tswitch_ms\twrite_ms\tpromote_ms\tacked\tlost_tx\texec_ms\n' +} > "$OUT/trials.tsv" +{ + echo "# 승격 실험 조건 ($(date -Is))" + echo "mysql image : $IMG" + echo "server opts : ${SERVER_OPTS[*]}" + echo "replica workers : (판마다 시작 후 기록하지 않음 — 기본값, 8.0.46 은 replica_parallel_workers 기본 4)" + echo "trials : $TRIALS (앞 paced·stress 각 첫 판은 버림)" + echo "paced : ${PACED_ROWS_PER_S} 행/초(문서 §8-1 가정) ${PACED_SECS}s · stress: ${STRESS_CONNS} 연결 ${STRESS_SECS}s" + echo "제외 : 감지·판단·앱 연결 전환 시간" +} > "$OUT/CONDITIONS.txt" + +seen_paced=0; seen_stress=0; n=0 +for arm in $TRIALS; do + n=$((n + 1)); kind=본판 + if [ "$arm" = paced ] && [ "$seen_paced" = 0 ]; then kind=버림; seen_paced=1; fi + if [ "$arm" = stress ] && [ "$seen_stress" = 0 ]; then kind=버림; seen_stress=1; fi + trial "$n" "$arm" "$kind" +done +log "완료 → $OUT/trials.tsv" diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/README.md b/loadtest/results/server-prep-and-ship-cost-2026-11-08/README.md new file mode 100644 index 00000000..b4929c6c --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/README.md @@ -0,0 +1,104 @@ +# 새 서버 준비 시간 · binlog 반출 업로드 비용 (2026-11-08, EC2) + +설계 문서: [`../../../docs/decisions/rpo-rto-targets.md`](../../../docs/decisions/rpo-rto-targets.md) §4 · §5 구성 2 · §6-2 +짝 실험: [`../binlog-replay-time-2026-11-07/`](../binlog-replay-time-2026-11-07/README.md) (재생 시간) · +[`../hostloss-rpo-2026-11-07/`](../hostloss-rpo-2026-11-07/README.md) (호스트 상실 때 잃는 양) +무대: **EC2 `m6i.xlarge` 3대** · gp3 (node1 200GB · node2·3 100GB) · `ap-northeast-2d` · AL2023 · MySQL **8.0.46**(`mysql:8.0`) · +진짜 S3(같은 리전) · RUN_ID `shipcost-ec2-20261108-024203` · 커밋 `8200b521` + +두 칸 모두 앞 실험들이 «안 쟀다» 고 남긴 빈칸이다 — 재생 실험 §2 의 «새 서버 준비 ⬜», 호스트 상실 실험 §1 의 «업로드 시간은 따로 안 쟀다». + +## 0. 한 줄 + +1. **빈 EC2 를 띄워 복구 대상 MySQL 이 쿼리를 받기까지 약 140초**(3대: 139.5 · 139.7 · 142.9초). 그중 약 107~110초가 + `bootstrap.sh`(docker·이미지·저장소 준비)다 +2. **가정 피크(초당 45행)에서 binlog 반출 한 번은 1초도 안 걸린다** — 60초 주기 파일 약 5.8MB 를 0.48~0.51초, + 300초 주기 약 29MB 를 0.70~0.71초에 올렸다. `FLUSH BINARY LOGS` 는 52~56ms. + → 호스트 상실 실험이 남긴 «최대 손실 = 주기 + 업로드 시간» 의 업로드 시간은 이 조건에서 **1초 미만**이다 + +## 1. 새 서버 준비 시간 + +`run-instances` 를 부른 시각을 0 으로 둔 누적 시각(초): + +| 단계 | node1 | node2 | node3 | +|---|---:|---:|---:| +| 커널 기동 (`/proc/uptime` 역산) | 6.6 | 6.9 | 7.2 | +| user-data 시작 | 12.6 | 12.4 | 12.8 | +| `bootstrap.sh` 내려받기 끝 | 12.9 | 12.4 | 12.8 | +| `bootstrap.sh` 끝 | 120.1 | 120.1 | 123.1 | +| **복구 대상 MySQL 이 쿼리를 받음** | **139.7** | **139.5** | **142.9** | + +- 큰 덩어리는 `bootstrap.sh` 약 107~110초다(패키지·docker·MySQL/XtraBackup 이미지 내려받기·저장소 받기. `ROLE=db`, Flyway 건너뜀). + 그 안의 단계별 시간은 남기지 않았다 +- 마지막 약 20초는 새 `mysql:8.0` 컨테이너가 빈 데이터 디렉터리를 초기화하고 쿼리를 받을 때까지다 + (임시 초기화 서버에 속지 않으려고 `@@skip_networking = 0` 을 기다렸다) +- 3대를 몇 초 간격으로 동시에 띄웠다 — 준비 시간은 «박스를 몇 번 띄웠나» 가 반복 수라서 3대가 곧 3회다 +- `launch_times.tsv` 의 `t_running_ms` 는 `aws ec2 wait instance-running` 이 돌아온 시각인데, 그 명령은 15초 간격으로 묻는다 — + **쓰지 않는다.** 대신 박스 안에서 잰 커널 기동 시각을 쓴다 + +## 2. 반출 업로드 비용 + +반출 = 주기마다 `FLUSH BINARY LOGS` 로 지금 파일을 닫고, 닫힌 파일을 `aws s3 cp` 로 올린다(호스트 상실 실험과 같은 방식). +한 번 반출할 때마다 한 줄을 남겼고, 그게 반복 단위다. 팔 순서는 ABBA(60 → 300 → 300 → 60)라 «팔» 과 «순서» 가 섞이지 않는다. + +### 2-1. 가정 피크 (`paced` — 초당 45행, 문서 §8-1 가정) ← 인용할 값 + +| 반출 주기 | 반출 횟수 | 파일 크기 | **업로드** | FLUSH | +|---|---:|---|---:|---:| +| 60초 | 14 | 5.76~6.03MB | **481~505ms** | 52~56ms | +| 300초 | 4 | 28.7~28.9MB | **697~707ms** | 52~56ms | + +- 업로드 시간은 파일이 5배 커져도 약 1.4배만 늘었다 — 이 크기에선 S3 요청 하나의 고정 비용이 대부분이다 +- 하루 반출 횟수는 60초 주기 1,440번 · 300초 주기 288번이다. 요금은 이 결과에서 계산하지 않았다 +- 🔴 초당 45행은 **측정이 아니라 문서 가정(피크)**이다. «가정 피크 기준» 을 떼고 인용하지 않는다 + +### 2-2. 쉬지 않고 쓸 때 (`stress` — 4개 연결이 최대 속도) — 상한 확인용, 운영 값 아님 + +`--max-binlog-size=1G` 라 파일이 1GB 에서 잘린다. 1GB 파일 기준: + +| 반출 주기 | 1GB 파일 업로드 | 비고 | +|---|---|---| +| 60초 | 2.26~2.68초 (6건) | | +| 300초 | 첫 2~3개 **9.7~12.3초**, 나머지 2.2~3.3초 (10건) | 한 번에 1GB 파일 5개가 닫혀 차례로 올림 | + +FLUSH 는 68~178ms 였다(파일이 클 때 첫 FLUSH). + +- 🟡 300초 주기에서 앞쪽 파일만 느렸던 이유는 **확인하지 않았다.** 추정: 오래된 파일이 페이지 캐시에서 밀려나 gp3 기본 처리량 + (초당 125MB → 1GB 약 8.6초)으로 디스크에서 읽혔고, 그동안 writer 도 같은 디스크에 쓰고 있었다. 최근 파일은 캐시에 있어 2초대였다 +- 쓰기량이 이만큼 크면 «업로드 시간» 이 주기 대비 무시할 수 없게 된다 — §R5(반출 주기 한계)의 신호가 여기서 보인다 + +## 3. 조건 — 인용할 때 떼면 안 되는 것 + +1. **준비 시간은 «빈 AL2023 에서 직접 부트스트랩» 경로**다. 이미지를 미리 구워 둔 AMI 로 띄우면 bootstrap 몫이 줄어든다 — 그건 안 쟀다 +2. 준비 시간에 **사람 시간(장애 알아채기 · 결정 · 실행)과 백업·binlog 내려받기·붓기는 들어 있지 않다** — 그건 §4 처럼 다른 실험 값을 더한다 +3. 업로드는 같은 리전 S3 · m6i.xlarge 네트워크다. 다른 리전으로 내보내면 늘 수 있다 +4. FLUSH 가 그 순간 쓰기를 얼마나 막는지는 안 쟀다(writer 지연을 기록하지 않았다) +5. 한 박스 · 한 라운드다 + +## 4. 구성 2 의 복구 시간에 대입하면 + +구성 2 의 호스트 상실 RTO ≈ **새 서버 준비 + 백업 내려받기·붓기 + binlog 내려받기·재생** (+ 사람 시간). + +| 단계 | 값 | 출처 | +|---|---|---| +| 새 서버 준비 | 약 140초 | 이 실험 §1 | +| 백업 붓기 (물리, 1억 행) | 약 174초 | [`backup-restore-rto-rpo.md`](../../../docs/decisions/backup-restore-rto-rpo.md) | +| binlog 재생 (DAU 1,000 가정 하루치 ≈ 0.6GB) | 약 80~95초 | [재생 실측](../binlog-replay-time-2026-11-07/README.md) | +| **합** | **약 6.5~7분** | | + +⚠️ 서로 다른 실험의 값을 더한 것이라 «한 번에 쟀더니 7분» 이 아니다. 백업은 1억 행(합성) 값이고, 백업·binlog 를 S3 에서 내려받는 +시간(1,000만 행 백업 약 2.5초 — 호스트 상실 실험)과 사람 시간은 빠져 있다. 백업 이후 시간이 길수록 재생 칸이 커진다(4일치 약 5분 20초). + +## 5. 재현 + +```bash +bash launch.sh <커밋 SHA> # 3대: node1 = 준비 + 반출 비용, node2·3 = 준비만. 결과는 S3 의 RUN_ID 아래 +OUT=$PWD/_out S3_BASE= bash ship_cost.sh # 로컬 스모크 (가짜 S3 — 업로드 값은 의미 없음) +``` + +산출물: `ec2/launch_times.tsv` · `ec2/node*/prep.tsv` · `ec2/node*/box.txt` · `ec2/node1/ship/ship_cost.tsv`(반출 1건 = 1줄) · +`ec2/node1/ship/segments.tsv`(팔별 총량) · `ec2/node1/ship_run.log` · `ec2/node*.round.log`. +올린 binlog 사본은 크기 때문에 S3 에만 둔다 — `s3://shadowfit-measure-055447613012/shadowfit/shipcost-ec2-20261108-024203/shipstore/`. + +🟡 `node*.round.log` 의 `bootstrap.sh: line 281: … command not found` 줄들은 bootstrap 의 `.env` 생성 단계에서 나온 것이다. +같은 로그에서 «compose 해석 확인 ✅» 가 나왔고 `ROLE=db` 경로는 끝까지 갔다 — 이 실험 값에는 영향이 없다고 본다(원인은 따로 안 봤다). diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/launch_times.tsv b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/launch_times.tsv new file mode 100644 index 00000000..e56f5e6c --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/launch_times.tsv @@ -0,0 +1,4 @@ +node instance_id t_api_ms t_running_ms +node1 i-07c0aee0f8991cfa8 1794105727722 1794105745963 +node2 i-004785bb78b71ab42 1794105731434 1794105745942 +node3 i-07880d983c03eefd7 1794105736130 1794105746164 diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1.round.log b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1.round.log new file mode 100644 index 00000000..dd946c67 --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1.round.log @@ -0,0 +1,204 @@ +== start 2026-11-08T02:42:20+00:00 node=node1 SHA=8200b521ed696076638a247b589ce63aec833d92 RUN_ID=shipcost-ec2-20261108-024203 CAP_MIN=150 RUN_SHIP=1 + +──── root SSH (다른 박스가 이 박스로 붙는 라운드용, #642) ──── + ec2-user 의 authorized_keys 를 root 로 복사(강제명령 줄 제거), PermitRootLogin prohibit-password + +──── 패키지 ──── + buildx 0.12.2 → 0.19.3 으로 올린다 (compose build 는 0.17+ 를 요구한다) + +──── 저장소 — https://github.com/Shadowfit/init.git @ 8200b521ed696076638a247b589ce63aec833d92 ──── + 커밋: 8200b521 + +──── .env ──── +bootstrap.sh: line 281: compose: command not found +bootstrap.sh: line 281: shadowfit-ai: command not found +bootstrap.sh: line 281: INTERNAL_API_TOKEN:?: command not found +bootstrap.sh: line 281: MYSQL_EXPORTER_PASSWORD: command not found +bootstrap.sh: line 281: :?: command not found +bootstrap.sh: line 281: shadowfit-ai: command not found +bootstrap.sh: line 281: INTERNAL_API_TOKEN: command not found +bootstrap.sh: line 281: unused-in-this-role: command not found + MYSQL_ROOT_PASSWORD 를 rig 기본값과 맞췄다 (+ 해석만 되면 되는 변수들) + compose 해석 확인 ✅ (mysql 만 띄워도 파일 전체가 해석된다) + +──── MySQL 컨테이너 ──── +time="2026-11-08T02:43:14Z" level=warning msg="/root/init/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion" + Image mysql:8.0 Pulling + edf85873f64e Pulling fs layer 0B + 6ef6c7b50a93 Pulling fs layer 0B + e3e5d1ac74c1 Pulling fs layer 0B + 0d74d296605b Pulling fs layer 0B + 297d04cfe470 Pulling fs layer 0B + 4c8a3e0d4e4b Pulling fs layer 0B + a63160a5eda1 Pulling fs layer 0B + 7534d1db9f8d Pulling fs layer 0B + 49ec2dab01d9 Pulling fs layer 0B + ab24264a27e9 Pulling fs layer 0B + 96d30d9fbee8 Pulling fs layer 0B + 0d74d296605b Waiting 0B + 297d04cfe470 Waiting 0B + 4c8a3e0d4e4b Waiting 0B + a63160a5eda1 Waiting 0B + 7534d1db9f8d Waiting 0B + 49ec2dab01d9 Waiting 0B + ab24264a27e9 Waiting 0B + 96d30d9fbee8 Waiting 0B + 6ef6c7b50a93 Downloading 884B + 6ef6c7b50a93 Verifying Checksum 0B + 6ef6c7b50a93 Download complete 0B + e3e5d1ac74c1 Downloading 15.75kB + edf85873f64e Downloading 474.5kB + e3e5d1ac74c1 Download complete 0B + edf85873f64e Downloading 45.14MB + edf85873f64e Verifying Checksum 0B + edf85873f64e Download complete 0B + edf85873f64e Extracting 491.5kB + edf85873f64e Extracting 5.407MB + edf85873f64e Extracting 8.356MB + edf85873f64e Extracting 17.69MB + 0d74d296605b Downloading 64.9kB + edf85873f64e Extracting 26.54MB + 297d04cfe470 Downloading 2.607kB + 297d04cfe470 Verifying Checksum 0B + 297d04cfe470 Download complete 0B + 0d74d296605b Verifying Checksum 0B + 0d74d296605b Download complete 0B + edf85873f64e Extracting 32.44MB + 4c8a3e0d4e4b Downloading 335B + 4c8a3e0d4e4b Verifying Checksum 0B + 4c8a3e0d4e4b Download complete 0B + edf85873f64e Extracting 42.27MB + edf85873f64e Extracting 44.24MB + edf85873f64e Extracting 47.31MB + edf85873f64e Pull complete 0B + 6ef6c7b50a93 Extracting 884B + 6ef6c7b50a93 Extracting 884B + 6ef6c7b50a93 Pull complete 0B + e3e5d1ac74c1 Extracting 32.77kB + e3e5d1ac74c1 Extracting 783.6kB + e3e5d1ac74c1 Extracting 783.6kB + e3e5d1ac74c1 Pull complete 0B + 0d74d296605b Extracting 65.54kB + a63160a5eda1 Downloading 507.3kB + 7534d1db9f8d Downloading 316B + 7534d1db9f8d Verifying Checksum 0B + 7534d1db9f8d Download complete 0B + 0d74d296605b Extracting 2.621MB + a63160a5eda1 Downloading 41.65MB + 49ec2dab01d9 Downloading 538.9kB + a63160a5eda1 Verifying Checksum 0B + a63160a5eda1 Download complete 0B + 0d74d296605b Extracting 6.173MB + 0d74d296605b Pull complete 0B + 297d04cfe470 Extracting 2.607kB + 297d04cfe470 Extracting 2.607kB + 297d04cfe470 Pull complete 0B + 4c8a3e0d4e4b Extracting 335B + 4c8a3e0d4e4b Extracting 335B + 4c8a3e0d4e4b Pull complete 0B + a63160a5eda1 Extracting 524.3kB + 49ec2dab01d9 Downloading 41.51MB + a63160a5eda1 Extracting 7.34MB + 49ec2dab01d9 Downloading 84.46MB + 49ec2dab01d9 Downloading 128MB + 49ec2dab01d9 Verifying Checksum 0B + 49ec2dab01d9 Download complete 0B + a63160a5eda1 Extracting 13.63MB + a63160a5eda1 Extracting 23.07MB + ab24264a27e9 Downloading 3.502kB + ab24264a27e9 Downloading 5.327kB + ab24264a27e9 Verifying Checksum 0B + ab24264a27e9 Download complete 0B + a63160a5eda1 Extracting 32.51MB + 96d30d9fbee8 Download complete 0B + a63160a5eda1 Extracting 40.37MB + a63160a5eda1 Extracting 48.76MB + a63160a5eda1 Extracting 49.93MB + a63160a5eda1 Pull complete 0B + 7534d1db9f8d Extracting 316B + 7534d1db9f8d Extracting 316B + 7534d1db9f8d Pull complete 0B + 49ec2dab01d9 Extracting 557.1kB + 49ec2dab01d9 Extracting 9.47MB + 49ec2dab01d9 Extracting 17.83MB + 49ec2dab01d9 Extracting 22.28MB + 49ec2dab01d9 Extracting 28.41MB + 49ec2dab01d9 Extracting 31.75MB + 49ec2dab01d9 Extracting 32.31MB + 49ec2dab01d9 Extracting 32.87MB + 49ec2dab01d9 Extracting 33.42MB + 49ec2dab01d9 Extracting 33.98MB + 49ec2dab01d9 Extracting 34.54MB + 49ec2dab01d9 Extracting 35.65MB + 49ec2dab01d9 Extracting 36.21MB + 49ec2dab01d9 Extracting 37.32MB + 49ec2dab01d9 Extracting 37.88MB + 49ec2dab01d9 Extracting 38.44MB + 49ec2dab01d9 Extracting 38.99MB + 49ec2dab01d9 Extracting 39.55MB + 49ec2dab01d9 Extracting 40.11MB + 49ec2dab01d9 Extracting 41.22MB + 49ec2dab01d9 Extracting 42.34MB + 49ec2dab01d9 Extracting 42.89MB + 49ec2dab01d9 Extracting 43.45MB + 49ec2dab01d9 Extracting 44.56MB + 49ec2dab01d9 Extracting 45.12MB + 49ec2dab01d9 Extracting 45.68MB + 49ec2dab01d9 Extracting 46.24MB + 49ec2dab01d9 Extracting 47.91MB + 49ec2dab01d9 Extracting 49.58MB + 49ec2dab01d9 Extracting 52.36MB + 49ec2dab01d9 Extracting 60.72MB + 49ec2dab01d9 Extracting 69.07MB + 49ec2dab01d9 Extracting 77.43MB + 49ec2dab01d9 Extracting 86.34MB + 49ec2dab01d9 Extracting 95.81MB + 49ec2dab01d9 Extracting 106.4MB + 49ec2dab01d9 Extracting 117MB + 49ec2dab01d9 Extracting 127MB + 49ec2dab01d9 Extracting 129.4MB + 49ec2dab01d9 Pull complete 0B + ab24264a27e9 Extracting 5.327kB + ab24264a27e9 Extracting 5.327kB + ab24264a27e9 Pull complete 0B + 96d30d9fbee8 Extracting 120B + 96d30d9fbee8 Extracting 120B + 96d30d9fbee8 Pull complete 0B + Image mysql:8.0 Pulled + Network init_shadowfit-net Creating + Network init_shadowfit-net Creating + Volume init_mysql_data Creating + Volume init_mysql_data Creating + Volume init_mysql_data Created + Volume init_mysql_data Created + Network init_shadowfit-net Created + Network init_shadowfit-net Created + Container shadowfit-mysql Creating + Container shadowfit-mysql Created + Container shadowfit-mysql Starting + Container shadowfit-mysql Started + 준비 대기... — 떴다 (TCP 인증 질의 연속 2회) + +──── percona-toolkit 이미지 ──── +docker.io/percona/percona-toolkit:latest + +──── xtrabackup 이미지 (P3 백업 · P4 복제) — percona/percona-xtrabackup:8.0 ──── +docker.io/percona/percona-xtrabackup:8.0 + +──── 스키마 — Flyway 마이그레이션 ──── + SKIP_FLYWAY=1 — 건너뛴다. 從 R1·R3 은 «측정 대상 부재» 로 찍힌다 + +──── 준비됨 ──── + 작업 디렉터리 : /root/init + 커밋 : 8200b521 (8200b521ed696076638a247b589ce63aec833d92) + 디스크 여유 : 193G + MySQL : 8.0.46 + +다음: + cd /root/init + S3_BASE=s3://<버킷>/<프리픽스> nohup bash loadtest/aws/run_all.sh > /root/run_all.log 2>&1 & + + ⚠️ nohup 없이 & 만 붙이면 SSH 가 끊길 때 같이 죽는다. +shadowfit-mysql +== rig exit 0 +== finish: DONE (2026-11-08T04:02:49+00:00) diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/box.txt b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/box.txt new file mode 100644 index 00000000..0196c327 --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/box.txt @@ -0,0 +1,7 @@ +instance_type=m6i.xlarge +4 + total used free shared buff/cache available +Mem: 15 0 10 0 4 14 +Swap: 0 0 0 +Filesystem Size Used Avail Use% Mounted on +/dev/nvme0n1p1 200G 7.0G 194G 4% / diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/prep.tsv b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/prep.tsv new file mode 100644 index 00000000..8386b258 --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/prep.tsv @@ -0,0 +1,5 @@ +node1 boot 1794105734338 +node1 userdata_start 1794105740328 +node1 bootstrap_dl 1794105740594 +node1 bootstrap_done 1794105847843 +node1 target_ready 1794105867415 diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/ship/segments.tsv b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/ship/segments.tsv new file mode 100644 index 00000000..1a40977e --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/ship/segments.tsv @@ -0,0 +1,9 @@ +seg rate interval_s secs tx rows binlog_bytes +1 paced 60 480 852 21300 45837917 +2 paced 300 900 1598 39950 85970638 +3 paced 300 900 1598 39950 85970638 +4 paced 60 480 852 21300 45837925 +5 stress 60 240 80357 2008925 4323091560 +6 stress 300 600 193340 4833500 10401414137 +7 stress 300 600 195261 4881525 10504761183 +8 stress 60 240 82712 2067800 4449787170 diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/ship/ship_cost.tsv b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/ship/ship_cost.tsv new file mode 100644 index 00000000..bb92333c --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/ship/ship_cost.tsv @@ -0,0 +1,43 @@ +seg rate interval_s k file bytes flush_ms put_ms +1 paced 60 1 binlog.000001 6025629 54 500 +1 paced 60 2 binlog.000002 5756656 54 494 +1 paced 60 3 binlog.000003 5810442 54 489 +1 paced 60 4 binlog.000004 5756639 53 501 +1 paced 60 5 binlog.000005 5810451 54 487 +1 paced 60 6 binlog.000006 5810445 56 505 +1 paced 60 7 binlog.000007 5756634 53 493 +2 paced 300 1 binlog.000001 28943830 55 705 +2 paced 300 2 binlog.000002 28728618 53 704 +3 paced 300 1 binlog.000001 28943830 56 697 +3 paced 300 2 binlog.000002 28728618 52 707 +4 paced 60 1 binlog.000001 6025637 56 487 +4 paced 60 2 binlog.000002 5756656 53 498 +4 paced 60 3 binlog.000003 5810442 53 488 +4 paced 60 4 binlog.000004 5756639 53 485 +4 paced 60 5 binlog.000005 5810451 53 490 +4 paced 60 6 binlog.000006 5810445 55 481 +4 paced 60 7 binlog.000007 5756634 52 486 +5 stress 60 1 binlog.000001 1073926874 73 2261 +5 stress 60 2 binlog.000002 109856827 0 893 +5 stress 60 3 binlog.000003 1073926897 74 2679 +5 stress 60 4 binlog.000004 43899823 0 759 +5 stress 60 5 binlog.000005 1073926911 82 2345 +5 stress 60 6 binlog.000006 49979027 0 813 +6 stress 300 1 binlog.000001 1073819294 178 11947 +6 stress 300 2 binlog.000002 1073926904 0 10189 +6 stress 300 3 binlog.000003 1073873052 0 9724 +6 stress 300 4 binlog.000004 1073926930 0 2242 +6 stress 300 5 binlog.000005 1073926886 0 2388 +6 stress 300 6 binlog.000006 195019953 0 1032 +7 stress 300 1 binlog.000001 1073873078 165 12304 +7 stress 300 2 binlog.000002 1073819303 0 10413 +7 stress 300 3 binlog.000003 1073873069 0 3274 +7 stress 300 4 binlog.000004 1073819295 0 2246 +7 stress 300 5 binlog.000005 1073819305 0 2297 +7 stress 300 6 binlog.000006 190931279 0 1018 +8 stress 60 1 binlog.000001 1073873089 77 2528 +8 stress 60 2 binlog.000002 240264526 0 1109 +8 stress 60 3 binlog.000003 1073926907 68 2348 +8 stress 60 4 binlog.000004 54713319 0 794 +8 stress 60 5 binlog.000005 1073873075 68 2423 +8 stress 60 6 binlog.000006 40994715 0 781 diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/ship_run.log b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/ship_run.log new file mode 100644 index 00000000..e4ad0daa --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node1/ship_run.log @@ -0,0 +1,17 @@ +[shipcost] ── 세그먼트 1 rate=paced interval=60s 길이=480s +[shipcost] 세그먼트 1 끝 — 트랜잭션 852 · 행 21300 · binlog 43MB +[shipcost] ── 세그먼트 2 rate=paced interval=300s 길이=900s +[shipcost] 세그먼트 2 끝 — 트랜잭션 1598 · 행 39950 · binlog 81MB +[shipcost] ── 세그먼트 3 rate=paced interval=300s 길이=900s +[shipcost] 세그먼트 3 끝 — 트랜잭션 1598 · 행 39950 · binlog 81MB +[shipcost] ── 세그먼트 4 rate=paced interval=60s 길이=480s +[shipcost] 세그먼트 4 끝 — 트랜잭션 852 · 행 21300 · binlog 43MB +[shipcost] ── 세그먼트 5 rate=stress interval=60s 길이=240s +[shipcost] 세그먼트 5 끝 — 트랜잭션 80357 · 행 2008925 · binlog 4122MB +[shipcost] ── 세그먼트 6 rate=stress interval=300s 길이=600s +[shipcost] 세그먼트 6 끝 — 트랜잭션 193340 · 행 4833500 · binlog 9919MB +[shipcost] ── 세그먼트 7 rate=stress interval=300s 길이=600s +[shipcost] 세그먼트 7 끝 — 트랜잭션 195261 · 행 4881525 · binlog 10018MB +[shipcost] ── 세그먼트 8 rate=stress interval=60s 길이=240s +[shipcost] 세그먼트 8 끝 — 트랜잭션 82712 · 행 2067800 · binlog 4243MB +[shipcost] 완료 → /root/rig_out/ship/ship_cost.tsv diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node2.round.log b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node2.round.log new file mode 100644 index 00000000..8efa404e --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node2.round.log @@ -0,0 +1,203 @@ +== start 2026-11-08T02:42:23+00:00 node=node2 SHA=8200b521ed696076638a247b589ce63aec833d92 RUN_ID=shipcost-ec2-20261108-024203 CAP_MIN=40 RUN_SHIP=0 + +──── root SSH (다른 박스가 이 박스로 붙는 라운드용, #642) ──── + ec2-user 의 authorized_keys 를 root 로 복사(강제명령 줄 제거), PermitRootLogin prohibit-password + +──── 패키지 ──── + buildx 0.12.2 → 0.19.3 으로 올린다 (compose build 는 0.17+ 를 요구한다) + +──── 저장소 — https://github.com/Shadowfit/init.git @ 8200b521ed696076638a247b589ce63aec833d92 ──── + 커밋: 8200b521 + +──── .env ──── +bootstrap.sh: line 281: compose: command not found +bootstrap.sh: line 281: shadowfit-ai: command not found +bootstrap.sh: line 281: INTERNAL_API_TOKEN:?: command not found +bootstrap.sh: line 281: MYSQL_EXPORTER_PASSWORD: command not found +bootstrap.sh: line 281: :?: command not found +bootstrap.sh: line 281: shadowfit-ai: command not found +bootstrap.sh: line 281: INTERNAL_API_TOKEN: command not found +bootstrap.sh: line 281: unused-in-this-role: command not found + MYSQL_ROOT_PASSWORD 를 rig 기본값과 맞췄다 (+ 해석만 되면 되는 변수들) + compose 해석 확인 ✅ (mysql 만 띄워도 파일 전체가 해석된다) + +──── MySQL 컨테이너 ──── +time="2026-11-08T02:43:17Z" level=warning msg="/root/init/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion" + Image mysql:8.0 Pulling + edf85873f64e Pulling fs layer 0B + 6ef6c7b50a93 Pulling fs layer 0B + e3e5d1ac74c1 Pulling fs layer 0B + 0d74d296605b Pulling fs layer 0B + 297d04cfe470 Pulling fs layer 0B + 4c8a3e0d4e4b Pulling fs layer 0B + a63160a5eda1 Pulling fs layer 0B + 7534d1db9f8d Pulling fs layer 0B + 49ec2dab01d9 Pulling fs layer 0B + ab24264a27e9 Pulling fs layer 0B + 96d30d9fbee8 Pulling fs layer 0B + 297d04cfe470 Waiting 0B + 4c8a3e0d4e4b Waiting 0B + a63160a5eda1 Waiting 0B + 7534d1db9f8d Waiting 0B + 49ec2dab01d9 Waiting 0B + ab24264a27e9 Waiting 0B + 96d30d9fbee8 Waiting 0B + 0d74d296605b Waiting 0B + edf85873f64e Downloading 474.5kB + e3e5d1ac74c1 Downloading 15.75kB + 6ef6c7b50a93 Downloading 884B + 6ef6c7b50a93 Verifying Checksum 0B + e3e5d1ac74c1 Download complete 0B + 6ef6c7b50a93 Download complete 0B + edf85873f64e Downloading 40.86MB + edf85873f64e Download complete 0B + edf85873f64e Extracting 491.5kB + edf85873f64e Extracting 5.407MB + edf85873f64e Extracting 8.356MB + edf85873f64e Extracting 17.69MB + edf85873f64e Extracting 26.54MB + 297d04cfe470 Downloading 2.607kB + 297d04cfe470 Verifying Checksum 0B + 297d04cfe470 Download complete 0B + 0d74d296605b Downloading 64.9kB + 0d74d296605b Verifying Checksum 0B + 0d74d296605b Download complete 0B + edf85873f64e Extracting 32.44MB + 4c8a3e0d4e4b Downloading 335B + 4c8a3e0d4e4b Verifying Checksum 0B + 4c8a3e0d4e4b Download complete 0B + edf85873f64e Extracting 42.27MB + edf85873f64e Extracting 44.24MB + edf85873f64e Extracting 47.31MB + edf85873f64e Pull complete 0B + 6ef6c7b50a93 Extracting 884B + 6ef6c7b50a93 Extracting 884B + 6ef6c7b50a93 Pull complete 0B + e3e5d1ac74c1 Extracting 32.77kB + e3e5d1ac74c1 Extracting 783.6kB + e3e5d1ac74c1 Extracting 783.6kB + e3e5d1ac74c1 Pull complete 0B + 0d74d296605b Extracting 65.54kB + 7534d1db9f8d Downloading 316B + 7534d1db9f8d Verifying Checksum 0B + 7534d1db9f8d Download complete 0B + a63160a5eda1 Downloading 507.3kB + 0d74d296605b Extracting 2.621MB + 49ec2dab01d9 Downloading 538.9kB + a63160a5eda1 Downloading 34.54MB + a63160a5eda1 Verifying Checksum 0B + a63160a5eda1 Download complete 0B + 0d74d296605b Extracting 6.173MB + 0d74d296605b Pull complete 0B + 297d04cfe470 Extracting 2.607kB + 297d04cfe470 Extracting 2.607kB + 297d04cfe470 Pull complete 0B + 4c8a3e0d4e4b Extracting 335B + 4c8a3e0d4e4b Extracting 335B + 49ec2dab01d9 Downloading 40.9MB + 4c8a3e0d4e4b Pull complete 0B + a63160a5eda1 Extracting 524.3kB + 49ec2dab01d9 Downloading 83.29MB + a63160a5eda1 Extracting 7.34MB + 49ec2dab01d9 Downloading 119.9MB + a63160a5eda1 Extracting 13.63MB + 49ec2dab01d9 Verifying Checksum 0B + 49ec2dab01d9 Download complete 0B + a63160a5eda1 Extracting 23.07MB + ab24264a27e9 Downloading 3.502kB + ab24264a27e9 Downloading 5.327kB + ab24264a27e9 Verifying Checksum 0B + ab24264a27e9 Download complete 0B + a63160a5eda1 Extracting 31.98MB + 96d30d9fbee8 Downloading 120B + 96d30d9fbee8 Verifying Checksum 0B + 96d30d9fbee8 Download complete 0B + a63160a5eda1 Extracting 39.85MB + a63160a5eda1 Extracting 47.71MB + a63160a5eda1 Extracting 49.93MB + a63160a5eda1 Pull complete 0B + 7534d1db9f8d Extracting 316B + 7534d1db9f8d Extracting 316B + 7534d1db9f8d Pull complete 0B + 49ec2dab01d9 Extracting 557.1kB + 49ec2dab01d9 Extracting 9.47MB + 49ec2dab01d9 Extracting 17.83MB + 49ec2dab01d9 Extracting 22.28MB + 49ec2dab01d9 Extracting 28.41MB + 49ec2dab01d9 Extracting 31.75MB + 49ec2dab01d9 Extracting 32.31MB + 49ec2dab01d9 Extracting 32.87MB + 49ec2dab01d9 Extracting 33.42MB + 49ec2dab01d9 Extracting 33.98MB + 49ec2dab01d9 Extracting 34.54MB + 49ec2dab01d9 Extracting 35.65MB + 49ec2dab01d9 Extracting 36.21MB + 49ec2dab01d9 Extracting 37.32MB + 49ec2dab01d9 Extracting 37.88MB + 49ec2dab01d9 Extracting 38.44MB + 49ec2dab01d9 Extracting 38.99MB + 49ec2dab01d9 Extracting 39.55MB + 49ec2dab01d9 Extracting 40.11MB + 49ec2dab01d9 Extracting 41.22MB + 49ec2dab01d9 Extracting 42.34MB + 49ec2dab01d9 Extracting 42.89MB + 49ec2dab01d9 Extracting 43.45MB + 49ec2dab01d9 Extracting 44.56MB + 49ec2dab01d9 Extracting 45.68MB + 49ec2dab01d9 Extracting 46.24MB + 49ec2dab01d9 Extracting 47.91MB + 49ec2dab01d9 Extracting 49.58MB + 49ec2dab01d9 Extracting 52.36MB + 49ec2dab01d9 Extracting 60.72MB + 49ec2dab01d9 Extracting 69.07MB + 49ec2dab01d9 Extracting 77.43MB + 49ec2dab01d9 Extracting 86.34MB + 49ec2dab01d9 Extracting 95.81MB + 49ec2dab01d9 Extracting 106.4MB + 49ec2dab01d9 Extracting 117MB + 49ec2dab01d9 Extracting 127MB + 49ec2dab01d9 Extracting 129.4MB + 49ec2dab01d9 Pull complete 0B + ab24264a27e9 Extracting 5.327kB + ab24264a27e9 Extracting 5.327kB + ab24264a27e9 Pull complete 0B + 96d30d9fbee8 Extracting 120B + 96d30d9fbee8 Extracting 120B + 96d30d9fbee8 Pull complete 0B + Image mysql:8.0 Pulled + Network init_shadowfit-net Creating + Volume init_mysql_data Creating + Volume init_mysql_data Creating + Network init_shadowfit-net Creating + Volume init_mysql_data Created + Volume init_mysql_data Created + Network init_shadowfit-net Created + Network init_shadowfit-net Created + Container shadowfit-mysql Creating + Container shadowfit-mysql Created + Container shadowfit-mysql Starting + Container shadowfit-mysql Started + 준비 대기... — 떴다 (TCP 인증 질의 연속 2회) + +──── percona-toolkit 이미지 ──── +docker.io/percona/percona-toolkit:latest + +──── xtrabackup 이미지 (P3 백업 · P4 복제) — percona/percona-xtrabackup:8.0 ──── +docker.io/percona/percona-xtrabackup:8.0 + +──── 스키마 — Flyway 마이그레이션 ──── + SKIP_FLYWAY=1 — 건너뛴다. 從 R1·R3 은 «측정 대상 부재» 로 찍힌다 + +──── 준비됨 ──── + 작업 디렉터리 : /root/init + 커밋 : 8200b521 (8200b521ed696076638a247b589ce63aec833d92) + 디스크 여유 : 94G + MySQL : 8.0.46 + +다음: + cd /root/init + S3_BASE=s3://<버킷>/<프리픽스> nohup bash loadtest/aws/run_all.sh > /root/run_all.log 2>&1 & + + ⚠️ nohup 없이 & 만 붙이면 SSH 가 끊길 때 같이 죽는다. +shadowfit-mysql +== finish: DONE (2026-11-08T02:44:32+00:00) diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node2/box.txt b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node2/box.txt new file mode 100644 index 00000000..b92d9e81 --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node2/box.txt @@ -0,0 +1,7 @@ +instance_type=m6i.xlarge +4 + total used free shared buff/cache available +Mem: 15 0 10 0 4 14 +Swap: 0 0 0 +Filesystem Size Used Avail Use% Mounted on +/dev/nvme0n1p1 100G 6.3G 94G 7% / diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node2/prep.tsv b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node2/prep.tsv new file mode 100644 index 00000000..89ec0d9b --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node2/prep.tsv @@ -0,0 +1,5 @@ +node2 boot 1794105738319 +node2 userdata_start 1794105743799 +node2 bootstrap_dl 1794105743830 +node2 bootstrap_done 1794105851582 +node2 target_ready 1794105870961 diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node3.round.log b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node3.round.log new file mode 100644 index 00000000..1b543bdc --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node3.round.log @@ -0,0 +1,205 @@ +== start 2026-11-08T02:42:28+00:00 node=node3 SHA=8200b521ed696076638a247b589ce63aec833d92 RUN_ID=shipcost-ec2-20261108-024203 CAP_MIN=40 RUN_SHIP=0 + +──── root SSH (다른 박스가 이 박스로 붙는 라운드용, #642) ──── + ec2-user 의 authorized_keys 를 root 로 복사(강제명령 줄 제거), PermitRootLogin prohibit-password + +──── 패키지 ──── + buildx 0.12.2 → 0.19.3 으로 올린다 (compose build 는 0.17+ 를 요구한다) + +──── 저장소 — https://github.com/Shadowfit/init.git @ 8200b521ed696076638a247b589ce63aec833d92 ──── + 커밋: 8200b521 + +──── .env ──── +bootstrap.sh: line 281: compose: command not found +bootstrap.sh: line 281: shadowfit-ai: command not found +bootstrap.sh: line 281: INTERNAL_API_TOKEN:?: command not found +bootstrap.sh: line 281: MYSQL_EXPORTER_PASSWORD: command not found +bootstrap.sh: line 281: :?: command not found +bootstrap.sh: line 281: shadowfit-ai: command not found +bootstrap.sh: line 281: INTERNAL_API_TOKEN: command not found +bootstrap.sh: line 281: unused-in-this-role: command not found + MYSQL_ROOT_PASSWORD 를 rig 기본값과 맞췄다 (+ 해석만 되면 되는 변수들) + compose 해석 확인 ✅ (mysql 만 띄워도 파일 전체가 해석된다) + +──── MySQL 컨테이너 ──── +time="2026-11-08T02:43:24Z" level=warning msg="/root/init/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion" + Image mysql:8.0 Pulling + edf85873f64e Pulling fs layer 0B + 6ef6c7b50a93 Pulling fs layer 0B + e3e5d1ac74c1 Pulling fs layer 0B + 0d74d296605b Pulling fs layer 0B + 297d04cfe470 Pulling fs layer 0B + 4c8a3e0d4e4b Pulling fs layer 0B + a63160a5eda1 Pulling fs layer 0B + 7534d1db9f8d Pulling fs layer 0B + 49ec2dab01d9 Pulling fs layer 0B + ab24264a27e9 Pulling fs layer 0B + 96d30d9fbee8 Pulling fs layer 0B + a63160a5eda1 Waiting 0B + 297d04cfe470 Waiting 0B + 7534d1db9f8d Waiting 0B + 49ec2dab01d9 Waiting 0B + ab24264a27e9 Waiting 0B + 4c8a3e0d4e4b Waiting 0B + 96d30d9fbee8 Waiting 0B + 0d74d296605b Waiting 0B + 6ef6c7b50a93 Downloading 884B + 6ef6c7b50a93 Download complete 0B + e3e5d1ac74c1 Downloading 15.75kB + edf85873f64e Downloading 474.5kB + e3e5d1ac74c1 Download complete 0B + edf85873f64e Downloading 46.09MB + edf85873f64e Verifying Checksum 0B + edf85873f64e Download complete 0B + edf85873f64e Extracting 491.5kB + edf85873f64e Extracting 5.407MB + edf85873f64e Extracting 8.356MB + edf85873f64e Extracting 17.69MB + edf85873f64e Extracting 27.03MB + 0d74d296605b Downloading 64.9kB + 297d04cfe470 Downloading 2.607kB + 297d04cfe470 Verifying Checksum 0B + 297d04cfe470 Download complete 0B + 0d74d296605b Verifying Checksum 0B + 0d74d296605b Download complete 0B + edf85873f64e Extracting 32.44MB + 4c8a3e0d4e4b Downloading 335B + 4c8a3e0d4e4b Verifying Checksum 0B + 4c8a3e0d4e4b Download complete 0B + edf85873f64e Extracting 41.78MB + edf85873f64e Extracting 44.24MB + edf85873f64e Extracting 47.31MB + edf85873f64e Pull complete 0B + 6ef6c7b50a93 Extracting 884B + 6ef6c7b50a93 Extracting 884B + 6ef6c7b50a93 Pull complete 0B + e3e5d1ac74c1 Extracting 32.77kB + e3e5d1ac74c1 Extracting 783.6kB + e3e5d1ac74c1 Extracting 783.6kB + e3e5d1ac74c1 Pull complete 0B + 0d74d296605b Extracting 65.54kB + a63160a5eda1 Downloading 507.3kB + 7534d1db9f8d Downloading 316B + 7534d1db9f8d Verifying Checksum 0B + 7534d1db9f8d Download complete 0B + 49ec2dab01d9 Downloading 540.7kB + 0d74d296605b Extracting 2.687MB + a63160a5eda1 Downloading 36.06MB + a63160a5eda1 Verifying Checksum 0B + a63160a5eda1 Download complete 0B + 0d74d296605b Extracting 6.173MB + 49ec2dab01d9 Downloading 41.37MB + 0d74d296605b Pull complete 0B + 297d04cfe470 Extracting 2.607kB + 297d04cfe470 Extracting 2.607kB + 297d04cfe470 Pull complete 0B + 4c8a3e0d4e4b Extracting 335B + 4c8a3e0d4e4b Extracting 335B + 4c8a3e0d4e4b Pull complete 0B + a63160a5eda1 Extracting 524.3kB + 49ec2dab01d9 Downloading 85.55MB + a63160a5eda1 Extracting 6.816MB + 49ec2dab01d9 Downloading 127MB + 49ec2dab01d9 Verifying Checksum 0B + 49ec2dab01d9 Download complete 0B + a63160a5eda1 Extracting 14.16MB + a63160a5eda1 Extracting 23.59MB + ab24264a27e9 Downloading 3.502kB + ab24264a27e9 Downloading 5.327kB + ab24264a27e9 Verifying Checksum 0B + ab24264a27e9 Download complete 0B + a63160a5eda1 Extracting 33.03MB + 96d30d9fbee8 Downloading 120B + 96d30d9fbee8 Verifying Checksum 0B + 96d30d9fbee8 Download complete 0B + a63160a5eda1 Extracting 40.89MB + a63160a5eda1 Extracting 49.28MB + a63160a5eda1 Extracting 49.93MB + a63160a5eda1 Pull complete 0B + 7534d1db9f8d Extracting 316B + 7534d1db9f8d Extracting 316B + 7534d1db9f8d Pull complete 0B + 49ec2dab01d9 Extracting 557.1kB + 49ec2dab01d9 Extracting 9.47MB + 49ec2dab01d9 Extracting 17.83MB + 49ec2dab01d9 Extracting 22.28MB + 49ec2dab01d9 Extracting 28.41MB + 49ec2dab01d9 Extracting 31.75MB + 49ec2dab01d9 Extracting 32.31MB + 49ec2dab01d9 Extracting 32.87MB + 49ec2dab01d9 Extracting 33.42MB + 49ec2dab01d9 Extracting 33.98MB + 49ec2dab01d9 Extracting 34.54MB + 49ec2dab01d9 Extracting 35.65MB + 49ec2dab01d9 Extracting 36.21MB + 49ec2dab01d9 Extracting 37.32MB + 49ec2dab01d9 Extracting 37.88MB + 49ec2dab01d9 Extracting 38.44MB + 49ec2dab01d9 Extracting 38.99MB + 49ec2dab01d9 Extracting 39.55MB + 49ec2dab01d9 Extracting 40.11MB + 49ec2dab01d9 Extracting 40.67MB + 49ec2dab01d9 Extracting 41.78MB + 49ec2dab01d9 Extracting 42.34MB + 49ec2dab01d9 Extracting 42.89MB + 49ec2dab01d9 Extracting 43.45MB + 49ec2dab01d9 Extracting 44.56MB + 49ec2dab01d9 Extracting 45.12MB + 49ec2dab01d9 Extracting 45.68MB + 49ec2dab01d9 Extracting 46.24MB + 49ec2dab01d9 Extracting 47.91MB + 49ec2dab01d9 Extracting 49.58MB + 49ec2dab01d9 Extracting 52.36MB + 49ec2dab01d9 Extracting 60.72MB + 49ec2dab01d9 Extracting 69.07MB + 49ec2dab01d9 Extracting 77.43MB + 49ec2dab01d9 Extracting 86.34MB + 49ec2dab01d9 Extracting 95.81MB + 49ec2dab01d9 Extracting 106.4MB + 49ec2dab01d9 Extracting 117MB + 49ec2dab01d9 Extracting 125.9MB + 49ec2dab01d9 Extracting 129.4MB + 49ec2dab01d9 Pull complete 0B + ab24264a27e9 Extracting 5.327kB + ab24264a27e9 Extracting 5.327kB + ab24264a27e9 Pull complete 0B + 96d30d9fbee8 Extracting 120B + 96d30d9fbee8 Extracting 120B + 96d30d9fbee8 Pull complete 0B + Image mysql:8.0 Pulled + Network init_shadowfit-net Creating + Volume init_mysql_data Creating + Volume init_mysql_data Creating + Network init_shadowfit-net Creating + Volume init_mysql_data Created + Volume init_mysql_data Created + Network init_shadowfit-net Created + Network init_shadowfit-net Created + Container shadowfit-mysql Creating + Container shadowfit-mysql Created + Container shadowfit-mysql Starting + Container shadowfit-mysql Started + 준비 대기... — 떴다 (TCP 인증 질의 연속 2회) + +──── percona-toolkit 이미지 ──── +docker.io/percona/percona-toolkit:latest + +──── xtrabackup 이미지 (P3 백업 · P4 복제) — percona/percona-xtrabackup:8.0 ──── +docker.io/percona/percona-xtrabackup:8.0 + +──── 스키마 — Flyway 마이그레이션 ──── + SKIP_FLYWAY=1 — 건너뛴다. 從 R1·R3 은 «측정 대상 부재» 로 찍힌다 + +──── 준비됨 ──── + 작업 디렉터리 : /root/init + 커밋 : 8200b521 (8200b521ed696076638a247b589ce63aec833d92) + 디스크 여유 : 94G + MySQL : 8.0.46 + +다음: + cd /root/init + S3_BASE=s3://<버킷>/<프리픽스> nohup bash loadtest/aws/run_all.sh > /root/run_all.log 2>&1 & + + ⚠️ nohup 없이 & 만 붙이면 SSH 가 끊길 때 같이 죽는다. +shadowfit-mysql +== finish: DONE (2026-11-08T02:44:40+00:00) diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node3/box.txt b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node3/box.txt new file mode 100644 index 00000000..b92d9e81 --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node3/box.txt @@ -0,0 +1,7 @@ +instance_type=m6i.xlarge +4 + total used free shared buff/cache available +Mem: 15 0 10 0 4 14 +Swap: 0 0 0 +Filesystem Size Used Avail Use% Mounted on +/dev/nvme0n1p1 100G 6.3G 94G 7% / diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node3/prep.tsv b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node3/prep.tsv new file mode 100644 index 00000000..7e163d88 --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2/node3/prep.tsv @@ -0,0 +1,5 @@ +node3 boot 1794105743308 +node3 userdata_start 1794105748948 +node3 bootstrap_dl 1794105748980 +node3 bootstrap_done 1794105859245 +node3 target_ready 1794105879047 diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2_userdata.sh b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2_userdata.sh new file mode 100644 index 00000000..81f2253e --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ec2_userdata.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# EC2 무인 라운드 — ① 새 서버 준비 시간 ② 반출 업로드 비용 (RPO·RTO 분기점 §6-2 후속) +# 규칙은 binlog-replay-time-2026-11-07/ec2_userdata.sh 와 같다(커밋 SHA 고정·grep 단언·감시견·terminate). +# +# ① 준비 시간 = 이 스크립트가 «빈 AL2023 박스를 복구 대상으로 쓸 수 있는 상태» 까지 가는 시간. +# 구간마다 시각(ms)을 prep.tsv 에 남긴다. 호출 쪽(launch.sh)이 run-instances 호출·running 상태 시각을 따로 남긴다. +# boot 커널이 뜬 시각 (현재 − /proc/uptime) +# userdata_start 이 스크립트 시작 +# bootstrap_dl bootstrap.sh 내려받기 끝 +# bootstrap_done bootstrap.sh 끝 (docker·이미지·repo 준비) +# target_ready 복구 대상으로 쓸 mysql:8.0 컨테이너가 실제 쿼리를 받음 (skip_networking=0) +# 🔴 이것은 «빈 박스에서 직접 부트스트랩» 경로다. 미리 구워 둔 이미지(AMI)로 띄우면 더 짧다 — 그건 안 쟀다. +# +# ② RUN_SHIP=1 인 박스만 ship_cost.sh 를 돌린다. +# +# 아래 SHA·RUN_ID 등 대문자 자리표시자(골뱅이 두 쌍으로 감싼 것)는 띄우기 직전에 launch.sh 가 채운다. +set -uo pipefail +SHA=@@SHA@@ +RUN_ID=@@RUN_ID@@ +S3=s3://shadowfit-measure-055447613012/shadowfit/$RUN_ID +CAP_MIN=@@CAP_MIN@@ +RUN_SHIP=@@RUN_SHIP@@ +NODE=@@NODE@@ +RIG=/root/init/loadtest/results/server-prep-and-ship-cost-2026-11-08 +OUT=/root/rig_out +LOG=/root/round.log +exec > >(tee -a "$LOG") 2>&1 +ms() { date +%s%3N; } +T_UD=$(ms) +T_BOOT=$(( T_UD - $(awk '{printf "%d", $1*1000}' /proc/uptime) )) +mkdir -p "$OUT" +mark() { printf '%s\t%s\t%s\n' "$NODE" "$1" "$2" >> "$OUT/prep.tsv"; } +mark boot "$T_BOOT"; mark userdata_start "$T_UD" + +up() { aws s3 cp "$LOG" "$S3/$NODE.round.log" --only-show-errors || true; aws s3 sync "$OUT" "$S3/out/$NODE" --only-show-errors || true; } +finish() { echo "== finish: $1 ($(date -Is))"; up; aws s3 cp - "$S3/FINISHED.$NODE" <<< "$1 $(date -Is)" || true; shutdown -h now; } + +echo "== start $(date -Is) node=$NODE SHA=$SHA RUN_ID=$RUN_ID CAP_MIN=$CAP_MIN RUN_SHIP=$RUN_SHIP" +( sleep $((CAP_MIN * 60)); echo "🔴 상한 ${CAP_MIN}분 도달 — 끈다"; finish CAP ) & +( while sleep 600; do up; done ) & + +cd /root +curl -fsSL "https://raw.githubusercontent.com/Shadowfit/init/$SHA/loadtest/aws/bootstrap.sh" -o bootstrap.sh || finish BOOTSTRAP_DOWNLOAD +mark bootstrap_dl "$(ms)" +grep -q 'ROLE=${ROLE:-db}' bootstrap.sh || finish BOOTSTRAP_ASSERT +ROLE=db REF=$SHA SKIP_FLYWAY=1 bash bootstrap.sh || finish BOOTSTRAP_FAIL +mark bootstrap_done "$(ms)" + +# 복구 대상 준비 — 실제 쿼리를 받을 때까지(임시 초기화 서버의 소켓에 속지 않으려고 skip_networking 을 본다) +docker ps --format '{{.Names}}' | grep -E '^shadowfit-' | xargs -r docker stop +docker run -d --name prep_target -e MYSQL_ROOT_PASSWORD=repl mysql:8.0 >/dev/null +for i in $(seq 1 300); do + [ "$(docker exec prep_target mysql -uroot -prepl -N -s -e 'SELECT @@skip_networking;' 2>/dev/null)" = 0 ] && break + sleep 1 +done +mark target_ready "$(ms)" +docker rm -f prep_target >/dev/null 2>&1 +{ echo "instance_type=$(curl -s -H "X-aws-ec2-metadata-token: $(curl -s -X PUT http://169.254.169.254/latest/api/token -H 'X-aws-ec2-metadata-token-ttl-seconds: 60')" http://169.254.169.254/latest/meta-data/instance-type)"; nproc; free -g; df -h /; } > "$OUT/box.txt" +up + +if [ "$RUN_SHIP" = "1" ]; then + [ -f "$RIG/ship_cost.sh" ] || finish RIG_MISSING + grep -q 'ship_one' "$RIG/ship_cost.sh" || finish RIG_ASSERT + grep -q '_tpl' /root/init/loadtest/results/binlog-replay-time-2026-11-07/_writer_common.sh || finish WRITER_ASSERT + env OUT=$OUT/ship WORK=/root/rig_work S3_BASE=$S3/shipstore \ + SEG_SECS_60=480 SEG_SECS_300=900 STRESS_SECS_60=240 STRESS_SECS_300=600 \ + bash "$RIG/ship_cost.sh" > "$OUT/ship_run.log" 2>&1 + echo "== rig exit $?" +fi +finish "DONE" diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/launch.sh b/loadtest/results/server-prep-and-ship-cost-2026-11-08/launch.sh new file mode 100644 index 00000000..5d93b957 --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/launch.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# 이 라운드의 EC2 를 띄운다 — 3대(m6i.xlarge): node1 은 준비 시간 + 반출 비용, node2·3 은 준비 시간만. +# 준비 시간은 «박스를 몇 번 띄워 봤나» 가 곧 반복 수라서 3대를 같은 방식으로 동시에 띄운다. +# +# 🔴 커밋 SHA 로 고정한다 — 이 SHA 가 GitHub 에 올라가 있어야 한다(브랜치 URL 은 CDN 캐시로 옛 파일을 준다). +# 사용: bash launch.sh (DRY=1 이면 --dry-run) +set -euo pipefail +export MSYS_NO_PATHCONV=1 +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SHA=${1:?SHA 가 필요하다} +git ls-remote origin | grep -q "^$SHA" || { echo "🔴 SHA $SHA 가 원격에 없다 — 먼저 푸시"; exit 1; } +RUN_ID=shipcost-ec2-$(date -u +%Y%m%d-%H%M%S) +REGION=ap-northeast-2 +AMI=$(aws ec2 describe-images --region $REGION --owners amazon \ + --filters "Name=name,Values=al2023-ami-2023*-kernel-6.12-x86_64" "Name=state,Values=available" \ + --query "reverse(sort_by(Images,&CreationDate))[:1].ImageId" --output text) +SUBNET=subnet-0f9ea5dacb7260d34 # ap-northeast-2d (이전 라운드와 같은 AZ) +SG=sg-0382132d2f8b5f6ce +LAUNCH_LOG=$HERE/_launch_times.tsv +printf 'node\tinstance_id\tt_api_ms\tt_running_ms\n' > "$LAUNCH_LOG" +echo "RUN_ID=$RUN_ID AMI=$AMI SHA=$SHA" + +for node in 1 2 3; do + if [ "$node" = 1 ]; then ship=1; cap=150; vol=200; else ship=0; cap=40; vol=100; fi + ud=$(mktemp) + sed -e "s|@@SHA@@|$SHA|" -e "s|@@RUN_ID@@|$RUN_ID|" -e "s|@@CAP_MIN@@|$cap|" \ + -e "s|@@RUN_SHIP@@|$ship|" -e "s|@@NODE@@|node$node|" "$HERE/ec2_userdata.sh" > "$ud" + grep -qE '@@[A-Z_]+@@' "$ud" && { echo "🔴 채우지 않은 자리표시자가 있다"; exit 1; } + t_api=$(date +%s%3N) + id=$(aws ec2 run-instances --region $REGION --image-id "$AMI" --instance-type m6i.xlarge \ + --key-name shadowfit-measure --security-group-ids $SG --subnet-id $SUBNET \ + --iam-instance-profile Name=shadowfit-measure \ + --instance-initiated-shutdown-behavior terminate \ + --block-device-mappings "[{\"DeviceName\":\"/dev/xvda\",\"Ebs\":{\"VolumeSize\":$vol,\"VolumeType\":\"gp3\",\"DeleteOnTermination\":true}}]" \ + --tag-specifications "ResourceType=instance,Tags=[{Key=Project,Value=shadowfit-measure},{Key=RunId,Value=$RUN_ID}]" \ + "ResourceType=volume,Tags=[{Key=Project,Value=shadowfit-measure}]" \ + --user-data "$(cat "$ud")" ${DRY:+--dry-run} --query 'Instances[0].InstanceId' --output text 2>&1) || { echo "$id"; [ -n "${DRY:-}" ] && continue; exit 1; } + [ -n "${DRY:-}" ] && { echo "node$node dry-run: $id"; continue; } + echo "node$node → $id" + printf 'node%s\t%s\t%s\t\n' "$node" "$id" "$t_api" >> "$LAUNCH_LOG" +done +[ -n "${DRY:-}" ] && exit 0 + +# running 상태가 된 시각(호출 쪽에서 본 값) — 노드마다 병렬로 기다려야 순서가 값에 안 섞인다 +tmpd=$(mktemp -d) +while IFS=$'\t' read -r node id t_api _; do + ( aws ec2 wait instance-running --region $REGION --instance-ids "$id"; date +%s%3N > "$tmpd/$node" ) & +done < <(tail -n +2 "$LAUNCH_LOG") +wait +printf 'node\tinstance_id\tt_api_ms\tt_running_ms\n' > "$LAUNCH_LOG.new" +while IFS=$'\t' read -r node id t_api _; do + printf '%s\t%s\t%s\t%s\n' "$node" "$id" "$t_api" "$(cat "$tmpd/$node")" >> "$LAUNCH_LOG.new" +done < <(tail -n +2 "$LAUNCH_LOG") +mv "$LAUNCH_LOG.new" "$LAUNCH_LOG" +aws s3 cp "$LAUNCH_LOG" "s3://shadowfit-measure-055447613012/shadowfit/$RUN_ID/launch_times.tsv" --only-show-errors +echo "RUN_ID=$RUN_ID"; cat "$LAUNCH_LOG" diff --git a/loadtest/results/server-prep-and-ship-cost-2026-11-08/ship_cost.sh b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ship_cost.sh new file mode 100644 index 00000000..2ae4201b --- /dev/null +++ b/loadtest/results/server-prep-and-ship-cost-2026-11-08/ship_cost.sh @@ -0,0 +1,194 @@ +#!/bin/bash +# 반출 업로드 비용 — binlog 를 N초마다 닫아 S3 로 올릴 때 «한 번에 얼마가 드나» (RPO·RTO 분기점 §6-2 후속) +# +# 호스트 상실 rig(hostloss_rpo.sh)은 «잃은 시간» 만 쟀고 업로드 자체의 비용(파일 크기·FLUSH 시간·S3 업로드 시간)은 +# 안 쟀다. 그 README 가 «주기 + 업로드 시간으로 잡아야 한다» 고 적었는데 그 업로드 시간이 이 rig 의 출력이다. +# +# 재는 것 (반출 1건마다 한 줄): +# flush_ms FLUSH BINARY LOGS 가 끝나기까지 +# bytes 닫힌 파일 크기 +# put_ms 그 파일을 S3 로 올리는 데 걸린 시간 (EC2 에선 진짜 S3, 로컬 스모크에선 가짜 — 스모크 값은 의미 없음) +# +# 팔 (쓰기 속도 × 반출 주기), 각 팔 안에서 반출이 여러 번 일어나므로 «반출 1건» 이 반복 단위다: +# rate=paced 문서 §8-1 가정 피크(45행/초)로 쓴다 — 운영에 가까운 값 +# rate=stress 연결 여럿이 쉬지 않고 쓴다 — 상한 확인용. 운영 값이 아니다 +# interval 60, 300 초. 순서는 ABBA (60 300 300 60) 라 «팔» 과 «판 순서» 가 섞이지 않는다 +# +# 🔴 쓰기 속도 가정(45행/초)은 측정이 아니라 문서 가정이다 — 결과에 «가정 피크 기준» 이라고 적을 것. +# 🔴 같은 리전 S3 · 단일 기종 · 한 번의 라운드. 다른 리전으로 반출하면 업로드 시간이 달라진다. + +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="${OUT:-$HERE/_out}" +WORK="${WORK:-$HERE/_work}" +mkdir -p "$OUT" "$WORK" + +IMG=${MYSQL_IMAGE:-mysql:8.0} +PW=repl +SRC=sc_src +SRC_VOL=sc_src_data +S3_BASE=${S3_BASE:-} # s3:// 면 진짜 S3, 아니면 가짜(로컬 스모크) +INTERVALS=${INTERVALS:-"60 300 300 60"} # ABBA — 이 순서 그대로 돈다 +RATES=${RATES:-"paced stress"} +PACED_ROWS_PER_S=${PACED_ROWS_PER_S:-45} # 문서 §8-1 피크 가정 +ROWS_PER_TX=${ROWS_PER_TX:-25} +STRESS_CONNS=${STRESS_CONNS:-4} +# 세그먼트 길이(초): 반출이 최소 이 정도 일어나게 — 60초 주기는 짧게, 300초 주기는 길게 +SEG_SECS_60=${SEG_SECS_60:-600} +SEG_SECS_300=${SEG_SECS_300:-1200} +STRESS_SECS_60=${STRESS_SECS_60:-300} +STRESS_SECS_300=${STRESS_SECS_300:-600} +SERVER_OPTS=(--log-bin=binlog --binlog-format=ROW --gtid-mode=OFF --enforce-gtid-consistency=OFF + --innodb-flush-log-at-trx-commit=1 --sync-binlog=1 + --innodb-buffer-pool-size=${BUFFER_POOL:-1G} --innodb-redo-log-capacity=${REDO_CAPACITY:-2G} + --max-binlog-size=1G) + +log() { printf '[shipcost] %s\n' "$*" >&2; } +die() { log "🔴 $*"; exit 1; } +q() { docker exec -i "$1" mysql -uroot -p"$PW" -N -s -e "$2" 2>/dev/null; } +now_ms() { date +%s%3N; } +source "$HERE/../binlog-replay-time-2026-11-07/_writer_common.sh" + +wait_ready() { + local i + for i in $(seq 1 300); do [ "$(q "$1" "SELECT @@skip_networking;")" = 0 ] && return 0; sleep 1; done + die "$1 가 5분 안에 쿼리를 안 받는다" +} + +mountpoint_of() { docker volume inspect -f '{{.Mountpoint}}' "$1" 2>/dev/null; } + +# 닫힌 binlog 한 파일을 올린다 → 크기와 업로드 ms 를 출력. 마운트포인트에 닿으면 그대로(EC2), +# 안 닿으면(Docker Desktop) docker cp 로 꺼낸 뒤 올린다 — 그 경우 put_ms 는 cp 시간을 빼고 업로드만 잰다. +ship_one() { # $1 파일명 $2 키 → stdout "bytes put_ms" + local f=$1 key=$2 mp path bytes t0 t1 + mp=$(mountpoint_of "$SRC_VOL") + if [ -n "$mp" ] && [ -r "$mp/$f" ]; then path="$mp/$f" + else path="$WORK/_cp_$f"; docker cp "$SRC:/var/lib/mysql/$f" "$path" >/dev/null; fi + bytes=$(stat -c%s "$path") + t0=$(now_ms) + if [[ "$S3_BASE" == s3://* ]]; then aws s3 cp "$path" "$S3_BASE/$key" --only-show-errors + else mkdir -p "$WORK/_fake_s3/$(dirname "$key")" && cp "$path" "$WORK/_fake_s3/$key"; fi + t1=$(now_ms) + [[ "$path" == "$WORK"/_cp_* ]] && rm -f "$path" + echo "$bytes $((t1 - t0))" +} + +# 반출기 — iv 초마다 FLUSH → 새로 닫힌 파일을 올린다. 한 줄 = 반출 1건. +shipper() { # $1 seg $2 rate $3 interval $4 시작 파일 $5 기록 파일 + local seg=$1 rate=$2 iv=$3 from=$4 rec=$5 k=0 f + local -A shipped=() # 세그먼트마다 binlog 이름이 처음부터 다시 시작하므로 파일이 아니라 이 상태로 중복을 거른다 + while docker inspect "$SRC" >/dev/null 2>&1; do + sleep "$iv" + local t0 t1 fl closed + t0=$(now_ms); q "$SRC" "FLUSH BINARY LOGS;" >/dev/null || break; t1=$(now_ms); fl=$((t1 - t0)) + closed=$(q "$SRC" "SHOW BINARY LOGS;" | awk '{print $1}' | sed '$d') || break + for f in $closed; do + [[ "$f" < "$from" ]] && continue + [ -n "${shipped[$f]:-}" ] && continue + shipped[$f]=1 + k=$((k + 1)) + read -r bytes put <<<"$(ship_one "$f" "shipcost/$seg/$f")" || break 2 + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$seg" "$rate" "$iv" "$k" "$f" "$bytes" "$fl" "$put" >> "$rec" + fl=0 # FLUSH 는 주기마다 한 번 — 한 주기에 파일이 둘 이상 닫혀도 FLUSH 시간은 첫 줄에만 + done + done +} + +# paced writer — 한 트랜잭션(ROWS_PER_TX 행 + 세션 행 UPDATE) 뒤 gap 만큼 쉰다 +make_paced_proc() { + cat > "$WORK/_paced.sql" <<'SQL' +DELIMITER // +DROP PROCEDURE IF EXISTS lab.paced_run // +CREATE PROCEDURE lab.paced_run(IN p_session BIGINT, IN p_rows INT, IN p_secs INT, IN p_gap DOUBLE) +BEGIN + DECLARE v_end DATETIME(6); + DECLARE v_off INT DEFAULT 0; + SET v_end = SYSDATE(6) + INTERVAL p_secs SECOND; + WHILE SYSDATE(6) < v_end DO + START TRANSACTION; + INSERT INTO lab.pose_data_scale (session_id, timestamp_sec, joint_coordinates, sync_rate, is_correct, feedback_message) + SELECT p_session, ts, jc, 75.0, 1, fb FROM lab._tpl WHERE n >= v_off AND n < v_off + p_rows; + UPDATE lab.session_probe SET version = version + 1, last_active_at = SYSDATE(6) WHERE id = p_session; + COMMIT; + SET v_off = (v_off + p_rows) % (750 - p_rows); + DO SLEEP(p_gap); + END WHILE; +END // +DELIMITER ; +SQL + docker exec -i "$SRC" mysql -uroot -p"$PW" < "$WORK/_paced.sql" 2>/dev/null || die "paced 프로시저 생성 실패" +} + +setup() { + docker rm -f "$SRC" >/dev/null 2>&1 || true + docker volume rm -f "$SRC_VOL" >/dev/null 2>&1 || true; docker volume create "$SRC_VOL" >/dev/null + docker network create sc_net >/dev/null 2>&1 || true + docker run -d --name "$SRC" --network sc_net -e MYSQL_ROOT_PASSWORD="$PW" -v "$SRC_VOL:/var/lib/mysql" \ + "$IMG" --server-id=1 "${SERVER_OPTS[@]}" >/dev/null + wait_ready "$SRC" + q "$SRC" " + CREATE DATABASE lab; + CREATE TABLE lab.pose_data_scale ( + id bigint NOT NULL AUTO_INCREMENT, session_id bigint NOT NULL, timestamp_sec double NOT NULL, + joint_coordinates text COLLATE utf8mb4_unicode_ci NOT NULL, sync_rate double DEFAULT NULL, + is_correct tinyint(1) DEFAULT 1, feedback_message varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id, created_at) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + CREATE TABLE lab.session_probe (id BIGINT PRIMARY KEY, version BIGINT NOT NULL DEFAULT 0, + last_active_at DATETIME(6) NULL) ENGINE=InnoDB;" || die "스키마 생성 실패" + local i vals="" + for i in $(seq 1 "$STRESS_CONNS"); do vals="$vals${vals:+,}($((100000 + i)),0,NULL)"; done + q "$SRC" "INSERT INTO lab.session_probe VALUES $vals;" + setup_writer "$SRC" + make_paced_proc +} + +segment() { # $1 seg번호 $2 rate $3 interval + local seg=$1 rate=$2 iv=$3 secs i pids=() from + if [ "$rate" = paced ]; then [ "$iv" = 60 ] && secs=$SEG_SECS_60 || secs=$SEG_SECS_300 + else [ "$iv" = 60 ] && secs=$STRESS_SECS_60 || secs=$STRESS_SECS_300; fi + log "── 세그먼트 $seg rate=$rate interval=${iv}s 길이=${secs}s" + # 세그먼트마다 같은 출발 상태: 표를 비우고 binlog 를 처음으로 + q "$SRC" "TRUNCATE TABLE lab.pose_data_scale; RESET MASTER;" >/dev/null + from=$(q "$SRC" "SHOW MASTER STATUS;" | awk '{print $1}') + local rec="$OUT/ship_cost.tsv" + local c0; c0=$(q "$SRC" "SELECT SUM(version) FROM lab.session_probe;") + + if [ "$rate" = paced ]; then + local gap; gap=$(awk -v r="$ROWS_PER_TX" -v p="$PACED_ROWS_PER_S" 'BEGIN{printf "%.3f", r/p}') + docker run --rm --network sc_net "$IMG" mysql -h "$SRC" -uroot -p"$PW" -N -B \ + -e "CALL lab.paced_run(100001, $ROWS_PER_TX, $secs, $gap);" >/dev/null 2>&1 & + pids+=($!) + else + for i in $(seq 1 "$STRESS_CONNS"); do + docker run --rm --network sc_net "$IMG" mysql -h "$SRC" -uroot -p"$PW" --unbuffered -N -B \ + -e "CALL lab.load_run($((100000 + i)), $ROWS_PER_TX, $secs);" >/dev/null 2>&1 & + pids+=($!) + done + fi + sleep 3 + shipper "$seg" "$rate" "$iv" "$from" "$rec" & + local ship_pid=$! + for i in "${pids[@]}"; do wait "$i" 2>/dev/null || true; done + kill "$ship_pid" 2>/dev/null || true; wait "$ship_pid" 2>/dev/null || true + + local c1 rows bytes_total + c1=$(q "$SRC" "SELECT SUM(version) FROM lab.session_probe;") + rows=$(q "$SRC" "SELECT COUNT(*) FROM lab.pose_data_scale;") + bytes_total=$(q "$SRC" "SHOW BINARY LOGS;" | awk '{s+=$2} END{print s+0}') + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$seg" "$rate" "$iv" "$secs" "$((c1 - c0))" "$rows" "$bytes_total" >> "$OUT/segments.tsv" + log "세그먼트 $seg 끝 — 트랜잭션 $((c1 - c0)) · 행 $rows · binlog $((bytes_total / 1024 / 1024))MB" +} + +printf 'seg\trate\tinterval_s\tk\tfile\tbytes\tflush_ms\tput_ms\n' > "$OUT/ship_cost.tsv" +printf 'seg\trate\tinterval_s\tsecs\ttx\trows\tbinlog_bytes\n' > "$OUT/segments.tsv" +setup +seg=0 +for rate in $RATES; do + for iv in $INTERVALS; do + seg=$((seg + 1)) + segment "$seg" "$rate" "$iv" + done +done +docker rm -f "$SRC" >/dev/null 2>&1 || true +log "완료 → $OUT/ship_cost.tsv"