iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0
Claude AI

奇幻塔防開發實錄:用 Claude 打造一款有靈魂的塔防遊戲系列 第 24 篇

Day 24:監控告警上線:CloudWatch 與雲端預警系統,讓伺服器不再無聲陣亡

  • 分享至 

  • xImage
  •  

claude_24

系列:奇幻塔防開發實錄:用 Claude 打造一款有靈魂的塔防遊戲
今日工具:Claude Code / Terraform(modules/monitoring)/ CloudWatch Logs Insights
今日進度:結構化請求日誌、六個告警接到 email、兩次演練,以及一個沒有任何指標的盲點

前言

Day 23 結束在一個問題:上線之後它出事了,誰會知道?標題寫「讓伺服器不再無聲陣亡」,但這個架構已經沒有伺服器可以陣亡——沒有 task 會被 OOM kill、沒有 CPU 會滿。服務照樣會死,只是死法換了一批:Lambda 逾時、併發被打滿、DynamoDB 熱分區,以及最惡劣的一種——一次都沒被呼叫。

今天的目標很具體:出事時五分鐘內我的信箱要有一封信,而且要故意弄壞它兩次來證明,不是 terraform apply 過了就當有監控。

一、先讓日誌有結構

告警的原料是指標,指標的原料是日誌。Day 8 選 log/slog 的 JSON handler 就是為了今天。backend/internal/adapter/httpapi/middleware/logging.go 讓每個請求輸出一行:

start := time.Now()
next.ServeHTTP(ww, r) // ww 攔得到 status
logger.Info("http_request",
	"method", r.Method, "path", r.URL.Path, "status", ww.Status(),
	"latency_ms", time.Since(start).Milliseconds(),
	"request_id", chimw.GetReqID(r.Context()),
	"remote_ip", r.RemoteAddr) // 只准進 log,不得用於授權或限流

有了 latency_ms,Logs Insights 一行就能算 p99:filter msg = "http_request" | stats pct(latency_ms, 99) as p99 by path。上線八天的基準線:/api/v1/levels/{id} p50 12ms、p99 64ms;/story/choose p50 27ms、p99 81ms。這是日常流量的伺服器端數字,不是壓測——Day 26 那組 41/210 ms 是 hey 打滿時量的。

堅持 JSON 而不是人類好讀的純文字,是因為讀日誌的不是人,是查詢。而也牽出今天最貴的一行設定:Lambda 的 logging_config.log_format 必須是 Text。設成 JSON,Lambda 會把每行 stdout 再包一層它自己的 JSON,level 就被埋進字串裡,第二節那個 metric filter 會永遠比對不到、永遠不叫。一個看起來像日誌美化的選項,決定了整條告警鏈路會不會動。

request_id 這件事在 Lambda 上比在容器上麻煩:API Gateway access log 的 $context.requestId、Lambda REPORT 行的 RequestId、我們自己寫的 request_id,是三個不同的 id。 能把兩邊串起來的只有 Lambda 的 aws_request_id。

還有一個刻意的取捨:middleware 一律用 Info,ERROR 由知道出了什麼事的那一層自己標(slog.Error),因為 middleware 只看得到狀態碼,看不到原因。代價很明確:哪天有條 500 路徑忘了寫 slog.Error,app-errors 就不會叫,只剩 apigw-5xx 接得住——兩個告警互相補位是設計,不是巧合。

claude_24_diagram_01

二、modules/monitoring:五個基礎設施告警+一個應用層告警

告警全部送到一個 SNS topic,訂閱我的 email。應用層告警靠一個 metric filter:pattern = "{ $.level = \"ERROR\" }",把 ERROR 行數變成 Emberhold/API 的 ErrorCount;default_value = "0" 是關鍵:沒有 log 的分鐘要算 0,否則 alarm 會一直卡在 INSUFFICIENT_DATA。基礎設施告警長這樣:

