diff --git a/docs/decisions/replication-lag-and-semisync.md b/docs/decisions/replication-lag-and-semisync.md index db4b306d..dcf517c9 100644 --- a/docs/decisions/replication-lag-and-semisync.md +++ b/docs/decisions/replication-lag-and-semisync.md @@ -1,6 +1,6 @@ # 복제 — 「한 대가 죽어도 된다」를 지연과 대가로 바꾼다 -작성일: 2026-08-13 · 갱신: 2026-09-02 (cross-AZ 대조 라운드 회수 — §0-5) +작성일: 2026-08-13 · 갱신: 2026-11-07 (Q4 미복귀 원인 분리 — §0-2) 상태: ✅ **같은 AZ·다른 AZ 두 라운드 모두 완료.** Q1·Q2 에 값이 붙었다 — **같은 AZ(08-22)** 는 반동기의 대가 처리량 −5.9% · 커밋 p95 +13.0%, 단일 핫세션이면 −10.3% 로 거의 두 배(H3). **다른 AZ(09-02, §0-5)** 는 같은 대가가 **처리량 −17.2% · @@ -126,7 +126,7 @@ AZ 무관). Q4·Q5 는 로컬 라운드(2026-08-17)가 닫았고, **Q3 은 「 > 이게 없었다면 아래 Q3 의 `miss 0/30` 을 **「창이 없다」는 결론으로 발표했을 것이다.** > §2 가 팔 C 를 둔 이유가 정확히 이것이었다. -### Q4 ✅ — 반동기는 조용히 강등되고, 돌아와도 자동 복구되지 않았다 +### Q4 ✅ — 반동기는 조용히 강등되고, 돌아와도 자동 복구되지 않았다 (원인은 복구 절차 — 11-07) 리플리카를 죽인 직후: @@ -138,9 +138,18 @@ AZ 무관). Q4·Q5 는 로컬 라운드(2026-08-17)가 닫았고, **Q3 은 「 - 🔴 **리플리카가 돌아와도 `status=OFF`·`clients=0`** 이었다. 사람이 안 보면 장애가 끝난 뒤에도 계속 비동기로 돈다 -> ⚠️ **미검증**: 자동 미복구가 rig 의 복구 절차(리플리카 재기동 후 `START REPLICA` 만) 탓인지 +> ~~⚠️ **미검증**: 자동 미복구가 rig 의 복구 절차(리플리카 재기동 후 `START REPLICA` 만) 탓인지 > 갈라내지 않았다. 리플리카 쪽 `rpl_semi_sync_replica_enabled` 재설정·IO 스레드 재시작이 -> 필요한지는 안 봤다. +> 필요한지는 안 봤다.~~ +> +> ✅ **2026-11-07 닫힘 — 원인은 복구 절차였다**([결과](../../loadtest/results/replication-semisync-recovery-2026-11-07/README.md), +> 로컬 · 8.0.46 · 본판 12 + 버림 2). 리플리카 쪽 반동기를 켜는 방법만 바꿔 리플리카를 죽였다 살렸다: +> `SET GLOBAL` **3/3 미복귀**(버림 포함 4/4, 재기동에 값이 0 으로 돌아감 — 08-17 재현) · 그 뒤 수동 재설정 + IO +> 재시작 **3/3 복귀** · 🔴 `SET PERSIST` **3/3 미복귀** — 값은 1 로 살아 있는데 재기동 때 자동으로 +> 붙는 수신 스레드가 반동기 없이 붙었고, IO 스레드만 재시작하자 돌아왔다 · **시작 옵션** +> (`plugin-load-add` + `loose-...enabled=ON`) **3/3 복귀**(버림 포함 4/4), 추가 조작 없음. +> ⚠️ `SET PERSIST` 가 왜 안 되는지(수신 스레드가 붙는 시점과 설정 반영 순서)는 로그에서 읽은 +> **추론**이고 계측하지 않았다. 리플리카 1대 · `docker stop/start` · 포지션 기반 조건이다. ### Q5 ✅ — SBS 는 거짓말이 아니라 **침묵**한다 @@ -890,3 +899,12 @@ Q1 은 통째로 인위 지연이다. 앵커 같이 갱신). 같은 잔존 문장을 `experiment-inventory.md` · `AWS-RIDE-ALONG.md` · `loadtest/aws/README.md` 에서도 고쳤다. 08-22 결과 README 와 `28-remaining-work-plan.md` 는 그 시점 기준으로 사실이라 그대로 둔다. + +- 2026-11-07: ✅ **Q4 의 «미검증» 단서를 닫았다 — 자동 미복구는 복구 절차 탓이었다** (로컬 · 8.0.46 · + [결과](../../loadtest/results/replication-semisync-recovery-2026-11-07/README.md)). 리플리카 쪽 반동기를 + 켜는 방법 넷을 팔로 두고(A `SET GLOBAL` · B 그 뒤 수동 재설정 + IO 재시작 · C `SET PERSIST` · D 시작 옵션) + 리플리카를 죽였다 살렸다. 본판 기준 A 3/3 미복귀(08-17 재현) · B 3/3 복귀 · 🔴 **C 3/3 미복귀** · D 3/3 복귀(A·D 는 버림판 포함 4/4). + C 는 «설정만 영속하면 된다» 는 예상을 깼다 — 값은 살아 있는데 재기동 때 자동으로 붙는 수신 스레드가 + 반동기 없이 붙었고, IO 재시작만으로 돌아왔다. 그 순서 문제는 **추론**이다(계측 안 함). + D 1회차는 `loose-` 없이 줘서 이미지 첫 기동(`--initialize`)이 `unknown variable` 로 거부했다 — 부산물로 남긴다. + A·B·C 는 라틴 방격 · 버림 1, D 는 시작 옵션이 달라 따로 돌아 판 순서 균형이 없다. diff --git a/loadtest/results/replication-semisync-recovery-2026-11-07/README.md b/loadtest/results/replication-semisync-recovery-2026-11-07/README.md new file mode 100644 index 00000000..8430f526 --- /dev/null +++ b/loadtest/results/replication-semisync-recovery-2026-11-07/README.md @@ -0,0 +1,93 @@ +# 반동기 미복귀 원인 분리 (2026-11-07, 로컬) + +설계: [`../../../docs/decisions/replication-lag-and-semisync.md`](../../../docs/decisions/replication-lag-and-semisync.md) §0-2 Q4 +앞 라운드: [`../replication-2026-08-17/`](../replication-2026-08-17/) — `_out/Q4_semisync_degrade.txt` +무대: 로컬 Docker · `mysql:8.0` = **8.0.46** 양쪽 · 소스 1 + 리플리카 1 · 포지션 기반(`gtid_mode=OFF`) · +`wait_point=AFTER_SYNC` · `timeout=10000ms` · `wait_for_replica_count=1` + +## 0. 한 줄 + +**08-17 의 «리플리카가 돌아와도 반동기로 안 돌아온다» 는 MySQL 의 성질이 아니라 복구 절차 탓이었다.** +그런데 원인이 하나가 아니었다 — 리플리카 쪽 설정을 `SET GLOBAL` 로 켜면 재기동에 사라지고, +`SET PERSIST` 로 살려둬도 **재기동 때 자동으로 붙는 수신 스레드가 반동기 없이 붙는다.** +설정을 **시작 옵션**(`--plugin-load-add` + `--loose-rpl-semi-sync-replica-enabled=ON`)으로 주면 +추가 조작 없이 1초 안에 돌아왔다. + +## 1. 질문 + +08-17 Q4 의 단서 — «자동 미복구가 rig 의 복구 절차(리플리카 재기동 후 `START REPLICA` 만) 탓인지 +갈라내지 않았다» — 를 닫는다. 이진 사실이라 로컬 무대로 충분하다(처리량·지연은 안 잰다). + +## 2. 팔과 판 + +| 팔 | 리플리카 반동기를 켜는 방법 | 재기동 뒤 복구 조작 | +|---|---|---| +| A | `SET GLOBAL` (08-17 rig 과 같음 — `replication_probe.sh:276`) | `START REPLICA` 만 (08-17 과 같음) | +| B | `SET GLOBAL` | `START REPLICA` → `SET GLOBAL ..._enabled=1` → IO 스레드 재시작 | +| C | `SET PERSIST` | `START REPLICA` 만 | +| D | 시작 옵션 (`docker-compose.armD.yml`) | `START REPLICA` 만 | + +- A·B·C: 버림 1(A) + 본판 9, 라틴 방격(ABC·BCA·CAB) — 판 순서와 팔을 가른다. 판마다 + `RESET PERSIST` 로 앞 판의 영속 설정이 새지 않게 했다 +- D: 리플리카 시작 옵션이 달라 **같은 무대에서 섞을 수 없어 따로** 돌았다 — 버림 1 + 본판 3. + 🔴 그래서 D 는 A·B·C 와 판 순서가 균형 잡혀 있지 않다(결과가 4/4 로 갈려 판정엔 영향 없다고 본다) +- 판마다: 리플리카 `docker stop` → 소스에 3행 → `docker start` → **복구 조작 전에** 리플리카 + `@@rpl_semi_sync_replica_enabled` 를 찍음(원인 증거) → 팔별 복구 → 최대 20초 쓰며 소스 `status` 관찰 +- 판정 «복귀» = 소스 `status=ON` · `clients=1` · 복구 뒤 쓰기가 `yes_tx` 로 셈 + +## 3. 결과 + +| 팔 | 판 | 재기동 직후 리플리카 `enabled` | 결과 | +|---|---:|---|---| +| A | 본판 3 + 버림 1 | **0** | **본판 3/3 미복귀** (버림 포함 4/4, `OFF`/0) — 08-17 재현 | +| B | 본판 3 | 0 → 수동으로 1 | **3/3 복귀**, 1초 | +| C | 본판 3 | **1** | **3/3 미복귀** (`OFF`/0) ← 예상 밖 | +| D | 본판 3 + 버림 1 | **1** | **본판 3/3 복귀** (버림 포함 4/4), 1초 | + +원본: `_out/trials.tsv`(A·B·C) · `_out_armD/trials.tsv`(D) · `_out*/D0_stage.txt`(무대) + +공통 관측(전체 관측 14판 = 본판 12 + 버림 2): 리플리카를 멈춘 뒤 **첫 쓰기 10,510~13,281ms** — 설정 timeout 10,000ms 를 +그대로 문다(08-17 의 10,658ms 와 같은 결). 그 뒤 `status=OFF`, 에러 없이 통과. + +### 3-1. C 진단 — 설정은 켜져 있는데 왜 안 돌아오나 + +C 한 판을 컨테이너를 남긴 채(`KEEP=1`) 돌려 확인했다 — `_diag/C_diagnosis.txt`. + +- 재기동 뒤 리플리카 `@@rpl_semi_sync_replica_enabled=1` 인데 **`Rpl_semi_sync_replica_status=OFF`** +- 리플리카 로그: 수신 스레드가 재기동 시 **자동으로** 소스에 붙었다(12:20:12.553) — + `ready for connections`(12:20:12.604)보다 먼저 +- 설정은 건드리지 않고 **IO 스레드만 재시작** → 2초 뒤 소스 `ON`/1, 리플리카 `status=ON` + +⚠️ **추론(검증 안 함)**: 반동기 역할은 수신 스레드가 소스에 붙는 순간 정해지는데, C 는 그 순간에 +영속 설정이 아직 반동기 쪽에 반영되지 않았던 것으로 보인다. D 는 플러그인과 설정을 시작 옵션으로 +서버 기동 초기에 올려서 그 순서 문제가 없는 것으로 읽는다. 기동 순서를 직접 계측하진 않았다. + +### 3-2. D 의 부산물 — `loose-` 가 없으면 기동을 거부한다 + +1회차(`run_armD_fail1.log`)는 리플리카가 안 떴다. `mysql` 이미지의 첫 기동(`--initialize`)은 +`--plugin-load-add` 를 무시하는데, 그 상태에서 플러그인 변수를 만나 `unknown variable +'rpl-semi-sync-replica-enabled=ON'` 으로 중단했다. 플러그인 변수는 `loose-` 접두사로 줘야 한다. + +## 4. 조건 — 인용할 때 떼면 안 되는 것 + +1. **로컬 · 같은 호스트 · 8.0.46** 의 이진 사실이다. 시간 값(1초·2초)은 폴링 간격이라 인용하지 않는다 +2. 리플리카 **1대** · `wait_for_replica_count=1`. 리플리카 2대 이상에서 한 대가 죽는 경우는 안 봤다 +3. 재기동은 `docker stop/start` 다 — 호스트 재부팅·OOM kill 등 다른 죽음은 안 봤다 +4. 포지션 기반 복제다. GTID 켠 무대에서 같은지는 안 봤다 +5. D 는 따로 돌아 A·B·C 와 판 순서 균형이 없다(§2) + +## 5. 운영으로 옮기면 (제안 — 이 프로젝트 운영엔 리플리카가 없다) + +- 리플리카 반동기는 **시작 옵션/옵션 파일**로 켠다(`plugin-load-add` + `loose-...enabled=ON`). + `SET GLOBAL` 은 재기동에 사라지고, `SET PERSIST` 만으로는 이 무대에서 복귀하지 않았다 +- 그래도 재기동 뒤엔 **소스 `Rpl_semi_sync_source_status`·`clients` 를 확인**하고, 아니면 리플리카 + IO 스레드를 재시작한다. 알람은 `status=OFF` 와 `no_tx` 증가에 건다 — 이 실험의 A·C 는 에러 없이 + 비동기로 계속 돌았다 + +## 6. 재현 + +```bash +bash semisync_recovery.sh # A·B·C 10판 +OUT="$PWD/_out_armD" COMPOSE_OVERRIDE=docker-compose.armD.yml ORDER="D D D D" bash semisync_recovery.sh +OUT="$PWD/_diag" ORDER="C" KEEP=1 bash semisync_recovery.sh # C 진단 — 컨테이너 남김, 끝나면 compose down -v +``` diff --git a/loadtest/results/replication-semisync-recovery-2026-11-07/_diag/C_diagnosis.txt b/loadtest/results/replication-semisync-recovery-2026-11-07/_diag/C_diagnosis.txt new file mode 100644 index 00000000..f75781f7 --- /dev/null +++ b/loadtest/results/replication-semisync-recovery-2026-11-07/_diag/C_diagnosis.txt @@ -0,0 +1,11 @@ +# C 팔 진단 (2026-11-07, KEEP=1 로 C 한 판 뒤 컨테이너를 남겨 확인) +## 재기동 뒤 (복구 조작 = START REPLICA 만) +source Rpl_semi_sync_source_status=OFF clients=0 +replica @@rpl_semi_sync_replica_enabled=1 Rpl_semi_sync_replica_status=OFF +replica 로그: 수신 스레드가 'ready for connections' 보다 먼저 소스에 붙었다 + 12:20:12.553701Z [MY-014001] Replica receiver thread ... connected to source ← 재기동 시 자동 시작 + 12:20:12.604736Z [MY-010931] mysqld: ready for connections + (12:21:37 의 MY-014001 은 아래 수동 IO 재시작) +## STOP/START REPLICA IO_THREAD 만 (SET 없음) → 2초 뒤 +source status=ON clients=1 +replica Rpl_semi_sync_replica_status=ON diff --git a/loadtest/results/replication-semisync-recovery-2026-11-07/_diag/D0_stage.txt b/loadtest/results/replication-semisync-recovery-2026-11-07/_diag/D0_stage.txt new file mode 100644 index 00000000..7f890baa --- /dev/null +++ b/loadtest/results/replication-semisync-recovery-2026-11-07/_diag/D0_stage.txt @@ -0,0 +1,17 @@ +# D0 — 무대 +# 생성: 2026-11-07T12:19:34Z UTC +source version 8.0.46 +source server_id 1 +source gtid_mode OFF +source binlog_format ROW +source sync_binlog 1 +source innodb_flush_log_at_trx_commit 1 +source rpl_semi_sync_source_enabled 1 +source rpl_semi_sync_source_timeout 10000 +source rpl_semi_sync_source_wait_point AFTER_SYNC +source rpl_semi_sync_source_wait_for_replica_count 1 +replica version 8.0.46 +replica server_id 2 +replica read_only 1 +source plugin rpl_semi_sync_source ACTIVE +replica plugin rpl_semi_sync_replica ACTIVE diff --git a/loadtest/results/replication-semisync-recovery-2026-11-07/_diag/trials.tsv b/loadtest/results/replication-semisync-recovery-2026-11-07/_diag/trials.tsv new file mode 100644 index 00000000..6a42c052 --- /dev/null +++ b/loadtest/results/replication-semisync-recovery-2026-11-07/_diag/trials.tsv @@ -0,0 +1,2 @@ +trial kind arm pre_status/clients first_write_ms down_status no_tx rep_enabled_after_restart rep_plugin_after_restart io_running_after_restart on_after_s post_status/clients yes_tx_delta verdict pre_on_s +1 버림 C ON/1 10574 OFF 2 1 ACTIVE Yes - OFF/0 0 미복귀 1 diff --git a/loadtest/results/replication-semisync-recovery-2026-11-07/_out/D0_stage.txt b/loadtest/results/replication-semisync-recovery-2026-11-07/_out/D0_stage.txt new file mode 100644 index 00000000..49fae0b9 --- /dev/null +++ b/loadtest/results/replication-semisync-recovery-2026-11-07/_out/D0_stage.txt @@ -0,0 +1,17 @@ +# D0 — 무대 +# 생성: 2026-11-07T12:04:10Z UTC +source version 8.0.46 +source server_id 1 +source gtid_mode OFF +source binlog_format ROW +source sync_binlog 1 +source innodb_flush_log_at_trx_commit 1 +source rpl_semi_sync_source_enabled 1 +source rpl_semi_sync_source_timeout 10000 +source rpl_semi_sync_source_wait_point AFTER_SYNC +source rpl_semi_sync_source_wait_for_replica_count 1 +replica version 8.0.46 +replica server_id 2 +replica read_only 1 +source plugin rpl_semi_sync_source ACTIVE +replica plugin rpl_semi_sync_replica ACTIVE diff --git a/loadtest/results/replication-semisync-recovery-2026-11-07/_out/trials.tsv b/loadtest/results/replication-semisync-recovery-2026-11-07/_out/trials.tsv new file mode 100644 index 00000000..a28cf1cf --- /dev/null +++ b/loadtest/results/replication-semisync-recovery-2026-11-07/_out/trials.tsv @@ -0,0 +1,11 @@ +trial kind arm pre_status/clients first_write_ms down_status no_tx rep_enabled_after_restart rep_plugin_after_restart io_running_after_restart on_after_s post_status/clients yes_tx_delta verdict pre_on_s +1 버림 A ON/1 10755 OFF 2 0 ACTIVE Yes - OFF/0 0 미복귀 1 +2 본판 A ON/1 11197 OFF 25 0 ACTIVE Yes - OFF/0 0 미복귀 1 +3 본판 B ON/1 10850 OFF 48 0 ACTIVE Yes 1 ON/1 2 복귀 1 +4 본판 C ON/1 10904 OFF 50 1 ACTIVE Yes - OFF/0 0 미복귀 1 +5 본판 B ON/1 13281 OFF 73 0 ACTIVE Yes 1 ON/1 2 복귀 1 +6 본판 C ON/1 10551 OFF 75 1 ACTIVE Yes - OFF/0 0 미복귀 1 +7 본판 A ON/1 10510 OFF 98 0 ACTIVE Yes - OFF/0 0 미복귀 1 +8 본판 C ON/1 12062 OFF 121 1 ACTIVE Yes - OFF/0 0 미복귀 1 +9 본판 A ON/1 11235 OFF 144 0 ACTIVE Yes - OFF/0 0 미복귀 1 +10 본판 B ON/1 11475 OFF 167 0 ACTIVE Yes 1 ON/1 2 복귀 1 diff --git a/loadtest/results/replication-semisync-recovery-2026-11-07/_out_armD/D0_stage.txt b/loadtest/results/replication-semisync-recovery-2026-11-07/_out_armD/D0_stage.txt new file mode 100644 index 00000000..6e39bda1 --- /dev/null +++ b/loadtest/results/replication-semisync-recovery-2026-11-07/_out_armD/D0_stage.txt @@ -0,0 +1,17 @@ +# D0 — 무대 +# 생성: 2026-11-07T12:32:20Z UTC +source version 8.0.46 +source server_id 1 +source gtid_mode OFF +source binlog_format ROW +source sync_binlog 1 +source innodb_flush_log_at_trx_commit 1 +source rpl_semi_sync_source_enabled 1 +source rpl_semi_sync_source_timeout 10000 +source rpl_semi_sync_source_wait_point AFTER_SYNC +source rpl_semi_sync_source_wait_for_replica_count 1 +replica version 8.0.46 +replica server_id 2 +replica read_only 1 +source plugin rpl_semi_sync_source ACTIVE +replica plugin rpl_semi_sync_replica ACTIVE diff --git a/loadtest/results/replication-semisync-recovery-2026-11-07/_out_armD/trials.tsv b/loadtest/results/replication-semisync-recovery-2026-11-07/_out_armD/trials.tsv new file mode 100644 index 00000000..01aac0cb --- /dev/null +++ b/loadtest/results/replication-semisync-recovery-2026-11-07/_out_armD/trials.tsv @@ -0,0 +1,5 @@ +trial kind arm pre_status/clients first_write_ms down_status no_tx rep_enabled_after_restart rep_plugin_after_restart io_running_after_restart on_after_s post_status/clients yes_tx_delta verdict pre_on_s +1 버림 D ON/1 11288 OFF 2 1 ACTIVE Yes 1 ON/1 2 복귀 1 +2 본판 D ON/1 10752 OFF 4 1 ACTIVE Yes 1 ON/1 2 복귀 1 +3 본판 D ON/1 11195 OFF 6 1 ACTIVE Yes 1 ON/1 2 복귀 1 +4 본판 D ON/1 10798 OFF 8 1 ACTIVE Yes 1 ON/1 2 복귀 1 diff --git a/loadtest/results/replication-semisync-recovery-2026-11-07/docker-compose.armD.yml b/loadtest/results/replication-semisync-recovery-2026-11-07/docker-compose.armD.yml new file mode 100644 index 00000000..63c4d2c8 --- /dev/null +++ b/loadtest/results/replication-semisync-recovery-2026-11-07/docker-compose.armD.yml @@ -0,0 +1,22 @@ +# D 팔 — 리플리카 반동기를 «시작 옵션» 으로 켠다 (MySQL 문서가 권하는 방식 — 옵션 파일과 같은 효과) +# +# 기본 compose 위에 겹쳐 쓴다: docker compose -f docker-compose.yml -f docker-compose.armD.yml +# command 는 목록 전체가 교체되므로 기본 compose 의 replica 옵션을 그대로 옮기고 두 줄만 더한다. +# 컨테이너 설정이 달라 A·B·C 와 한 무대에서 섞어 돌릴 수 없다 — 그래서 D 는 따로 돈다. + +services: + replica: + command: + - --server-id=2 + - --log-bin=binlog + - --binlog-format=ROW + - --gtid-mode=OFF + - --log-replica-updates=ON + - --enforce-gtid-consistency=OFF + - --innodb-flush-log-at-trx-commit=1 + - --sync-binlog=1 + - --read-only=ON + - --plugin-load-add=semisync_replica.so + # loose- = 모르는 변수면 무시. 이미지 첫 기동의 --initialize 는 플러그인을 안 올려서, + # 접두사 없이 쓰면 «unknown variable» 로 기동을 거부한다(1회차가 실제로 그랬다 — run_armD_fail1.log) + - --loose-rpl-semi-sync-replica-enabled=ON diff --git a/loadtest/results/replication-semisync-recovery-2026-11-07/docker-compose.yml b/loadtest/results/replication-semisync-recovery-2026-11-07/docker-compose.yml new file mode 100644 index 00000000..ec212e4e --- /dev/null +++ b/loadtest/results/replication-semisync-recovery-2026-11-07/docker-compose.yml @@ -0,0 +1,47 @@ +# 반동기 미복귀 원인 분리 rig — 소스 1 + 리플리카 1 (로컬 전용) +# +# 08-17 rig(../replication-2026-08-17/docker-compose.yml)과 같은 설정이다. 바꾼 것은 +# 컨테이너·네트워크 이름뿐 — 이 저장소에 세션이 둘 이상 붙어 08-17 rig 이 같은 이름으로 +# 떠 있을 수 있어서 충돌을 피한다. +# +# 이 무대가 답하는 것은 «리플리카가 돌아왔을 때 반동기가 다시 붙는가» 라는 이진 사실뿐이다. +# 처리량·지연은 재지 않는다(같은 디스크·RTT ≈ 0). + +services: + source: + image: mysql:8.0 + container_name: srec_source + command: + - --server-id=1 + - --log-bin=binlog + - --binlog-format=ROW + - --gtid-mode=OFF + - --log-replica-updates=ON + - --enforce-gtid-consistency=OFF + - --innodb-flush-log-at-trx-commit=1 + - --sync-binlog=1 + environment: + MYSQL_ROOT_PASSWORD: repl + MYSQL_DATABASE: replprobe + networks: [srec] + + replica: + image: mysql:8.0 + container_name: srec_replica + command: + - --server-id=2 + - --log-bin=binlog + - --binlog-format=ROW + - --gtid-mode=OFF + - --log-replica-updates=ON + - --enforce-gtid-consistency=OFF + - --innodb-flush-log-at-trx-commit=1 + - --sync-binlog=1 + - --read-only=ON + environment: + MYSQL_ROOT_PASSWORD: repl + networks: [srec] + +networks: + srec: + name: srec_net diff --git a/loadtest/results/replication-semisync-recovery-2026-11-07/run.log b/loadtest/results/replication-semisync-recovery-2026-11-07/run.log new file mode 100644 index 00000000..57b4a31c --- /dev/null +++ b/loadtest/results/replication-semisync-recovery-2026-11-07/run.log @@ -0,0 +1,59 @@ +[srec] compose up + Network srec_net Creating + Network srec_net Created + Container srec_replica Creating + Container srec_source Creating + Container srec_source Created + Container srec_replica Created + Container srec_replica Starting + Container srec_source Starting + Container srec_replica Started + Container srec_source Started +# D0 — 무대 +# 생성: 2026-11-07T12:04:10Z UTC +source version 8.0.46 +source server_id 1 +source gtid_mode OFF +source binlog_format ROW +source sync_binlog 1 +source innodb_flush_log_at_trx_commit 1 +source rpl_semi_sync_source_enabled 1 +source rpl_semi_sync_source_timeout 10000 +source rpl_semi_sync_source_wait_point AFTER_SYNC +source rpl_semi_sync_source_wait_for_replica_count 1 +replica version 8.0.46 +replica server_id 2 +replica read_only 1 +source plugin rpl_semi_sync_source ACTIVE +replica plugin rpl_semi_sync_replica ACTIVE +[srec] ── 판 1 (버림) 팔 A +1 버림 A ON/1 10755 OFF 2 0 ACTIVE Yes - OFF/0 0 미복귀 1 +[srec] ── 판 2 (본판) 팔 A +2 본판 A ON/1 11197 OFF 25 0 ACTIVE Yes - OFF/0 0 미복귀 1 +[srec] ── 판 3 (본판) 팔 B +3 본판 B ON/1 10850 OFF 48 0 ACTIVE Yes 1 ON/1 2 복귀 1 +[srec] ── 판 4 (본판) 팔 C +4 본판 C ON/1 10904 OFF 50 1 ACTIVE Yes - OFF/0 0 미복귀 1 +[srec] ── 판 5 (본판) 팔 B +5 본판 B ON/1 13281 OFF 73 0 ACTIVE Yes 1 ON/1 2 복귀 1 +[srec] ── 판 6 (본판) 팔 C +6 본판 C ON/1 10551 OFF 75 1 ACTIVE Yes - OFF/0 0 미복귀 1 +[srec] ── 판 7 (본판) 팔 A +7 본판 A ON/1 10510 OFF 98 0 ACTIVE Yes - OFF/0 0 미복귀 1 +[srec] ── 판 8 (본판) 팔 C +8 본판 C ON/1 12062 OFF 121 1 ACTIVE Yes - OFF/0 0 미복귀 1 +[srec] ── 판 9 (본판) 팔 A +9 본판 A ON/1 11235 OFF 144 0 ACTIVE Yes - OFF/0 0 미복귀 1 +[srec] ── 판 10 (본판) 팔 B +10 본판 B ON/1 11475 OFF 167 0 ACTIVE Yes 1 ON/1 2 복귀 1 +[srec] 완료 → /e/init-wt-srec/loadtest/results/replication-semisync-recovery-2026-11-07/_out/trials.tsv + Container srec_source Stopping + Container srec_replica Stopping + Container srec_replica Stopped + Container srec_replica Removing + Container srec_source Stopped + Container srec_source Removing + Container srec_replica Removed + Container srec_source Removed + Network srec_net Removing + Network srec_net Removed diff --git a/loadtest/results/replication-semisync-recovery-2026-11-07/run_armD.log b/loadtest/results/replication-semisync-recovery-2026-11-07/run_armD.log new file mode 100644 index 00000000..49c1475f --- /dev/null +++ b/loadtest/results/replication-semisync-recovery-2026-11-07/run_armD.log @@ -0,0 +1,47 @@ +[srec] compose up + Network srec_net Creating + Network srec_net Created + Container srec_replica Creating + Container srec_source Creating + Container srec_source Created + Container srec_replica Created + Container srec_replica Starting + Container srec_source Starting + Container srec_source Started + Container srec_replica Started +# D0 — 무대 +# 생성: 2026-11-07T12:32:20Z UTC +source version 8.0.46 +source server_id 1 +source gtid_mode OFF +source binlog_format ROW +source sync_binlog 1 +source innodb_flush_log_at_trx_commit 1 +source rpl_semi_sync_source_enabled 1 +source rpl_semi_sync_source_timeout 10000 +source rpl_semi_sync_source_wait_point AFTER_SYNC +source rpl_semi_sync_source_wait_for_replica_count 1 +replica version 8.0.46 +replica server_id 2 +replica read_only 1 +source plugin rpl_semi_sync_source ACTIVE +replica plugin rpl_semi_sync_replica ACTIVE +[srec] ── 판 1 (버림) 팔 D +1 버림 D ON/1 11288 OFF 2 1 ACTIVE Yes 1 ON/1 2 복귀 1 +[srec] ── 판 2 (본판) 팔 D +2 본판 D ON/1 10752 OFF 4 1 ACTIVE Yes 1 ON/1 2 복귀 1 +[srec] ── 판 3 (본판) 팔 D +3 본판 D ON/1 11195 OFF 6 1 ACTIVE Yes 1 ON/1 2 복귀 1 +[srec] ── 판 4 (본판) 팔 D +4 본판 D ON/1 10798 OFF 8 1 ACTIVE Yes 1 ON/1 2 복귀 1 +[srec] 완료 → /e/init-wt-srec/loadtest/results/replication-semisync-recovery-2026-11-07/_out_armD/trials.tsv + Container srec_replica Stopping + Container srec_source Stopping + Container srec_source Stopped + Container srec_source Removing + Container srec_replica Stopped + Container srec_replica Removing + Container srec_source Removed + Container srec_replica Removed + Network srec_net Removing + Network srec_net Removed diff --git a/loadtest/results/replication-semisync-recovery-2026-11-07/run_armD_fail1.log b/loadtest/results/replication-semisync-recovery-2026-11-07/run_armD_fail1.log new file mode 100644 index 00000000..dda12f00 --- /dev/null +++ b/loadtest/results/replication-semisync-recovery-2026-11-07/run_armD_fail1.log @@ -0,0 +1,12 @@ +[srec] compose up + Network srec_net Creating + Network srec_net Created + Container srec_source Creating + Container srec_replica Creating + Container srec_source Created + Container srec_replica Created + Container srec_replica Starting + Container srec_source Starting + Container srec_replica Started + Container srec_source Started +[srec] !! srec_replica 가 안 뜬다 diff --git a/loadtest/results/replication-semisync-recovery-2026-11-07/semisync_recovery.sh b/loadtest/results/replication-semisync-recovery-2026-11-07/semisync_recovery.sh new file mode 100644 index 00000000..facd5278 --- /dev/null +++ b/loadtest/results/replication-semisync-recovery-2026-11-07/semisync_recovery.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# 반동기 미복귀 — 원인이 MySQL 인가 08-17 rig 의 복구 절차인가 +# +# 설계 문서: docs/decisions/replication-lag-and-semisync.md §0-2 Q4 +# 08-17 라운드는 «리플리카가 돌아와도 status=OFF·clients=0» 을 관측했고, 원인을 +# «rig 의 복구 절차(재기동 후 START REPLICA 만) 탓인지 갈라내지 않았다» 로 남겼다. +# +# 가설 — 08-17 rig 은 리플리카 쪽 반동기를 SET GLOBAL(메모리 전용)로 켰다 +# (../replication-2026-08-17/replication_probe.sh:276). 재기동하면 그 값이 기본값 OFF 로 +# 돌아가고, 리플리카는 «반동기 아님» 으로 다시 붙는다 → 소스 clients=0. +# +# 팔 (판마다 리플리카를 죽였다 살린다) +# A 08-17 재현 — SET GLOBAL 로 켬 · 재기동 후 START REPLICA 만 +# B 수동 재설정 — A 와 같이 켬 · 재기동 후 리플리카에서 반동기를 다시 켜고 IO 스레드 재시작 +# C 영속 설정 — SET PERSIST 로 켬 · 재기동 후 START REPLICA 만 +# D 시작 옵션 — --plugin-load-add + --rpl-semi-sync-replica-enabled=ON · 재기동 후 START REPLICA 만 (따로 돈다) +# +# 판 설계 — 버림 1(A) + 본판 9. 순서를 라틴 방격(ABC·BCA·CAB)으로 돌려 «팔» 과 «판 순서» 를 +# 가른다. 판마다 RESET PERSIST 로 앞 판(특히 C)의 영속 설정이 새지 않게 한다. +# +# 판정 — 복귀 = 재기동 후 소스 status=ON 이고 clients=1 이고, 복구 뒤 쓰기가 yes_tx 로 센다. +# 원인 증거 = 재기동 직후(복구 조작 전) 리플리카의 @@rpl_semi_sync_replica_enabled 값. + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="${OUT:-$HERE/_out}" +mkdir -p "$OUT" + +SRC=srec_source +REP=srec_replica +MP='-uroot -prepl' +SP=rpl_semi_sync_source +RP=rpl_semi_sync_replica + +ORDER="${ORDER:-A A B C B C A C A B}" # 첫 판은 버림판 +# D 팔은 리플리카 시작 옵션이 달라 따로 돈다: COMPOSE_OVERRIDE=docker-compose.armD.yml ORDER="D D D D" +COMPOSE=(-f "$HERE/docker-compose.yml") +[ -n "${COMPOSE_OVERRIDE:-}" ] && COMPOSE+=(-f "$HERE/$COMPOSE_OVERRIDE") + +log() { printf '[srec] %s\n' "$*" >&2; } +sql() { docker exec -i "$1" mysql $MP -N -s -e "$2" 2>/dev/null; } +gstat() { sql "$SRC" "SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='${SP}_$1';"; } +rstat() { docker exec -i "$REP" mysql $MP -e "SHOW REPLICA STATUS\G" 2>/dev/null | awk -F': *' -v k="$1" '$0 ~ "^[ ]*"k":" {print $2; exit}'; } +now_ms() { date +%s%3N; } + +wait_up() { + local c=$1 i + for i in $(seq 1 90); do + docker exec "$c" mysqladmin ping -h 127.0.0.1 $MP >/dev/null 2>&1 && return 0 + sleep 1 + done + log "!! $c 가 안 뜬다"; return 1 +} + +# 리플리카 IO/SQL 이 Yes 가 될 때까지 +wait_repl() { + local i + for i in $(seq 1 30); do + [ "$(rstat Replica_IO_Running)" = Yes ] && [ "$(rstat Replica_SQL_Running)" = Yes ] && return 0 + sleep 1 + done + return 1 +} + +# 소스 status 가 ON 이 될 때까지 최대 N 초 — 그동안 1초마다 한 행씩 쓴다 +# (반동기 소스는 리플리카가 따라잡은 뒤 다음 트랜잭션에서 ON 으로 올라온다) +poll_on() { + local n=$1 i + for i in $(seq 1 "$n"); do + sql "$SRC" "INSERT INTO srecdb.t (note) VALUES ('poll');" >/dev/null || true + [ "$(gstat status)" = ON ] && { echo "$i"; return 0; } + sleep 1 + done + echo "-"; return 1 +} + +setup() { + log "compose up" + docker compose "${COMPOSE[@]}" -p srec up -d >/dev/null + wait_up "$SRC"; wait_up "$REP" + + sql "$SRC" "CREATE USER IF NOT EXISTS 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'replpw'; + GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';" + local coord file pos + coord=$(sql "$SRC" "SHOW MASTER STATUS;") + file=$(printf '%s' "$coord" | awk '{print $1}'); pos=$(printf '%s' "$coord" | awk '{print $2}') + [ -n "$file" ] && [ -n "$pos" ] || { log "!! 소스 좌표 못 읽음"; return 1; } + sql "$REP" "STOP REPLICA; RESET REPLICA ALL; + CHANGE REPLICATION SOURCE TO SOURCE_HOST='$SRC', SOURCE_PORT=3306, + SOURCE_USER='repl', SOURCE_PASSWORD='replpw', + SOURCE_LOG_FILE='$file', SOURCE_LOG_POS=$pos, GET_SOURCE_PUBLIC_KEY=1; + START REPLICA;" + # 스키마는 좌표 뒤에 만든다 — 복제를 타고 넘어가야 리플리카에도 생긴다(08-17 rig 의 교훈) + sql "$SRC" "CREATE DATABASE IF NOT EXISTS srecdb;" + sql "$SRC" "CREATE TABLE IF NOT EXISTS srecdb.t (id BIGINT AUTO_INCREMENT PRIMARY KEY, note VARCHAR(32), at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6));" + wait_repl || { log "!! 복제가 안 붙었다"; return 1; } + + # 플러그인 설치는 mysql.plugin 에 기록돼 재기동 뒤에도 로드된다 — 팔이 가르는 것은 + # «플러그인이 있나» 가 아니라 «enabled 변수가 살아남나» 다 + sql "$SRC" "INSTALL PLUGIN $SP SONAME 'semisync_source.so';" || true + sql "$REP" "INSTALL PLUGIN $RP SONAME 'semisync_replica.so';" || true + sql "$SRC" "SET GLOBAL ${SP}_enabled = 1;" # 소스는 재기동하지 않는다 — 팔과 무관 + # 소스 timeout 은 08-17 과 같은 기본 10000ms 그대로 + + { + echo "# D0 — 무대" + echo "# 생성: $(date -u '+%Y-%m-%dT%H:%M:%SZ') UTC" + for v in version server_id gtid_mode binlog_format sync_binlog innodb_flush_log_at_trx_commit \ + ${SP}_enabled ${SP}_timeout ${SP}_wait_point ${SP}_wait_for_replica_count; do + printf 'source %-44s %s\n' "$v" "$(sql "$SRC" "SELECT @@$v;")" + done + for v in version server_id read_only; do + printf 'replica %-44s %s\n' "$v" "$(sql "$REP" "SELECT @@$v;")" + done + printf 'source plugin %-37s %s\n' "$SP" "$(sql "$SRC" "SELECT PLUGIN_STATUS FROM information_schema.PLUGINS WHERE PLUGIN_NAME='$SP';")" + printf 'replica plugin %-37s %s\n' "$RP" "$(sql "$REP" "SELECT PLUGIN_STATUS FROM information_schema.PLUGINS WHERE PLUGIN_NAME='$RP';")" + } > "$OUT/D0_stage.txt" + cat "$OUT/D0_stage.txt" >&2 +} + +# 판 하나. 한 줄 TSV 를 stdout 으로 낸다. +trial() { + local idx=$1 arm=$2 kind=$3 + log "── 판 $idx ($kind) 팔 $arm" + + # 0. 판 시작 상태로 맞춘다 — 영속 설정 제거 → 팔 방식으로 켬 → 반동기 ON 확인 + sql "$REP" "RESET PERSIST IF EXISTS ${RP}_enabled;" + if [ "$arm" = C ]; then + sql "$REP" "SET PERSIST ${RP}_enabled = 1;" + elif [ "$arm" = D ]; then + : # 시작 옵션(--rpl-semi-sync-replica-enabled=ON)이 켠다 — 런타임 조작 없음 + else + sql "$REP" "SET GLOBAL ${RP}_enabled = 1;" + fi + sql "$REP" "STOP REPLICA IO_THREAD; START REPLICA IO_THREAD;" + wait_repl || log "!! 판 시작 복제 이상" + local pre_on + pre_on=$(poll_on 15) || true + local pre_status pre_clients + pre_status=$(gstat status); pre_clients=$(gstat clients) + + # 1. 리플리카를 죽이고 소스에 쓴다 — 첫 쓰기가 timeout 을 무는지, 강등되는지 + docker stop "$REP" >/dev/null + local w0 w1 first_ms + w0=$(now_ms); sql "$SRC" "INSERT INTO srecdb.t (note) VALUES ('down1');"; w1=$(now_ms) + first_ms=$((w1 - w0)) + sql "$SRC" "INSERT INTO srecdb.t (note) VALUES ('down2'), ('down3');" + local down_status no_tx_down + down_status=$(gstat status); no_tx_down=$(gstat no_tx) + + # 2. 되살린다 — 복구 조작 «전» 에 리플리카 쪽 값을 찍는다(원인 증거) + docker start "$REP" >/dev/null + wait_up "$REP" + local rep_enabled_after rep_plugin_after io_auto + rep_enabled_after=$(sql "$REP" "SELECT @@${RP}_enabled;") + rep_plugin_after=$(sql "$REP" "SELECT PLUGIN_STATUS FROM information_schema.PLUGINS WHERE PLUGIN_NAME='$RP';") + io_auto=$(rstat Replica_IO_Running) + + # 3. 팔별 복구 절차 + sql "$REP" "START REPLICA;" || true # 이미 돌고 있으면 경고만 난다 — 08-17 과 같은 조작 + if [ "$arm" = B ]; then + sql "$REP" "SET GLOBAL ${RP}_enabled = 1;" + sql "$REP" "STOP REPLICA IO_THREAD; START REPLICA IO_THREAD;" + fi + wait_repl || log "!! 복구 후 복제 이상" + + # 4. 복귀 판정 — 최대 20초 동안 쓰면서 ON 을 기다린다 + local yes0 on_after post_status post_clients yes_delta verdict + yes0=$(gstat yes_tx) + on_after=$(poll_on 20) || true + sql "$SRC" "INSERT INTO srecdb.t (note) VALUES ('after');" + post_status=$(gstat status); post_clients=$(gstat clients) + yes_delta=$(( $(gstat yes_tx) - yes0 )) + if [ "$post_status" = ON ] && [ "$post_clients" = 1 ] && [ "$yes_delta" -gt 0 ]; then verdict=복귀; else verdict=미복귀; fi + + printf '%s\t%s\t%s\t%s/%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s/%s\t%s\t%s\t%s\n' \ + "$idx" "$kind" "$arm" "$pre_status" "$pre_clients" "$first_ms" "$down_status" "$no_tx_down" \ + "$rep_enabled_after" "$rep_plugin_after" "$io_auto" "$on_after" \ + "$post_status" "$post_clients" "$yes_delta" "$verdict" "$pre_on" +} + +main() { + setup + local tsv="$OUT/trials.tsv" + printf 'trial\tkind\tarm\tpre_status/clients\tfirst_write_ms\tdown_status\tno_tx\trep_enabled_after_restart\trep_plugin_after_restart\tio_running_after_restart\ton_after_s\tpost_status/clients\tyes_tx_delta\tverdict\tpre_on_s\n' > "$tsv" + local i=0 arm kind + for arm in $ORDER; do + i=$((i + 1)) + kind=$([ "$i" = 1 ] && echo 버림 || echo 본판) + trial "$i" "$arm" "$kind" | tee -a "$tsv" + done + log "완료 → $tsv" + [ "${KEEP:-0}" = 1 ] || docker compose "${COMPOSE[@]}" -p srec down -v >/dev/null +} + +main "$@"