cover

服務掛了,你知道原因嗎?如果你的回答是「看 log」,那你只用到了可觀測性的三分之一。更常見的是另一種:告警設了三十條,半夜響了沒人理,因為前二十九次都是假的。

結論:問題不是「要不要設告警」,是「設了沒人理」

Metrics、Logs、Traces 三支柱大家都知道,AWS 對應 CloudWatch、CloudWatch Logs、X-Ray。這篇不重講工具,講四條原則和三個預設坑:

  1. 每條告警都要對應一個動作——收到了不知道該幹嘛的告警,刪掉。這是過濾噪音唯一有效的標準。
  2. 告症狀,不告原因——告「結帳失敗率超過 2%」,不要告「CPU 超過 80%」。CPU 高不一定有事,結帳掛了一定有事。
  3. 分級對應通道——會吵醒人的、上班看的、只進儀表板的,三條路分開。
  4. 每條附處理手冊連結——半夜被叫起來的人需要的是步驟,不是一個數字。

三個預設坑:log driver 預設會卡死你的程式、log 預設永不過期、CloudWatch 的抑制只做得到 Alertmanager 的一部分。「抑制」的意思是上游掛了,就別再噴下游的告警;Alertmanager 是 Prometheus 那套裡負責分派告警的元件。

一、八條告警,每條對應一個斷裂點

常見的指標表長這樣:「EC2 CPU > 80%、RDS 記憶體 < 500 MB」——這些是原因不是症狀,而且門檻是憑感覺寫的。這張表每條都是使用者會感覺到、或錢會流走的事。

#告警指標為什麼是這條
1ALB 的 5xx 比率HTTPCode_Target_5XX_Count ÷ RequestCount使用者實際感受,最該有的一條
2ALB 回應時間 p99TargetResponseTime 的 p99不要用平均值,平均會把尾巴藏掉
3NAT 配置不到 portErrorPortAllocation > 0官方定義:「大於零代表同時連線太多」——這是唯一看得到 NAT 撞上限的地方(見 第 2 篇)
4資料庫連線數逼近上限RDS DatabaseConnections;有 Proxy 再看 DatabaseConnectionsCurrentlySessionPinned見 第 6 篇,釘住率高 Proxy 等於沒裝
5佇列積壓、死信佇列非空SQS ApproximateNumberOfMessagesVisible死信佇列本來就不該有東西,有就是壞訊息
6ECS 跑不到預期數量RunningTaskCount < desired 持續 N 分鐘擴容失敗、部署壞掉(部署失敗另有事件可接,見表下)
7對外部依賴的錯誤率自訂指標,分依賴金流變慢不該把會員登入一起拖死,要分開看
8預算超標AWS Budgets 的預測值異常費用常常是被入侵或忘記關的第一個訊號

第 6 條的「部署壞掉」還有一個更直接的訊號。ECS 部署失敗時,會送出一個 EventBridge 事件 SERVICE_DEPLOYMENT_FAILED(EventBridge 是 AWS 傳遞「某件事發生了」這類事件的服務)。把它接到同一條通知,細節見 第 10 篇。

第 3 條設的時候,統計方式和維度要選對。同一頁官方文件寫,ErrorPortAllocation 的單位是次數(Count),建議用 Sum,也就是把這段時間內配置失敗的次數加總。

維度是「這個數字要按什麼分開看」的篩選條件。NAT gateway 分 zonal(以單一可用區為單位)和 regional(以整個區域為單位)兩種。zonal 的只用 NatGatewayId 一個維度。regional 的要 NatGatewayId 加上 AvailabilityZone,按可用區分開看。

同一頁還有 ActiveConnectionCount,是目前開著的連線數,建議看 Max,適合看趨勢。但真的撞到上限時,先跳的是 ErrorPortAllocation。

加碼一條:ACM 憑證續期失敗。 ACM(AWS Certificate Manager)是 AWS 管 HTTPS 憑證的服務。DNS 驗證是在你的 DNS 加一筆 CNAME 記錄,證明這個網域是你的。用 DNS 驗證的憑證會自動續期,但官方文件列了兩個前提:

  • 憑證正被某個 AWS 服務使用。
  • 驗證用的 CNAME 記錄都還在公開 DNS 上查得到。

少一個就續不了。驗不過的時候,ACM 會在到期前 30、15、7、3、1 天發 AWS Health 和 EventBridge 事件。這些事件沒接到 SNS(AWS 的通知服務,負責把訊息推到 Email、Slack 這些地方),就沒有人會收到。等憑證真的過期,整站 HTTPS 一起掛。

