DBTrail 첫 사용기

Devmo·4일 전

WHERE 빠진 UPDATE 한 번이면 복구가 꽤 번거롭습니다. 백업을 다른 서버에 올리고, 사고 직전까지 binlog 돌리고, 필요한 행만 뽑아서 다시 넣고. 그 사이에 정상적으로 바뀐 데이터랑 안 섞이게 맞추는 것도 일입니다. 망가진 행만 딱 되돌리는 방법은 없나 찾다가 DBTrail을 보게 됐고, 로컬 Docker로 한번 돌려 봤습니다.

🧐 DBTrail은 뭐 하는 도구인가

📌 한 줄로

MySQL binlog(PostgreSQL은 WAL)를 복제 프로토콜로 계속 읽으면서, 모든 INSERT / UPDATE / DELETE의 변경 전 값, 변경 후 값을 별도 인덱스 DB에 쌓아 두는 도구입니다. 사고가 나면 DB 전체를 복원하지 않고 망가진 행만 되돌리는 undo SQL을 만들어 줍니다.

  • 라이선스: Apache-2.0 (상업용도 가능)
  • 지원 DB: MySQL 8.0+ (RDS, Aurora 포함), PostgreSQL, MariaDB(alpha)
  • CLI는 bintrail, 웹 콘솔은 bintrail-console

DB Trail remembers every row change: searchable, reversible, yours.
(dbtrail README)

⚖️ 기존 방식(PITR)이랑 뭐가 다른지

UPDATE orders SET amount = 0를 WHERE 없이 날렸다고 치면 흐름이 이렇게 갈립니다.

항목백업 + binlog 재생 (PITR)DBTrail
작업 단위DB 전체사고 난 행만
소요 시간수십 분 ~ 수 시간 (DB 크기에 비례)몇 분
사고 이후 정상 변경되살린 데이터와 섞여서 따로 맞춰야 함건드리지 않음
추가 자원복원용 서버, 디스크인덱스용 MySQL 하나

🧰 주요 기능

1. 복구

필터로 대상 행을 고르면 undo SQL을 만들어 줍니다. 콘솔이 SQL을 직접 실행하는 일은 없고, 사람이 보고 직접 돌리는 구조입니다. ON DELETE CASCADE로 같이 날아간 자식 행까지 살리는 recover-cascade도 있습니다.

2. 변경 이력 조회

언제, 어떤 행이, 뭐에서 뭐로 바뀌었는지랑 그걸 바꾼 원래 SQL(query_text)까지 남습니다. 누가 바꿨는지(DB 사용자, 호스트, 클라이언트 프로그램)는 상용 버전(EE)에만 있습니다.

3. Time-travel

SELECT * FROM orders WHERE id = 1 AS OF '5 minutes ago' 이런 식으로 과거 시점 행을 조회하는 기능입니다. 출발점으로 백업(baseline)이 있어야 합니다.

4. 그 외

verify(복구 결과가 원본이랑 맞는지 검증), status(이력에 빈 구간 있는지), MCP 연동(Claude Desktop 같은 데서 자연어로 이력 조회), S3 Parquet 아카이브 정도.

📌 백업 대용은 아닙니다.
디스크가 나가거나 DB가 통째로 날아가는 건 여전히 백업으로 막아야 하고, 레플리카나 HA 용도도 아닙니다. 사람이 친 실수를 되돌리는 쪽에 맞춰진 도구입니다.

🗄️ 저장은 어떻게 하나

처음엔 원본 DB를 통째로 복제해 두는 줄 알았는데 아니었습니다. 모든 테이블의 변경이 인덱스 DB의 binlog_events 테이블 하나에 이벤트 단위로 쌓입니다. binlog 파일을 그대로 들고 있는 것도 아니고, 해석해서 검색하기 좋은 행 단위 레코드로 바꿔서 넣습니다.

컬럼내용
event_type1 = INSERT, 2 = UPDATE, 3 = DELETE
pk_values대상 행의 PK (복합키는 42\|7 형태)
changed_columnsUPDATE에서 실제로 바뀐 컬럼 목록
row_before / row_after컬럼 이름이 붙은 JSON. INSERT는 after만, DELETE는 before만
query_text변경을 만든 원래 SQL 문장

