דילוג לתוכן

גיבוי, שחזור לנקודת זמן ותרגול שחזור

גבו את Quire, שחזרו אותו לנקודת זמן והוכיחו זאת באמצעות תרגול השחזור.

הצגה כ-Markdown

התכנון נמצא בסעיף 8 של docs/architecture/23-ops.md. זו runbook למוצר Docker Compose. היא נכתבה כדי שמישהו שלא כתב אותה יוכל לפעול לפיה; אם שלב כלשהו אינו ברור, זה פגם במסמך.

מה מוגן ואיך

נכס אופן ההגנה היכן
מסד הנתונים WAL נשמר ברציפות, לכל היותר כל 60 שניות, מההפעלה הראשונה pgwal volume
מסד הנתונים גיבויי בסיס באמצעות pg_basebackup, מדי יום כברירת מחדל (backup-scheduler) pgbackup volume
מסד הנתונים עותקים מוצפנים של גיבויי הבסיס וה-WAL כל חמש דקות (backup-offsite) מאגר נפרד שתציינו
קבצים ה-volume של files. העתיקו אותו בכלי הגיבוי של המארח או השתמשו באחסון אובייקטים עם גרסאות files volume
סודות docker/.env, ובעיקר QUIRE_MASTER_KEY (וכן QUIRE_MASTER_KEY_RETIRED אם עדיין משתמשים בו), QUIRE_BACKUP_ENCRYPTION_KEY ו-docker/secrets/audit-signing-key.pem שמרו עותק מחוץ למארח הזה
אינדקסי חיפוש, מטמונים והמרות אינם מגובים; נבנים מחדש

יעדים: נקודת שחזור שמרוחקת לכל היותר 60 שניות מהכשל, ושחזור בתוך 60 דקות למסד נתונים של 500 GB.

שתי טעויות נפוצות. מסד נתונים ששוחזר ללא הקבצים שלו מציג עמודים שבורים. מסד ששוחזר ללא QUIRE_MASTER_KEY אינו יכול לפענח את פרטי הגישה של SSO,‏ webhook ואינטגרציות שבו; עד לסיום החלפת המפתח הראשי בלי פריטים שלא נפתרו (key-rotation.md), נכללים בכך גם המפתחות שפרשו. שניהם חלק מהגיבוי.

יצירת גיבויים

גיבוי בסיס של כל האשכול:

docker compose -f docker/compose.yaml --profile backup run --rm backup

הוא שומר את גיבויי הבסיס החדשים ביותר לפי QUIRE_BACKUP_KEEP (ברירת מחדל 5) ומוחק מקטעי WAL שהגיבוי הישן ביותר כבר אינו צריך, כדי שהארכיון לא יגדל ללא גבול. תזמנו אותו מדי יום באמצעות cron או systemd timer במארח:

15 2 * * * cd /srv/quire && docker compose -f docker/compose.yaml --profile backup run --rm backup >> /var/log/quire-backup.log 2>&1

או תנו ל-stack לתזמן אותו: פרופיל backup מריץ את backup-scheduler, שיוצר גיבוי בסיס כל QUIRE_BACKUP_INTERVAL_HOURS שעות (ברירת מחדל 24), ואת backup-offsite שמתואר בהמשך.

docker compose -f docker/compose.yaml --profile backup up -d

עותקים מוצפנים מחוץ למארח

שני ה-volumes נמצאים באותו מארח כמו מסד הנתונים, וגיבוי במחשב שנכשל אינו גיבוי. backup-offsite מעתיק כל גיבוי בסיס וכל מקטע WAL שנשמר למאגר נפרד דרך ממשק האחסון, מצפין אותם ושומר לפי מדיניות השמירה:

  • הצפנה. AES-256-GCM באמצעות QUIRE_BACKUP_ENCRYPTION_KEY (או הקובץ שמציינת QUIRE_BACKUP_ENCRYPTION_KEY_FILE): 32 bytes שנוצרו באמצעות openssl rand -hex 32. לכל קובץ nonce ותג אימות משלו, כך שאי אפשר לקרוא עותק ללא המפתח וכל שינוי בו מזוהה. שמרו את המפתח יחד עם QUIRE_MASTER_KEY, רחוק מהמארח וממאגר הגיבויים. בלי המפתח אין אפשרות לשחזר.
  • מיקום. QUIRE_BACKUP_STORAGE_DRIVER הוא s3,‏ azure או local (דיסק מרוחק שמחובר ב-QUIRE_BACKUP_STORAGE_ROOT). אלה הגדרות אחסון הקבצים עם הקידומת QUIRE_BACKUP_:‏ QUIRE_BACKUP_S3_ENDPOINT,‏ QUIRE_BACKUP_S3_BUCKET,‏ QUIRE_BACKUP_S3_ACCESS_KEY_ID וכן הלאה. השתמשו ב-bucket אחר מזה של הקבצים, ועדיף גם בחשבון אחר, עם פרטי גישה שיכולים לכתוב אך לא למחוק אם הספק מאפשר זאת.
  • שמירה. גיבויי הבסיס החדשים ביותר לפי QUIRE_BACKUP_OFFSITE_KEEP (ברירת מחדל QUIRE_BACKUP_KEEP, ואם אינו מוגדר אז 7), יחד עם ה-WAL הדרוש לישן ביותר. קבוצות ומקטעים ישנים יותר נמחקים מהמאגר.
  • מועד. בכל QUIRE_BACKUP_SHIP_INTERVAL_SECONDS שניות (ברירת מחדל 300). ההעברה idempotent: פריטים שכבר נשמרו מדולגים, וגיבוי בסיס נחשב שמור רק לאחר כתיבת ה-manifest שלו, אחרון.

