skip to content
Running Otter

从一次误报告警聊起:HTTP 状态码完全指南

/ 10 min read

故事的开始:一条看起来很吓人的告警

某天,我的 Grafana 突然推来一条告警:

⚠️ 站点 sqlforge.dengshu.ovh 当前错误率达到 100%,超过阈值 10%。

100%?! 整个站挂了?

我赶紧打开 ClickHouse 查日志,发现真相是这样的:

时间段总请求错误数错误率
13:105360%
13:2012216.67%
12:1012325%

所谓的「100% 错误率」,其实是在某个 5 分钟窗口内,只有几个请求,而且全是 404(Not Found)——一些爬虫在请求早已不存在的旧版静态资源:

GET /assets/js/auth.js → 404
GET /assets/js/qr_modal.js → 404
GET /assets/js/message.js → 404
GET /assets/GanjingAbout-0xqqgv2B.js → 404

站点本身完全正常。是告警规则把 404 也算作了「错误」,导致误报。

HTTP 状态码:三位数的大学问

HTTP 状态码是服务器对客户端请求的标准化回答。三位数字,第一位决定分类:

1xx → 信息性:「我收到了,还在处理」
2xx → 成功:「搞定了」
3xx → 重定向:「你要的东西搬家了」
4xx → 客户端错误:「你的请求有问题」
5xx → 服务端错误:「我这边出问题了」

1xx:信息响应(你几乎见不到)

状态码名称大白话
100Continue「你先发的请求头我看了,没问题,继续发 body 吧」
101Switching Protocols「好的,我们切换到 WebSocket 协议了」
103Early Hints「正式响应还没好,但你可以先加载这些资源」

1xx 是中间状态,浏览器自动处理,你在开发和运维中基本不会直接碰到它们。

2xx:成功(好消息)

状态码名称大白话常见场景
200OK「给你,一切正常」最常见,页面正常加载
201Created「资源已创建」POST 创建新用户后
204No Content「操作成功了,但没有内容返回」DELETE 删除操作
206Partial Content「给你请求的那一部分」视频拖动、断点续传

3xx:重定向(搬家通知)

状态码名称大白话区别
301Moved Permanently「永久搬家了,更新你的书签」搜索引擎会更新索引
302Found「临时搬家,下次还来这个地址」搜索引擎保留原地址
304Not Modified「跟上次一样,用你的缓存就行」节省带宽
307Temporary Redirect「临时跳转,保持原请求方法」POST 不会变成 GET
308Permanent Redirect「永久跳转,保持原请求方法」301 的严格版

实际应用:当你改了博客的 URL 结构,用 301 把旧链接指向新链接,这样搜索引擎的排名不会丢失。

4xx:客户端错误(是你的问题,不是我的)

这是最容易被误会的一类。很多人把 4xx 当成「服务出错了」,但其实它的意思是 “客户端发了一个有问题的请求” ——服务器本身是正常的。

状态码名称大白话该紧张吗?
400Bad Request「请求格式不对,我看不懂」看情况,可能是前端 bug
401Unauthorized「你谁?先登录」正常,认证机制在工作
403Forbidden「我知道你是谁,但你没权限」⚠️ 大量 403 需要排查
404Not Found「这个地址没有东西」😴 通常无需关注
405Method Not Allowed「这个接口不支持 GET/POST」API 配置问题
422Unprocessable Entity「格式对但内容不合法」正常,表单验证在工作
429Too Many Requests「你请求太频繁了,休息一下」正常,限流在工作

5xx:服务端错误(我的锅)

这才是真正需要你紧张的:

状态码名称大白话严重程度
500Internal Server Error「出 bug 了,我也不知道怎么回事」🔴 立即排查
502Bad Gateway「我作为代理,后端服务没回应」🔴 上游服务挂了
503Service Unavailable「服务暂时不可用(过载/维护)」🟡 可能是重启中
504Gateway Timeout「等后端服务等太久了,超时了」🔴 后端性能问题

在我的 Nginx 反向代理架构里:

  • 502 通常意味着 Docker 容器挂了或没启动
  • 504 说明应用处理太慢(比如大查询卡住了)
  • 503 可能是在 docker compose restart 的过程中

这三个才是告警应该关注的。

错误 ≠ 故障:运维监控的正确姿势

经过这次误报,我重新审视了告警规则。核心问题是之前的 SQL:

-- ❌ 之前的写法:把所有 4xx 都当「错误」
countIf(status >= 400) * 100.0 / count()

优化后的写法:

-- ✅ 只监控 5xx(真正的服务端故障)
SELECT
toStartOfMinute(now()) as time,
host,
round(countIf(status >= 500) * 100.0 / count(), 2) as error_rate
FROM default.nginx_access_log
WHERE timestamp >= now() - INTERVAL 10 MINUTE
GROUP BY host
HAVING count() >= 30 -- 最低请求量:防止低流量误报
AND countIf(status >= 500) >= 2 -- 最低错误数:排除偶发异常
ORDER BY error_rate DESC

对比一下改动的逻辑:

维度改前改后为什么
错误范围status >= 400status >= 5004xx 是客户端的问题,不应告警
时间窗口5 分钟10 分钟低流量站点需要更大窗口
最低样本量10 个请求30 个请求防止几个请求就触发
最低错误数2 个 5xx单个偶发错误不报

速查表

最后,一张图总结不同状态码在监控中的处理策略:

┌─────────────────────────────────────────────────────┐
│ HTTP 状态码监控策略 │
├──────────┬────────────────────────────────────────────┤
│ 2xx ✅ │ 正常,不需要监控(可以统计趋势) │
│ 3xx ↗️ │ 正常,不需要告警(过多 301 可能是配置问题) │
│ 4xx ⚠️ │ 不告警,Dashboard 观察即可 │
│ └ 404 │ 忽略(爬虫、旧链接、正常现象) │
│ └ 401 │ 忽略(认证机制正常工作) │
│ └ 403 │ 关注趋势(大量可能=权限配置异常) │
│ └ 429 │ 关注趋势(大量可能=被攻击或限流太严) │
│ 5xx 🔴 │ 必须告警!每一个都应该被调查 │
│ └ 500 │ 应用 bug,查日志 │
│ └ 502 │ 上游服务挂了,查容器状态 │
│ └ 503 │ 过载或重启中,查负载 │
│ └ 504 │ 超时,查上游性能 │
└──────────┴────────────────────────────────────────────┘

总结

三位数的 HTTP 状态码,背后是一整套客户端—服务端协作的设计哲学:

  • 2xx 是好消息
  • 3xx 是搬家通知
  • 4xx 是「你的请求有问题」——服务端在正常工作
  • 5xx 是「我的服务出问题了」——需要立刻行动

搞清楚这个区别,你就能写出更精准的告警规则,减少无意义的告警噪音,把注意力放在真正重要的事情上。

参考资料