Загружаем каталог…
Загружаем каталог…
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_type 1 = INSERT, 2 = UPDATE, 3 = DELETE pk_values 대상 행의 PK (복합키는 42|7 형태) changed_columns UPDATE에서 실제로 바뀐 컬럼 목록 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 때문에 그
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
DBTrail 첫 사용기. WHERE 빠진 UPDATE 한 번이면 복구가 꽤 번거롭습니다. 백업을 다른 서버에 올리고, 사고 직전까지 binlog 돌리고, 필요한 행만 뽑아서 다시 넣고. 그 사이에 정상적으로 바뀐 데이터랑 안 섞이게 맞추는 것도 일입니다. 망가진 행만 딱 되돌리는 방법은 없나 찾다가 DBTrail 을 보게 됐고, 로컬 Docker로 한번 돌려 봤습니다. 🧐 DBTrail은 뭐 하는 도구인가 📌 한 줄로 MySQL binlog(PostgreSQL은 WAL)를 복제 프로토콜로 계속 읽으면서, 모든 INSERT / UPDATE / DELETE 의 변경 전 값, 변경 후 값 을 별도 인덱스 DB에 쌓아 두는 도구입니다. 사고가 나면 DB 전체를 복원하지 않고 망가진 행만 되돌리는 undo…
Открыть источник