Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/decisions/replication-lag-and-semisync.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 복제 — 「한 대가 죽어도 된다」를 지연과 대가로 바꾼다

작성일: 2026-08-13 · 갱신: 2026-11-07 (Q4 미복귀 원인 분리 — §0-2)
작성일: 2026-08-13 · 갱신: 2026-11-07 (Q4 미복귀 원인 분리 · 리플리카 2대 — §0-2)
상태: ✅ **같은 AZ·다른 AZ 두 라운드 모두 완료.** Q1·Q2 에 값이 붙었다 —
**같은 AZ(08-22)** 는 반동기의 대가 처리량 −5.9% · 커밋 p95 +13.0%, 단일 핫세션이면
−10.3% 로 거의 두 배(H3). **다른 AZ(09-02, §0-5)** 는 같은 대가가 **처리량 −17.2% ·
Expand Down Expand Up @@ -150,6 +150,11 @@ AZ 무관). Q4·Q5 는 로컬 라운드(2026-08-17)가 닫았고, **Q3 은 「
> (`plugin-load-add` + `loose-...enabled=ON`) **3/3 복귀**(버림 포함 4/4), 추가 조작 없음.
> ⚠️ `SET PERSIST` 가 왜 안 되는지(수신 스레드가 붙는 시점과 설정 반영 순서)는 로그에서 읽은
> **추론**이고 계측하지 않았다. 리플리카 1대 · `docker stop/start` · 포지션 기반 조건이다.
>
> ✅ **2026-11-07 — 리플리카 2대면 «1대 죽음 = 곧바로 강등» 을 피한다**([결과](../../loadtest/results/replication-semisync-two-replicas-2026-11-07/README.md),
> 로컬 · 본판 9 + 버림 1). `wait_for_replica_count=1` 에서 1대를 죽이면 **4/4 버팀**(첫 쓰기가
> timeout 을 안 물고 3건 모두 반동기로 셈). 같은 설정에서 2대 다 죽이면 3/3 강등(양성 대조군), `count=2` 면 1대만
> 죽어도 3/3 강등. 🔴 부산물: 강등된 뒤에도 소스 `clients` 가 수 초간 줄지 않았다 — 알람은 `status`·`no_tx` 에 건다.

### Q5 ✅ — SBS 는 거짓말이 아니라 **침묵**한다