一條 alarm 長這樣——死信佇列那條最簡單也最重要:

aws cloudwatch put-metric-alarm --alarm-name "dlq-not-empty" \
  --namespace AWS/SQS --metric-name ApproximateNumberOfMessagesVisible \
  --dimensions Name=QueueName,Value=orders-dlq \
  --statistic Maximum --period 60 --threshold 0 \
  --comparison-operator GreaterThanThreshold --evaluation-periods 1 \
  --alarm-actions arn:aws:sns:ap-northeast-1:123456789012:oncall

注意 CPU 沒有在表上。 CPU 進儀表板、不進告警。它是你收到第 1 條告警之後去查的東西,不是把你叫醒的東西。

二、CloudWatch 比 Alertmanager 弱在哪

從 K8s 過來的人最容易在這裡失望。Alertmanager 原生有分組、抑制、靜音、路由四樣,CloudWatch 對得上的只有一部分:

  • 分組——同類告警合併成一則。CloudWatch 沒有直接對應,十個 task 同時不健康就是十則。
  • 抑制——上游掛了就別噴下游的告警。CloudWatch 沒有通用的 inhibition rule;最接近的是 composite alarm 的 ActionsSuppressor。composite alarm 是把好幾條 alarm 用 AND / OR / NOT 組成一條的組合告警;ActionsSuppressor 只壓得住 composite alarm 自己的動作(下面講)。
  • 靜音——維護時段把通知關掉。CloudWatch 用 Alarm Mute Rules 做這件事。它只壓通知、照樣判斷:靜音期間 alarm 照樣評估、狀態照樣變,只是動作不執行。時段結束時,CloudWatch 會照 alarm 當下的狀態,把被壓掉的動作補發。規格是這樣:時段用 cron(排程語法)或 at(...) 排,持續 1 分鐘到 15 天;一條規則最多指定 100 個 alarm;metric alarm(盯單一指標的一般 alarm)和 composite alarm 都能掛。
  • 路由——Alertmanager 依標籤樹狀分派;CloudWatch 只能靠 SNS topic 分流,粗得多。

後果:同一個根因噴一整串,大家開始無視告警,告警疲勞就從這裡開始。

能做的是 composite alarm:官方文件說它把多個 alarm 用 AND / OR / NOT 組合,只在組合條件成立時發。用法是把根因和衍生綁在一起,只讓根因那則發:

aws cloudwatch put-composite-alarm --alarm-name "db-down-root-cause" \
  --alarm-rule 'ALARM("rds-connections-maxed") AND ALARM("alb-5xx-rate")' \
  --alarm-actions arn:aws:sns:ap-northeast-1:123456789012:oncall

然後把 alb-5xx-rate 自己的通知拿掉,它只當 composite 的輸入。這不是抑制,是「把衍生告警降級成條件」——效果接近,但要自己一條一條綁。

部署時段也能用同一招擋掉:在 composite alarm 的規則裡加上 NOT ALARM(DeploymentInProgress),意思是「部署中」那條 alarm 沒在響的時候才算數。這是官方 API 文件自己舉的例子。

真正叫「抑制」的是 composite alarm 的 ActionsSuppressor,用途是上游已經在響,這條就先別發。官方文件寫,指定的 suppressor alarm(例如「資料庫掛了」)處於 ALARM 時,這個 composite alarm 不執行動作;suppressor 回到 OK 才恢復。限制在名字裡:只有 composite alarm 能被壓,一般的 metric alarm 要先包進 composite 才享受得到。

兩條 alarm 變狀態的時間不一定對得上,所以另有兩個參數補時間差。ActionsSuppressorWaitPeriod:composite 比 suppressor 先進 ALARM 時,最多等幾秒讓 suppressor 跟上,等不到才發。ActionsSuppressorExtensionPeriod:suppressor 回 OK 之後,再多壓幾秒,讓 composite 自己也有時間回 OK。文件建議兩個都設 60 秒,因為 metric alarm 每分鐘評估一次。

通知的管線就一條:

CloudWatch Alarm → SNS topic → Slack / PagerDuty / Email

分級就分成不同 SNS topic,接不同通道——原則第 3 條的「會吵醒人的」和「上班看的」各一個 topic,「只進儀表板的」不接 topic。

三、Log driver 預設會卡死你的程式

這條很少有人提,我整理架構筆記時才對到。

ECS 預設用 awslogs driver 把容器的 stdout / stderr 送到 CloudWatch。Docker 的 log driver 有兩種投遞模式,Docker 官方文件寫得很直白:預設是 blocking——driver 送不出去的時候,容器寫 stdout 的那一行會卡住;原文是「當 STDERR 或 STDOUT 卡住時,應用程式很可能以意想不到的方式失敗」。