אותה פקודה פועלת גם בהרצה ידנית:

docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts ship
docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts verify

כדי לשחזר במארח חדש, החזירו תחילה קבוצת גיבוי ואז בצעו את השלבים שלהלן, תוך שימוש בתיקייה שהורדתם במקום ה-volume pgbackup וב-wal-archive שהורדתם במקום pgwal:

bun apps/worker/src/backups/main.ts fetch base-20260924T021500Z /srv/restore

שחזור לנקודת זמן

השתמשו בכך לאחר אובדן נתונים: ייבוא שגוי, קורס שנמחק או מיגרציית contract שיש לבטל. הפעולה מחליפה את מסד הנתונים הפעיל, לכן תרגלו אותה קודם באמצעות התרגול שלהלן.

  1. בחרו את זמן היעד, ב-UTC, רגע לפני הנזק: 2026-09-24 09:30:00+00. יומן הביקורת (/admin/audit) מציג לרוב את הרגע.
  2. עצרו את כל מי שכותב: docker compose -f docker/compose.yaml stop web content worker scheduler collab
  3. שמרו את האשכול שנפגע עד לאימות השחזור:
    docker compose -f docker/compose.yaml stop postgres
    docker run --rm -v quire_postgres18-data:/from -v quire_postgres-damaged:/to alpine cp -a /from/. /to/
  4. חלצו ל-volume הנתונים את גיבוי הבסיס החדש ביותר שקדם ליעד, ובקשו שחזור ממוקד:
    docker run --rm -v quire_pgbackup:/backups:ro -v quire_postgres18-data:/var/lib/postgresql postgres:18-alpine sh -euc '
      base="$(ls -1d /backups/base-* | sort | tail -n 1)"   # or the one before the target
      rm -rf /var/lib/postgresql/18/docker && mkdir -p /var/lib/postgresql/18/docker
      tar -xzf "$base/base.tar.gz" -C /var/lib/postgresql/18/docker
      touch /var/lib/postgresql/18/docker/recovery.signal
      chown -R postgres:postgres /var/lib/postgresql/18/docker && chmod 700 /var/lib/postgresql/18/docker'
  5. בצעו שחזור: הפעילו את Postgres פעם אחת עם הגדרות השחזור מתוך override של Compose, כדי שהקובץ הרגיל יישאר ללא שינוי:
    # docker/compose.recover.yaml
    services:
      postgres:
        command: [postgres, -c, "restore_command=cp /var/lib/postgresql/wal-archive/%f %p",
                  -c, "recovery_target_time=2026-09-24 09:30:00+00",
                  -c, recovery_target_action=promote, -c, archive_mode=off,
                  -c, max_connections=200, -c, hba_file=/etc/postgresql/pg_hba.conf]
    ה-override מחליף את כל הפקודה, לכן הוא חוזר על שתי ההגדרות הדרושות לשחזור: max_connections לא נמוך מזה של הראשי (אחרת השחזור נעצר עם “insufficient parameter settings”) ו-pg_hba.conf שמחובר כ-volume.
    docker compose -f docker/compose.yaml -f docker/compose.recover.yaml up -d postgres
    docker compose -f docker/compose.yaml logs -f postgres   # wait for "database system is ready"
  6. בדקו לפני שמתירים כניסה: את שרשרת הביקורת (docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts) ושהנתונים שאבדו חזרו.
  7. חזרו למצב רגיל: docker compose -f docker/compose.yaml up -d. כך Postgres מופעל מחדש כשהארכוב פעיל ומתחיל ציר זמן WAL חדש. צרו מיד גיבוי בסיס חדש.

שם הפרויקט quire מופיע כקידומת של כל volume;‏ docker volume ls מציג את השמות המדויקים.

