2026年8月22日 星期六

網站上線後才是開始: Log、備份、監控與效能

封包旅行記 EP07|從部署走向營運

網站上線後才是開始:
Log、備份、監控與效能

Django網站可以開啟,只代表部署完成;智慧養殖、產銷平台與水井三寶場域真正要長期運作,還必須在出錯時找得到原因、資料遺失時救得回來、異常發生時有人知道、使用量增加時仍然回得動。

一、上線不是終點,而是系統生命週期的起點

Log|留下證據誰在何時做了什麼?在哪一層失敗?可否用Request ID串起整條路徑?
備份|保住成果資料刪錯、主機故障或程式更新失敗時,能恢復到哪個時間點?
監控|提早知道網站掛掉、API變慢、錯誤率上升或ESP32斷線時,誰會收到通知?
效能|量測改善慢在網路、Django、資料庫、檔案系統,還是外部服務?

營運能力=看得見+救得回+叫得到人+持續改善。沒有Log只能猜,沒有還原演練的備份只是希望,沒有告警的監控只是漂亮圖表,沒有量測的效能改善只是碰運氣。

二、先定義「什麼叫系統正常」

智慧養殖系統不能只監看首頁是否回200。即使網站正常,裝置可能已經兩小時沒有上傳,儀表板仍顯示昨天最後一筆水溫。

層級正常條件例失敗時代表
網站存活/health/live/能快速回200Web 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 LogURL、狀態碼、回應大小與回應時間哪個API慢?錯誤率何時上升?
Error Log載入、WSGI與部署錯誤為何Reload後網站無法啟動?
Server/應用LogDjango logger、Traceback及自訂事件是哪個裝置或資料造成例外?
Task LogScheduled/Always-on Task執行結果備份、清理或監控排程是否真的完成?

平台介面與方案限制可能調整,請以PythonAnywhere目前帳號的Web與Tasks頁面為準。正式營運應建立固定巡檢責任,而不是出事後才第一次找Log。

七、備份不是複製檔案,而是一套可還原的制度

備什麼資料庫、媒體檔、環境設定、部署文件與程式版本。
多久一次依可接受資料損失RPO決定,例如每日完整+更頻繁增量。
放哪裡不可只留在同一帳號或同一主機;至少一份異地/離線。
怎麼證明定期在隔離環境還原,核對筆數、附件與核心流程。

先定義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檔案當成可靠備份。也不要只備資料庫而忘記使用者上傳的照片、附件與對應程式版本。

還原演練的五步驟

  1. 建立隔離的空白測試資料庫。
  2. 用備份檔還原,不覆蓋正式環境。
  3. 執行Migration/相容性檢查。
  4. 核對關鍵表筆數、最新時間與附件。
  5. 用Postman/pytest跑核心API流程並記錄實際RTO。

九、監控:圖表不是目的,能採取行動才是

LatencyP50、P95與P99延遲,不只看平均值。
Traffic每分鐘請求數、活躍裝置與資料寫入量。
Errors4xx、5xx、例外與失敗工作比例。
SaturationCPU、記憶體、磁碟、連線池與Worker負荷。

智慧養殖還要增加資料新鮮度:每座魚塭最後上傳時間、離線裝置數、離線佇列深度、警報未確認時間。

監控項目告警例第一個行動
網站可用性連續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。

十二、效能第一原則:先量測,找出真正瓶頸

瀏覽器/ESP32總耗時Access Log伺服器耗時網路傳輸與連線成本

PythonAnywhere官方說明建議比較瀏覽器Network總時間與Access Log回應時間,先區分網路與Web App。接著再把Django處理拆成資料庫、檔案、外部API與Python運算。

瓶頸觀察證據常見改善
網路/TLSClient慢,但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版本會變動;正式操作前應以目前使用版本及帳號方案的官方文件為準。

封包旅行記 EP07|真正的上線,不是網址能打開;而是故障能追、資料能救、異常有人知道、系統能持續變好。

沒有留言:

張貼留言