網站上線後才是開始:
Log、備份、監控與效能
Django網站可以開啟,只代表部署完成;智慧養殖、產銷平台與水井三寶場域真正要長期運作,還必須在出錯時找得到原因、資料遺失時救得回來、異常發生時有人知道、使用量增加時仍然回得動。
一、上線不是終點,而是系統生命週期的起點
營運能力=看得見+救得回+叫得到人+持續改善。沒有Log只能猜,沒有還原演練的備份只是希望,沒有告警的監控只是漂亮圖表,沒有量測的效能改善只是碰運氣。
二、先定義「什麼叫系統正常」
智慧養殖系統不能只監看首頁是否回200。即使網站正常,裝置可能已經兩小時沒有上傳,儀表板仍顯示昨天最後一筆水溫。
| 層級 | 正常條件例 | 失敗時代表 |
|---|---|---|
| 網站存活 | /health/live/能快速回200 | Web worker、程式或平台可能無法服務 |
| 服務就緒 | /health/ready/可連資料庫與必要服務 | 網站進程活著,但尚不能完成真正工作 |
| API品質 | 成功率、P95延遲符合門檻 | 部分功能壞掉或逐漸變慢 |
| 資料新鮮度 | 各ESP32最近上傳時間未超過預期 | 裝置、Wi-Fi、MQTT/HTTP或排程可能中斷 |
| 業務正確性 | 溶氧警報能產生、通知並被確認 | 技術層看似正常,但場域流程失效 |
三、Log第一課:不要只寫「發生錯誤」
一筆有用的Log至少要回答:時間、嚴重程度、事件、request_id、裝置或場域代碼、結果與可採取的下一步。
2026-08-22T07:45:10+08:00 INFO
event=reading.accepted request_id=7bc2...
device_id=POND01-ESP32-01 event_uuid=POND01-000062
status=201 duration_ms=84
2026-08-22T07:46:03+08:00 WARNING
event=reading.rejected request_id=2f91...
device_id=POND01-ESP32-01 reason=ph_out_of_range
status=400 duration_ms=12
應該記錄
事件名稱、Request ID、匿名或內部裝置ID、狀態碼、耗時、錯誤分類、重試次數與程式版本。
不應記錄
完整Token、密碼、Session Cookie、個資、完整信用卡資料或未遮罩的敏感Payload。
Log本身也是敏感資料。必須限制存取、設定保存期限、避免無限增長,且不能讓使用者輸入破壞Log格式或偽造紀錄。
四、Django Logging設定與使用
# settings.py:教學版,以console交給部署平台收集
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{asctime} {levelname} {name} {message}",
"style": "{",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"loggers": {
"aquaculture": {
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},
"django.request": {
"handlers": ["console"],
"level": "WARNING",
"propagate": False,
},
},
}
# views.py
import logging
logger = logging.getLogger("aquaculture.api")
def record_success(request_id, device_id, event_uuid, duration_ms):
logger.info(
"event=reading.accepted request_id=%s device_id=%s "
"event_uuid=%s duration_ms=%d",
request_id, device_id, event_uuid, duration_ms,
)
def record_failure(request_id, device_id):
logger.exception(
"event=reading.failed request_id=%s device_id=%s",
request_id, device_id,
)
logger.exception()應在例外處理區使用,它會留下Stack Trace。Django官方建議以Python logging取得具名Logger,並透過LOGGING設定Handler、Formatter與Level。
| Level | 用途 | 場域例 |
|---|---|---|
| DEBUG | 開發診斷細節 | 序列化前後欄位;正式環境通常不大量開啟 |
| INFO | 正常的重要事件 | 資料接收、備份完成、裝置重新上線 |
| WARNING | 尚能服務但值得注意 | 資料過舊、重試增加、磁碟逼近門檻 |
| ERROR | 某項操作失敗 | 資料庫寫入失敗、外部通知失敗 |
| CRITICAL | 系統性重大故障 | 核心資料庫不可用或大量API全面失敗 |
五、Request ID:把ESP32、API與資料庫串成一條線
ESP32每筆事件已有event_uuid,HTTP請求再加入X-Request-ID。若Client未提供,Django產生新的UUID,回應時也帶回,學生便能從裝置Serial Log一路查到伺服器Log。
# middleware.py(簡化示例)
import uuid
class RequestIdMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
incoming = request.headers.get("X-Request-ID", "")
request.request_id = incoming[:80] if incoming else str(uuid.uuid4())
response = self.get_response(request)
response["X-Request-ID"] = request.request_id
return response
不要無條件信任Header。限制長度與格式,避免把任意使用者輸入直接插入結構化Log;Request ID只用於追蹤,不是安全憑證。
六、PythonAnywhere上要看哪幾種Log?
| Log | 主要內容 | 適合回答 |
|---|---|---|
| Access Log | URL、狀態碼、回應大小與回應時間 | 哪個API慢?錯誤率何時上升? |
| Error Log | 載入、WSGI與部署錯誤 | 為何Reload後網站無法啟動? |
| Server/應用Log | Django logger、Traceback及自訂事件 | 是哪個裝置或資料造成例外? |
| Task Log | Scheduled/Always-on Task執行結果 | 備份、清理或監控排程是否真的完成? |
平台介面與方案限制可能調整,請以PythonAnywhere目前帳號的Web與Tasks頁面為準。正式營運應建立固定巡檢責任,而不是出事後才第一次找Log。
七、備份不是複製檔案,而是一套可還原的制度
先定義RPO與RTO
| 指標 | 問題 | 範例 |
|---|---|---|
| RPO | 最多可接受遺失多久的資料? | 水質資料最多遺失1小時 |
| RTO | 故障後多久必須恢復服務? | 4小時內恢復API與儀表板 |
3-2-1原則:至少3份資料、2種不同媒介、1份異地。再加上加密、權限、保留週期與還原演練,才形成真正的備份策略。
八、不同資料庫的備份方式不能混用
# PostgreSQL:自訂格式,使用pg_restore還原
pg_dump -Fc -d DATABASE_NAME -f backup_YYYYMMDD.dump
pg_restore --clean --if-exists -d RESTORE_TEST_DB backup_YYYYMMDD.dump
# MySQL:邏輯備份示意
mysqldump --single-transaction DATABASE_NAME > backup_YYYYMMDD.sql
# SQLite:網站低流量或停止寫入時,以sqlite3一致性備份
sqlite3 db.sqlite3 ".backup 'backup_YYYYMMDD.sqlite3'"
帳密不要直接寫在可被其他使用者看到的命令或程式碼中;應使用平台提供的安全環境變數或憑證機制。命令選項需依資料庫版本、權限與託管平台調整。
不要直接複製正在大量寫入的SQLite檔案當成可靠備份。也不要只備資料庫而忘記使用者上傳的照片、附件與對應程式版本。
還原演練的五步驟
- 建立隔離的空白測試資料庫。
- 用備份檔還原,不覆蓋正式環境。
- 執行Migration/相容性檢查。
- 核對關鍵表筆數、最新時間與附件。
- 用Postman/pytest跑核心API流程並記錄實際RTO。
九、監控:圖表不是目的,能採取行動才是
智慧養殖還要增加資料新鮮度:每座魚塭最後上傳時間、離線裝置數、離線佇列深度、警報未確認時間。
| 監控項目 | 告警例 | 第一個行動 |
|---|---|---|
| 網站可用性 | 連續3次健康檢查失敗 | 確認平台狀態、Error Log與最近部署 |
| API錯誤率 | 5分鐘內5xx超過門檻 | 依Request ID抽樣Traceback |
| API延遲 | P95連續10分鐘超標 | 分解網路、Web、DB與外部服務時間 |
| 裝置資料新鮮度 | 預期5分鐘上傳,15分鐘未收到 | 查LWT、Wi-Fi、電源與離線佇列 |
| 備份 | 排程失敗、檔案為0或超過期限 | 停止自動刪除舊備份並人工處理 |
| 磁碟 | 可用空間低於設定門檻 | 查Log、媒體與備份增長來源 |
十、健康檢查:Liveness與Readiness要分開
# views.py(簡化示例)
from django.db import connection
from django.http import JsonResponse
def live(request):
return JsonResponse({"status": "alive"})
def ready(request):
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
return JsonResponse({"status": "ready"})
except Exception:
return JsonResponse({"status": "not_ready"}, status=503)
健康端點要快、穩定且不洩密。不要回傳資料庫密碼、Token、完整例外或伺服器內部路徑;也不要讓Liveness依賴所有外部服務,否則短暫外部故障可能引起不必要重啟。
十一、告警要包含「誰、何時、怎麼處理」
| 不好的告警 | 可行動的告警 |
|---|---|
| 網站壞了! | PROD API 5xx達12%,開始07:42,影響/api/v1/readings/,Runbook:OPS-API-01,值班:A組 |
| 裝置離線 | POND01-ESP32-01已15分鐘無資料,最後RSSI -76、最後LWT offline、最近event_uuid與場域聯絡方式 |
同一問題重複發送數百次會造成告警疲勞。應設定持續時間、合併、冷卻與恢復通知;重大告警必須有負責人及Runbook。
十二、效能第一原則:先量測,找出真正瓶頸
PythonAnywhere官方說明建議比較瀏覽器Network總時間與Access Log回應時間,先區分網路與Web App。接著再把Django處理拆成資料庫、檔案、外部API與Python運算。
| 瓶頸 | 觀察證據 | 常見改善 |
|---|---|---|
| 網路/TLS | Client慢,但Access Log處理快 | 縮小Payload、連線重用、選擇合適部署區域 |
| N+1查詢 | 清單筆數越多,SQL數量線性增加 | select_related()、prefetch_related() |
| 缺索引 | 依device_id、measured_at查詢越來越慢 | 依實際Query與執行計畫建立索引 |
| 傳太多資料 | API回傳數萬筆水質紀錄 | 分頁、時間區間、只取必要欄位 |
| 同步重工作 | 寄信、AI分析或報表拖慢Request | 移至背景工作並提供狀態 |
| 靜態/媒體檔 | 大量圖片由Django直接處理 | 正確Static/Media服務、壓縮與快取 |
| Worker不足 | 單次不慢,但多人同時使用就排隊 | 縮短請求、增加合適Worker或方案資源 |
十三、Django ORM效能:少查、查對、只取需要的資料
# 不佳:迴圈中每次存取pond,可能形成N+1查詢
readings = PondReading.objects.all()[:100]
for reading in readings:
print(reading.pond.name)
# 改善:ForeignKey使用select_related一次Join
readings = (
PondReading.objects
.select_related("pond")
.only("event_uuid", "measured_at", "water_temp_c", "pond__name")
.order_by("-measured_at")[:100]
)
# 常用查詢可考慮複合索引,但必須先量測
class PondReading(models.Model):
device_id = models.CharField(max_length=80)
measured_at = models.DateTimeField()
class Meta:
indexes = [
models.Index(fields=["device_id", "-measured_at"]),
]
索引不是免費午餐。它會占空間並增加寫入成本;應以真實查詢、資料量與執行計畫決定。快取也會帶來資料過期與失效策略問題,不應拿來掩蓋錯誤查詢。
十四、部署後的日、週、月維運節奏
| 頻率 | 建議工作 |
|---|---|
| 每日自動 | 健康檢查、備份結果、5xx、裝置離線、磁碟與憑證到期監控 |
| 每週人工 | 抽查Log、慢API、備份檔、未確認告警及使用量趨勢 |
| 每月 | 隔離還原演練、帳號/Token盤點、套件更新評估、效能基準比較 |
| 每次部署 | Migration備份、部署清單、Smoke Test、版本標記與Rollback方案 |
| 每次事故後 | 無責檢討:時間線、根因、影響、復原與防止再發措施 |
PythonAnywhere排程:可用Scheduled Task執行備份檢查或Django Management Command;功能與額度依帳號方案而異。排程「被建立」不代表「有成功」,必須監控Exit Code、輸出與備份新鮮度。
十五、故障演練:讓學生真的把系統救回來
No-AI|Log追蹤
教師提供ESP32 Serial Log、Access Log與Django Traceback;學生用event_uuid與Request ID重建故障時間線,找出第一個失敗點。
AI Pair|維運假設
請AI提出網站變慢的可能原因,學生依量測證據排序、駁回無證據猜測,並設計每個假設的最小驗證。
Challenge AI|災難復原
在隔離環境模擬資料誤刪、備份還原、裝置斷線與API延遲;量測RPO/RTO並完成Runbook與事後檢討。
十六、上線營運驗收清單
| 檢核 | 通過證據 |
|---|---|
| Log可追查 | 可用Request ID從ESP32追到API結果,且無敏感資料 |
| 備份可還原 | 在隔離環境成功還原並通過Postman/pytest核心測試 |
| 監控可告警 | 網站、5xx、延遲、資料新鮮度、備份與磁碟皆有門檻 |
| 告警有人處理 | 每項重大告警有負責人、Runbook與升級路徑 |
| 效能有基準 | 記錄P50/P95、Query數與資料量,改善前後可比較 |
| 部署可回復 | 每個版本有標記、Migration計畫、Smoke Test與Rollback方案 |
十七、延伸閱讀(官方文件)
- Django:How to configure and use logging
- Django:Deployment checklist
- Django:Database access optimization
- Django:Cache framework
- PythonAnywhere:Why is my site slow?
- PythonAnywhere:Scheduled tasks
- PostgreSQL:SQL Dump與還原
部署平台、資料庫與Django版本會變動;正式操作前應以目前使用版本及帳號方案的官方文件為準。
沒有留言:
張貼留言