컬럼 이름을 붙이려고 테이블 구조(스키마 스냅샷)만 따로 저장해 둡니다. 그래서 한계가 두 개 있습니다.

  • 등록 이후 변경만 기록됩니다. 등록 전부터 있던 데이터는 이력에 없고, 처음 바뀔 때 row_before에 직전 값이 행 전체로 남는 정도입니다.
  • 이벤트만으로는 "특정 시점의 테이블 전체"를 못 만듭니다. 그러려면 baseline(mydumper로 뜬 전체 덤프)을 켜야 하고, 실제 데이터 사본은 이때만 생깁니다.

보관 기간은 기본 30일이고, 그 뒤는 S3에 Parquet로 아카이브할 수 있습니다.


🛠️ 사용법

🧩 구성

로컬에 바이너리 까는 게 싫어서, 공식 install.sh가 안에서 쓰는 docker-compose.yml에서 필요한 서비스만 가져오고 감시할 MySQL을 하나 붙였습니다.

  • 소스 DB에는 읽기만 합니다. 쓰기, 락, 스키마 변경 없음.
  • 서버를 등록할 때마다 인덱스 MySQL에 bintrail_idx_<id>라는 전용 DB가 생깁니다. 기본으로 만든 bintrail_index에 쌓이는 줄 알고 한참 찾았습니다.

🐳 1. docker-compose 작성

x-bintrail-compose-version: &compose_version 1

volumes:
  source-data:
  bintrail-index-data:
  bintrail-state:

