KB-1853

S174-INV-01: PG Backup RCA Report

10 min read Revision 1

S174-INV-01 — RCA: PG Backup Hỏng (Directus Daily Backup)

Ngày điều tra: 2026-04-08 Trạng thái: READ-ONLY — chưa fix gì


1. Timeline

Ngày Sự kiện
2026-02-14 mysql-backup.sh tạo lần đầu (Birth time)
2026-02-28 Nội dung script chỉnh lần cuối (Modify time)
2026-03-11 02:00 CET Backup thật cuối cùng: directus_2026-03-11_0200.sql.gz — 19,175,166 bytes (MySQL dump header xác nhận)
2026-03-11 (ban ngày) Container incomex-mysql bị dừng/xóa — bắt đầu chuẩn bị migrate MySQL → PG
2026-03-12 02:00 CET Backup rỗng đầu tiên: 20 bytes (empty gzip)
2026-03-13 14:35 +0700 Commit d4947aa — "migrate Directus from MySQL to PostgreSQL (S115)" (#496). Xóa incomex-mysql khỏi docker-compose, thay bằng postgres:16 container postgres
2026-03-12 → 2026-04-03 28 ngày mysql-backup.sh chạy đêm, mỗi đêm tạo 1 file 20 bytes. Log chỉ ghi "Starting MySQL backup..." không có completion/error
2026-04-03 10:35 CET Script renamed → mysql-backup.sh.retired (ctime evidence). Cron entry bị xóa khỏi crontab
2026-04-03 → hôm nay Không có standalone daily Directus PG backup nào. VPS gdrive backup (backup-to-gdrive.sh) vẫn hoạt động — có PG dump 35M

Tổng thời gian backup rỗng: 28 ngày (Mar 12 → Apr 3) Tổng thời gian không có standalone daily backup: 36 ngày (Mar 12 → Apr 8, hôm nay)


2. Chain of Failure — 5 Tầng

Tầng 1 — Hiện tượng

File 20 bytes chứa gì?

$ xxd directus_2026-03-12_0200.sql.gz
00000000: 1f8b 0800 0000 0000 0003 0300 0000 0000  ................
00000010: 0000 0000                                ....

→ Empty gzip: gzip header (10 bytes) + gzip footer (10 bytes). gzip nhận empty stdin → output 20 bytes. Decompress ra 0 bytes.

Lần cuối backup thật: 2026-03-11 (19MB). Bắt đầu rỗng: 2026-03-12.

Cron entry (trong repo infra/cron/incomex-crontab):

0 2 * * * /opt/incomex/scripts/mysql-backup.sh >> /opt/incomex/backups/mysql/backup.log 2>&1

Chạy dưới user root.

Tầng 2 — Cơ chế fail

Container incomex-mysql có tồn tại không?

$ docker ps -a --filter 'name=mysql'
NAMES     STATUS    IMAGE
(trống)

→ KHÔNG. Bị xóa khi migrate sang PG. Commit d4947aa (Mar 13) thay incomex-mysql (MySQL 8.0) bằng postgres (PG 16) trong docker-compose.

Tại sao vẫn tạo file 20 bytes?

Script mysql-backup.sh:

set -euo pipefail
docker exec incomex-mysql mysqldump ... 2>/dev/null | gzip > "${BACKUP_DIR}/directus_${DATE}.sql.gz"

Chuỗi failure:

  1. Shell tạo file output TRƯỚC khi pipeline chạy (do redirect > file)
  2. docker exec incomex-mysql fail → "Error response from daemon: No such container" → bị nuốt bởi 2>/dev/null
  3. gzip nhận empty stdin → output 20 bytes empty gzip
  4. pipefail → pipeline return non-zero → set -e exit script ngay
  5. Block if [ $? -eq 0 ] không bao giờ chạy → không có "Backup completed" hay "ERROR" message

Tại sao log không có error?

  • 2>/dev/null trên docker exec nuốt error message "No such container"
  • Mục đích ban đầu: suppress benign MySQL warnings
  • Hậu quả: suppress LUÔN critical "container not found" error
  • Log chỉ ghi dòng echo "Starting MySQL backup..." rồi script exit silently

Exit code?

  • Script exit non-zero (do pipefail + set -e), nhưng cron KHÔNG monitor exit code. Stderr từ script (nếu có) redirect vào backup.log cùng stdout, nhưng script đã nuốt error bên trong.

Tầng 3 — Vì sao không ai biết

Monitoring file size backup?

  • disk-monitor.sh: chỉ check disk usage % (df /). KHÔNG check backup size/freshness.
  • Không có script nào stat hoặc ls -l backup files rồi alert.

Điều 31 integrity checks?

$ grep -r 'backup' /opt/incomex/deploys/web-test/scripts/integrity/
(trống)

cron-integrity.sh, watchdog-monitor.sh, scanner-counts.sh KHÔNG có bất kỳ check backup nào.

Telegram alert?

$ grep -r 'TELEGRAM\|telegram\|send_alert\|notify' mysql-backup.sh.retired qdrant-backup.sh backup-to-gdrive.sh
(trống)

KHÔNG có script backup nào gửi alert khi fail. Không có Telegram, Slack, hay notification nào.

Uptime Kuma?

$ grep -i backup monitors-baseline.csv
(trống)

→ Không có monitor cho backup health.

Tầng 4 — Vì sao drift xảy ra

Commit retire MySQL container (d4947aa, Mar 13) đã làm gì?

  • ✅ Đổi docker-compose: incomex-mysqlpostgres
  • ✅ Đổi Directus env: DB_CLIENT: mysql → PG
  • KHÔNG update infra/cron/incomex-crontab (vẫn giữ mysql-backup.sh)
  • KHÔNG update/replace mysql-backup.sh thành pg-backup.sh
  • KHÔNG grep cron/systemd cho dependencies trên incomex-mysql
  • KHÔNG test backup sau migrate
$ git log --oneline -- 'infra/cron/*' '**/mysql-backup*'
bc1ae1d feat: add cron and logrotate configs to GitOps (WEB-87) (#403)

→ File cron KHÔNG BAO GIỜ được update sau commit ban đầu (#403). Commit #496 (migrate PG) không touch cron hay backup scripts.

Có checklist "retire container = update dependencies"?

  • Chưa xác định. Không tìm thấy checklist trong repo.

Commit retire có test backup sau đó?

  • Không có evidence. Commit #496 stats: 7 files changed — toàn compose/env/scripts. Không có backup test.

Tầng 5 — Gốc rễ thật

Lỗi người hay lỗi hệ thống?

CẢ HAI. Lỗi người (quên update cron/backup script khi retire MySQL) được nhân lên bởi lỗi hệ thống (không có cơ chế detect failure):

  1. Không có "dependency graph" khi retire container → không ai biết mysql-backup.sh phụ thuộc incomex-mysql
  2. Không có backup health monitoring → 28 ngày rỗng không ai hay
  3. 2>/dev/null nuốt error → failure hoàn toàn im lặng
  4. Không có post-migration test cho backup

Luật nào trong Hiến pháp lẽ ra phải chặn?

  • Điều 31 (Integrity Audit): Hiện chỉ audit data integrity, KHÔNG audit infrastructure health (backup, container dependencies). → Cần mở rộng.
  • Điều 33 (Pre-merge checks): Nếu có rule "khi xóa container, grep tất cả cron/script/config cho tên container đó" → đã chặn được.
  • Điều 22 (Monitoring): Nếu có backup freshness monitor → phát hiện ngay ngày đầu.

Nếu không sửa, lặp lại? CÓ. Bất kỳ khi nào rename/retire container mà quên update dependency → cùng pattern. Ví dụ: nếu mai đổi postgrespg-17, tất cả script gọi docker exec postgres sẽ hỏng theo.


3. Gốc rễ thật (1 câu)

Nguyên nhân sâu nhất là: Không có cơ chế nào (checklist, grep, monitoring, hoặc test) đảm bảo rằng khi retire 1 container, tất cả dependencies (cron, script, config) được cập nhật theo — kết hợp với backup script nuốt error (2>/dev/null) và hệ thống monitoring không cover backup health.


4. Đề xuất fix (chờ Desktop duyệt)

Lớp 1 — Fix trực tiếp

# Hành động Chi tiết
1a Tạo pg-backup.sh Thay docker exec incomex-mysql mysqldump bằng docker exec postgres pg_dump -U directus -d directus. Output → /opt/incomex/backups/pg/. KHÔNG dùng 2>/dev/null trên docker exec.
1b Update crontab Thay entry mysql-backup.sh bằng pg-backup.sh. Sync infra/cron/incomex-crontab trong repo.
1c Verify backup-to-gdrive.sh Script này ĐANG hoạt động (PG dump 35M, upload OK). Xác nhận nó là backup chính hay chỉ supplemental.
1d Fix workflow backup /opt/workflow/postgres/backup.sh gọi container workflow-postgres (không tồn tại). Container thật tên postgres. Sửa script hoặc deprecate nếu redundant với gdrive backup.

Lớp 2 — Fix hệ thống (chặn drift tương lai)

# Hành động Luật liên quan
2a Backup health monitor — Thêm check vào Điều 31 hoặc watchdog: verify backup file > 1MB và < 24h tuổi. Alert qua Telegram nếu fail. Điều 22, 31
2b Container dependency grep — Thêm vào Điều 33 pre-merge: khi docker-compose thay đổi container_name, auto-grep tất cả /opt/incomex/scripts/, crontab, systemd cho tên cũ. Block merge nếu có reference chưa update. Điều 33
2c Cấm 2>/dev/null trên docker exec trong backup scripts. Dùng 2>>error.log thay vì suppress. Coding standard
2d Post-migration backup test — Thêm vào migration checklist: sau bất kỳ DB/container migration, chạy backup thủ công 1 lần, verify file size > threshold. Checklist
2e Uptime Kuma push monitor — Backup script gọi Kuma push endpoint khi thành công. Nếu không nhận push trong 25h → alert. Điều 22

5. Phát hiện phụ: Workflow PG Backup cũng hỏng

/opt/workflow/postgres/backup.sh (cron 0 2 * * *) gọi container workflow-postgresKHÔNG tồn tại. Log:

Error response from daemon: No such container: workflow-postgres

Failing liên tục từ ít nhất 2026-03-15. Container PG thật tên postgres. Đây là issue riêng, cần fix cùng đợt.


6. Tình trạng backup hiện tại

Hệ thống Trạng thái Ghi chú
mysql-backup.sh (daily Directus) ❌ DEAD Retired Apr 3. Không có thay thế.
backup-to-gdrive.sh (nightly full VPS) ✅ OK PG dump 35M + Qdrant 63M → Google Drive. Đây là backup PG DUY NHẤT đang hoạt động.
/opt/workflow/postgres/backup.sh ❌ DEAD Container name sai. Failing từ Mar 15.
Qdrant backup (qdrant-backup.sh) ✅ OK Snapshots 54-76MB, 7-day retention.

Chờ Desktop duyệt trước khi fix bất kỳ thứ gì.