技术指南
Linux 日志排障:固定时间窗、关联请求与安全轮转
用固定时间窗、boot、unit 与 request ID 串联 journal、代理和应用日志,并安全验证保留与轮转。
日志排障不是在多个文件里搜索一个报错词,而是先固定事件边界,再判断每条记录由谁产生、经过哪条采集链、可能在哪里丢失。一次可复核的调查至少要写清 UTC 起止时间、主机、boot ID、systemd unit、请求或任务标识,以及当时部署版本;没有这些坐标,来自不同重启和不同请求的相似文本很容易被拼成错误结论。
先写一张事件坐标卡
| 字段 | 示例 | 用途 |
|---|---|---|
| 时间窗 | 2026-08-15 21:40:00–21:45:00 UTC | 让 journal、代理与应用落在同一范围 |
| 主机与 boot | api-02 / journalctl --list-boots | 隔离重启前后的同名服务 |
| 工作负载 | example-api.service / 容器 ID | 避免搜索到另一实例 |
| 关联标识 | request_id=req-7f3 / job_id=42 | 串起一次请求的多层记录 |
| 变更版本 | 镜像 digest / Git SHA / 配置版本 | 把异常与发布边界对齐 |
时间窗要包含触发前后的缓冲,但不应直接导出整台主机几天的日志。先用用户报告、监控告警或一次受控复现取得锚点;再把所有工具统一到 UTC,并记录命令执行时主机时钟是否可信。
确认日志实际存在哪里
systemd journal、传统 syslog 文件和应用自写文件不是天然的三份副本。journald 的 `Storage=` 可以是 volatile、persistent、auto 或 none;传统 syslog 守护进程既可能从 journal 文件读取,也可能接收转发。先检查有效配置和文件位置,再决定搜索范围。
systemd --version
sudo systemctl status systemd-journald --no-pager
sudo systemd-analyze cat-config systemd/journald.conf
sudo journalctl --disk-usage
sudo journalctl --list-boots --no-pager
sudo find /var/log/journal /run/log/journal \
-maxdepth 2 -type f -name '*.journal*' -ls 2>/dev/null用 boot、unit 与固定时间窗收窄 journal
`--since` 与 `--until` 约束时间,`-b` 约束启动会话,`-u` 约束 unit;这些条件组合后再检查优先级、PID 和消息。相对时间适合现场浏览,事件记录应保存绝对 UTC 时间,避免复盘时窗口漂移。
START='2026-08-15 21:40:00 UTC'
END='2026-08-15 21:45:00 UTC'
UNIT='example-api.service'
sudo journalctl -b 0 -u "$UNIT" \
--since "$START" --until "$END" \
--no-pager -o short-full
sudo journalctl -b 0 -k \
--since "$START" --until "$END" --no-pager不要只截 `tail -n 100`。繁忙主机的最后一百行可能完全越过事故窗口,而低流量主机又可能把很久以前的事件混进来。若服务重启,分别记录启动前后的 unit 状态,并用 `_BOOT_ID`、`_SYSTEMD_UNIT`、`PRIORITY`、`_PID` 和 `SYSLOG_IDENTIFIER` 判断记录身份。
优先读取结构化字段
journal 原生保存字段,Nginx 也能通过 `log_format escape=json` 输出 JSON。结构化查询比按空格切列更稳健:本地化时间、带空格消息、引号与转义都不会让固定列号悄悄错位。字段值仍由各组件定义,不能因为格式是 JSON 就假定内容可信。
sudo journalctl -b 0 -u "$UNIT" \
--since "$START" --until "$END" \
-o json \
--output-fields=__REALTIME_TIMESTAMP,_BOOT_ID,_SYSTEMD_UNIT,PRIORITY,_PID,SYSLOG_IDENTIFIER,MESSAGE \
| jq -c '{time:.__REALTIME_TIMESTAMP,boot:._BOOT_ID,unit:._SYSTEMD_UNIT,priority:.PRIORITY,pid:._PID,id:.SYSLOG_IDENTIFIER,message:.MESSAGE}'让代理与应用共享关联标识
HTTP 状态或某个 IP 的峰值只能提出假设,不能单独证明攻击或应用根因。先让入口层记录 request ID、Host、规范化路径、状态、耗时和上游状态,再在应用日志写入同一 request ID;随后按固定时间窗关联。不要记录 Authorization、Cookie、请求体或可能携带 token 的完整查询串。
log_format incident escape=json
'{"time":"$time_iso8601",'
'"request_id":"$request_id",'
'"remote_addr":"$remote_addr",'
'"host":"$host","uri":"$uri",'
'"status":$status,"request_time":$request_time,'
'"upstream_status":"$upstream_status"}';
access_log /var/log/nginx/access.json incident;REQUEST_ID='req-7f3'
jq -c --arg id "$REQUEST_ID" \
'select(.request_id == $id)' /var/log/nginx/access.json
sudo journalctl -b 0 -u "$UNIT" \
--since "$START" --until "$END" -o json \
| jq -c --arg id "$REQUEST_ID" \
'select(.MESSAGE | strings | contains($id))'按故障类型补齐相邻证据
- 进程退出:记录 `systemctl status`、unit journal、退出状态、重启计数和部署事件,不把最后一条应用日志自动视为根因。
- 内存压力:在同一 boot/time window 查看 kernel journal、cgroup 指标和应用内存;单独看到 killed 或超时不足以证明 OOM。
- 认证异常:按 sshd、sudo、PAM 或应用实际 unit 与事件字段查询;不要假设 `/var/log/auth.log` 必然存在,也不要按固定空格列提取账号和地址。
- Web 错误:关联入口状态、upstream status、request time 与应用 request ID;5xx 增长需要先区分入口、网络和应用层。
- 重启或时间跳变:切换目标 boot,核对 NTP/时钟事件;跨 boot 的相似 PID 没有身份连续性。
导出最小充分证据包
调查副本应与工作日志分离,并以最小权限保存。journal 的 export 输出保留字段和值,JSON 副本方便分析;两者都可能包含账号、内部地址、命令行或用户数据。对外分享前从副本脱敏,保留未修改原件的校验和和访问记录。
umask 077
CASE_DIR='incident-20260815-req-7f3'
mkdir -p "$CASE_DIR"
sudo journalctl -b 0 -u "$UNIT" \
--since "$START" --until "$END" \
-o export > "$CASE_DIR/journal.export"
sudo journalctl -b 0 -u "$UNIT" \
--since "$START" --until "$END" \
-o json > "$CASE_DIR/journal.jsonl"
sha256sum "$CASE_DIR"/* > "$CASE_DIR/SHA256SUMS"
chmod 0600 "$CASE_DIR"/*把保留策略当成容量与证据设计
`SystemMaxUse=` 与 `RuntimeMaxUse=` 分别约束 persistent 和 volatile journal 的空间,`MaxRetentionSec=` 可增加时间边界;实际回收只删除 archived journal 文件,因此当前用量可能暂时高于目标。`journalctl --vacuum-*` 同样只处理归档文件,调查期间先导出、校验并确认法律与组织保留要求,再决定是否轮转和清理。
高频服务还要检查 journald 的 `RateLimitIntervalSec=` 与 `RateLimitBurst=` 以及 unit 自己的覆盖值。看到 dropped-message 提示时,应该修复日志洪泛和监控缺口,而不是无条件关闭限流;远程副本也不能弥补已经在源端丢弃的记录。
先 dry-run,再验证轮转后的写入者
logrotate 的 `--debug` 不更改日志或 state file,适合先检查匹配和计划;独立演练可用 `--state` 指向临时状态。标准 rename/create 轮转后,写日志的进程必须重新打开文件,通常由官方包的 postrotate 信号完成。不要从博客复制服务名或 PID 路径。
sudo logrotate --debug /etc/logrotate.conf
sudo logrotate --verbose \
--state /var/lib/logrotate/status \
/etc/logrotate.conf
sudo systemctl status logrotate.timer --no-pager
sudo journalctl -u logrotate.service \
--since '24 hours ago' --no-pager远程日志要有独立故障域
远程采集的价值是让主机被破坏或磁盘损坏后仍保留副本,但只有接收端独立授权、传输经过认证与加密、发送队列有界且丢弃可告警时才成立。不要直接向互联网开放未认证的 syslog 端口,也不要宣称 TCP 等同绝不丢失;源端队列、网络、接收端容量和保留策略都需要演练。
本次隔离演练验证了什么
VPScope 在 Ubuntu systemd 255 上启动一次性 unit,写入带同一 request ID 的 info 与 warning 记录。使用当前 boot、绝对 UTC 窗口和 unit 过滤后,JSON 输出保留了相同 `_BOOT_ID`、目标 `_SYSTEMD_UNIT`、syslog identifier、优先级 6/4 与两条消息;使用 `/proc` 的带连字符 UUID 直接作为 `--boot` 参数失败,因此公开步骤改用 `-b 0` 或 `--list-boots` 输出的精确 ID。
随后在 `/tmp` 创建独立 logrotate 配置和 state file。debug 模式确认不写状态;两次受控强制轮转留下 `.1` 与 `.2`,新 active log 按 0640 创建。第一代归档仍保留原文件 0644,证明 `create` 不会回溯修改归档权限。演练文件已删除,系统 journal 配置、`/etc/logrotate*`、生产日志和服务均未修改。
结束调查前的验收清单
- UTC 时间窗、主机、boot、unit/实例、关联标识和部署版本均已记录。
- 每条结论能指向原始字段或明确命令;症状、证据与推断没有混写。
- journal、文件、容器和远程采集的真实路径及缺失边界已经确认。
- 原始最小证据包权限受限且有校验和;分享副本完成脱敏。
- 修复后用同一请求或任务重测,并验证正常路径、告警和日志连续性。
- 轮转、保留、速率限制与远程传输经过受控演练;临时文件和诊断配置已清理。