resource "aws_cloudwatch_metric_alarm" "lambda_duration_p99" {
  namespace          = "AWS/Lambda"
  metric_name        = "Duration"
  extended_statistic = "p99"
  threshold          = 800
  evaluation_periods = 2                   # 單一尖峰不叫,連兩個窗才叫
  treat_missing_data = "notBreaching"      # 沒流量 = 沒問題,不是壞了
  alarm_actions      = local.alarm_actions # ok_actions 指同一個 topic
}

其他五個結構相同,差別只在指標與閾值:

告警 指標(Sum,period 300) 閾值 為什麼是這個數
lambda-errors AWS/Lambda Errors ≥ 1 只算函式層真爆掉(panic、逾時、init 失敗),不含我們回的 500
lambda-throttles AWS/Lambda Throttles ≥ 1 reserved_concurrent_executions = 10 被打滿,玩家吃 502
lambda-duration-p99 AWS/Lambda Duration p99 > 800ms,連 2 窗(六個裡唯一不是 ≥ 的) 基準線 64ms 的 12 倍
apigw-5xx AWS/ApiGateway 5XXError(維度 ApiName) ≥ 5 一次 500 可能是單一壞請求,五次是真的壞了
ddb-throttles AWS/DynamoDB ThrottledRequests ≥ 1 on-demand 仍有單分區 3,000 RCU/1,000 WCU 的上限
app-errors Emberhold/API ErrorCount ≥ 1 任何一行 ERROR 我都要知道

六個裡我最沒把握的是 lambda-duration-p99 的 800ms:那是從 64ms 往上推 12 倍猜的,等真的有一群玩家才知道對不對——先寫下來,錯了再改,比空著好。

閾值是跟 Claude Code 討論出來的。它的第一版把 apigw-5xx 設成「≥ 1 就叫」,我反問:deploy-api.yml 更新完程式碼、新版第一次被呼叫的那幾秒,會不會有零星失敗?它查了 Day 16 的部署流程後同意改成 5,並主動指出 Errors 該是 1。這一輪對話 15 分鐘,讓我在 apply 前避掉「每次部署都收到告警信,三天後把通知關掉」的結局。告警的敵人不是漏報,是狼來了。

恢復通知跟出事通知一樣重要:半夜收到一封 ALARM,十分鐘後沒有 OK,我知道要起床;有 OK,我可以繼續睡。

sns_topic_subscription 用 email protocol 有一個坑:apply 之後 AWS 會寄確認信,沒按就永遠停在 PendingConfirmation,alarm 進 ALARM 也不會有信。我第一次演練前 20 分鐘就是這樣沒掉的。

三、故意弄壞它:兩次演練

告警沒被觸發過就等於不存在。backend 裡有一條只有 FAULT_INJECT=1 才註冊的路由 GET /api/v1/debug/fault,印一行 ERROR 再回 500。Claude Code 一開始建議用 query string 觸發(/healthz?boom=1),我否決:任何人都能打的故障開關不是演練工具,是 DoS 入口。

開關不用重新部署,改環境變數就好——但有個小雷:

# --environment 是「整包覆蓋」,漏一個變數就少一個
aws lambda update-function-configuration --function-name emberhold-prod-api \
  --environment 'Variables={GAMEDATA_DIR=/var/task/data,SAVES_TABLE=emberhold-prod,
JWT_SECRET_PARAM=/emberhold/prod/jwt-secret,LOG_LEVEL=info,FAULT_INJECT=1}'

我第一次只寫了 FAULT_INJECT=1,SAVES_TABLE 整個不見,API 當場退回記憶體倉儲——演練還沒開始就製造了一個真故障。加上 wait function-updated-v2,開關前後 12 秒。

claude_24_diagram_02

4 分 48 秒由 period = 300 與 CloudWatch 的評估延遲決定,不是我能調快的;要更快得把 period 縮到 60 秒,代價是更多誤報。對一款遊戲,五分鐘可以接受;如果這是付款服務,答案會不一樣——而這正是閾值不該抄別人的理由。

