tva
← Insights

การกู้คืนจากภัยพิบัติสำหรับบริการที่โฮสต์เอง: กลยุทธ์การสำรองข้อมูลของเรา

การโฮสต์เองให้การควบคุมที่บริการที่จัดการไม่มีให้: คุณเลือก hardware, เครือข่าย, ที่ตั้งข้อมูล และโครงสร้างต้นทุน แต่ในความเป็นจริง การควบคุมนั้นมาพร้อมกับความรับผิดชอบที่บริการที่จัดการดูแลอย่างเงียบๆ: เมื่อมีอะไรผิดพลาด มันเป็นปัญหาของคุณทั้งหมด สำหรับการติดตั้งบนเซิร์ฟเวอร์เดียวที่รันบริการ production หลายอย่าง ได้แก่ Supabase, CRM, DAM, Git server กลยุทธ์การสำรองข้อมูลและการกู้คืนไม่ใช่เรื่องรอง มันคือความแตกต่างระหว่างเหตุการณ์ที่สามารถกู้คืนได้กับการสูญเสียข้อมูลถาวร

บทความนี้บันทึกกลยุทธ์การสำรองข้อมูลที่เรารันสำหรับ environment ที่โฮสต์เองในระดับ production บริการที่เกี่ยวข้องคือ Supabase stack (PostgreSQL เป็น data store หลัก), CRM ที่โฮสต์เอง (Twenty ซึ่งรัน PostgreSQL ด้วย), Gitea Git server และระบบ Digital Asset Management ทั้งหมดรันเป็น Docker containers บน Hetzner VPS ตัวเดียว กลยุทธ์ต้องคำนึงถึงทั้งหมดโดยไม่สร้างช่วงเวลา load ที่ยอมรับไม่ได้หรือ dependencies ที่ซับซ้อนในการ orchestration

เหตุใดเซิร์ฟเวอร์เดียวจึงเพิ่มความเสี่ยง

การติดตั้งบนเซิร์ฟเวอร์เดียวไม่มีความซ้ำซ้อนในตัว ไม่มี standby replica, ไม่มี multi-availability-zone failover, ไม่มีการกู้คืนฐานข้อมูลอัตโนมัติที่ทริกเกอร์โดย health check เมื่อเซิร์ฟเวอร์ล้มเหลว ไม่ว่าจะด้วย hardware failure, การอัปเกรดที่ผิดพลาด, ปัญหา storage หรือเหตุการณ์ด้านความปลอดภัย เส้นทางการกู้คืนเดียวคือการสำรองข้อมูล หากการสำรองข้อมูลไม่สมบูรณ์ ล้าสมัย หรือไม่ได้ทดสอบ เส้นทางการกู้คืนก็มีข้อจำกัดตามนั้น

นี่คือรูปแบบภัยคุกคามที่แตกต่างจากบริการ cloud ที่จัดการ ซึ่ง hardware failure แทบมองไม่เห็นเพราะผู้ให้บริการ infrastructure ดูแล failover บน VPS เดียว คุณวางแผนอย่างชัดเจนสำหรับโหมดความล้มเหลวที่บริการที่จัดการทำให้เป็น abstract การวางแผนไม่ซับซ้อนมาก แต่ต้องทำก่อนเหตุการณ์ ไม่ใช่ระหว่างเหตุการณ์

สิ่งที่ต้องสำรองข้อมูล

บริการแต่ละอย่างมีโปรไฟล์ข้อมูลของตัวเอง การเข้าใจว่าข้อมูลใดที่แทนที่ไม่ได้และอยู่ที่ใดคือเงื่อนไขเบื้องต้นสำหรับการออกแบบกลยุทธ์การสำรองข้อมูล

Supabase จัดเก็บข้อมูลในอินสแตนซ์ PostgreSQL ที่จัดการโดย Supabase stack ฐานข้อมูลมีข้อมูลแอปพลิเคชันทั้งหมด ข้อมูลผู้ใช้ และ metadata auth และ storage ของ Supabase เอง storage buckets (การอัปโหลดไฟล์) จัดเก็บบนดิสก์และต้องจัดการแยกจากการสำรองข้อมูลฐานข้อมูล

