15、日志与监控模式:结构化日志、日志分级、链路追踪(TraceId)、Metrics收集、告警规则设计
日志和监控,说白了就是系统的「黑匣子」和「仪表盘」。
我见过太多团队,线上出了故障,第一反应是「上服务器翻日志」。结果呢?日志文件几百兆,grep 半天找不到关键信息。为什么?因为日志写得随心所欲,没有结构,没有分级,更别提链路追踪了。
今天我们就来聊聊,怎么把日志和监控这件事,做得像样一点。
一、结构化日志:别再打印字符串了
我记得刚入行那会儿,写日志都是这样的:
logger.info("用户 " + userId + " 下单成功,订单号:" + orderId);
这种写法有什么问题?你想想看,如果我要统计今天所有订单的 userId 分布,怎么办?只能写正则表达式去匹配字符串。效率低,还容易出错。
我个人习惯,从 2016 年开始全面转向结构化日志。说白了,就是把日志内容变成键值对的形式:
logger.info("order_created",
"userId", userId,
"orderId", orderId,
"amount", amount,
"timestamp", System.currentTimeMillis());
在 JSON 格式下,它长这样:
{
"event": "order_created",
"userId": "u12345",
"orderId": "ord67890",
"amount": 299.00,
"timestamp": 1712345678000,
"level": "INFO",
"service": "order-service"
}
核心要点:结构化日志的字段应该包含「事件名」「关键业务ID」「时间戳」「服务名」「日志级别」。这样后续做分析、告警、聚合,都方便得多。
我在项目中遇到过,某次线上排查慢查询,就是因为日志里没有记录 SQL 的执行耗时。后来我们强制要求:所有数据库操作日志,必须包含 sql、costMs、rows 三个字段。排查效率直接翻倍。
二、日志分级:别什么都打 INFO
日志分级,这个大家应该都懂。但实际做的时候,很多人分不清边界。
我见过最离谱的,是把「数据库连接失败」打成 DEBUG 级别。你想想看,连接都失败了,还 DEBUG?这要是线上出了故障,告警都发不出去。
我个人建议的分级标准:
| 级别 | 使用场景 | 示例 |
|---|---|---|
| ERROR | 系统无法自动恢复的异常 | 数据库连接失败、第三方接口超时 |
| WARN | 不影响主流程但值得关注 | 缓存穿透、降级触发、重试次数超过阈值 |
| INFO | 关键业务节点 | 订单创建、支付回调、用户注册 |
| DEBUG | 开发调试用,线上默认关闭 | 方法入参、中间变量、循环细节 |
注意:线上环境不要开 DEBUG 级别。我曾经见过一个团队,线上开了 DEBUG,结果日志量暴涨 100 倍,磁盘直接打满,服务挂了。嗯,血的教训。
三、链路追踪:TraceId 是救命稻草
微服务架构下,一个请求可能要经过 5、6 个服务。如果每个服务各自打日志,出了问题你根本串不起来。
解决方案?TraceId。
说白了,就是在请求入口生成一个全局唯一的 ID,然后透传到所有下游服务。每个服务打印日志时,都带上这个 TraceId。
我习惯的做法:
// 在网关或入口处生成 TraceId
String traceId = UUID.randomUUID().toString().replace("-", "");
MDC.put("traceId", traceId);
// 下游服务通过 RPC 或 HTTP 头获取
// 比如 Dubbo 用 RpcContext,HTTP 用 Header
日志里长这样:
2025-04-01 10:00:00.123 [traceId=abc123] [service=order] INFO order_created userId=u1
2025-04-01 10:00:00.456 [traceId=abc123] [service=payment] INFO payment_success orderId=ord1
2025-04-01 10:00:00.789 [traceId=abc123] [service=notification] INFO notify_sent userId=u1
你看,只要 grep 一下 abc123,整个请求的完整路径就出来了。
小技巧:TraceId 最好在网关层生成,不要每个服务自己生成。否则你没法把多个服务的日志串起来。另外,建议把 TraceId 放在日志格式的最前面,方便肉眼快速定位。
四、Metrics 收集:用数据说话
日志是「发生了什么」,Metrics 是「发生了什么程度」。
我常用的 Metrics 类型就三种:
- 计数器(Counter):比如请求总数、错误总数。只增不减。
- 直方图(Histogram):比如接口响应时间分布。可以算 P50、P99。
- 仪表盘(Gauge):比如当前在线人数、队列长度。可增可减。
举个例子,监控一个下单接口:
// 计数器:记录请求次数
metrics.counter("order.create.total").inc();
// 直方图:记录耗时
metrics.histogram("order.create.cost").update(costMs);
// 仪表盘:记录当前并发数
metrics.gauge("order.create.concurrent").set(currentConcurrent);
这些数据收集上来之后,配合 Prometheus + Grafana,就能画出漂亮的监控面板。
我记得有一次,线上突然出现大量超时。我们一看 Grafana,发现 order.create.cost 的 P99 从 200ms 飙升到了 5s。再一看,数据库连接池的活跃连接数打满了。5 分钟定位问题,这就是 Metrics 的价值。
五、告警规则设计:别让告警变成噪音
告警这件事,做少了不行,做多了更不行。
我见过最夸张的团队,一天收到 2000 条告警。结果呢?运维直接把告警群屏蔽了。这叫「狼来了」效应。
我个人总结的告警设计原则:
- 少而精:每条告警都必须有明确的处理动作。如果收到告警你只能「先看看」,那这条告警就不该发。
- 分级处理:P0 告警(服务宕机)直接电话 + 短信;P1 告警(错误率飙升)发群消息;P2 告警(磁盘使用率超过 80%)发邮件。
- 避免抖动:比如错误率偶尔抖动一下,不要立刻告警。我习惯加一个「持续 N 分钟」的条件。
举个例子:
# 告警规则示例(PromQL)
# 错误率超过 5% 且持续 5 分钟
rate(http_requests_total{status="5xx"}[5m]) / rate(http_requests_total[5m]) > 0.05
避坑指南:我曾经设计过一条告警:「CPU 使用率超过 90% 就告警」。结果呢?业务高峰期 CPU 长期 95%,告警刷屏。后来改成「CPU 超过 90% 且持续 10 分钟」,噪音立刻消失了。嗯,阈值和持续时间,两者缺一不可。
六、知识体系总览
下面这张图,是我对日志与监控模式的整体理解。你可以把它当成一个「检查清单」:
这张图把五个核心模块串在了一起。从上到下看,就是一套完整的「采集 → 存储 → 分析 → 告警」链路。
好了,关于日志与监控模式,我就聊这么多。记住一句话:没有监控的系统,就像闭着眼睛开车。 你永远不知道下一秒会撞上什么。
公众号:蓝海资料掘金营,微信deep3321