翻成白話:CloudWatch 那邊限流、或網路抖一下,你的程式在 print 一行 log 的地方停住,然後 health check 失敗、task 被踢、滾動重啟——根因是 log 送不出去,但你看到的是「服務不健康」。

兩條路:

"logConfiguration": {
  "logDriver": "awslogs",
  "options": {
    "awslogs-group": "/ecs/web-app",
    "awslogs-region": "ap-northeast-1",
    "mode": "non-blocking",
    "max-buffer-size": "25m"
  }
}

non-blocking 用一個記憶體緩衝區接 log,滿了就丟——文件說「丟訊息通常比卡住應用程式好」。預設緩衝只有 1 MB,正式環境要調大。代價是尖峰時可能掉 log,這是明確的取捨:寧可掉幾行 log,不要服務停。

另一條路是 FireLens(Fluent Bit 當 sidecar):不會阻塞、還能把 log 路由到 S3 或 OpenSearch,省 CloudWatch 的錢。量大的正式環境走這條。

四、Log 預設永不過期,而且每筆要帶關聯 ID

官方文件原話:「預設情況下,log 資料無限期存放在 CloudWatch Logs」。建 log group 的時候不設保留期,它就一直存、一直算錢(成本面見 第 11 篇)。

aws logs create-log-group --log-group-name /ecs/web-app
aws logs put-retention-policy --log-group-name /ecs/web-app --retention-in-days 30

關聯 ID:每筆 log 帶一個 request ID,跨服務用同一個。沒有它,Logs Insights 再快也只能看到「某個服務某一秒出錯」,串不出「這個使用者的這次請求經過哪五個服務、在第三個掛掉」。ALB 會給每個請求一個 X-Amzn-Trace-Id,直接沿用它就好——順便把 ALB 的 access log 開起來(預設是關的)。一個 request 從外面進來、經過一層層元件再回去,這一路上能留紀錄的地方裡,ALB 的 access log 最重要。

Logs Insights 留一個最常用的查法:

fields @timestamp, @message
| filter @message like /ERROR/
| stats count() as errors by bin(5m)
| sort @timestamp desc

五、追蹤:ECS 沒有 DaemonSet,所以是 sidecar

X-Ray 要在每個 task 旁邊放一個 daemon 容器收 trace——K8s 用 DaemonSet 一個 node 放一個,Fargate 沒有 node,只能每個 task 都帶一個 sidecar。

{
  "name": "xray-daemon",
  "image": "amazon/aws-xray-daemon",
  "cpu": 32, "memoryReservation": 256,
  "portMappings": [{"containerPort": 2000, "protocol": "udp"}]
}

要可攜的話 sidecar 換成 OpenTelemetry Collector,程式用 OTel SDK,後端接 X-Ray 或自架的 Tempo 都行——換平台不用改程式碼。

Container Insights 一行開,看 task 層級的 CPU / 記憶體 / 網路:

aws ecs update-cluster-settings --cluster prod-cluster \
  --settings name=containerInsights,value=enabled

這篇的驗證標準

主張❌ 不算✅ 算
告警會響alarm 是 OK 狀態人為觸發一次,通知真的到指定通道——SNS 訂閱沒確認、Slack webhook 過期,設定是綠的但永遠不會響
log 串得起來log 有進 CloudWatch拿一個關聯 ID,跨服務查得到完整鏈路
log driver 不會卡設了 non-blocking壓測時故意讓 CloudWatch 限流,服務不掉

實戰 Checklist

  • 八條告警設了,CPU 不在裡面;每條有動作、有 runbook 連結
  • 死信佇列 > 0 就告警
  • awslogs 改 non-blocking 並調大緩衝,或改 FireLens
  • 每個 log group 有保留期;每筆 log 帶關聯 ID;ALB access log 開了
  • 每季人為觸發一次告警,確認通知真的到

從 K8s 過來的話

Prometheus + Alertmanager + Loki + Tempo 那套,AWS 對應 CloudWatch Metrics + Alarms + Logs + X-Ray——但 Alertmanager 那一段是退步的,心理準備要有。DaemonSet 收 log 和 trace 的做法在 Fargate 上不存在,全部變 sidecar。

你在 K8s 上用慣的 OTel 可以原封不動搬過來:sidecar 換成 OTel Collector,後端指到 X-Ray。這是少數「自架經驗直接可用」的地方。細節在 K8s 監控。

相關文章


監控的目的不是收集資料,是在出事之前就看到苗頭——或至少在出事的時候知道去哪裡找答案。