在 Day 19 完成 Redis SHA-256 雜湊快取 與 Flask-Limiter 限流機制 後,PrescriptionVLM 系統已具備高效能的回應速度與 API 流量防範。
然而,後端系統中儲存的「長輩歷史用藥紀錄」與「關懷卡片狀態」是整個應用的核心資料。如果宿主機突發斷電、磁區損壞或容器遭誤刪,導致 SQLite 資料庫檔案(prescription_vlm.db)損毀,會失去長期紀錄。
今天我們將為 PrescriptionVLM 系統建立 資料庫自動化備份與備援復原機制。完成項目包含:
restore_db.py) — 提供一鍵解密與資料庫結構快速還原腳本,系統遭遇損壞時能快速復原服務。在生產環境中直接複製 SQLite 檔案容易造成資料損毀,因為系統可能正在寫入資料。
為此我們採用 SQLite 官方提供的 Online Backup API,其優勢如下:
requirements.txt)請開啟專案根目錄的 requirements.txt,加入背景排程與加密套件:
APScheduler==3.10.4
cryptography==42.0.5
backup_service.py)在專案根目錄新建 backup_service.py 模組,處理排程執行、熱備份與滾動清理:
import os
import sqlite3
import shutil
from datetime import datetime, timedelta
from cryptography.fernet import Fernet
from logger import logger
DB_PATH = "prescription_vlm.db"
BACKUP_DIR = "backups"
RETENTION_DAYS = 7
# 從環境變數讀取金鑰,若無則自動生成
ENCRYPTION_KEY = os.getenv("BACKUP_ENCRYPTION_KEY", Fernet.generate_key().decode())
fernet = Fernet(ENCRYPTION_KEY.encode() if isinstance(ENCRYPTION_KEY, str) else ENCRYPTION_KEY)
def perform_sqlite_backup():
"""執行 SQLite 安全熱備份與加密檔案寫入"""
if not os.path.exists(DB_PATH):
logger.warning(f"資料庫檔案 {DB_PATH} 不存在,跳過備份")
return
os.makedirs(BACKUP_DIR, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
temp_backup_file = os.path.join(BACKUP_DIR, f"temp_{timestamp}.db")
final_enc_file = os.path.join(BACKUP_DIR, f"backup_{timestamp}.db.enc")
try:
# 1. 使用 sqlite3 online backup API 進行一致性快照
src_conn = sqlite3.connect(DB_PATH)
dst_conn = sqlite3.connect(temp_backup_file)
with dst_conn:
src_conn.backup(dst_conn)
src_conn.close()
dst_conn.close()
# 2. 對備份檔案進行 AES 加密
with open(temp_backup_file, "rb") as f_in:
data = f_in.read()
encrypted_data = fernet.encrypt(data)
with open(final_enc_file, "wb") as f_out:
f_out.write(encrypted_data)
# 清除未加密的暫存檔
os.remove(temp_backup_file)
logger.info(f"資料庫熱備份與加密成功: {final_enc_file}")
# 3. 滾動清理超過 7 天的舊備份
cleanup_old_backups()
except Exception as e:
logger.error(f"資料庫備份失敗: {str(e)}")
def cleanup_old_backups():
"""清理超過 Retention 天數的備份檔"""
now = datetime.now()
for filename in os.listdir(BACKUP_DIR):
file_path = os.path.join(BACKUP_DIR, filename)
if filename.endswith(".db.enc"):
file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
if now - file_time > timedelta(days=RETENTION_DAYS):
os.remove(file_path)
logger.info(f"已自動清理過期備份檔: {filename}")
app.py)開啟 app.py,導入 APScheduler 並於服務啟動時建立備份任務。
from apscheduler.schedulers.background import BackgroundScheduler
from backup_service import perform_sqlite_backup
# 初始化 BackgroundScheduler
scheduler = BackgroundScheduler()
# 設定每天凌晨 02:00 執行熱備份(測試階段可改為每 1 小時或每 10 分鐘)
scheduler.add_job(func=perform_sqlite_backup, trigger="cron", hour=2, minute=0, id="db_backup_job")
scheduler.start()
logger.info("資料庫定時備份排程器已啟動")
restore_db.py)在專案根目錄建立一鍵復原腳本 restore_db.py:
import sys
import os
import shutil
from cryptography.fernet import Fernet
BACKUP_DIR = "backups"
TARGET_DB = "prescription_vlm.db"
def restore_latest_backup():
"""解密並還原最新的加密備份檔"""
key = os.getenv("BACKUP_ENCRYPTION_KEY")
if not key:
print("錯誤:未設定 BACKUP_ENCRYPTION_KEY 環境變數")
return
fernet = Fernet(key.encode())
enc_files = [os.path.join(BACKUP_DIR, f) for f in os.listdir(BACKUP_DIR) if f.endswith(".db.enc")]
if not enc_files:
print("未找到任何加密備份檔 (.db.enc)")
return
# 取得最新修改時間的備份檔
latest_enc_file = max(enc_files, key=os.path.getmtime)
print(f"找到最新備份檔: {latest_enc_file}")
try:
with open(latest_enc_file, "rb") as f:
encrypted_data = f.read()
decrypted_data = fernet.decrypt(encrypted_data)
# 備份現有毀損的 DB (若存在)
if os.path.exists(TARGET_DB):
shutil.copy(TARGET_DB, f"{TARGET_DB}.corrupted.bak")
with open(TARGET_DB, "wb") as f:
f.write(decrypted_data)
print(f"資料庫已成功還原至: {TARGET_DB}")
except Exception as e:
print(f"還原過程發生錯誤: {str(e)}")
if __name__ == "__main__":
restore_latest_backup()
1. 手動觸發備份驗證
在 Terminal 進入容器或直接於本地執行測試備份:
python -c "from backup_service import perform_sqlite_backup; perform_sqlite_backup()"
backups/ 資料夾,且裡面包含 backup_YYYYMMDD_HHMMSS.db.enc 加密檔。2. 模擬資料庫損壞與一鍵復原
故意刪除目前的資料庫檔案:
rm prescription_vlm.db
執行一鍵復原腳本:
python restore_db.py
資料庫已成功還原至: prescription_vlm.db,且 Flask 服務重啟後能正常讀取之前的用藥歷史紀錄。測試通過後,將 Day 20 的修改提交至 GitHub:
git add .
git commit -m "保留雙引號 改填寫自己要記錄的標記 ex.鐵人賽第二十天"
git push
今天我們透過 SQLite Online Backup API、Fernet 加密 與 APScheduler 背景排程,為 PrescriptionVLM 系統建構了自動化熱備份與災難復原防線,確保長輩的用藥資料即使遭遇硬體障礙也能快速還原。
明天(Day 21),我們將針對整體系統進行 Docker 容器多階段建構與映像檔輕量化,優化部署體積,打造更輕量的生產環境版本!