服务器出问题的时候,日志是唯一知道"当时到底发生了什么"的地方。但日志文件多、格式杂、量大,很多人打开 /var/log 就懵了:该看哪个?怎么看才有效率?grep 出来几十万行又该怎么办?
这篇文章把 Linux 日志分成三层来讲:系统层(systemd journald 与 /var/log 下的传统文件)、Web 层(Nginx 的 access 与 error 日志)、应用层(业务自己的日志)。会给出 journalctl 的常用参数组合、日志轮转的配置方法、Nginx 日志字段的逐项含义,以及一批可以直接复制使用的 awk 统计命令,用来快速算出 Top IP、状态码分布、慢请求这些关键指标。
先搞清楚日志体系的两套并存
现代 Linux 有两套日志机制并存,理解它们的分工是第一步。
systemd journald 是二进制日志系统,收集所有由 systemd 管理的服务输出、内核消息和早期启动信息。用 journalctl 查询,优点是结构化、支持按服务和优先级过滤、支持时间范围,缺点是二进制格式不能用 grep 直接搜。
rsyslog(或 syslog-ng) 是传统文本日志系统,把日志按设施(facility)和优先级写入 /var/log 下的文本文件。这些文件可以用 grep、awk 直接处理,也是大多数老脚本和监控工具依赖的对象。
两者常有重叠:同一条 sshd 日志既在 journald 里,也在 /var/log/secure 里。查问题时优先用 journalctl(更结构化和完整),做文本统计或者交给外部采集工具时用 /var/log 下的文件。
日志文件的位置两系有差异,这是必须记住的:
| 用途 | RHEL / Rocky / CentOS | Debian / Ubuntu |
|---|---|---|
| 通用系统消息 | /var/log/messages |
/var/log/syslog |
| 认证与安全 | /var/log/secure |
/var/log/auth.log |
| 启动信息 | /var/log/boot.log、dmesg |
/var/log/boot.log、dmesg |
| 定时任务 | /var/log/cron |
在 syslog 中,grep CRON |
| 邮件 | /var/log/maillog |
/var/log/mail.log |
| 内核环形缓冲 | dmesg、/var/log/dmesg |
同左 |
journalctl:systemd 日志的核心命令
先了解日志占用了多少空间,这一步在磁盘告警时很有用:
journalctl --disk-usage
常用参数组合:
# 按服务查看
journalctl -u nginx --no-pager
journalctl -u nginx -u php-fpm --no-pager # 多个服务
# 实时跟踪(等价于 tail -f)
journalctl -u nginx -f
# 时间范围
journalctl -u nginx --since '2026-10-10 09:00:00' --until '2026-10-10 10:00:00'
journalctl --since '-30 min' --no-pager
journalctl --since 'yesterday' --until 'today'
journalctl --since '-2 days' -u mysql --no-pager
# 按优先级:emerg(0) alert(1) crit(2) err(3) warning(4) notice(5) info(6) debug(7)
journalctl -p err -b # 本次启动以来的错误
journalctl -p warning --since today
journalctl -p 3 --since '-1 hour' # 数字形式,3 即 err
# 按启动次数
journalctl -b # 本次启动
journalctl -b -1 # 上一次启动
journalctl --list-boots # 列出所有启动记录
# 只看内核消息(等价于 dmesg)
journalctl -k
journalctl -k -b -1
# 输出格式
journalctl -u nginx -o json-pretty --no-pager | head -n 40
journalctl -u nginx -o short-precise --no-pager
journalctl -u nginx -o cat --no-pager # 只要消息正文,不要元数据
# 反向显示最新的在前面
journalctl -u nginx -r --no-pager | head -n 50
# 清理空间
sudo journalctl --vacuum-size=500M
sudo journalctl --vacuum-time=7d
sudo journalctl --vacuum-files=5
几个实用技巧。--no-pager 一定要养成习惯加上,否则输出会进入分页器,重定向和复制都不方便。-o json-pretty 适合交给脚本处理或者导入到日志系统。-b -1 在排查"重启前为什么挂了"这类问题时无可替代,因为重启后很多现场信息只存在于上一次启动的日志里。
持久化配置: 很多发行版默认把 journald 日志存在 /run/log/journal(内存盘),重启后就没了。要保留历史必须创建持久化目录:
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
journalctl --disk-usage
也可以编辑 /etc/systemd/journald.conf,设置 SystemMaxUse=1G、MaxRetentionSec=1month 限制占用。修改后重启 systemd-journald 生效。
风险提示: journalctl --vacuum-size 会真的删除日志数据。如果正在排查安全事件,先备份 /var/log/journal 再清理。保留周期建议不少于七天,否则事后复盘无从下手。
传统文本日志怎么看
即使有了 journald,文本日志仍然是主力,因为 grep、awk 处理起来最直接。
系统消息。 RHEL 系看 /var/log/messages,Debian、Ubuntu 看 /var/log/syslog。这里记录 OOM killer、硬件错误、服务启停、网络变化等:
sudo tail -n 100 /var/log/messages
sudo grep -i 'oom\|killed process' /var/log/messages
sudo grep -i 'error\|fail' /var/log/syslog | tail -n 50
OOM 是重点排查对象,内核杀掉进程时会留下清晰记录:
sudo grep -i -A 5 'out of memory' /var/log/messages
sudo journalctl -k | grep -i 'oom\|killed'
认证日志。 SSH 登录、sudo 提权、用户切换都在这里:
# RHEL、Rocky、CentOS
sudo tail -n 50 /var/log/secure
sudo grep 'Failed password' /var/log/secure | awk '{print $11}' | sort | uniq -c | sort -rn | head
sudo grep 'Accepted' /var/log/secure | tail -n 20
sudo grep 'authentication failure' /var/log/secure | tail -n 20
# Debian、Ubuntu
sudo tail -n 50 /var/log/auth.log
sudo grep 'Failed password' /var/log/auth.log | grep -oP 'from \K[0-9.]+' | sort | uniq -c | sort -rn | head
统计爆破来源是日常巡检的固定动作,如果某个 IP 出现几百次失败,就该考虑封禁或者确认是不是自己的服务配错了密码。
内核日志。 dmesg 读的是内核环形缓冲区,看硬件、驱动、网络接口和 OOM:
sudo dmesg -T | tail -n 50 # -T 显示可读时间
sudo dmesg -T | grep -i 'error\|warn\|link'
sudo dmesg -T --level=err,warn
sudo dmesg -C # 清空缓冲区,谨慎使用
注意 dmesg 缓冲区大小有限,重启后早期信息会被覆盖,长期留存要用 journald 或 /var/log/dmesg。另外非 root 用户可能因为 kernel.dmesg_restrict 无法读取,需要 sudo。
定时任务日志。 RHEL 系在 /var/log/cron,Debian、Ubuntu 需要过滤:
# RHEL、Rocky、CentOS
sudo grep CROND /var/log/cron | tail -n 30
# Debian、Ubuntu
sudo grep CRON /var/log/syslog | tail -n 30
日志里出现 CMD 行说明任务触发了,只有 pam_unix 而没有 CMD 通常是权限问题或任务被 cron.allow 限制。
logrotate:防止日志撑爆磁盘
所有长期运行的日志都必须轮转。logrotate 由 systemd timer 每天触发,主配置在 /etc/logrotate.conf,各服务的配置放在 /etc/logrotate.d/。
cat /etc/logrotate.conf
ls /etc/logrotate.d/
sudo logrotate -d /etc/logrotate.d/nginx # 调试模式,只显示将做什么
sudo logrotate -f /etc/logrotate.d/nginx # 强制执行一次
一个完整的自定义配置示例:
/var/log/app/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 appuser appgroup
sharedscripts
postrotate
/usr/bin/systemctl reload app >/dev/null 2>&1 || true
endscript
}
参数含义:
| 参数 | 含义 |
|---|---|
daily / weekly / monthly |
轮转周期 |
rotate N |
保留 N 份历史 |
compress |
压缩历史文件 |
delaycompress |
延后一轮再压缩,配合 postrotate 使用 |
missingok |
文件不存在不报错 |
notifempty |
空文件不轮转 |
copytruncate |
复制后截断原文件,适合无法重开日志的程序 |
create 模式 属主 属组 |
新建空日志文件的权限 |
dateext |
用日期做后缀而非序号 |
sharedscripts |
多个文件只执行一次脚本 |
postrotate / endscript |
轮转后执行的命令 |
关键点:轮转后程序仍持有旧文件的文件描述符,必须通知它重新打开。postrotate 里用 systemctl reload 或发送 USR1 信号(Nginx 用 kill -USR1 $(cat /var/run/nginx.pid))来解决。如果程序不支持重开日志,就用 copytruncate。
Nginx 默认已经配好了 logrotate,直接看 /etc/logrotate.d/nginx 即可,不要重复添加,否则会轮转两次导致日志丢失。
Nginx 日志字段解读
Nginx 的访问日志格式由 log_format 定义,默认是 combined 或者 main。先看当前配置:
grep -A 10 'log_format' /etc/nginx/nginx.conf
grep -E 'access_log|error_log' /etc/nginx/nginx.conf /etc/nginx/conf.d/*.conf
典型的 main 格式:
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
一条真实日志长这样:
203.0.113.25 - - [10/Oct/2026:14:32:07 +0800] "GET /api/user?id=123 HTTP/1.1" 200 1532 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" "-"
逐字段拆解:
| 字段 | 示例值 | 含义 |
|---|---|---|
$remote_addr |
203.0.113.25 | 客户端 IP,有代理时是代理 IP |
$remote_user |
- | HTTP 认证用户名,没有则为空 |
$time_local |
10/Oct/2026:14:32:07 +0800 | 本地时间 |
$request |
GET /api/user?id=123 HTTP/1.1 | 方法、URI、协议 |
$status |
200 | 响应状态码 |
$body_bytes_sent |
1532 | 响应体字节数,不含头部 |
$http_referer |
- | 来源页 |
$http_user_agent |
Mozilla/5.0 ... | 客户端标识 |
$http_x_forwarded_for |
- | 代理链上的原始 IP |
关键提醒: 如果前面挂了负载均衡或 CDN,$remote_addr 会全是代理的 IP,统计 Top IP 毫无意义。必须在 Nginx 里配置 real_ip_header X-Forwarded-For 和 set_real_ip_from,或者直接用 $http_x_forwarded_for 字段。这是统计结果失真的头号原因。
若要统计响应时间,默认的 main 格式不够,需要加入 $request_time 和 $upstream_response_time:
log_format timed '$remote_addr - [$time_local] "$request" '
'$status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'"$http_referer" "$http_user_agent"';
$request_time 是从收到请求第一个字节到写完响应的总耗时,$upstream_response_time 是转发给后端(如 PHP-FPM、Tomcat)的耗时。两者相减就能知道时间花在 Nginx 还是后端。修改 log_format 后要 nginx -t 校验再 systemctl reload nginx。
错误日志的默认格式包含时间、级别、进程号、连接号和具体信息:
2026/10/10 14:35:12 [error] 12345#0: *67890 open() "/var/www/html/favicon.ico" failed (2: No such file or directory), client: 203.0.113.25, server: example.com, request: "GET /favicon.ico HTTP/1.1", host: "example.com"
错误级别有 debug、info、notice、warn、error、crit、alert、emerg。生产环境一般配 error_log /var/log/nginx/error.log warn;,不要长期开 debug,会产生海量输出并影响性能。
Nginx 日志的常用 awk 统计
以下命令可以直接使用,注意字段编号要和你的 log_format 匹配,先用 head -1 确认一下。
统计访问量最高的 IP(Top 20):
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -n 20
统计某一天的 Top IP:
awk -v d='10/Oct/2026' '$4 ~ d {print $1}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -n 20
状态码分布:
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
awk '$9>=500 {print $9}' /var/log/nginx/access.log | sort | uniq -c
找出 5xx 最多的 URL:
awk '$9>=500 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -n 20
统计慢请求(假设第 11 个字段是 rt=0.532):
awk '{for(i=1;i<=NF;i++) if($i ~ /^rt=/) {t=$i}} t!="" && substr(t,4)+0 > 1 {print substr(t,4), $7, $9}' \
/var/log/nginx/access.log | sort -rn | head -n 20
如果用的是标准 main 格式没有 request_time,可以用 awk 按 URL 聚合请求次数粗略判断:
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -n 30
计算每分钟请求数(看流量尖峰在哪个时刻):
awk '{print substr($4,2,17)}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -n 20
awk '{print substr($4,2,14)}' /var/log/nginx/access.log | uniq -c # 按分钟,需日志已排序
统计 User-Agent 分布,识别爬虫:
awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -n 20
统计流量总量(字节):
awk '{s+=$10} END {printf "总流量: %.2f MB\n", s/1024/1024}' /var/log/nginx/access.log
实时观察最新请求:
tail -f /var/log/nginx/access.log | awk '{print $1, $7, $9}'
处理压缩过的历史日志不用先解压:
zcat /var/log/nginx/access.log.*.gz | awk '{print $1}' | sort | uniq -c | sort -rn | head -n 20
zgrep ' 500 ' /var/log/nginx/access.log.2.gz | head
应用日志的处理要点
应用日志格式由程序自己决定,常见的有 JSON 行格式和普通文本两种。JSON 格式适合用 jq 处理:
tail -n 100 /var/log/app/app.log | jq -r 'select(.level=="error") | "\(.time) \(.msg)"'
cat /var/log/app/app.log | jq -r '.level' | sort | uniq -c
多行堆栈是 grep 的难点。 Java 异常堆栈有几十行,grep 只能匹配到第一行。用 grep -A 显示后续行:
grep -n -A 30 'Exception' /var/log/app/app.log | head -n 80
grep -n -B 5 -A 30 'NullPointerException' app.log
定位到大致行号后,用 sed 提取整段:
sed -n '1200,1400p' /var/log/app/app.log
按时间窗口提取是最高频的需求,用 sed 按时间范围取:
sed -n '/2026-10-10 14:00/,/2026-10-10 15:00/p' /var/log/app/app.log
awk '$0 >= "2026-10-10 14:00:00" && $0 <= "2026-10-10 15:00:00"' /var/log/app/app.log
关联多层日志才是真正的排查。比如一次 502 的完整链路是:Nginx error 日志显示 connect() failed (111: Connection refused) while connecting to upstream → 查 PHP-FPM 的 journal 日志发现 server reached pm.max_children → 查慢请求日志确认某个接口耗时过高 → 查数据库慢查询。四层日志的时间戳对齐后,根因就很清楚了。
常见问题
journalctl 提示没有日志或者只有本次启动的?
说明 journald 没有持久化。创建 /var/log/journal 目录并执行 systemd-tmpfiles --create --prefix /var/log/journal,重启 systemd-journald 后新日志就会持久保存。注意之前的历史无法找回,只能从本次开始。
磁盘满了,日志能直接删吗?
可以但有讲究。journald 的用 journalctl --vacuum-size=500M 安全清理,不要直接 rm -rf /var/log/journal。文本日志清理后,持有文件描述符的程序仍会往已删除的 inode 写,空间不会释放,需要 systemctl reload 或重启服务让它们重新打开文件。
统计出来的 Top IP 全是内网地址怎么办?
说明前面有反向代理或负载均衡,$remote_addr 记录的是代理 IP。配置 set_real_ip_from 与 real_ip_header X-Forwarded-For,或者改用日志里的 $http_x_forwarded_for 字段统计。
为什么日志里的时间和 date 命令对不上?
Nginx 的 $time_local 受服务器时区影响,应用日志可能用了 UTC。统一做法是所有服务器设 timedatectl set-timezone Asia/Shanghai,并让应用输出带时区的时间戳,避免跨时区关联时的混乱。
日志轮转后程序还在写旧文件怎么办?
因为程序持有的是 inode 而非文件名。在 postrotate 里让程序重新打开日志(Nginx 用 kill -USR1),或者改用 copytruncate 模式。用 lsof +L1 或 lsof | grep deleted 可以查出这类文件。
总结
- 日志分三层:系统层用
journalctl -u 服务 --since查,Web 层看 Nginx 的 access 与 error,应用层看业务自己的输出,排查时按时间对齐三层联动。 journalctl的黄金组合是-u指定服务、--since限定时间、-p err过滤级别、-b -1看上次启动、--no-pager便于复制。- 所有日志都要配 logrotate,并在 postrotate 里通知程序重新打开日志文件,否则空间不会真正释放。
- Nginx 统计前先确认日志格式与字段编号,有代理时必须处理
X-Forwarded-For,否则 Top IP 全是代理地址。 - 需要定位慢请求就在
log_format里加$request_time和$upstream_response_time,用两者差值区分问题是出在 Nginx 还是后端。
企业QQ咨询