services:
  # ── 감시 대상(소스) MySQL ────────────────────────────────────────────
  source-mysql:
    container_name: dbtrail-source-mysql
    image: mysql:8.4.9
    command:
      - "--server-id=1"
      - "--log-bin=mysql-bin"
      - "--binlog-format=ROW"
      - "--binlog-row-image=FULL"
      - "--gtid-mode=ON"
      - "--enforce-gtid-consistency=ON"
      - "--binlog-rows-query-log-events=ON"   # 원래 SQL 문장도 binlog에 기록 (dbtrail query_text)
      - "--binlog-row-metadata=FULL"          # 행 이벤트에 컬럼명 포함 (스키마 드리프트 감지)
      - "--mysql-native-password=ON"          # 콘솔 내장 mydumper(arm64)가 caching_sha2_password 미지원 → dbtrail 계정용
    environment:
      MYSQL_ROOT_PASSWORD: rootpw
    ports:
      - "127.0.0.1:3306:3306"
    volumes:
      - source-data:/var/lib/mysql
      - ./source-init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -prootpw --silent"]
      interval: 5s
      timeout: 5s
      retries: 30
      start_period: 20s
    restart: unless-stopped

  # ── dbtrail 인덱스 저장소 ────────────────────────────────────────────
  index-mysql:
    container_name: dbtrail-index-mysql
    image: mysql:8.4.9
    command:
      - "--innodb-flush-log-at-trx-commit=2"
      - "--sort-buffer-size=4M"
      - "--max-allowed-packet=1G"
      - "--skip-log-bin"
    environment:
      MYSQL_ROOT_PASSWORD: indexpw
      MYSQL_DATABASE: bintrail_index
    ports:
      - "127.0.0.1:3307:3306"   # 공부용: 인덱스 테이블(binlog_events 등) 직접 조회
    volumes:
      - bintrail-index-data:/var/lib/mysql
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -pindexpw --silent"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s
    restart: unless-stopped

  # ── dbtrail 본체 + 웹 콘솔 ───────────────────────────────────────────
  bintrail:
    container_name: dbtrail-console
    image: ghcr.io/dbtrail/bintrail-console:${BINTRAIL_TAG:-latest}
    ports:
      - "127.0.0.1:${CONSOLE_PORT:-18090}:8090"
    environment:
      # 비워 두면 콘솔의 "+ Add server"로 직접 등록합니다 (README 참고).
      SOURCE_DSN: ${SOURCE_DSN:-}
      SCHEMAS: ${SCHEMAS:-}
      INDEX_DSN: "root:indexpw@tcp(index-mysql:3306)/bintrail_index"
      BINTRAIL_INDEX_DATADIR_RO: /var/lib/bintrail-index-ro
      BINTRAIL_CONSOLE_LISTEN: 0.0.0.0:8090
      BINTRAIL_CONSOLE_SERVERS: /var/lib/bintrail/console-servers.yaml
      BINTRAIL_CONSOLE_ARCHIVE_STAGING: /var/lib/bintrail/archives
      BINTRAIL_CONSOLE_AUTH: /var/lib/bintrail/console-auth.yaml
      BINTRAIL_CONSOLE_MCP_TOKEN_FILE: /var/lib/bintrail/console-mcp-token.yaml
      BINTRAIL_CONSOLE_ALLOW_SETUP: "1"
      # 백업(baseline) / 검증: 콘솔의 Create backup, Verification 버튼 활성화
      BINTRAIL_CONSOLE_BASELINE_TRIGGER: "1"
      BINTRAIL_CONSOLE_BASELINE_LOCK_MODE: ftwrl          # 스냅샷 순간 전역 락 (BACKUP_ADMIN 필요)
      BINTRAIL_CONSOLE_BASELINE_STAGING: /var/lib/bintrail/baseline-staging
      BINTRAIL_CONSOLE_VERIFY_TRIGGER: "1"
      BINTRAIL_COMPOSE_VERSION: *compose_version
      BINTRAIL_TELEMETRY: ${BINTRAIL_TELEMETRY:-off}
    volumes:
      - bintrail-state:/var/lib/bintrail
      - bintrail-index-data:/var/lib/bintrail-index-ro:ro
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        set -e
        # 콘솔은 Backup dir을 직접 만들지 않으므로 미리 생성 (Backup settings의 Backup dir과 일치시킬 것)
        mkdir -p /var/lib/bintrail/baselines/dbtrail-source-mysql
        if [ -n "$$SOURCE_DSN" ] && [ -n "$$SCHEMAS" ]; then
          exec bintrail-console watch --source-dsn "$$SOURCE_DSN" --index-dsn "$$INDEX_DSN" --schemas "$$SCHEMAS"
        elif [ -n "$$SOURCE_DSN" ]; then
          exec bintrail-console watch --source-dsn "$$SOURCE_DSN" --index-dsn "$$INDEX_DSN"
        fi
        exec bintrail-console watch --index-dsn "$$INDEX_DSN"
    healthcheck:
      test: ["CMD-SHELL", "wget -q -T 5 -O /dev/null http://127.0.0.1:8090/api/healthz"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    depends_on:
      index-mysql:
        condition: service_healthy
      source-mysql:
        condition: service_healthy
    restart: unless-stopped
소스 설정필수 여부이유
binlog_format=ROW, binlog_row_image=FULL필수행 전체의 전후 값이 있어야 undo SQL을 만들 수 있음
binlog_rows_query_log_events=ON권장원래 SQL 문장이 query_text에 남음
binlog_row_metadata=FULL권장행 이벤트에 컬럼명이 들어가서 스키마 드리프트 감지 가능
테이블마다 PRIMARY KEY권장사전 점검 항목. 행을 특정할 키가 필요

dbtrail 계정 권한은 스트리밍용, 백업용 두 줄로 나뉩니다.

-- source-init/01-init.sql
CREATE USER 'dbtrail'@'%' IDENTIFIED WITH mysql_native_password BY 'dbtrailpw';
GRANT REPLICATION SLAVE, REPLICATION CLIENT, SELECT ON *.* TO 'dbtrail'@'%';
GRANT RELOAD, BACKUP_ADMIN, SHOW VIEW ON *.* TO 'dbtrail'@'%';

-- 데모용 DB
CREATE DATABASE shop;

CREATE TABLE shop.test (
  id    INT PRIMARY KEY AUTO_INCREMENT,
  test  VARCHAR(50)  NOT NULL
);

스트리밍만 할 거면 첫 번째 GRANT로 충분하고, 두 번째는 백업(baseline)이랑 Time-travel 쓸 때 필요합니다. mysql_native_password로 만든 이유는 아래 트러블슈팅에 있습니다.

💡 의미 없는 빈 테이블(test) 만드는 이유

테이블이 하나도 없는 스키마는 등록이 안 됩니다.
등록할 때 스키마 스냅샷을 찍어야 해서 그렇습니다. 그래서 테이블은 init SQL로 먼저 만들어 두고 데이터는 등록 후에 넣었습니다.

docker compose up -d

🖥️ 2. 콘솔에서 감시 대상 서버 등록

http://127.0.0.1:18090에 처음 들어가면 콘솔 계정부터 만들고, + Add server로 소스 DB를 등록합니다.

폼 아래에 소스 계정용 GRANT 문이 나와 있어서 그대로 복사해 쓰면 됩니다. RDS나 Aurora처럼 BACKUP_ADMIN을 못 주는 환경용 대안(LOCK TABLES)도 주석으로 적혀 있습니다.

📌 Host에는 127.0.0.1 말고 source-mysql을 넣어야 합니다.
콘솔이 컨테이너 안에서 돌기 때문에, 콘솔 입장에서 localhost는 자기 자신입니다. compose 서비스명을 써야 붙습니다.

Save를 누르면 사전 점검(preflight)이 돕니다. 결과는 통과, 경고(진행은 됨), 실패(등록 불가), 건너뜀 네 가지로 나옵니다.

등록 뒤에 있는 Test 버튼은 연결 확인용입니다. 사전 점검을 다시 돌리는 건 아니고 응답 시간이랑 MySQL 버전 정도만 보여 줍니다.

🔍 3. 샘플 데이터 넣고 이벤트 확인

RUNNING 뜬 다음에 DBeaver로 고객 5명, 주문 8건을 넣었습니다. 등록 이후 변경이니까 INSERT 13건이 전부 Events에 보입니다.

Overview 맨 위 Restore coverage에 지금 어느 구간까지 되돌릴 수 있는지가 나옵니다. 이벤트 기준으로는 인덱스가 생긴 뒤부터, 테이블 전체 기준으로는 첫 백업(06:09:52) 이후부터라고 따로 보여 줍니다.

Events 화면에서는 type:UPDATE, pk:4, col:amount, shop.orders 같은 걸로 검색할 수 있고, 행마다 변경 전후 diff가 붙어 있습니다.

행을 펼치면 컬럼별로 before(빨강), after(초록)가 나오고, 오른쪽 아래 Undo this change를 누르면 그 이벤트 하나만 되돌리는 SQL이 만들어집니다. 여기서도 만들기만 하고 실행은 안 합니다.

위에서 만들어진 sql 문을 직접 DB 에 실행을 하면 됩니다.

인덱스 DB를 직접 열어 보면 콘솔에서 본 게 그대로 binlog_events에 들어 있습니다. 아래는 orders 4번 행 하나의 이력 전체입니다.

changed_columns만 봐도 뭐가 바뀌었는지 알 수 있고, 한 이벤트의 row_after가 다음 이벤트의 row_before로 이어지는 구조입니다.

query_text에는 클라이언트가 붙인 주석까지 같이 남습니다. DBeaver에서 날린 쿼리는 ApplicationName=DBeaver ... <Script-8.sql>까지 찍혀서, 어느 툴의 어느 스크립트 탭에서 실행했는지도 보입니다.

🚑 4. 사고 재현 -> 복구

시나리오 A: WHERE 빠진 UPDATE

UPDATE orders SET amount = 0;

주문 8건 금액이 전부 0원이 됩니다. 복구는 이런 순서로 갑니다.

Restore 화면에서 Schema shop, Table orders, 시간 범위(UTC)를 넣고 Preview rows로 대상 확인, 그다음 Generate undo SQL.

나온 SQL 을 분석해보면, BEGIN / COMMIT으로 묶여 있고 최신 변경부터 거꾸로 되돌립니다. 문장마다 원본 이벤트 번호, 시각, GTID가 주석으로 달려 있고, 바뀐 컬럼만이 아니라 행 전체를 변경 전 값으로 덮어씁니다.

-- Generated by bintrail recover at 2026-09-19 07:55:36 UTC
-- Events to reverse: 1
-- IMPORTANT: Review carefully before applying to production.
-- NOTE: applying this script fires the target's own triggers (e.g. AFTER INSERT/UPDATE),
--       which can double-apply side effects the original triggers already logged as their
--       own events (reversed above only if this script's filters cover the tables those
--       triggers write). AUTO_INCREMENT/serial counters are NOT restored by this script.
--       See docs/query-and-recovery.md -> Restore limitations.

BEGIN;
SET time_zone = '+00:00';
SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION';

-- [21] reverse UPDATE on shop.orders pk=8 at 2026-09-18 20:01:54 gtid=520ab2fc-...:15
UPDATE `shop`.`orders` SET `amount` = 25000, `created_at` = '2026-09-18 19:03:39', `customer_id` = 5, `id` = 8, `product` = '충전기', `status` = 'DELIVERED' WHERE `id` = 8;
-- [20] reverse UPDATE on shop.orders pk=7 ...
...
COMMIT;

스크립트 맨 위 주석의 의미는 아래와 같습니다.

  • 대상 테이블에 트리거가 있으면 undo SQL 돌릴 때 트리거도 다시 돌아서, 부수 효과가 두 번 들어갈 수 있음
  • AUTO_INCREMENT 카운터는 안 돌려놓음

DB 에서 실행하면 금액이 원래대로 돌아옵니다. 되돌린 UPDATE도 binlog에 남아서, 위 이력 캡처의 26번 이벤트(0 -> 350000)처럼 복구한 기록까지 이력에 쌓입니다.

시나리오 C: ON DELETE CASCADE

테이블끼리 연관관계가 ON DELETE CASCADE 와 같이 걸려있을 때의 상황입니다.

DELETE FROM customers WHERE grade = 'VIP';

VIP 고객 3명(id 1, 2, 5)이 지워지고, FK ON DELETE CASCADE 때문에 그 고객들 주문 5건도 같이 지워집니다. 근데 인덱스를 보면 customers 삭제 3건만 있고 orders 삭제는 하나도 없습니다.

InnoDB가 CASCADE를 binlog보다 아래 단계에서 처리하기 때문이고, MySQL 쪽에도 오래된 이슈로 올라가 있습니다. (MySQL Bug #32506 - Foreign key cascades do not appear when binlog_format = 'ROW')

binlog만 보는 도구였으면 부모(customers)만 살리고 자식(orders)은 빠진 SQL이 나왔을 겁니다. DBTrail은 등록할 때 FK 관계를 인덱스 DB의 fk_constraints 테이블에 저장해 두고, 복구할 때 "cascade-aware"로 동작합니다. Restore에서 customers 삭제 시각 범위로 undo SQL을 뽑으면 이렇게 나옵니다.

  • 맨 위에 MySQL Bug 32506을 직접 언급하면서, CASCADE로 지워진 자식 행 5건을 다시 만든다고 적혀 있습니다.
  • 자식 INSERT가 부모 INSERT보다 먼저 나오다 보니 스크립트 전체를 SET FOREIGN_KEY_CHECKS=0으로 감쌉니다.
  • INCOMPLETE RECOVERY 경고도 같이 붙습니다. shop.orders에 백업(baseline)이 없어서, 조회 구간 안에서 한 번도 안 바뀐 자식 행은 못 살린다는 뜻입니다. 백업이 있으면 거기서 자식 행을 가져오는 "Phase-2 baseline fallback"이 돈다고 합니다.

이번 데모는 자식 행이 전부 등록 후에 INSERT된 거라 이력이 있었고, 5건 다 복원됐습니다.

-- [0] reverse DELETE on shop.orders pk=8 at 2026-09-18 20:08:09
INSERT INTO `shop`.`orders` (`amount`, `created_at`, `customer_id`, `id`, `product`, `status`) VALUES (25000, '2026-09-18 19:03:39', 5, 8, '충전기', 'DELIVERED');
-- [35] reverse DELETE on shop.customers pk=5 at 2026-09-18 20:11:36 gtid=520ab2fc-...
INSERT INTO `shop`.`customers` (`email`, `grade`, `id`, `name`) VALUES ('woosung@example.com', 'VIP', 5, '정우성');

부모 행에는 실제 이벤트 번호([35])가 붙는데 자식 행은 [0]입니다. 시각도 삭제된 시각이 아니라 그 행이 마지막으로 바뀐 시각(20:08:09, 시나리오 A 복구했을 때)이고요. binlog에 없는 걸 이력에서 추론해서 만든 거라 번호가 이렇게 찍힙니다.

⚠️ 결국 추론입니다.
자식 행이 등록 이후 한 번도 안 바뀌었고 백업도 없으면 살릴 방법이 없습니다. 콘솔이 경고는 띄워 주지만, 확실하게 하려면 CASCADE를 RESTRICT로 바꾸고 애플리케이션에서 자식을 직접 DELETE하는 게 맞습니다. 그래야 binlog에 남습니다.

⏪ 5. 한 행의 과거 보기 (Row at a point in time)

콘솔은 Restore 화면 하나에 위쪽 "undo SQL 생성" 카드랑 아래쪽 "Row at a point in time" 카드가 같이 있습니다. 아래쪽이 Time-travel입니다.

버튼동작
Show stateAt 시각에 그 행이 어떤 값이었는지 (삭제된 상태였는지 포함)
Show history백업 시점 값 -> 변경1 -> 변경2 ... 타임라인

Schema shop, Table orders, PK 4 넣고 Show history를 누르면 모달로 타임라인이 뜹니다.

맨 위가 BASELINE(백업 시점 값)이고, 그 밑으로 UPDATE마다 행 전체가 나오는데 바뀐 컬럼에만 밑줄이 쳐집니다. 단계마다 Restore to this state 버튼이 있어서, 원하는 시점을 누르면 그 상태로 돌리는 SQL이 나옵니다.

백업이 출발점이라 At은 첫 백업 시각 이후로 넣어야 합니다. 그 전 시각을 넣으면 출발점이 없어서 안 나옵니다.

여러 번 바뀐 행을 특정 시점으로 돌릴 땐 이 화면이 제일 편했습니다. 위 캡처에서 amount = 999999가 되기 직전으로 돌리고 싶으면 세 번째 단계(06:13:04, status = DELIVERED)에서 Restore to this state를 누르면 됩니다. 45번, 46번 이벤트는 같은 초(06:13:04)에 일어나서 Since / Until로는 못 나누는데, 타임라인에서 고르면 그런 문제가 없습니다. 위 카드의 Latest per row(행마다 최근 N개 변경만 되돌림)로도 비슷하게 할 수 있습니다.

🔧 트러블슈팅

1. Backups 화면에 no such file or directory

Backup settings에서 Backup dir을 지정해도 콘솔이 폴더를 안 만들어 줍니다. compose 시작 명령에 mkdir -p를 넣어서 해결했습니다. 아예 Create backup 버튼이 안 보이면 BINTRAIL_CONSOLE_BASELINE_TRIGGER=1이 빠진 겁니다.

2. Create backup 실패: Plugin caching_sha2_password could not be loaded

Plugin caching_sha2_password could not be loaded: ... libmariadb3/plugin/caching_sha2_password.so

콘솔 안에 들어 있는 mydumper가 MariaDB 클라이언트 라이브러리로 빌드돼 있는데, arm64(Apple Silicon) 이미지엔 이 인증 플러그인 파일이 없는 걸로 보입니다. 스트리밍(Go 드라이버)은 멀쩡하고 백업만 실패했습니다. 소스에 --mysql-native-password=ON을 켜고 dbtrail 계정을 mysql_native_password로 만들어서 일단 넘어갔는데, TLS로 붙이거나 amd64 이미지에서도 같은 문제가 나는지는 따로 봐야 할 것 같습니다.


📝 사용 후기

👍 장점

1. 행 단위로 복구합니다.

사고 난 행만 골라서 undo SQL을 만듭니다. PITR처럼 복원용 서버를 따로 띄울 필요가 없고, 복구 대상이 아닌 행에 사고 이후 들어온 변경은 건드리지 않습니다.

2. 소스 DB엔 읽기 권한만 있으면 됩니다.

스트리밍에 필요한 건 REPLICATION SLAVE, REPLICATION CLIENT, SELECT뿐입니다. 콘솔은 SQL을 실행하지 않고, 스크립트는 BEGIN / COMMIT으로 묶여서 문장마다 원본 이벤트 번호, 시각, GTID가 붙어 나옵니다.

3. 행 단위로 변경 이력을 볼 수 있습니다.

이벤트마다 changed_columns, row_before, row_after, query_text가 저장되고, query_text엔 클라이언트 주석(DBeaver 스크립트 이름 같은 것)까지 들어갑니다.

4. FK CASCADE 삭제도 복구 대상에 들어갑니다.

binlog에 안 남는 CASCADE 삭제에 대해 등록할 때 preflight 경고를 주고, 복구할 땐 cascade-aware undo SQL로 자식 행을 다시 만듭니다. 이력도 백업도 없어서 못 살리는 행이 있으면 INCOMPLETE RECOVERY 경고가 뜹니다.

5. 한 행의 복구 시점을 이벤트 단위로 고를 수 있습니다.

Row history 타임라인의 Restore to this state로 단계를 찍을 수 있어서, 같은 초에 일어난 변경도 나눠서 되돌릴 수 있습니다.

6. Docker compose로 올릴 수 있습니다.

소스 DB 빼면 인덱스 MySQL이랑 콘솔 컨테이너 두 개가 전부입니다.

👎 단점

1. 등록 전 데이터는 이력이 없습니다.

이력은 서버 등록 시점부터 쌓입니다. 테이블 전체를 특정 시점으로 돌리거나 Row at a point in time을 쓰려면 baseline 백업을 따로 설정해야 합니다.

2. 운영할 인덱스 DB가 하나 더 생깁니다.

모든 행 변경의 전후 값을 JSON으로 저장하니까 인덱스 DB 용량은 소스 쓰기량에 비례해서 늘어납니다. 기본 보관은 30일이고, 그 이상 남기려면 S3 아카이브를 설정해야 합니다.

3. 누가 바꿨는지는 상용 버전(EE) 기능입니다.

오픈소스 버전엔 DB 사용자, 호스트, 클라이언트 프로그램 정보가 없습니다. 앱 커넥션 풀에서 실행된 쿼리는 query_text만 보고는 누가 했는지 알 수 없습니다.

4. 문서랑 실제 이미지 화면이 다릅니다.

GitHub main 브랜치 문서엔 Recover 화면이 따로 있는데, 0.83.0 이미지에선 Restore 화면에 합쳐져 있습니다.

5. arm64 이미지에서 백업(mydumper)이 MySQL 기본 인증을 지원하지 않습니다.

caching_sha2_password 플러그인 파일이 없어서 백업이 실패하고, mysql_native_password 계정으로 돌아가야 합니다. 스트리밍, 복구는 영향 없습니다.

6. 시간 입력, 표시가 UTC 고정입니다.

Since / Until, At 입력값이랑 event_timestamp가 다 UTC입니다. KST랑 9시간 차이.

7. undo SQL로 못 돌리는 게 있습니다.

생성된 스크립트 머리말에 적힌 제약입니다. 대상 테이블의 트리거가 undo SQL 실행 때 다시 돌고, AUTO_INCREMENT 카운터는 복원되지 않습니다.

🏁 정리

행 단위로 되돌리는 방식 자체는 좋았습니다. WHERE 빠진 UPDATE 같은 실수에 PITR보다 훨씬 가볍게 대응할 수 있고, CASCADE처럼 binlog에 안 남는 부분까지 커버가 되기 때문에 분명히 좋은 툴 입니다.

다만 결론적으로 실무에 바로 쓰기는 아직 어려울 것 같습니다.

  • 아직 0.x 버전이고, 문서랑 실제 이미지 화면이 벌써 안 맞습니다. 업데이트 때마다 동작이 바뀔 수 있다는 얘기라 운영 DB에 붙이기엔 불안합니다.
  • 백업(baseline)이 arm64에서 기본 인증으로 안 돌아서 우회가 필요했습니다. 백업이 없으면 CASCADE 복구도 부분 복구로 끝날 수 있습니다.
  • 누가 바꿨는지는 EE에만 있어서, 오픈소스만으로는 감사 로그 용도로 쓰기엔 부족합니다.
  • 인덱스 DB를 하나 더 운영해야 하는데, 쓰기가 많은 서비스에서 용량이나 지연이 어느 정도일지는 이번 데모로는 확인이 안 됐습니다.

당분간은 데모 프로젝트 정도에 붙여 두고 버전 올라가는 걸 지켜보는 정도가 맞을 것 같습니다.

🔗 Ref

0개의 댓글