做运维和架构这行,时间久了都会有一个感觉:日志分析这件事,真正的门槛不在命令背得熟不熟,而在策略。命令是死的,grep、awk、journalctl 就那些参数,任何人花两周都能背下来;但面对一台故障机器、几 G 日志文件的时候,先看什么、过滤掉什么、统计什么、把哪几条日志串成一条完整证据链,这套东西才是拉开差距的地方。
这篇是系列第三篇。前两篇聊了 Linux 基础命令和几个常见服务的日志特征,这篇重点放在“策略”两个字上:日志分级怎么定、采集轮转怎么规划、日常排查按什么套路走、告警阈值怎么设置才有意义。文章会按“源头治理—分析套路—告警响应—踩坑实录”的顺序展开,适合正在从新手向资深运维过渡的同学,也适合刚接手一套复杂业务系统的架构师参考。后面所有命令我都基于实际生产环境验证过的写法,可以直接抄。
1. 日志分析策略的第一步:给日志分级并建立排查边界
1.1 日志为什么要有“策略”而不是“搜关键词”
很多刚入行的同事问我,日志分析不就三个命令吗?tail、grep、awk,出了问题直接去日志里搜 ERROR 不就行了。理论上没错,但现实往往是一搜搜出来几千行,而且一大半是无关干扰。
我举一个真实的例子。某天凌晨支付服务报“下单超时”,新同事在日志文件里 grep ERROR,出来 5000 多行,包括各种网络抖动重试、缓存超时、熔断降级,看得头皮发麻。而我接手后做的第一件事,是先把排查边界画出来:时间边界定在故障前 5 分钟到故障后 10 分钟,模块边界定在支付接口调用链,级别边界只看 ERROR 和 FATAL。这么一框,真正需要关注的日志就剩一小段,几分钟就定位到是数据库连接池被慢查询打满。
这就好比家里接到电话说水管漏水。你不可能把整栋楼的水管都拆开查,肯定先问清楚:哪个房间、什么时候开始漏、漏得严重不严重。日志分析的“策略”,本质就是这一套先问问题再动手的排查逻辑。没有策略直接翻日志,等于不用索引直接全表扫描,运气好能中,运气不好就淹死在日志海里。
那问题来了:这个“策略”到底怎么建?我的建议是,不要等到出故障才想,而是提前把日志分级、边界定义、分析顺序这三件事固化下来,变成团队都能遵守的约定。
1.2 一套通用的日志分级模型
日志分级不是越细越好,太细了反而没人执行。20 年下来,我认为一套够用的分级模型只需要四层:
| 级别 | 典型内容 | 排查时的关注点 |
|---|---|---|
| ERROR / FATAL | 服务异常、启动失败、数据库连接失败、未捕获异常 | 优先看堆栈和上下文,这是故障的主战场 |
| WARN / INFO | 重试、降级、性能退化、状态变更 | 主要看趋势和频率,单条往往不是问题 |
| ACCESS / AUDIT | 登录、接口调用、权限变更、敏感操作 | 安全审计、合规留痕、异常访问行为分析 |
| DEBUG | 业务细节、变量值、流程分支 | 平时关闭,出问题时临时开启 |
注意一个关键点:ERROR 之间也有区别。有的 ERROR 是业务预期内的(比如用户取消支付、第三方回调超时),有的是系统级故障(比如磁盘满、OOM)。如果只是机械地认为 ERROR 就必须处理,那告警疲劳、误判会把人折磨到怀疑人生。正确做法是在日志格式里给错误分类预留字段,比如[BIZ]表示业务异常,[SYS]表示系统异常,后面写告警策略时就可以直接按字段过滤。
很多 Linux 面试题里会考日志分析的常用命令,但真正的工作场景里,能用好分级模型的人永远比背命令的人走得远。分级模型定下来之后,团队里任何一个人接手排障,都知道先看哪一层,这就是策略的第一重价值。
1.3 三个排查边界,提前画好用起来省一半时间
定完级别,还要定边界。我常用的边界有三个,每次排查前都会在脑子里过一遍。
第一个是时间边界。日志文件再大,绝大多数故障都集中在某个时间窗口内。先确定故障开始和结束的时间点,然后用--since、--until或者sed -n '/10:00:00/,/10:30:00/p'把范围切出来。没有时间边界就去翻全量日志,基本等同于大海捞针。
第二个是服务边界。一台机器上可能跑着十几个服务,日志文件也可能有几十个。确定是哪个服务出了问题,直接进对应日志目录,比在一堆文件里 grep 高效得多。多服务共用一个日志文件时,就靠日志里的服务名或模块名字段来过滤。
第三个是进程或请求边界。一个服务进程下面有好多线程,一个请求会经过接入层、业务层、存储层。要么记住异常线程的 PID,用 PID 去 grep 上下文;要么利用请求 ID(trace_id / request_id)把一次请求在所有日志里的痕迹串起来。边界定得越准,后面要分析的日志量就呈指数级下降。
2. 日志采集与轮转策略:源头治理比事后救火更重要
2.1 journald 和文本日志怎么选
很多系统默认用 systemd 的 journald 收集系统日志,这个设计非常好:它把内核日志、syslog、各服务标准输出都收拢到一起,用journalctl一条命令就能查询,而且自带结构化字段,比如_PID、_SYSTEMD_UNIT、_HOSTNAME,过滤起来非常方便。
但 journald 也有两个实际痛点。一是默认不持久化,重启机器后日志会丢;二是日志文件存储格式是二进制的,虽然journalctl能读,但想用其他分析工具直接处理时就不太顺手。所以我推荐的方式是混用:系统服务的标准输出交给 journald,用journalctl实时查;业务应用的关键日志单独以文本文件落盘,方便用传统命令分析和后续采集。
文本日志的好处是所有 Linux 系统都认,权限管理简单,日志分析工具链丰富,出了问题拷贝一个文件就能交给其他人排查。如果你面对的是一套老系统,没有结构化日志也没关系,文本日志足够搞定绝大多数场景。不同发行版默认配置略有差异,但无论 CentOS、Ubuntu 还是各种国产发行版,底层基本都是 systemd + rsyslog 这套体系,规则配置思路是通用的。
2.2 logrotate 配置实例与参数解释
日志文件最怕的就是无限增长。磁盘被日志打满,服务各种异常,这是生产环境最常见的低级事故之一。好在 Linux 自带的 logrotate 能解决这个问题,但我发现很多人只会写最简单的rotate 7,遇到服务仍占用旧文件句柄的问题就一头雾水。
下面这个配置是我比较常用的模板,拿一个应用日志样例来说明:
/var/log/myapp/app.log { daily rotate 7 compress delaycompress missingok notifempty copytruncate create 0640 app app }逐项解释一下关键参数。daily表示每天轮转一次,rotate 7表示保留 7 份历史日志。compress将轮转出的历史文件压缩成 gz,delaycompress则是延迟一天再压缩。为什么要延迟?因为有些应用进程在轮转瞬间可能还在写旧文件,立刻压缩会把正在写入的数据压坏,延迟一天等文件彻底冷却下来再压更安全。
copytruncate对某些不重开文件句柄的服务特别重要。默认 logrotate 会把旧日志改名、再新建一个同名文件,但如果应用始终持有旧文件的句柄,那改名后它继续往旧文件里写,新文件反而一直空着。copytruncate会先复制一份当前内容,再把原文件截断,应用无感知,不会出现日志漏写的问题。代价是复制和截断之间可能有少量日志丢失,但对大多数业务日志来说完全可接受。写完之后先测试:logrotate -d /etc/logrotate.d/myapp做空跑验证,确认无误再手动执行一次logrotate -f /etc/lograte.d/myapp。
2.3 采集侧的几个取舍
轮转只是第一步,采集侧的策略同样不能忽略。第一件要做的就是对磁盘用量设置上限。journald 默认日志增长可能很恐怖,建议在/etc/systemd/journald.conf里设置SystemMaxUse=500M,限制总占用。如果用的是 rsyslog,也要在配置里加上频率限制,避免某个服务疯狂打印日志把整个系统拖垮。
第二件是处理多行日志。Java 异常堆栈和 Python 的 traceback 都是一行开头、后面跟多行的形式,简单的行采集会把一条完整异常拆成几十条碎片,分析时特别头疼。这个问题最好在采集器层面解决:Filebeat 和 Logstash 都支持multiline合并规则,按“下一行是否以时间戳开头”来决定是否续接,这样能从源头规整日志格式。
第三件是敏感信息脱敏。业务日志里经常会混入密码、token、手机号、身份证号,如果不做处理就进日志平台,等于把用户隐私直接暴露给所有能查日志的人。建议在应用打印日志之前统一过滤,或者在采集端用正则替换,比如把密码字段的password=123456替换成password=***。这类策略越早定越好,出了安全事故再补就被动了。
3. 一套能直接复用的日志分析实操套路(journalctl + grep + awk)
3.1 journalctl 高效用法
先说 journalctl,因为它查系统服务日志确实好用。最核心的思路是“先缩小范围,再上过滤条件”,而不是直接journalctl全量输出然后慢慢翻。下面的命令组合我很常用:
journalctl -u nginx --since "-10min" -p err -x --no-pager这条命令的意思是:查看 nginx 服务最近 10 分钟内,级别在 err 及以上的日志,-x自动补充日志中引用的文档解释,--no-pager直接输出到终端,方便配合管道做后续处理。-u指定服务单位,--since指定时间窗口,-p指定日志级别,这几个参数配合使用,绝大多数系统服务排查场景都能覆盖。
如果想导出结构化的 JSON 日志做统计,加一个-o json-pretty,然后直接管道给 jq 等工具处理。比如:
journalctl -u payment-api --since "2024-06-15 10:00:00" -o json-pretty | jq 'select(.PRIORITY == 3) | .MESSAGE'这里有个容易忽略的细节:journald 记录的时间戳默认显示为本地时间,但如果系统时区设置不对,查出来的时间基准就是错的。排查前先date确认一下系统时间,否则你按“故障时间”去查日志,很可能什么都查不到。
3.2 文件日志的筛选与统计组合拳
对于文本日志,我的核心组合拳是“定位—上下文—统计”三步走。第一步用 grep 定位关键行,注意用-E支持扩展正则,避免把多个条件拆成多条管道。定位到少量目标行后,用-A和-B拉出前后文:
grep -n "ERROR" /var/log/myapp/app.log | head -20 grep -n -A 20 -B 5 "NullPointerException" /var/log/myapp/app.log第二步是统计,找出“到底哪里报错最多”。比如我要看某个接口的错误集中在哪个模块,就先拿到日志中模块字段对应的列,然后统计 Top 10:
grep "ERROR" app.log | awk '{print $6}' | sort | uniq -c | sort -rn | head -10这条命令的逻辑:先筛出所有 ERROR 行,awk 取出第 6 列(具体字段按实际日志格式调整),sort 排序让相同值相邻,uniq -c 做次数统计,再按次数倒序排列取前 10。这样一跑,哪个模块是故障热点、哪个服务在刷屏,一目了然。
如果要做时间维度分析,看某个时间段错误量的变化曲线,可以用 awk 截取时间字段并按分钟归并:
awk '/2024-06-15 10:/{print substr($0,1,19)}' app.log | uniq -cuniq 的原理是相邻去重,所以前面必须保证时间字段已经排好序,如果原始日志乱序,就先 sort 一下再 uniq -c。这个统计思路可以灵活改造,比如把时间粒度改成小时、把过滤条件从 ERROR 改成某个业务码,就能得到完全不同的观察角度。
3.3 把日志串成证据链:时间、PID、RequestID
单条日志只是孤证,真正能定位根因的是证据链。最常见的串联维度有三个。
第一个是时间线对齐。多服务部署时,同一个故障会同时落在多个服务的日志里,而且各自时间戳可能还有毫秒级偏差。我会把相关日志先按时间排序,再用 grep 或 sed 把同一时间窗口的日志提取出来,按时间交错排列,这样请求链路的前后关系就非常清楚。纯文本排序用sort -k1,2之类的参数按时间列排序即可。
第二个是 PID 关联。遇到线程池满了、CPU 飙高这类问题,先用 top 或 jstack 拿到异常线程的 PID,然后直接拿去日志里过滤:
grep "PID=12345" app.log | tail -50通过 PID 能把一次线程从创建、执行到报错的完整过程还原出来。这个方法尤其适合排查 JVM 服务和 C++ 服务。
第三个是 RequestID 追踪。现在稍微正规一点的系统都会在接入层生成一个 trace_id,通过 HTTP 头或日志字段向后端传递。只要日志里打了这个 ID,无论请求经过多少个服务,都能用一条命令把整条调用链捞出来:
grep "trace_id=8a2f5c1e9d" gateway.log | sort -k3 grep "trace_id=8a2f5c1e9d" payment-api.log | sort -k3这里我没法一步跨文件拼接,所以通常的做法是把每个服务的匹配结果分别导出成小文件,再合到一起按时间排序。遇到一次复杂的分布式调用超时,这个思路能节省数小时。我印象最深的一次排障:支付服务报连接池满,用 PID 关联日志后,发现是某个定时任务发起了一批慢 SQL,把池子占满了,而请求量大只是表象。没有证据链,这种隐蔽根因根本挖不出来。
4. 告警与响应策略:从“看到日志”到“处理完故障”的最后一公里
4.1 告警阈值怎么定才有意义
很多人第一次写告警规则时都会犯一个毛病:只要日志里出现 ERROR 就告警。结果一天几百条,夜里被电话轰炸,很快就没有人再认真看了。等真正出了大事故,告警响了一声没人接,反而误了时机。
正确的做法是先建立基线。统计过去 7 天每天同一时间窗口内 ERROR 数量,算出平均值和标准差,再以“平均值 + N 倍标准差”作为阈值基线。比如某服务的日均 ERROR 是 100 条,波动标准差是 20 条,那阈值可以定在 160 条左右。低于这个数都属于正常抖动,不需要打扰任何人。
更稳的组合方式是“比例 + 持续时间”。比如错误率超过 1% 且持续 3 分钟才触发告警,或者 5 分钟内错误数环比上升超过 200% 才触发。用比例而非绝对值,可以自动适应流量高峰和低谷的变化。我帮一家电商调过支付告警,原来是每天上百条误报,改成“错误率 + 持续时间”的组合策略后,一周只有 3 条有效告警,而且每一条都对应真实事故。
4.2 告警分级与降噪策略
告警必须分级,否则处理人的心态永远是“狼来了”。我习惯分成三档:
| 级别 | 定义 | 响应要求 |
|---|---|---|
| P1 | 业务不可用、数据丢失、核心服务宕机 | 5 分钟内响应,立即拉群、通知值班长 |
| P2 | 核心功能受损但系统可用,如支付成功率下降 | 30 分钟内响应,优先排查 |
| P3 | 非核心异常、潜在隐患,如某接口偶发超时 | 记录观察,白天处理 |
分级之后还要降噪。常用的降噪三招:一是收敛通知,同一个故障在告警恢复之前不重复发送,避免一个人一分钟收 10 条短信;二是加白名单,把已知业务预期的 ERROR 特征码排除掉,比如“用户取消支付”这种业务事件;三是确认机制,告警发出后如果 10 分钟内无人确认,自动升级到下一级。这套机制跑顺之后,值班同事的精神压力会小很多,真出问题的时候响应速度反而更快。
4.3 从日志到处理的完整闭环
日志分析的策略,最终要落到“能自动处理就自动处理,不能自动处理就走标准 SOP”。我见过太多团队,日志采集做得很完善,告警也响得很及时,但告警响应全靠某一位老师傅手忙脚乱地操作。这样的策略算不上闭环。
比较理想的闭环是:告警触发后,先由脚本自动尝试恢复。比如磁盘空间超出阈值,自动清理临时文件和过期备份;进程挂了,自动拉起并打印重启原因。自动处理不成功,再带着已经在告警信息里拼好的上下文转人工。这样排查时的“起跑线”就高了很多——值班人手上有的是“日志证据包加推荐处理动作”,而不是一条干巴巴的告警短信。
我还要求团队每次故障处理完,必须做一次日志复盘:当时哪些日志信息有用?哪些日志字段缺失导致多花了时间?这些结论反哺到采集策略和告警规则里。做上两三轮之后,告警的准确率和排障速度都会有质的提升。
5. 20年运维踩坑实录:日志分析里最容易翻车的四个细节
5.1 时区与时间戳陷阱
时区问题是日志分析里最隐蔽也最坑人的细节。有一次我们排查一个凌晨的支付超时,业务日志显示请求是 00:30 进来的,但系统日志显示同一时间服务已经在 08:30(前一天晚上)就出现了连接异常。两边时间差了 8 个小时,因为应用服务器时区设成了 UTC,而业务日志的框架按系统时区打印了 UTC 时间,到了人的脑子里又自动换成了北京时间,导致怎么都对不上。
遇到这种问题,手动转换可以用 date 命令:
date -d "2024-06-15 00:30:00 UTC" "+%Y-%m-%d %H:%M:%S %Z"这条命令把 UTC 时间转成本地时区时间,输出直观。但更根本的解决策略是:所有服务器、所有应用日志统一使用同一时区(国内业务一般统一 UTC+8),或者在日志格式里直接写入带时区的 ISO8601 时间戳,比如2024-06-15T00:30:00+08:00。这样不管是采集、存储还是人工分析,都不会被时区差异干扰。定这个规矩花不了十分钟,但能避免以后无数个深夜的“幽灵时间”。
5.2 ANSI 颜色码干扰统计结果
systemd 的 journalctl 输出默认是带颜色高亮的,如果直接管道给 grep、awk 处理,问题不大;但如果先把输出重定向到文件再统计,那些颜色转义序列会残留下来,变成一堆[36m之类的垃圾字符。统计结果会莫名奇妙地偏多或偏少,而且你还看不出来原因。
我踩过这个坑后养成了习惯:任何文本日志的原始文件尽量不带 ANSI 色码,应用打印时直接关闭颜色输出;如果拿到了带颜色的日志,先统一清洗一遍再分析。清洗命令如下:
sed -r 's/\x1B\[[0-9;]*[mK]//g' dirty.log > clean.log\x1B是 ESC 字符,后面的[0-9;]*[mK]匹配颜色代码序列。文件名带颜色码的日志先过一遍这条命令,再去 grep 正则就准确多了。我还见过有人因为在日志里搜索[36mERROR啥都搜不到,最后发现是颜色码把字符串分隔开了。这种问题不大,但一旦遇到,会浪费半小时。
5.3 大文件分析与管道缓冲问题
日志文件超过几个 G 时,管道的“断流”问题就显现了。常见场景是用 grep 从大文件中筛出结果,再接 head 只取前 10 行。head 读够 10 行就退出,但 grep 还在继续读文件,写管道时发现下游没了,就会收到 SIGPIPE 信号直接被终止。这时候 shell 会报 “Broken pipe”,grep 处理到一半就被杀掉了。
更隐蔽的问题是你拿到了前 10 行,但不确定这 10 行是否覆盖了所有关键信息。解决方案是不要用 head 截断管道,而是用 grep 的-m参数直接限制匹配行数:
grep -m 20 "FATAL" app.log这样 grep 找到 20 行后就主动停止,不会浪费资源继续扫全文件,也不会触发管道断开。另外,面对压缩过的老日志,直接zgrep、zcat就能处理,不用先解压。还有个小技巧:运行grep 文件时直接传文件名,不要先cat 文件 | grep,这样能省一次大文件读盘,对几个 G 的日志来说差别非常明显。
5.4 轮转间隙导致留证不足
有次生产故障,我们花了一整天才定位到根因,结果想回头翻出事当天某个节点的日志时,发现 logrotate 已经把当时的日志轮转并清理了,只剩压缩包。压缩包里的内容还不全,因为故障高峰期日志量巨大,没等到第二天就触发了按大小轮转,历史文件被覆盖。没有原始日志,那次复盘只能靠记忆拼凑,非常被动。
从此我定下规矩:任何一次故障响应,第一件事不是分析,而是先留证。哪怕是正在处理,也要先执行一条保存命令:
cp /var/log/myapp/app.log /var/log/myapp/archive/app.log.$(date +%F_%H%M).save如果日志文件特别大,来不及全量复制,就用 tail 保存尾部,因为大部分故障的现场都集中在最新的一段:
tail -c 200M /var/log/myapp/app.log > /var/log/myapp/archive/app.log.tail.save这个习惯救过我很多次。重要的业务日志建议直接送到中心化日志平台,保留至少 30 天,这样既不怕轮转清数据,又能跨服务器检索。本地轮转策略解决的是磁盘风险,中心化日志解决的是留证和检索,两者配合才算完整。
最后分享一个我沉淀多年的小习惯:接手任何一套新系统,第一件事不是看代码,而是花一个下午把日志的分级、轮转、采集、告警这四件事捋一遍。磨刀不误砍柴工,策略一旦建立起来,后面每一次排障都快很多。日志分析这件事,你把它当搜索,它就是一条命令的事;你把它当策略,它就是整个运维体系的指路标。这个系列后面还有一篇,计划专门写容器和云原生场景下的日志分析差异,有兴趣的话可以先关注,到时候对照着看收获会更大。