第二次演練換個載體:把 reserved_concurrent_executions 暫時改成 1,再用 hey -n 200 -c 20 打 /api/v1/gamedata。第 2 個併發開始就被拒,lambda-throttles 在下一個窗進 ALARM。重點不是它會響,而是我親眼看到「併發上限」長什麼樣:客戶端收到 502 不是 429,log 裡什麼都沒有,因為請求根本沒進到我的程式——沒有這個指標,這種故障在應用日誌裡完全隱形。

四、真正的盲點與成本

兩次演練都成功,但它們有一個共同前提:Lambda 有被呼叫到。

無伺服器監控真正的盲點是:「完全沒有被呼叫」是沒有指標的,而這不是假設。正式環境第一次 apply 全綠,每個請求卻都回 500:四個 Lambda 側指標一個資料點都沒有,函式連 log stream 都沒生出來,直接 aws lambda invoke 反而 200——aws_lambda_permission 的 source_arn 少了帳號 id,API Gateway 沒權限叫我的函式。六個告警裡唯一看得見的是 5XXError,而它看得見只因為指標名與維度剛好寫對;照 HTTP API 寫成 5xx/ApiId,alarm 一樣建得起來、一樣停在 OK、永遠不叫。

再往上一層就沒救了:CloudFront 的 /api/* behavior 設錯,請求連 API Gateway 都到不了,六個 alarm 因 treat_missing_data = "notBreaching" 全部停在 OK,信箱一片安詳,玩家看到白畫面。舊架構至少有 ALB 的 UnHealthyHostCount 給你一個「零台健康」的數字,函式沒被呼叫則連零都沒有。

補救有兩層。短期是 deploy-api.yml 最後那步部署後煙霧測試——今天之前它只是「順手驗一下」,現在是這套監控唯一的存在性檢查。長期要的是合成探測(排程 curl 或 Synthetics canary),一個從外面固定敲門的東西,進了 Day 29 的未完成清單。

其餘的收尾是 API Gateway access log 與 docs/runbook/alerts.md,另有一個無伺服器獨有的數字:冷啟動那一發從外面量是 1.6 秒,熱的之後是 0.15–0.25 秒。差了將近十倍,而差額幾乎都花在拉那個 44 MB 的容器映像上——這是 Day 16 換成映像打包時就知道要付的帳。玩家感受到的是含冷啟動的那個數字,不是伺服器端的 81ms。

成本很有意思:六個 alarm US$0.60、自訂指標 US$0.30、log 與 Insights 約 US$0.07。這個架構每月最貴的東西是「告警」,不是「運算」——真正跑遊戲的 Lambda + API Gateway + DynamoDB 加起來 US$0.04。

小結

今天的 Terraform 是 modules/monitoring/main.tf 的 146 行,真正花時間的是三件事:跟 Claude 討論閾值(避免狼來了)、兩次演練(證明六個告警不是裝飾),以及一個演練抓不到的盲點。

給想跟著做的讀者一句話:apply 完的第一件事不是看儀表板,是弄壞它。弄壞之後要再問一次——還有什麼壞法,是連指標都不會產生的? 我的六個告警全部停在 OK 的那一天,可能才是最該起床的那一天。

今日產出

  • [x] backend/internal/adapter/httpapi/middleware/logging.go;FAULT_INJECT 才註冊的 /api/v1/debug/fault
  • [x] infra/terraform/modules/monitoring/{main,variables,outputs}.tf(SNS、metric filter、六個 alarm,plan 累計 51 個資源)
  • [x] API Gateway access log → CloudWatch,保留 14 天;docs/runbook/alerts.md
  • [x] 兩次演練紀錄(本篇第三節)+一條進了未完成清單的合成探測

明日預告

Day 25:三個結局的收束:多重劇情分支的測試與 QA 策略——用選項序列驅動狀態機走到三個結局,再讓 Playwright 走一次。


上一篇
Day 23:敵軍波次平衡調校:用 Claude 協助遊戲數值設計與難度曲線分析
系列文
奇幻塔防開發實錄:用 Claude 打造一款有靈魂的塔防遊戲 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言