From b04d28600ba46b95d02791da0d9707e782679441 Mon Sep 17 00:00:00 2001 From: Khyojae Date: Wed, 11 Nov 2026 20:52:08 +0900 Subject: [PATCH] =?UTF-8?q?docs(decisions):=20=EC=9A=B4=EC=98=81=20binlog?= =?UTF-8?q?=20=EB=B0=98=EC=B6=9C=EA=B8=B0=20=EB=B6=84=EA=B8=B0=EC=A0=90=20?= =?UTF-8?q?=E2=80=94=20=EC=B8=A1=EC=A0=95=EC=9A=A9=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=EB=A5=BC=20=EC=9A=B4=EC=98=81=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=AF=BF=EA=B3=A0=20=EB=A7=A1=EA=B8=B8=20=EA=B2=83?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 구성 2(백업 + binlog 서버 밖 반출) 채택 뒤 남은 «반출기를 운영에서 어떻게 돌리나» 를 정리한다. 실무 세 갈래(관리형 · 서버 옆 업로더 · 실시간 받기)를 비교하고, 서버 옆 업로더(갈래 B)를 직접 구현하는 방향 안에서 D1~D8 항목별 후보·대가·추천을 적었다. 채택 표시는 없다. - D4 삭제: 자동 삭제를 끄고 S3 확인된 파일만 PURGE — 조용한 체인 끊김 대신 디스크 알림으로 - D5 잇기: S3 경로에 서버 구분 — 새 서버는 binlog 번호가 1부터라 덮어쓰기 위험 - D6 감지: 마지막 성공 시각 등 지표까지만, 알림은 #673~680 으로 넘김(운영 compose 에 Prometheus 없음) - D7 보호: 서버는 올리기·목록만 + 버킷 버전 관리 - 숫자(반출 주기 Q4 · 백업 주기·보존 기간 Q6)는 rpo-rto-targets.md 에 남긴다 Co-Authored-By: Claude Opus 5.5 --- docs/decisions/README.md | 1 + docs/decisions/binlog-shipper-ops.md | 306 +++++++++++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 docs/decisions/binlog-shipper-ops.md diff --git a/docs/decisions/README.md b/docs/decisions/README.md index fd0eaefa..5a2337a5 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -129,6 +129,7 @@ ## 인프라·운영 - [backup-restore-rto-rpo.md](./backup-restore-rto-rpo.md) — 백업·복구 — "되돌릴 수 있다"를 시간으로 (진행중 — #202 실측 완료, 병렬 덤프 등은 범위 밖으로 남음) +- [binlog-shipper-ops.md](./binlog-shipper-ops.md) — 운영 binlog 반출기 — 측정용 스크립트를 운영에서 믿고 맡길 것으로 (분기점 — 방향 «서버 옆 업로더 직접 구현», D1~D8 추천까지·채택 없음) - [major-version-upgrade-policy.md](./major-version-upgrade-policy.md) — 메이저 버전업 2건(springdoc 3.x·Gradle 9) (보류 — §8에 "재지 않은 것" 다수, 테스트·실측 안 됨) - [replication-lag-and-semisync.md](./replication-lag-and-semisync.md) — 복제 — "한 대가 죽어도 된다"를 지연과 대가로 (진행중 — 조건부 결론, 다른 AZ는 안 잼) - [rpo-rto-targets.md](./rpo-rto-targets.md) — RPO·RTO 목표 — "얼마 걸리나"에서 "얼마여야 하나"로 (진행중 — 방향 확정: 백업+binlog 서버 밖, 리플리카 보류, 반동기 미채택. 반출·백업 주기 숫자 미정) diff --git a/docs/decisions/binlog-shipper-ops.md b/docs/decisions/binlog-shipper-ops.md new file mode 100644 index 00000000..ae5fbe9d --- /dev/null +++ b/docs/decisions/binlog-shipper-ops.md @@ -0,0 +1,306 @@ +# 운영 binlog 반출기 — 측정용 스크립트를 «운영에서 믿고 맡길 것» 으로 바꾸는 분기점 + +작성일: 2026-11-11 +상태: 🟡 **분기점 — 후보와 추천까지. 채택은 사용자 confirm 뒤.** 큰 방향(실무 갈래 B 를 직접 구현, +§3 표의 «추천» 열)은 2026-11-11 대화에서 사용자가 «그 방향으로» 라고 했다 — 그래서 이 문서는 그 방향 안의 +세부 항목(D1~D8)을 정리한다. 각 항목의 채택 표시는 아직 없다. +연관: [`./rpo-rto-targets.md`](./rpo-rto-targets.md) (구성 2 채택 · Q4 반출 주기 · Q6 백업 주기·보존 기간) · +[`./backup-restore-rto-rpo.md`](./backup-restore-rto-rpo.md) (XtraBackup · PITR 실측) · +[호스트 상실 실측](../../loadtest/results/hostloss-rpo-2026-11-07/README.md) · +[준비·업로드 실측](../../loadtest/results/server-prep-and-ship-cost-2026-11-08/README.md) · +[`../tasks/31-production-readiness-plan.md`](../tasks/31-production-readiness-plan.md) §DB 백업 정책 + +--- + +## 0. 왜 이 문서인가 — 그리고 무엇을 다루지 않나 + +`rpo-rto-targets.md` 는 **구성 2(백업 + binlog 를 주기적으로 서버 밖에)** 를 채택했다(11-07). 그 구성이 +실제로 되는지·얼마를 잃는지·업로드가 얼마나 드는지는 EC2 에서 쟀다(11-07·11-08). 그런데 지금 반출기는 +**측정용 셸 함수 하나**(`loadtest/results/hostloss-rpo-2026-11-07/hostloss_rpo.sh:138` `shipper()`)뿐이다. + +측정용과 운영용의 차이는 «평소에 도느냐» 가 아니라 **«잘못됐을 때 아무도 모르게 약속이 깨지느냐»** 다. +측정용 반출기는 업로드가 한 번 실패하면 그냥 멈추고(`|| break 2`), 기록은 로컬 파일에만 있고, 멈춘 걸 +알려 주는 것이 없다. 판이 몇 분짜리라 괜찮았던 것이 운영에선 몇 주짜리 구멍이 된다. + +**다루는 것**: 반출기가 어디서 돌고, 언제 파일을 닫고, 무엇을 «올렸다» 로 치고, 언제 지우고, 멈춘 걸 +어떻게 알고, S3 쪽을 어떻게 지키나. + +**다루지 않는 것** (다른 문서에 그대로 둔다): +- **숫자 셋** — binlog 반출 주기(Q4), 백업 주기·보존 기간(Q6). `rpo-rto-targets.md` 에서 정한다. + 반출기 설계는 숫자가 없어도 정할 수 있다 — 주기는 설정값 하나다 +- 리플리카·장애 조치(구성 3) — 보류 중이다. 다시 열리면 이 문서도 다시 연다(§8) +- 백업(XtraBackup) 잡 자체의 운영 — 이 문서는 «백업이 남긴 binlog 위치» 를 받아 쓰는 쪽만 다룬다 + +--- + +## 1. 전제 — 이 설계가 맞춰야 하는 우리 조건 + +| 조건 | 값 | 출처 | +|---|---|---| +| DB 서버 | 1대, `docker-compose.prod.yml` 의 `mysql:8.0`. 데이터와 binlog 가 볼륨 `mysql_data` 하나에 같이 있다 | `docker-compose.prod.yml:39`·`:53`·`:226` | +| 리플리카 | 없음 (보류) | `rpo-rto-targets.md` Q5 | +| 운영자 | 1명. 알림이 휴대폰에 온다는 전제로 목표 RTO 를 잡았다 — 그 알림이 아직 없다 | `rpo-rto-targets.md` §6-3 | +| 쓰기량 | 가정 피크 초당 45행 (DAU 1,000 가정, 측정 아님) | `rpo-rto-targets.md` §8-1 | +| 백업 도구 | XtraBackup (EC2 에서 쓰기 중 백업 성공 확인) | `backup-restore-rto-rpo.md` | +| 위치 세는 법 | GTID 안 씀 — 파일 이름 + 위치. 운영 compose 에 GTID 설정이 없고, 측정도 `--gtid-mode=OFF` | `hostloss_rpo.sh:40` | +| 관측 | Prometheus·Grafana·node-exporter 는 **개발 compose 의 `obs` 프로필에만** 있다. 운영 compose 에는 없다 | `docker-compose.yml:319`·`:455` | +| 운영 호스트 | 🔴 **0대** — EC2 인지 다른 곳인지 미정 | `31-production-readiness-plan.md:6` | + +--- + +## 2. 실무에서는 어떻게 하나 + +binlog(또는 그에 해당하는 거래 로그)를 서버 밖에 두는 방법은 크게 세 갈래다. + +| 갈래 | 대표 예 | 방식 | 잃는 양 | +|---|---|---|---| +| **A. 관리형 DB** | AWS RDS | 매일 스냅샷 + 거래 로그를 **5분마다** S3 에 올림. 복구는 «가장 가까운 스냅샷 + 로그 재생» | 약 5분 | +| **B. 서버 옆 업로더** | Percona Operator for MySQL(PXC) 의 PITR | DB 옆 별도 컨테이너가 `timeBetweenUploads` 초마다 binlog 를 S3 에 올림. «마지막 업로드 성공 시각» 을 Prometheus 지표로 내보냄(`pxc_binlog_collector_last_upload_timestamp`) | 업로드 주기 | +| **C. 다른 서버에서 실시간으로 받기** | Percona Binlog Server, `mysqlbinlog --read-from-remote-server --stop-never` | 다른 서버가 복제 클라이언트처럼 붙어서 binlog 를 계속 받아 저장 | 수 초 | + +도구로는 **WAL-G** 가 B 를 구현한다(`binlog-push` 를 cron 으로, `binlog-replay` 로 복구, XtraBackup 연동). + +**우리 구성 2 는 B 다.** 측정에 쓴 방식(주기마다 FLUSH → 닫힌 파일을 S3 로)이 B 가 하는 일과 같은 모양이다. +C 는 `rpo-rto-targets.md` §5 의 구성 2' 이고, 서버 1대 상시가 필요해서 리플리카를 보류한 이유와 부딪힌다. + +출처: [Percona Operator PITR](https://docs.percona.com/percona-operator-for-mysql/pxc/backups-pitr.html) · +[Percona PITR 설계 블로그](https://www.percona.com/blog/point-in-time-recovery-in-kubernetes-operator-for-percona-xtradb-cluster-architecture-decisions/) · +[Percona Operator(PS) Binlog Server](https://docs.percona.com/percona-operator-for-mysql/ps/backups-pitr.html) · +[AWS RDS 백업·복구](https://docs.aws.amazon.com/prescriptive-guidance/latest/backup-recovery/rds.html) · +[WAL-G MySQL](https://github.com/ServiceNow/wal-g/blob/master/docs/MySQL.md) · +[XtraBackup 과 binlog](https://docs.percona.com/percona-xtrabackup/8.0/working-with-binary-logs.html) + +--- + +## 3. 한눈에 — 항목별 추천 + +| # | 항목 | 실무 | 추천 (우리에게) | 왜 | +|---|---|---|---|---| +| D1 | 어디서 도나 | B: DB 옆 컨테이너 | **같은 서버의 compose 사이드카** | 서버가 늘지 않는다. 서버를 잃으면 같이 멈추지만 «잃는 양 = 주기» 계산은 그대로다 | +| D2 | 파일을 언제 닫나 | RDS 5분, Percona 설정값 | **주기마다 `FLUSH BINARY LOGS`** | 측정한 방식. 반출 1번에 업로드 1초 미만 | +| D3 | 무엇을 «올렸다» 로 치나 | 도구가 기록 | **S3 에 실제로 있고 크기가 맞는 것** | 로컬 기록은 컨테이너가 다시 뜨면 사라진다 | +| D4 | 언제 지우나 | 올린 것만 | **S3 확인된 파일만 `PURGE`**, 자동 삭제 끔 | 운영자 1명 — 반출이 멈춘 걸 30일 안에 못 볼 수 있다 | +| D5 | 백업과 잇기 | `xtrabackup_binlog_info` | **같음 + 서버별 폴더 + 번호 연속 검사** | 새 서버는 binlog 번호가 처음부터 다시 시작한다 | +| D6 | 멈춘 걸 어떻게 아나 | Percona: 마지막 성공 시각 지표 | **같은 지표 + 디스크 지표** | 알림이 없으면 RPO 약속이 조용히 깨진다 | +| D7 | S3 를 어떻게 지키나 | 서버는 올리기만, 버전 관리·Object Lock | **서버는 올리기·목록만 + 버전 관리** | 호스트 상실에 `compose down -v`·랜섬웨어가 들어간다 | +| D8 | 직접 만들까 | 직접·WAL-G·Operator | **직접 (셸 + 지표)** | WAL-G 는 MySQL 쪽을 «불안정» 이라 스스로 밝힘. Operator 는 쿠버네티스 전용 | + +아래 §4 에서 하나씩 후보와 대가를 적는다. + +--- + +## 4. 항목별 분기 + +### D1. 어디서 도나 + +| 후보 | 장점 | 대가 | +|---|---|---| +| **ㄱ. 같은 서버, compose 사이드카 컨테이너** | 배포가 앱과 같은 방식(compose 한 벌). `mysql_data` 를 읽기 전용으로 붙이면 파일을 바로 읽는다. 재시작은 `restart: unless-stopped` 가 한다 | 서버를 잃으면 같이 멈춘다 — 마지막 반출 뒤 쌓인 만큼 잃는다(구성 2 의 정의 그대로) | +| ㄴ. 같은 서버, cron · systemd 타이머 | 컨테이너 하나 덜 | 배포·버전 관리가 compose 밖으로 빠진다. 운영 호스트가 미정이라 호스트 설정에 기대는 게 불리하다 | +| ㄷ. 다른 서버에서 끌어오기 | 잃는 양 수 초 | 서버 1대 상시 = 구성 2'. 리플리카를 보류한 이유(사람 시간이 대부분)와 같은 이유로 이득이 작다 | + +**추천: ㄱ.** 사이드카는 `mysql_data` 를 `:ro` 로 붙이고, MySQL 에는 반출 전용 계정으로 붙는다(D4 에서 권한). + +### D2. 파일을 언제 닫나 + +| 후보 | 장점 | 대가 | +|---|---|---| +| **ㄱ. 주기마다 `FLUSH BINARY LOGS` 로 닫고, 닫힌 파일을 올린다** | 측정한 방식. 잃는 양 = 주기(+ 업로드 1초 미만, 가정 피크) | 주기만큼 작은 파일이 생긴다(60초면 하루 1,440개). FLUSH 가 그 순간 쓰기를 얼마나 막는지 🔴 안 쟀다 | +| ㄴ. 파일이 `max_binlog_size` 만큼 차서 저절로 넘어갈 때만 올린다 | FLUSH 없음 | 🔴 쓰기가 적으면 몇 시간씩 안 닫힌다 → 잃는 양을 약속할 수 없다. 탈락 | +| ㄷ. 열린 파일도 `mysqlbinlog --raw --stop-never` 로 받아 두고 주기마다 올린다 | FLUSH 없음 | 덜 찬 파일을 S3 에 같은 이름으로 계속 덮어써야 한다 — D7(덮어쓰기 막기)과 충돌한다 | + +**추천: ㄱ.** 다만 D2-측정: FLUSH 가 쓰기를 막는 시간을 재야 한다(§7). 프로세스 죽음의 목표 RTO 19초가 +AI→Spring 재시도 한도에서 나왔듯, 쓰기가 막히는 시간에도 같은 한도가 걸린다. + +### D3. 무엇을 «올렸다» 로 치나 + +측정용은 로컬 파일(`$rec`)에 이름을 적었다. 컨테이너가 다시 뜨면 이 기록은 없어지고, 그러면 전부 다시 +올리거나(덮어쓰기 — D7 과 충돌) 기록이 남아도 실제 S3 와 어긋날 수 있다. + +| 후보 | 장점 | 대가 | +|---|---|---| +| **ㄱ. S3 가 정답 — 매 주기 S3 목록과 비교, 없거나 크기가 다른 것만 올린다** | 재시작·수동 개입 뒤에도 스스로 맞는다 | 주기마다 목록 요청 1번. 서버 권한에 «목록 보기» 가 필요하다(D7) | +| ㄴ. 로컬 기록 파일을 볼륨에 둔다 | 요청이 적다 | 기록과 S3 가 어긋날 수 있다(올리다 죽음 등) | + +**추천: ㄱ.** 올린 뒤에는 **크기 비교**로 확인한다. 내용 검증(체크섬)까지 할지는 D3-선택 — `aws s3 cp` 의 +체크섬 옵션이 우리가 쓸 CLI 버전에서 되는지 🟡 확인하지 않았다. + +### D4. 언제 지우나 ← 가장 위험한 곳 + +MySQL 8.0 은 `binlog_expire_logs_seconds`(기본 30일)가 지나면 binlog 를 지운다. 반출이 그보다 오래 멈춰 +있으면 **안 올린 파일이 지워지고, 그 뒤로는 백업 + binlog 체인이 끊긴다** — 다음 백업 전까지는 호스트를 +잃으면 마지막 백업까지밖에 못 돌아간다. + +| 후보 | 장점 | 대가 | +|---|---|---| +| ㄱ. 기본 자동 삭제 그대로 + 알림(D6)으로 막는다 | 설정 변경 없음, 디스크가 안 찬다 | 알림을 놓치면 체인이 조용히 끊긴다. «지워도 되나» 를 시간으로 판정한다 | +| **ㄴ. 자동 삭제를 끄고(`binlog_expire_logs_seconds=0`), 반출기가 S3 에 있다고 확인한 파일만 `PURGE BINARY LOGS TO` 로 지운다** | «지워도 되나» 를 «올렸나» 로 판정한다 | 반출이 멈추면 binlog 가 쌓여 **디스크가 찬다** — 위험이 «조용한 구멍» 에서 «시끄러운 장애» 로 바뀐다. 디스크 알림이 반드시 같이 있어야 한다 | + +**추천: ㄴ.** 조용한 구멍(나중에 복구할 때야 안다)보다 시끄러운 장애(디스크 알림이 운다)가 운영자 1명에게 +낫다. 실무 B·RDS 모두 «올린 것만 정리» 쪽이다. + +- 서버에 몇 개를 남길지(PURGE 를 어디까지 할지)는 🟡 미정 — 논리 사고 때 서버 안의 binlog 로 바로 + PITR 하려면 마지막 백업 이후 것은 남기는 게 편하다. «마지막 백업 시점 이전 + S3 확인됨» 을 지우는 안이 있다 +- 반출 계정 권한 (MySQL 8.0): `RELOAD`(FLUSH) · `REPLICATION CLIENT`(SHOW BINARY LOGS) · `BINLOG_ADMIN`(PURGE). + 앱 계정과 따로 둔다 + +### D5. 백업과 어떻게 잇나 + +복구 = 백업 + **그 백업이 기록한 binlog 위치부터** 끝까지 재생이다. XtraBackup 은 그 위치를 +`xtrabackup_binlog_info` 에 남긴다. + +정할 것 셋: + +1. **S3 경로에 서버 식별자를 넣는다** — `binlog//binlog.000123`. + 새 서버(복구 뒤)는 binlog 번호가 `000001` 부터 다시 시작한다. 경로에 서버 구분이 없으면 옛 서버 파일을 + 같은 이름으로 덮어쓴다 — WAL-G 에서 장애 조치 뒤 실제로 난 사고와 같은 모양이다 + ([wal-g #1636](https://www.github.com/wal-g/wal-g/issues/1636)). 후보: `server_uuid` vs 배포 때 정하는 이름 +2. **백업 옆에 시작 위치를 같이 둔다** — `backup/<시각>/` 안에 `xtrabackup_binlog_info` 를 그대로. + 복구 절차가 «가장 최근 백업 → 그 안의 위치 → 같은 서버 폴더의 binlog» 를 따라가게 +3. **번호가 빠짐없이 이어지는지 검사** — 반출할 때(빠지면 지표로 알림) vs 복구할 때(그때 알면 늦다). + **추천: 반출할 때.** 서버의 `SHOW BINARY LOGS` 와 S3 목록을 비교하면 공짜로 나온다 + +### D6. 멈춘 걸 어떻게 아나 + +반출이 멈춰도 서비스는 멀쩡히 돈다. 그래서 **알림이 없으면 아무도 모른다** — 이게 이 설계의 진짜 실패 모드다. + +내보낼 지표 (Percona 수집기와 같은 이름 짓기): + +| 지표 | 뜻 | 무엇을 잡나 | +|---|---|---| +| `binlog_shipper_last_success_timestamp_seconds` | 마지막으로 반출이 끝까지 성공한 시각 | 반출 멈춤 · 업로드 실패 계속 | +| `binlog_shipper_uploaded_total` | 올린 파일 수 | 돌고는 있는지 | +| `binlog_shipper_failures_total` | 실패 수 (단계별 라벨: flush · list · upload · purge) | 어디서 막혔는지 | +| `binlog_shipper_gap_files` | S3 에서 빠진 번호 수 (D5-3) | 체인 끊김 | +| 디스크 남은 양 | node-exporter 가 이미 낸다 | D4-ㄴ 의 대가 | + +내보내는 방법 후보: +- **ㄱ. node-exporter 의 textfile 수집기** — 반출기가 `.prom` 파일을 쓰고 node-exporter 가 읽는다. 셸에 맞다. + 지금 node-exporter 설정엔 이 수집기 경로가 없다(`docker-compose.yml:465`) +- ㄴ. 반출기가 작은 HTTP 로 직접 내보낸다 — 셸로는 번거롭다 +- ㄷ. Pushgateway — 컨테이너 하나 더 + +**추천: ㄱ.** 🔴 다만 **운영 compose 에는 Prometheus 자체가 없다**(§1). 이 지표를 누가 긁고 누가 휴대폰으로 +알리느냐는 모니터링 점검(#673~680)의 구멍과 같은 구멍이다 — 이 문서에서 따로 메우지 않고 거기로 넘긴다. + +**알림 기준(«몇 초 늦으면 울리나»)은 정하지 않는다.** 근거 없는 숫자를 박지 않는다. 후보 형태만 적는다: +«지금 − 마지막 성공 > 반출 주기 × k» — k 는 반출 주기(Q4)가 정해진 뒤 «몇 번 연속 실패까지 봐주나» 로 정한다. + +### D7. S3 를 어떻게 지키나 + +`rpo-rto-targets.md` 의 호스트 상실에는 «EBS 고장» 만이 아니라 **`compose down -v`·랜섬웨어** 가 들어 있다. +서버가 S3 에서 지울 수 있으면, 서버를 망가뜨린 같은 사고가 S3 도 지운다. + +| 후보 | 막는 것 | 대가 | +|---|---|---| +| ㄱ. 서버 권한: 올리기 · 목록만 (지우기 없음) | 서버에서 직접 지우기 | 같은 이름 덮어쓰기는 못 막는다(올리기 권한이 덮어쓰기도 한다) | +| **ㄴ. ㄱ + 버킷 버전 관리** | 덮어써도 옛 버전이 남는다 | 옛 버전도 요금. 정리는 수명주기 규칙으로 | +| ㄷ. ㄴ + Object Lock (보존 기간 동안 아무도 못 지움) | 관리자 계정이 털려도 | 보존 기간(Q6)이 정해져야 켤 수 있다. 잘못 걸면 우리도 못 지운다 | + +**추천: ㄴ.** ㄷ 은 보존 기간(Q6)이 정해진 뒤 다시 본다. + +같이 정할 것: +- **보존 기간 = S3 수명주기 규칙**으로 구현한다(서버가 지우지 않으니 서버 밖에서 지워야 한다). 숫자는 Q6 +- **암호화**: SSE-S3(기본, 추가 비용 없음) vs KMS(키 접근 기록이 남음, 요청 요금). 🟡 미결 — 우리 데이터에 + KMS 를 요구하는 규정은 확인하지 않았다 +- **자격증명**: 운영 호스트가 EC2 면 인스턴스 프로파일(키가 서버에 없음), 아니면 액세스 키를 비밀로 둔다. + 호스트가 정해질 때 고른다 +- **리전**: 같은 리전에서만 쟀다. 리전 장애는 범위 밖으로 둔다(리전이 통째로 죽는 경우까지 대비하는 건 + 리플리카보다도 뒤다) + +### D8. 직접 만들까, 도구를 쓸까 + +| 후보 | 장점 | 대가 | +|---|---|---| +| **ㄱ. 직접 — 측정용 `shipper()` 를 운영용으로** | 이미 EC2 에서 검증된 경로. 면적이 작다(셸 + 지표 파일). 동작을 끝까지 설명할 수 있다 | 재시도·검증·지표를 직접 짜야 한다. 버그도 우리 몫 | +| ㄴ. WAL-G (`binlog-push`) | 체인 관리·복구 명령이 이미 있다 | 문서가 MySQL 인터페이스를 «불안정» 이라 밝힘. 장애 조치 뒤 덮어쓰기([#1636](https://www.github.com/wal-g/wal-g/issues/1636)), 재시작 뒤 메모리 부족([#1665](https://www.github.com/wal-g/wal-g/issues/1665)) 보고. 8.0.46 에서 확인 안 됨 | +| ㄷ. Percona Operator | 실무 B 그 자체 | 쿠버네티스 전용 — 우리는 compose. 탈락 | + +**추천: ㄱ.** 서버 1대·장애 조치 없음이면 WAL-G 가 해결해 주는 어려운 부분(primary 가 바뀔 때 어디서 +이어 올리나)이 우리에겐 없다. 리플리카가 들어오면 다시 본다(§8). + +--- + +## 5. 추천안을 합치면 — 동작 순서 + +``` +[사이드카 binlog-shipper] (mysql_data :ro, 반출 전용 MySQL 계정, S3 올리기·목록 권한) + +주기마다: + 1. FLUSH BINARY LOGS 실패 → failures{stage=flush}++, 다음 주기 + 2. SHOW BINARY LOGS → 닫힌 파일 목록 (마지막 줄 = 지금 쓰는 파일, 제외) + 3. S3 binlog// 목록 실패 → failures{stage=list}++, 다음 주기 + 4. 닫힌 파일 중 S3 에 없거나 크기가 다른 것을 번호 순서대로 올림 + 각각 올린 뒤 크기 확인. 하나라도 실패 → failures{stage=upload}++, 그 뒤 번호는 이번엔 안 올림 + 5. 번호 빠짐 검사 → gap_files + 6. «S3 확인됨 + 마지막 백업 이전» 인 가장 큰 번호까지 PURGE BINARY LOGS TO + 7. 1~4 가 다 성공했으면 last_success_timestamp = 지금 + 8. 지표 파일(.prom) 원자적 교체 (임시 파일 → mv) + +SIGTERM (정상 종료) → 마지막으로 1~4 한 번 더 +``` + +- 4 에서 «번호 순서대로, 실패하면 거기서 멈춤» 인 이유: 뒤 번호만 올라가고 앞 번호가 빠지면 복구할 때 + 거기서 끊긴다. 앞이 없으면 뒤는 쓸모가 없다 +- 6 의 «마지막 백업 이전» 은 D4 의 🟡 미정 항목을 추천안대로 채운 것이다 + +**복구 쪽** (구성 2 의 복구 절차 — 이 문서는 반출기가 그 절차에 필요한 것을 남기는지만 본다): +가장 최근 `backup/<시각>/` → 그 안의 `xtrabackup_binlog_info` 의 파일·위치 → +`binlog/<그 서버 uuid>/` 에서 그 파일부터 끝까지 → `mysqlbinlog --start-position` 으로 재생. +이건 [호스트 상실 실측](../../loadtest/results/hostloss-rpo-2026-11-07/README.md) 이 7판 모두 끝까지 해 본 경로다. + +--- + +## 6. 실패 모드 — 무엇이 어떻게 드러나나 + +| 무엇이 잘못됨 | 추천안에서 어떻게 되나 | 누가 알아채나 | +|---|---|---| +| S3 일시 장애 · 네트워크 끊김 | 그 주기는 실패, 다음 주기에 밀린 것까지 올림 (D3-ㄱ) | 오래가면 last_success 지표 | +| 반출기 컨테이너 죽음 | compose 가 다시 띄움, S3 목록으로 이어서 올림 | 재시작 반복이면 last_success | +| 반출기가 오래 멈춤 | binlog 가 서버에 쌓임 (자동 삭제 꺼짐) | 디스크 지표 → 🔴 알림 필요 | +| 자격증명 만료 · 권한 바뀜 | 매 주기 실패 | failures{stage=upload} · last_success | +| 중간 번호 하나가 계속 안 올라감 | 그 뒤 번호도 멈춤(순서대로) — 숨지 않는다 | last_success · gap_files | +| 서버 통째로 상실 | 반출기도 같이 멈춤. 마지막 반출까지는 S3 에 있음 | (구성 2 의 정의 — 잃는 양 = 주기 + 업로드) | +| 서버에서 S3 를 지우려 함 (사고·공격) | 권한 없음. 덮어쓰기는 버전으로 남음 | S3 쪽 기록 | +| 복구 뒤 새 서버가 번호 1부터 다시 | 다른 서버 폴더로 감 (D5-1) | — | +| 🔴 **알림 경로 자체가 없음** | 위 «지표» 칸이 전부 아무에게도 안 간다 | **지금 상태.** #673~680 | + +--- + +## 7. 재야 할 것 · 확인 안 한 것 + +| 무엇 | 왜 | 어디서 | +|---|---|---| +| FLUSH 가 쓰기를 막는 시간 | D2 의 대가. 프로세스 죽음 RTO 19초와 같은 한도에 걸린다 | 로컬로 메커니즘은 확인 가능, 절대값은 EC2 | +| 반출 중 사이드카의 CPU·메모리 | 같은 서버에 MySQL 과 동거한다 | 가정 피크 부하에서 | +| `aws s3 cp` 체크섬 옵션 | D3 내용 검증을 할지 | CLI 버전 확인 | +| `binlog_expire_logs_seconds=0` + PURGE 경로 | D4-ㄴ 이 실제로 «올린 것만» 지우는지 | 로컬 | +| 반출 → 복구를 운영 설정 그대로 처음부터 끝까지 | `rpo-rto-targets.md` 의 «복구 훈련 한 번» 과 같은 일 — 같이 하면 된다 | EC2 | + +--- + +## 8. 다시 여는 조건 + +- **리플리카(구성 3)가 들어오면** — primary 가 바뀔 수 있다. 반출을 어느 서버에서 이어 할지, 위치를 + GTID 로 바꿀지(D5), WAL-G 같은 도구가 다시 후보가 된다(D8) +- **반출 주기를 아무리 줄여도 Q4 의 RPO 를 못 맞추면** — `rpo-rto-targets.md` R5. 갈래 C(실시간 받기)로 +- **쓰기량이 커서 업로드가 주기를 잡아먹으면** — 최대 속도로 쓸 때 1GB 파일이 2.3~12초 걸렸다 + ([실측 §2-2](../../loadtest/results/server-prep-and-ship-cost-2026-11-08/README.md)). 그 신호가 보이면 D2 를 다시 +- **운영 호스트가 쿠버네티스로 정해지면** — 갈래 B 를 Operator 로 받는 게 맞다(D8-ㄷ) + +--- + +## 9. 의식적으로 안 할 것 + +- **다른 리전 복제** — 리전 장애는 리플리카보다도 뒤의 문제다 +- **반출기 이중화** — 반출기가 죽어도 다시 뜨면 S3 목록으로 이어서 올린다. 그 사이 잃는 건 «주기 + 재시작 + 시간» 뿐이고, 그 동안 서버까지 잃을 확률을 위해 반출기를 두 개 두지 않는다 +- **알림 경로를 이 문서에서 만들기** — 지표까지만 만든다. 알림은 모니터링 쪽에서 한 번에 + +--- + +## 결정 로그 + +- 2026-11-11: 분기점 초안. 사용자와 실무 세 갈래(관리형 · 서버 옆 업로더 · 실시간 받기)를 비교하고 + «갈래 B 를 직접 구현, 우리 조건에 맞춘 열을 추천안으로» 라는 방향을 들은 뒤 작성. 항목 D1~D8 은 후보와 + 추천까지이고 채택 표시는 없다. 숫자(반출 주기 Q4 · 백업 주기·보존 기간 Q6)는 `rpo-rto-targets.md` 에 남긴다.