תרגול האימות המתוזמן

backup-offsite מריץ תרגול גם בכל QUIRE_BACKUP_DRILL_INTERVAL_HOURS שעות (ברירת מחדל 168, שבועית) ושוב בהרצה הבאה אחרי כישלון. הוא מוריד את גיבוי הבסיס האחרון מחוץ למארח ואת כל מקטעי ה-WAL שאחריו, מפענח כל אחד מהם (וכך מוכיח שהמפתח עדיין יכול לפתוח אותם ושלא שונו), משווה כל קובץ ל-manifest שלו, בודק שהארכיון הוא ספריית נתונים של Postgres ומוודא שאין פער ב-WAL מגיבוי הבסיס ואילך. הדוח נכתב במאגר בתור reports/drill-<time>.json וגם ביומן השירות; תרגול שנכשל מציין את הקובץ או את המקטע הראשון שחסר.

תרגול השחזור

גיבוי שמעולם לא שוחזר אינו גיבוי. התרגול משחזר ל-Postgres זמני שאין לו דבר במשותף עם הפעיל את גיבוי הבסיס המלא החדש ביותר שקדם ליעד, יחד עם ארכיון ה-WAL, ומוכיח שהתוצאה תקינה:

docker/scripts/restore-drill.sh                                  # to ninety minutes ago
docker/scripts/restore-drill.sh --target "2026-09-24 09:30:00+00"

היעד חייב להיות UTC ובדיוק בצורה הזאת. דרושים גיבוי בסיס ישן ממנו ו-WAL שמור מאוחר ממנו: בהתקנה חדשה, צרו גיבוי בסיס והמתינו למקטע הארכוב הבא (לכל היותר דקה אם יש כתיבות) לפני בחירת יעד שמאוחר מהגיבוי. התרגול דורש רק Docker ו-bash במארח.

כל שלב יכול להכשיל את התרגול:

  1. יכולת שחזור: האשכול הזמני מפעיל מחדש את הרשומות עד היעד ונפתח.
  2. שלמות: מספר השורות בכל טבלה לעומת מסד הנתונים הפעיל (tooling/restore-drill). הפעיל התקדם מאז היעד, ולכן מותר לכל טבלה הבדל בשני הכיוונים עד הגדול מבין 500 שורות ועשירית מגודלה (כתיבות גורמות לפיגור, ומחיקות גורמות לכך שבעותק המשוחזר יהיו יותר שורות); partition שנוצר אחרי היעד אינו טבלה שאבדה. בהתקנה עמוסה יותר הרחיבו את המותר באמצעות QUIRE_DRILL_MAX_BEHIND ו-QUIRE_DRILL_MAX_DRIFT_RATIO. טבלה חסרה או ריקה גורמת לכישלון.
  3. תקינות: שרשרת הגיבוב של הביקורת עוברת אימות בעותק המשוחזר.
  4. שמישות: תפקיד היישום קורא באמצעות אבטחה ברמת שורה.
  5. זמן: הזמן מתחילת התרגול ועד הצלחה נבדק מול QUIRE_DRILL_RTO_SECONDS (ברירת מחדל 3600).

התרגול אינו כותב לעולם למסד הנתונים הפעיל או ל-volumes שלו: ה-volumes של הגיבוי וה-WAL מחוברים לקריאה בלבד, והאשכול הזמני מוסר בסוף, בהצלחה או בכישלון.

הגדירו את QUIRE_DRILL_REPORT לנתיב כדי לכתוב דוח JSON בהצלחה או בכישלון, ותזמנו אותו ממארח Docker:

30 3 1 * * cd /srv/quire && QUIRE_DRILL_REPORT=/var/log/quire-drill.json docker/scripts/restore-drill.sh >> /var/log/quire-drill.log 2>&1

הריצו אותו מדי חודש ולפני כל שדרוג. תרגול שנכשל עוצר את השדרוג. פעם ברבעון בקשו ממישהו שלא כתב את ה-runbook לבצע שחזור אמיתי לנקודת זמן במארח נוסף, תוך שימוש במסמך הזה בלבד.

קבצים

קבצים מקומיים נמצאים ב-volume files. גבו אותו יחד עם מסד הנתונים ובאותו זמן, ושחזרו את שניהם ביחד:

docker run --rm -v quire_files:/files:ro -v "$PWD":/out alpine tar -czf /out/files-$(date -u +%Y%m%d).tar.gz -C /files .

באחסון אובייקטים הפעילו גרסאות bucket ושמרו גרסאות קודמות למשך 35 יום; במקרה כזה שחזור קבצים לנקודת זמן מתבצע באמצעות ה-bucket עצמו.

ניווט

הקלידו לחיפוש…

↑↓ ניווט↵ בחירהEsc סגירה