Expand Down Expand Up @@ -908,3 +913,8 @@ Q1 은 통째로 인위 지연이다.
반동기 없이 붙었고, IO 재시작만으로 돌아왔다. 그 순서 문제는 **추론**이다(계측 안 함).
D 1회차는 `loose-` 없이 줘서 이미지 첫 기동(`--initialize`)이 `unknown variable` 로 거부했다 — 부산물로 남긴다.
A·B·C 는 라틴 방격 · 버림 1, D 는 시작 옵션이 달라 따로 돌아 판 순서 균형이 없다.
- 2026-11-07: ✅ **리플리카 2대 실험** (로컬 · 8.0.46 · [결과](../../loadtest/results/replication-semisync-two-replicas-2026-11-07/README.md)).
Q4 에서 본 «리플리카 1대면 곧바로 조용히 강등» 을 실무식 구성(2대 + `wait_for_replica_count=1`)이 피하는지 확인.
본판 기준 P(count=1·1대 죽임) **4/4 버팀** · Q(count=1·2대 죽임) 3/3 강등(양성 대조군) · R(count=2·1대 죽임) 3/3 강등.
리플리카 반동기는 시작 옵션이라 10/10 자동 복귀. 🔴 강등 뒤에도 `clients` 가 바로 줄지 않아 알람 지표로 부적합(추론,
언제 줄어드는지는 안 잼). 같은 호스트라 «AZ 별 1대씩» 의 비용은 말하지 못한다.
2 changes: 1 addition & 1 deletion docs/decisions/rpo-rto-targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ DAU 와 비교해 판단을 닫지 않는다는 원칙과도 어긋난다([[feed
|---|---|---|---|
| R1 | **구성 2 의 실제 복구 시간**(새 서버 + 백업 붓기 + binlog 재생)이 Q5 에서 정할 RTO 를 넘는다 | §6-2 의 binlog 재생 시간 측정 · 정기 복구 훈련 | 비동기 리플리카(구성 3) |
| R2 | **데이터가 커져** 백업 붓기 시간이 늘어난다 — 174초는 1억 행(합성) 값이고 백업 문서 H5 가 «크기에 선형인가» 를 물었다 | 복구 훈련 때 붓기 시간 추세 | 먼저 물리 백업 유지 확인(논리는 약 7배 느림), 그다음 리플리카(구성 3) |
| R3 | **몇 분치도 못 잃는 데이터가 생긴다** — 예: 결제·포인트처럼 유실이 돈이 되는 데이터 | §3 데이터 분류에 새 A 등급 이상이 들어올 때 | 반동기(구성 4) — 단 리플리카 2대 이상으로 강등을 피하는 구성을 같이 본다. 또는 그 데이터만 저장소 분리(§7 을 다시 연다) |
| R3 | **몇 분치도 못 잃는 데이터가 생긴다** — 예: 결제·포인트처럼 유실이 돈이 되는 데이터 | §3 데이터 분류에 새 A 등급 이상이 들어올 때 | 반동기(구성 4) — 단 리플리카 2대 이상 + `wait_for_replica_count=1` 로 강등을 피한다(로컬 실측: 1대가 죽어도 4/4 버팀, [결과](../../loadtest/results/replication-semisync-two-replicas-2026-11-07/README.md)). 또는 그 데이터만 저장소 분리(§7 을 다시 연다) |
| R4 | **읽기 부하가 소스를 압박한다** | 소스 CPU·커넥션 풀 포화 지표 | 읽기 리플리카. 🔴 단 «운동 직후 리포트» 처럼 방금 쓴 것을 읽는 경로는 소스에서 읽는다 — 리플리카는 부하 아래 약 60초 밀렸다([복제 §0-3](./replication-lag-and-semisync.md)) |
| R5 | **binlog 반출 주기를 줄여도** Q4 의 RPO 를 못 맞춘다 | 반출 주기 실측 · 업로드 지연 | 실시간 스트리밍(구성 2') |
| R6 | **운영 호스트가 정해진다** | — | Q6(백업 주기·보존 기간)과 §4 의 미측정 칸을 그 호스트에서 다시 잰다 |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 반동기 리플리카 2대 — 한 대가 죽으면 강등되나 (2026-11-07, 로컬)

설계 문서: [`../../../docs/decisions/replication-lag-and-semisync.md`](../../../docs/decisions/replication-lag-and-semisync.md) §0-2 Q4 ·
[`../../../docs/decisions/rpo-rto-targets.md`](../../../docs/decisions/rpo-rto-targets.md) §8 R3
앞 실험: [`../replication-semisync-recovery-2026-11-07/`](../replication-semisync-recovery-2026-11-07/README.md) (리플리카 1대 · 미복귀 원인)
무대: 로컬 Docker · `mysql:8.0` = **8.0.46** · 소스 1 + 리플리카 2 · 포지션 기반 · `wait_point=AFTER_SYNC` ·
`timeout=10000ms` · 리플리카 반동기는 **시작 옵션**(앞 실험에서 재기동 뒤 자동 복귀가 확인된 방식)

## 0. 한 줄

**리플리카를 2대 두고 «1대만 받으면 커밋»(`wait_for_replica_count=1`)이면, 1대가 죽어도 강등되지 않았다**
— 첫 쓰기가 timeout 을 물지 않았고 쓰기 3건이 전부 반동기로 셌다(본판 4/4). 같은 설정에서 2대가 다 죽으면
강등됐고(3/3), «2대 다 받아야 커밋»(`count=2`)이면 1대만 죽어도 강등됐다(3/3). 08-17·11-07 의 «리플리카 1대면
곧바로 조용히 강등» 은 리플리카 수와 `count` 의 조합으로 피할 수 있다.

## 1. 질문

앞 실험들은 리플리카가 1대라 그 한 대가 죽으면 첫 쓰기가 timeout(10초)을 물고 비동기로 내려갔다.
실무는 리플리카를 여럿 두고 «그중 N대만 받으면 커밋» 으로 이걸 피한다고 알려져 있다 — 이 무대에서 그게
실제로 그렇게 동작하는지 본다. 이진 사실이라 로컬로 충분하다.

## 2. 팔과 판

| 팔 | 소스 `wait_for_replica_count` | 죽이는 것 | 기대 |
|---|---|---|---|
| **P** | 1 | 리플리카 2 | 버팀 (관심 팔) |
| **Q** | 1 | 리플리카 1·2 둘 다 | 강등 (**양성 대조군** — 계측이 강등을 잡는지) |
| **R** | 2 | 리플리카 2 | 강등 (둘 다 받아야 하니) |

- 버림 1(P) + 본판 9, 라틴 방격(PQR·QRP·RPQ). 판마다 `count` 만 바꾸고 소스는 재기동하지 않는다
- 판마다: 두 리플리카가 반동기로 붙은 상태(`ON`/2) 확인 → `docker stop` → 소스에 쓰기 3건(각각 지연 측정) →
소스 `status`·`yes_tx`/`no_tx` 증가분 → `docker start` → `ON`/2 로 돌아오는지
- 판정 «버팀» = 쓰기 뒤 `status=ON` · `yes_tx` +3 · `no_tx` +0. «강등» = `status=OFF` 또는 `no_tx` 증가

## 3. 결과

| 팔 | 본판 | 첫 쓰기 (ms) | 쓰기 뒤 status | yes_tx / no_tx 증가 | 판정 |
|---|---:|---|---|---|---|
| **P** count=1 · 1대 죽임 | 4 (+버림 1) | **941 ~ 1,482** (버림 1,956) | **ON** | **+3 / +0** | **4/4 버팀** (버림 포함 5/5) |
| Q count=1 · 2대 죽임 | 3 | **10,878 ~ 11,708** | OFF | +0 / +3 | 3/3 강등 |
| R count=2 · 1대 죽임 | 3 | **11,058 ~ 13,437** | OFF | +0 / +3 | 3/3 강등 |

원본: `_out/trials.tsv` · `_out/D0_stage.txt` · `run.log`. 스모크(팔당 1판, 같은 패턴): `_smoke/`.

- **Q 가 강등을 잡았다** — 같은 계측이 강등을 볼 수 있다는 뜻이라, P 의 «버팀» 은 «못 잡음» 이 아니다
- **R 은 Q 와 같은 얼굴**이다 — `count=2` 는 리플리카 2대여도 사실상 «한 대만 죽어도 강등» 이라, 1대 구성의 위험을
그대로 가진다
- 되살린 뒤 **10/10 전부 1초 안에 `ON`/2 로 복귀** — 시작 옵션 방식의 자동 복귀가 리플리카 2대에서도 재현됐다

### 3-1. 🔴 부산물 — `clients` 는 리플리카가 죽은 직후 바로 줄지 않았다

10판 모두 리플리카를 `docker stop` 한 뒤 쓰기 3건을 마친 시점(수 초 뒤)에도 소스 `Rpl_semi_sync_source_clients`
가 **2 그대로**였다 — Q·R 에서 이미 `status=OFF` 로 강등된 뒤에도. 소스가 끊긴 연결을 아직 정리하지 않은 것으로
읽는다(⚠️ 추론, 언제 줄어드는지는 안 쟀다).

→ 알람을 `clients` 에 걸면 **강등 직후를 놓친다.** 강등을 보려면 `status`(=OFF)와 `no_tx` 증가를 봐야 한다 —
11-07 미복귀 실험 §5 의 «알람은 `status=OFF` 와 `no_tx` 증가에» 를 뒷받침한다.

## 4. 조건 — 인용할 때 떼면 안 되는 것

1. **로컬 · 같은 호스트 · 8.0.46** 의 이진 사실이다. 쓰기 지연 절대값은 `docker exec` 왕복(약 1초)이 섞인 값이라
**10초 timeout 을 물었나 안 물었나를 가르는 데만** 쓴다. P 의 쓰기 3건 최대 2,713ms(판 2)는 평소 왕복보다
크지만 timeout 과는 자릿수가 다르다 — 리플리카를 죽이지 않은 기준판은 따로 안 쟀다
2. P 에서 죽인 것은 **리플리카 2 만**이다. 리플리카 1 만 죽이는 쪽은 안 돌렸다(대칭이라 같을 것으로 보지만 미확인)
3. 죽음은 `docker stop` 이다 — 네트워크 단절·느린 리플리카(살아 있지만 응답이 늦음)는 안 봤다. 특히
«느린 리플리카» 는 `count=1` 에서도 다른 한 대가 대신 받으니 영향이 작을 것으로 추정만 된다
4. 리플리카 2대 모두 같은 호스트라 «다른 AZ 에 1대씩» 의 비용은 이 무대로 말할 수 없다 — 그건 EC2 일이다

## 5. 운영으로 옮기면 (제안 — 이 프로젝트 운영엔 리플리카가 없다)

- 반동기를 쓴다면 **리플리카 2대 이상 + `wait_for_replica_count=1`** 이 «한 대가 죽으면 곧바로 조용히 강등» 을 피한다
- `count` 를 리플리카 수와 같게 두면(`count=2` · 2대) 1대 구성과 같은 위험으로 돌아간다
- 강등 알람은 `status=OFF`·`no_tx` 증가에 건다. `clients` 는 죽은 직후 바로 줄지 않았다(§3-1)

## 6. 재현

```bash
bash semisync_two_replicas.sh # 10판 (버림 1 + 라틴 방격 9)
OUT="$PWD/_smoke" ORDER="P Q R" bash semisync_two_replicas.sh # 스모크
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# D0 — 무대
# 생성: 2026-11-07T13:02:51Z 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
replica1 version 8.0.46
replica1 server_id 2
replica1 read_only 1
replica1 rpl_semi_sync_replica_enabled 1
replica2 version 8.0.46
replica2 server_id 3
replica2 read_only 1
replica2 rpl_semi_sync_replica_enabled 1
source status/clients ON/2
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
trial kind arm count killed pre_status/clients w1_ms w2_ms w3_ms down_status/clients yes_tx_delta no_tx_delta verdict back_s post_status/clients
1 버림 P 1 replica2 ON/2 1956 1223 1033 ON/2 3 0 버팀 1 ON/2
2 본판 P 1 replica2 ON/2 1482 2626 2713 ON/2 3 0 버팀 1 ON/2
3 본판 Q 1 replica1+replica2 ON/2 11557 1365 1123 OFF/2 0 3 강등 1 ON/2
4 본판 R 2 replica2 ON/2 13437 1121 1665 OFF/2 0 3 강등 1 ON/2
5 본판 Q 1 replica1+replica2 ON/2 10878 1395 1138 OFF/2 0 3 강등 1 ON/2
6 본판 R 2 replica2 ON/2 11058 916 1160 OFF/2 0 3 강등 1 ON/2
7 본판 P 1 replica2 ON/2 941 1470 764 ON/2 3 0 버팀 1 ON/2
8 본판 R 2 replica2 ON/2 11951 972 997 OFF/2 0 3 강등 1 ON/2
9 본판 P 1 replica2 ON/2 1122 1661 1142 ON/2 3 0 버팀 1 ON/2
10 본판 Q 1 replica1+replica2 ON/2 11708 1783 921 OFF/2 0 3 강등 1 ON/2
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# D0 — 무대
# 생성: 2026-11-07T12:54:22Z 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
replica1 version 8.0.46
replica1 server_id 2
replica1 read_only 1
replica1 rpl_semi_sync_replica_enabled 1
replica2 version 8.0.46
replica2 server_id 3
replica2 read_only 1
replica2 rpl_semi_sync_replica_enabled 1
source status/clients ON/2
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
trial kind arm count killed pre_status/clients w1_ms w2_ms w3_ms down_status/clients yes_tx_delta no_tx_delta verdict back_s post_status/clients
1 버림 P 1 replica2 ON/2 1198 1301 1391 ON/2 3 0 버팀 1 ON/2
2 본판 Q 1 replica1+replica2 ON/2 11738 1607 1397 OFF/2 0 3 강등 1 ON/2
3 본판 R 2 replica2 ON/2 11278 1327 787 OFF/2 0 3 강등 1 ON/2
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# 반동기 리플리카 2대 rig — 소스 1 + 리플리카 2 (로컬 전용)
#
# 11-07 미복귀 실험(../replication-semisync-recovery-2026-11-07/)과 같은 서버 설정이고,
# 리플리카 반동기는 그 실험에서 재기동 뒤 자동 복귀가 확인된 «시작 옵션» 방식으로 켠다(D 팔).
# 그래야 이번 실험의 판정(강등되나)이 «재기동 뒤 반동기가 안 붙는» 복구 절차 문제와 섞이지 않는다.
#
# 이 무대가 답하는 것은 «리플리카 한 대가 죽으면 소스가 강등되나» 라는 이진 사실과
# 첫 쓰기가 timeout 을 무는지다. 처리량·지연 크기는 재지 않는다(같은 호스트·RTT ≈ 0).

services:
source:
image: mysql:8.0
container_name: s2r_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
networks: [s2r]

replica1:
image: mysql:8.0
container_name: s2r_replica1
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 로 거부한다(11-07 실험 §3-2)
- --loose-rpl-semi-sync-replica-enabled=ON
environment:
MYSQL_ROOT_PASSWORD: repl
networks: [s2r]

replica2:
image: mysql:8.0
container_name: s2r_replica2
command:
- --server-id=3
- --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 로 거부한다(11-07 실험 §3-2)
- --loose-rpl-semi-sync-replica-enabled=ON
environment:
MYSQL_ROOT_PASSWORD: repl
networks: [s2r]

networks:
s2r:
name: s2r_net
Loading
Loading