S174-INV-01: PG Backup RCA Report
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:
- Shell tạo file output TRƯỚC khi pipeline chạy (do redirect
> file) docker exec incomex-mysqlfail → "Error response from daemon: No such container" → bị nuốt bởi2>/dev/nullgzipnhận empty stdin → output 20 bytes empty gzippipefail→ pipeline return non-zero →set -eexit script ngay- 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/nulltrê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
stathoặcls -lbackup 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-mysql→postgres - ✅ Đổ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.shthànhpg-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):
- Không có "dependency graph" khi retire container → không ai biết
mysql-backup.shphụ thuộcincomex-mysql - Không có backup health monitoring → 28 ngày rỗng không ai hay
2>/dev/nullnuốt error → failure hoàn toàn im lặng- 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 postgres → pg-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-postgres — KHÔ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ì.