Twenty CRM ขับเคลื่อนด้วยอินสแตนซ์ PostgreSQL ของตัวเอง แยกจากของ Supabase มีข้อมูลผู้ติดต่อ ข้อมูล opportunity และ workflow state ฐานข้อมูลนี้มีขนาดเล็กกว่าแต่ละเอียดอ่อนมาก การสูญเสียข้อมูล CRM ก่อความเสียหายในการดำเนินงานมากกว่าการสูญเสียข้อมูลแอปพลิเคชันที่สามารถสร้างใหม่ได้

Gitea จัดเก็บ repositories บนดิสก์เป็น Git objects แบบ bare ข้อมูล repository สามารถสร้างใหม่ได้จาก workstations ของนักพัฒนา (เนื่องจากทุก clone เป็นการสำรองข้อมูลเต็มรูปแบบ) แต่ข้อมูล issue tracker, ความคิดเห็น pull request และ team configuration อยู่ใน database ของ Gitea เท่านั้นและไม่มีอยู่ใน clone ใดๆ ต้องสำรองข้อมูลทั้ง Git objects และ Gitea database

DAM (Digital Asset Management) จัดเก็บไฟล์ต้นฉบับ derivatives ที่ประมวลผลแล้ว และ metadata ไฟล์ต้นฉบับคือส่วนที่แทนที่ไม่ได้ derivatives สามารถสร้างใหม่ได้ metadata อยู่ใน database ที่บันทึกความสัมพันธ์ไฟล์ tags และสิทธิ์การใช้งาน

การ orchestrate pg_dump

pg_dump ของ PostgreSQL เป็นเครื่องมือที่เหมาะสมสำหรับการสำรองข้อมูลฐานข้อมูลแบบ logical มันส่งออก SQL representation ของฐานข้อมูลที่สามารถกู้คืนไปยัง PostgreSQL เวอร์ชันที่เข้ากันได้ใดๆ ซึ่งพกพาได้มากกว่าการสำรองข้อมูลแบบ physical (ซึ่งต้องใช้ PostgreSQL เวอร์ชันเดียวกันและ binary layout เดิม)

สคริปต์การสำรองข้อมูลสำหรับแต่ละอินสแตนซ์ PostgreSQL ทำตามรูปแบบเดียวกัน:

#\!/usr/bin/env bash
set -euo pipefail

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DB_NAME="$1"
CONTAINER_NAME="$2"
BACKUP_DIR="/opt/backups/postgres"
OUTPUT_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz"

mkdir -p "${BACKUP_DIR}"

docker exec "${CONTAINER_NAME}"   pg_dump -U postgres -d "${DB_NAME}"   | gzip -9 > "${OUTPUT_FILE}"

echo "Backup complete: ${OUTPUT_FILE} ($(du -sh "${OUTPUT_FILE}" | cut -f1))"

set -euo pipefail ที่ด้านบนมีความสำคัญ: ทำให้สคริปต์ออกทันทีหากคำสั่งใดล้มเหลว รวมถึงคำสั่งใน pipeline หากไม่มี pipefail pg_dump ที่ล้มเหลวตามด้วย gzip ที่สำเร็จจะสร้างไฟล์ที่บีบอัดซึ่งมีข้อความข้อผิดพลาด ซึ่งความพยายามในการกู้คืนจะถือว่าเป็น backup ที่เสียหาย

output ถูกบีบอัดด้วย gzip -9 แบบ inline แทนที่จะเป็นขั้นตอนหลังประมวลผล วิธีนี้ช่วยให้ footprint บนดิสก์เล็กน้อยและหลีกเลี่ยงการเขียน dump ที่ไม่ได้บีบอัดลงดิสก์ก่อน ซึ่งสำคัญบนเซิร์ฟเวอร์ที่พื้นที่ว่างเป็นข้อจำกัดที่ต้องจัดการ

การกำหนดเวลาแบบสลับกัน

การรัน backup jobs ทั้งหมดพร้อมกันจะสร้างช่วงเวลา I/O contention ที่อาจทำให้บริการที่กำลังสำรองข้อมูลเสื่อมสมรรถนะ อินสแตนซ์ Supabase ที่มี load ไม่ได้รับประโยชน์จากการแข่งขันกับ pg_dump สำหรับ disk I/O การแก้ปัญหาคือการกำหนดเวลาแบบสลับกัน โดย backup job แต่ละงานเริ่มต้นในเวลาต่างกัน โดยเว้นระยะห่างเพียงพอเพื่อให้งานก่อนหน้าเสร็จสมบูรณ์ก่อนงานถัดไปเริ่ม

ตารางที่เราใช้ แสดงเป็น cron entries:

# Supabase PostgreSQL backup — 02:00 ทุกวัน
0 2 * * * /opt/scripts/pg-backup.sh supabase supabase-db

# Twenty CRM PostgreSQL backup — 02:30 ทุกวัน
30 2 * * * /opt/scripts/pg-backup.sh twenty twenty-db

# Gitea database backup — 03:00 ทุกวัน
0 3 * * * /opt/scripts/pg-backup.sh gitea gitea-db

# DAM database backup — 03:30 ทุกวัน
30 3 * * * /opt/scripts/pg-backup.sh dam dam-db

# Gitea repository objects — 04:00 ทุกวัน
0 4 * * * /opt/scripts/git-objects-backup.sh

# Storage files (Supabase + DAM originals) — 04:30 ทุกวัน
30 4 * * * /opt/scripts/files-backup.sh

ช่องว่าง 30 นาทีระหว่างงานค่อนข้างอนุรักษ์นิยม การสำรองข้อมูลฐานข้อมูลส่วนใหญ่เสร็จสมบูรณ์ในไม่กี่นาทีสำหรับฐานข้อมูลขนาดนี้ แต่ช่องว่างยังคำนึงถึงขั้นตอนการอัปโหลดที่ตามมาหลังจากการสำรองข้อมูลแต่ละครั้ง: ไฟล์ที่บีบอัดถูกอัปโหลดไปยัง object storage ก่อนที่งานถัดไปจะเริ่ม ดังนั้นดิสก์ local จึงไม่สะสม backup หลายวันพร้อมกัน

การอัปโหลด Object Storage และการเก็บรักษา

Backups ที่มีอยู่เฉพาะบนเซิร์ฟเวอร์เดียวกับบริการที่ป้องกันไม่ใช่ backups แต่เป็น snapshots ที่จะสูญหายในเหตุการณ์เดียวกับที่ทำลายข้อมูลที่ป้องกัน ทุก backup ต้องถูกอัปโหลดไปยังสถานที่จัดเก็บแยกต่างหากก่อนที่สำเนา local จะถือว่าสมบูรณ์

เราใช้ object storage provider ที่เข้ากันได้กับ S3 (แยกจาก VPS provider) และอัปโหลดด้วย rclone ซึ่งจัดการ retries, การถ่ายโอนที่ต่อได้และการตรวจสอบโดยอัตโนมัติ:

rclone copy "${OUTPUT_FILE}" "backup-remote:tva-backups/postgres/${DB_NAME}/"   --checksum   --transfers 1   --log-level INFO

flag --checksum ตรวจสอบการถ่ายโอนโดยใช้ MD5 checksums แทนเวลาแก้ไขและขนาด ซึ่งตรวจจับการเสียหายใดๆ ระหว่างการถ่ายโอน

การเก็บรักษาถูกบังคับใช้โดย cleanup job แยกต่างหากที่รันรายสัปดาห์ นโยบายการเก็บรักษาคือ: daily backups เก็บเจ็ดวัน, weekly backups (daily backup ของวันอาทิตย์ เปลี่ยนชื่อโดย weekly job) เก็บสี่สัปดาห์, monthly backups (อาทิตย์แรกของแต่ละเดือน) เก็บหกเดือน สิ่งนี้ให้ช่วงเวลาที่สมเหตุสมผลสำหรับการตรวจจับการสูญเสียข้อมูลที่ไม่ชัดเจนทันที

การ cleanup การเก็บรักษาใช้ rclone delete พร้อม filter บนเวลาแก้ไขแทนการลบตามรูปแบบชื่อไฟล์ ซึ่งน่าเชื่อถือกว่าเมื่อแบบแผนการตั้งชื่อไฟล์ไม่สม่ำเสมอ:

rclone delete "backup-remote:tva-backups/postgres/"   --min-age 7d   --include "*_daily_*"   --dry-run

flag --dry-run ใช้ระหว่างการทดสอบ cleanup job ลบออกเฉพาะหลังจากยืนยันว่า filter patterns ตรงกับสิ่งที่ควรลบพอดี

การทดสอบการกู้คืน

Backup ที่ไม่เคยทดสอบเป็นเพียงสมมติฐาน วิธีเดียวที่รู้ว่า backup สามารถกู้คืนได้คือการกู้คืนจริงๆ เรารันการทดสอบกู้คืนรายเดือนสำหรับแต่ละฐานข้อมูล โดยใช้ Docker container ชั่วคราวที่แยกจาก stack ที่ production:

#\!/usr/bin/env bash
set -euo pipefail

BACKUP_FILE="$1"
TEST_CONTAINER="restore-test-$(date +%s)"

# เริ่ม PostgreSQL container ชั่วคราว
docker run -d   --name "${TEST_CONTAINER}"   -e POSTGRES_PASSWORD=testpass   -e POSTGRES_DB=testdb   postgres:15-alpine

# รอให้ PostgreSQL พร้อม
sleep 5

# กู้คืน backup
zcat "${BACKUP_FILE}" | docker exec -i "${TEST_CONTAINER}"   psql -U postgres -d testdb

# ตรวจสอบ row counts ว่าตรงตามที่คาดหวัง
docker exec "${TEST_CONTAINER}"   psql -U postgres -d testdb   -c "SELECT schemaname, tablename, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 10;"

# ทำความสะอาด
docker stop "${TEST_CONTAINER}" && docker rm "${TEST_CONTAINER}"

การตรวจสอบ row count ไม่ครอบคลุมทั้งหมด มันตรวจสอบว่าตารางหลักมี row counts ที่สมเหตุสมผล ไม่ใช่ว่าทุก row ถูกต้อง การทดสอบที่ละเอียดกว่าจะรัน health checks ของแอปพลิเคชันเองกับฐานข้อมูลที่กู้คืน แต่การตรวจสอบ row count จับโหมดความล้มเหลวที่พบบ่อยที่สุด: backup ที่ถูกตัดทอน, การกู้คืนที่ล้มเหลวซึ่งสร้างฐานข้อมูลว่าง หรือ version incompatibility ที่ทำให้สูญเสียข้อมูลแบบเงียบๆ

การกู้คืนจริงมีลักษณะอย่างไร

กลยุทธ์การสำรองข้อมูลมีประโยชน์เท่ากับขั้นตอนการกู้คืนที่ใช้มันเท่านั้น runbook การกู้คืน ซึ่งเขียนเป็นเอกสาร Markdown ใน configuration repository ของเซิร์ฟเวอร์ บันทึกลำดับขั้นตอนที่แน่นอนเพื่อกู้คืนแต่ละบริการบน VPS ใหม่

ลำดับสำหรับความล้มเหลวของเซิร์ฟเวอร์ทั้งหมดคือ: จัดสรร VPS ใหม่, ติดตั้ง Docker และ application stack จาก configuration repository, ดาวน์โหลดไฟล์ backup ล่าสุดจาก object storage, กู้คืนแต่ละฐานข้อมูลตามลำดับ dependency (Supabase ก่อน แล้ว CRM แล้วอื่นๆ), กู้คืน file storage, อัปเดต DNS และตรวจสอบแต่ละบริการ เวลาที่ประเมินสำหรับการกู้คืนเต็มรูปแบบน้อยกว่าสองชั่วโมงหาก backup ทั้งหมดเป็นปัจจุบันและปฏิบัติตาม runbook โดยไม่มีการปรับปรุง

คำสำคัญในการประเมินนั้นคือ “ปฏิบัติตามโดยไม่มีการปรับปรุง” runbook ควรมีความเฉพาะเจาะจงพอที่บุคคลที่ไม่เคยแตะระบบก่อนจะสามารถดำเนินการได้สำเร็จ คำสั่งควร copy-paste ได้ ไม่ใช่อธิบายด้วยร้อยแก้ว ทุกขั้นตอนควรมีการตรวจสอบ ความคลุมเครือใน recovery runbook คือความรับผิดที่ทวีคูณภายใต้ความเครียดของเหตุการณ์จริง

เราทดสอบ runbook รายปีโดยดำเนินการกู้คืนเต็มรูปแบบไปยัง staging environment การทดสอบมักเปิดเผยอย่างน้อยหนึ่งขั้นตอนที่เปลี่ยนแปลงนับตั้งแต่ runbook ถูกเขียนครั้งสุดท้าย เวอร์ชัน Docker image ที่เปลี่ยน, key configuration ที่ถูกเปลี่ยนชื่อ, environment variable ที่ถูกเพิ่ม การค้นพบสิ่งเหล่านี้ในการทดสอบตามกำหนดดีกว่าค้นพบในระหว่างการกู้คืนจริงมาก

ข้อมูลเชิงลึกที่เกี่ยวข้อง

บทความที่เกี่ยวข้อง