概述
日志是系统运维的眼睛。从内核消息到应用日志、从安全审计到性能分析,日志贯穿了故障排查的每个环节。现代 Linux 采用 journald 作为系统日志守护进程,配合 logrotate 进行日志轮转,构成了完整的日志管理基础设施。本文深入 journald 的原理与配置、journalctl 的高级查询技巧、日志轮转策略、远程日志收集方案以及实战分析案例。
journald 原理
架构概览
journald 是 systemd 的系统日志组件,取代了传统的 syslog(rsyslog)。它接收来自内核、系统服务和应用程序的日志,统一存储为结构化的二进制格式。
[内核日志] [systemd 服务] [应用程序]
│ │ │
▼ ▼ ▼
[kmsg] [sd_journal_print] [syslog()/stdout]
│ │ │
└──────────┬───────┴──────────┬───────┘
▼ ▼
[journald] [/dev/log]
│
┌──────────┼──────────┐
▼ ▼ ▼
[持久日志] [运行时日志] [转发到 syslog]
/var/log/ /run/log/ /var/log/
journal/ journal/ messages
日志存储模式
# 查看当前存储模式
$ cat /etc/systemd/journald.conf | grep Storage
#Storage=auto
# 三种模式:
# auto(默认): /var/log/journal 存在则持久化,否则仅内存
# persistent: 强制持久化(自动创建 /var/log/journal)
# volatile: 仅内存(/run/log/journal)
| 模式 | 存储位置 | 重启后保留 | 适用场景 |
|---|---|---|---|
persistent | /var/log/journal | 是 | 生产环境 |
auto | /var/log/journal 或 /run/log/journal | 取决于目录 | 默认 |
volatile | /run/log/journal | 否 | 临时系统/安全要求 |
日志大小控制
# /etc/systemd/journald.conf
[Journal]
Storage=persistent
# 全局日志大小限制
SystemMaxUse=2G # 持久日志最大占用
SystemKeepFree=4G # 确保磁盘至少保留 4G 空闲
SystemMaxFileSize=100M # 单个日志文件最大大小
SystemMaxFiles=100 # 最多保留 100 个日志文件
# 运行时日志(内存)限制
RuntimeMaxUse=200M
RuntimeKeepFree=100M
RuntimeMaxFileSize=20M
RuntimeMaxFiles=20
# 日志保留时间
MaxRetentionSec=2week # 日志最多保留 2 周
MaxFileSec=1month # 单个文件最长 1 个月
# 转发配置
ForwardToSyslog=no # 不转发到 syslog(已废弃)
ForwardToKMsg=no
ForwardToConsole=no
ForwardToWall=yes # 紧急消息发送到所有终端
# 日志速率限制
RateLimitIntervalSec=30s
RateLimitBurst=10000 # 30 秒内最多 10000 条
日志大小查看与管理
# 查看日志磁盘占用
$ journalctl --disk-usage
Archived and active journals take up 1.2G in the file system.
# 立即清理日志到指定大小
$ journalctl --vacuum-size=500M
Vacuumed 123 archived journals (1.2G → 500M)
# 按时间清理
$ journalctl --vacuum-time=7d
Vacuumed 45 archived journals older than 7 days
# 按文件数量清理
$ journalctl --vacuum-files=10
Vacuumed 90 archived journals (keeping 10 files)
# 验证日志完整性
$ journalctl --verify
journalctl 查询技巧
基本查询
# 查看所有日志
$ journalctl
# 查看本次启动的日志
$ journalctl -b
# 查看上一次启动的日志
$ journalctl -b -1
# 查看指定启动的日志
$ journalctl --list-boots
IDX BOOT ID FIRST ENTRY LAST ENTRY
-2 abc123... Mon 2026-07-08 09:00:00 Mon 2026-07-08 18:00:00
-1 def456... Tue 2026-07-09 09:00:00 Tue 2026-07-09 18:00:00
0 ghi789... Wed 2026-07-10 09:00:00 Wed 2026-07-10 15:30:00
$ journalctl -b -2 # 查看前天启动的日志
时间过滤
# 今天日志
$ journalctl --since today
# 最近 1 小时
$ journalctl --since "1 hour ago"
# 指定时间范围
$ journalctl --since "2026-07-10 09:00:00" --until "2026-07-10 12:00:00"
# 相对时间
$ journalctl --since "1 hour ago" --until "10 min ago"
$ journalctl --since yesterday
$ journalctl --since "2026-07-01" --until "2026-07-10"
按单元过滤
# 查看指定服务日志
$ journalctl -u nginx
$ journalctl -u sshd
$ journalctl -u docker
# 多个服务
$ journalctl -u nginx -u php-fpm
# 查看服务本次启动以来的日志
$ journalctl -u nginx -b
# 查看服务上次失败的日志
$ journalctl -u nginx -b -1 --since "1 hour ago"
按优先级过滤
# 日志优先级
# 0: emerg - 紧急
# 1: alert - 警报
# 2: crit - 严重
# 3: err - 错误
# 4: warning - 警告
# 5: notice - 通知
# 6: info - 信息
# 7: debug - 调试
# 查看指定级别及更严重的日志
$ journalctl -p err # err 及以上
$ journalctl -p warning # warning 及以上
$ journalctl -p 3 # 等同 -p err
# 指定级别范围
$ journalctl -p warning..err # warning 到 err
按字段过滤
# 按进程 ID
$ journalctl _PID=12345
# 按用户 ID
$ journalctl _UID=1000
# 按 GID
$ journalctl _GID=1000
# 按可执行文件
$ journalctl _COMM=nginx
$ journalctl _EXE=/usr/sbin/nginx
# 按设备
$ journalctl _KERNEL_DEVICE=eth0
# 自定义字段(结构化日志)
$ journalctl SYSLOG_IDENTIFIER=myapp
$ journalctl MESSAGE_ID=abc123
输出格式控制
# 默认格式
$ journalctl -u nginx -n 5
# 简洁格式
$ journalctl -u nginx -n 5 -o short
# 完整格式(含完整时间戳)
$ journalctl -u nginx -n 5 -o short-precise
# JSON 格式(适合脚本处理)
$ journalctl -u nginx -n 5 -o json
# 漂亮 JSON 格式
$ journalctl -u nginx -n 5 -o json-pretty
# 按分类显示
$ journalctl -u nginx -n 5 -o cat
# 仅显示消息内容,不含时间戳
# 导出格式(用于备份/迁移)
$ journalctl -u nginx --since today -o export > nginx-logs.export
实时跟踪
# 实时跟踪(类似 tail -f)
$ journalctl -f
# 跟踪指定服务
$ journalctl -u nginx -f
# 跟踪多个服务
$ journalctl -u nginx -u php-fpm -f
# 只显示错误级别的实时日志
$ journalctl -f -p err
高级查询组合
# 组合查询:时间 + 服务 + 级别
$ journalctl --since "1 hour ago" -u nginx -p err
# 查看内核日志
$ journalctl -k
# 查看指定用户的日志
$ journalctl _UID=1000 --since today
# 查看被 killed 的进程
$ journalctl --since "1 hour ago" | grep -i "killed\|oom"
# 按消息内容过滤
$ journalctl --since today | grep "connection refused"
# 输出为 JSON 并用 jq 处理
$ journalctl -u nginx --since "1 hour ago" -o json | jq 'select(.MESSAGE | contains("error"))'
journalctl 速查表
| 需求 | 命令 |
|---|---|
| 本次启动日志 | journalctl -b |
| 上次启动日志 | journalctl -b -1 |
| 内核日志 | journalctl -k |
| 服务日志 | journalctl -u nginx |
| 错误日志 | journalctl -p err |
| 最近 1 小时 | journalctl --since "1 hour ago" |
| 实时跟踪 | journalctl -f |
| 磁盘占用 | journalctl --disk-usage |
| 清理日志 | journalctl --vacuum-size=500M |
| JSON 输出 | journalctl -o json |
| 仅消息内容 | journalctl -o cat |
| 指定 PID | journalctl _PID=12345 |
日志轮转(logrotate)
为什么需要 logrotate
journald 有自己的日志大小控制,但许多应用(如 Nginx、MySQL)直接写日志文件,不经过 journald。这些日志文件会持续增长,需要 logrotate 定期轮转、压缩和清理。
logrotate 工作原理
1. 检查配置文件(/etc/logrotate.d/*)
2. 判断日志文件是否达到轮转条件
3. 达到条件则执行轮转:
a. 重命名当前日志(app.log → app.log.1)
b. 可选:压缩旧日志(app.log.1 → app.log.1.gz)
c. 删除超过保留数量的旧日志
d. 创建新的空日志文件
e. 通知应用重新打开日志文件
主配置文件
# /etc/logrotate.conf — 全局配置
weekly # 每周轮转
rotate 4 # 保留 4 份
create # 轮转后创建新文件
compress # 压缩旧日志
dateext # 使用日期作为后缀
dateformat -%Y%m%d # 日期格式
include /etc/logrotate.d # 包含子配置目录
应用级配置示例
Nginx 日志轮转
# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily
rotate 30
missingok
notifempty
compress
delaycompress # 延迟一天压缩(方便排查)
sharedscripts # 多个日志共用一个脚本
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 $(cat /var/run/nginx.pid)
fi
endscript
}
MySQL 日志轮转
# /etc/logrotate.d/mysql
/var/log/mysql/*.log {
daily
rotate 14
missingok
create 640 mysql adm
compress
delaycompress
notifempty
sharedscripts
postrotate
mysqladmin flush-logs 2>/dev/null || true
endscript
}
应用日志轮转
# /etc/logrotate.d/myapp
/opt/myapp/logs/*.log {
daily
rotate 30
missingok
notifempty
compress
delaycompress
copytruncate # 复制后截断(不中断服务)
size 100M # 超过 100M 也轮转
dateext
dateformat -%Y%m%d-%H%M%S
}
关键参数详解
| 参数 | 说明 | 示例 |
|---|---|---|
daily/weekly/monthly | 轮转频率 | daily |
rotate N | 保留份数 | rotate 30 |
size | 大小阈值 | size 100M |
compress | 压缩旧日志 | compress |
delaycompress | 延迟一期压缩 | delaycompress |
missingok | 日志不存在不报错 | missingok |
notifempty | 空文件不轮转 | notifempty |
create | 轮转后创建新文件 | create 640 root root |
copytruncate | 复制后截断原文件 | copytruncate |
dateext | 使用日期后缀 | dateext |
sharedscripts | 共享脚本(多个日志执行一次) | sharedscripts |
postrotate/endscript | 轮转后执行脚本 | postrotate |
prerotate/endscript | 轮转前执行脚本 | prerotate |
copytruncate vs postrotate
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
postrotate | 重命名 + 通知应用重开文件 | 无数据丢失 | 需要应用支持信号 |
copytruncate | 复制 + 截断原文件 | 不需要应用配合 | 复制和截断之间可能丢日志 |
手动执行与调试
# 手动执行所有轮转
$ logrotate /etc/logrotate.conf
# 调试模式(dry-run,不实际执行)
$ logrotate -d /etc/logrotate.d/nginx
# 强制执行
$ logrotate -f /etc/logrotate.d/nginx
# 查看状态
$ cat /var/lib/logrotate/status
# 记录每个日志的最后轮转时间
# 详细输出
$ logrotate -v /etc/logrotate.d/nginx
logrotate 定时任务
# logrotate 通常通过 cron 或 systemd timer 执行
# Debian/Ubuntu: /etc/cron.daily/logrotate
# RHEL/CentOS: /etc/cron.daily/logrotate
# systemd timer 方式
$ systemctl list-timers | grep logrotate
$ systemctl cat logrotate.timer
结构化日志
传统日志的问题
# 非结构化日志
Jul 10 15:30:00 server nginx: 10.0.0.1 - - [10/Jul/2026:15:30:00 +0800] "GET /api/users HTTP/1.1" 200 1234
# 难以解析、难以查询、难以关联
journald 结构化日志
journald 原生支持结构化字段,应用程序可以通过以下方式写入结构化日志:
C 程序
#include <systemd/sd-journal.h>
int main() {
sd_journal_print(LOG_INFO, "User login successful");
// 结构化字段
sd_journal_send(
"MESSAGE=User %s logged in from %s",
"PRIORITY=6",
"USER_ID=12345",
"USERNAME=admin",
"SOURCE_IP=10.0.0.1",
"ACTION=login",
NULL
);
return 0;
}
Python
import logging
import systemd.journal
logger = logging.getLogger('myapp')
logger.addHandler(systemd.journal.JournalHandler())
logger.setLevel(logging.INFO)
# 普通日志
logger.info("User login successful")
# 结构化日志
logger.info("User login", extra={
'USER_ID': 12345,
'USERNAME': 'admin',
'SOURCE_IP': '10.0.0.1',
'ACTION': 'login'
})
# 查询
# journalctl USER_ID=12345
# journalctl ACTION=login
Shell 脚本
# 使用 systemd-cat 写入结构化日志
$ echo "Backup completed" | systemd-cat -t myapp -p info
# 带结构化字段
$ systemd-cat -t myapp -p info << EOF
BACKUP_ID=20260710
STATUS=success
DURATION=3600
FILES=12345
SIZE=10GB
Backup completed successfully
EOF
# 查询
$ journalctl -t myapp BACKUP_ID=20260710
Go 程序
package main
import (
"log/syslog"
"os"
)
func main() {
writer, _ := syslog.New(syslog.LOG_INFO|syslog.LOG_USER, "myapp")
logger := log.New(writer, "", 0)
logger.Println("Application started")
// 或使用 coreos/go-systemd 库
// 直接写入 journald 结构化字段
}
日志关联(Request ID)
# 在请求入口生成 Request ID,贯穿整个调用链
$ journalctl REQUEST_ID=abc-123-def
# 可追踪一个请求在多个服务中的完整日志
# 应用实现
# 1. API 网关生成 Request ID
# 2. 通过 HTTP Header 传递给下游
# 3. 每个服务将 Request ID 写入 journald 结构化字段
# 4. 使用 journalctl REQUEST_ID=xxx 查询完整链路
远程日志收集
方案对比
| 方案 | 协议 | 可靠性 | 性能 | 适用场景 |
|---|---|---|---|---|
| rsyslog + TCP | TCP | 高 | 中 | 传统方案 |
| journald → rsyslog | TCP/UDP | 中 | 中 | 过渡方案 |
| Fluentd/Fluent Bit | TCP/HTTP | 高 | 高 | 容器化/云原生 |
| Promtail + Loki | HTTP | 高 | 高 | Grafana 生态 |
| Filebeat + ELK | TCP | 高 | 高 | ELK 生态 |
journald 转发到 rsyslog
# /etc/systemd/journald.conf
[Journal]
ForwardToSyslog=yes
# /etc/rsyslog.conf 或 /etc/rsyslog.d/remote.conf
# 转发到远程日志服务器
*.* @@log-server.sre.wang:514 # TCP
# 或
*.* @log-server.sre.wang:514 # UDP
# 按设施转发
local0.* @@log-server:514
*.info;mail.none;authpriv.none @@log-server:514
systemd journal-upload
# 使用 systemd-journal-upload 发送到远程 journal 服务器
$ systemctl enable --now systemd-journal-upload
# 配置
# /etc/systemd/journal-upload.conf
[Upload]
URL=https://journal-collector.sre.wang/upload
# 或使用 push 模式
# KeyFile=/etc/ssl/private/journal-upload.key
# CertificateFile=/etc/ssl/certs/journal-upload.crt
# TrustedCertificateFile=/etc/ssl/certs/ca.crt
Fluent Bit 采集 journald
# /etc/fluent-bit/fluent-bit.conf
[SERVICE]
Flush 5
Log_Level info
[INPUT]
Name systemd
Tag host.*
Systemd_Filter _SYSTEMD_UNIT=nginx.service
Systemd_Filter _SYSTEMD_UNIT=sshd.service
DB /var/log/flb_journald.db
[OUTPUT]
Name forward
Match *
Host fluentd.sre.wang
Port 24224
日志收集架构
[服务器 A] [服务器 B] [服务器 C]
journald journald journald
│ │ │
▼ ▼ ▼
[Fluent Bit] [Fluent Bit] [Fluent Bit]
│ │ │
└──────────┬───────────────────┴──────────────────┬───────────┘
▼ ▼
[Fluentd/Loki] [Elasticsearch]
│ │
▼ ▼
[Grafana/Kibana] [Kibana Dashboard]
日志磁盘占用控制
journald 磁盘控制
# 1. 配置大小上限
# /etc/systemd/journald.conf
SystemMaxUse=2G
SystemKeepFree=4G
MaxRetentionSec=2week
# 2. 重启 journald
$ systemctl restart systemd-journald
# 3. 查看当前占用
$ journalctl --disk-usage
# 4. 立即清理
$ journalctl --vacuum-size=1G
$ journalctl --vacuum-time=7d
应用日志磁盘控制
# 使用 logrotate 控制大小
# /etc/logrotate.d/app-logs
/var/log/myapp/*.log {
daily
rotate 30
compress
delaycompress
size 500M
maxsize 2G # 单个文件最大 2G
notifempty
copytruncate
}
# 使用脚本监控日志目录大小
#!/bin/bash
# /usr/local/bin/log-size-monitor.sh
LOG_DIR="/var/log/myapp"
MAX_SIZE_GB=10
CURRENT_SIZE=$(du -sg $LOG_DIR | awk '{print $1}')
if [ $CURRENT_SIZE -gt $MAX_SIZE_GB ]; then
# 触发清理
find $LOG_DIR -name "*.log.*" -mtime +7 -delete
journalctl --vacuum-size=500M
echo "Log cleanup triggered: ${CURRENT_SIZE}GB > ${MAX_SIZE_GB}GB" | \
systemd-cat -t log-monitor -p warning
fi
磁盘空间监控
# 使用 systemd timer 定期检查
# /etc/systemd/system/log-monitor.service
[Unit]
Description=Log Size Monitor
[Service]
Type=oneshot
ExecStart=/usr/local/bin/log-size-monitor.sh
# /etc/systemd/system/log-monitor.timer
[Unit]
Description=Run Log Size Monitor hourly
[Timer]
OnCalendar=hourly
Persistent=true
[Install]
WantedBy=timers.target
$ systemctl enable --now log-monitor.timer
日志分析实战
案例 1:SSH 暴力破解分析
# 统计最近 24 小时 SSH 失败登录的 Top IP
$ journalctl -u sshd --since "24 hours ago" | \
grep "Failed" | \
awk '{print $(NF)}' | \
sort | uniq -c | sort -rn | head -20
# 统计被尝试的用户名
$ journalctl -u sshd --since "24 hours ago" | \
grep "Failed" | \
awk '{for(i=1;i<=NF;i++) if($i=="for") print $(i+1)}' | \
sort | uniq -c | sort -rn | head -20
# 统计每小时失败次数
$ journalctl -u sshd --since "24 hours ago" | \
grep "Failed" | \
awk '{print substr($3,1,2)":00"}' | \
sort | uniq -c
案例 2:服务故障排查
# Nginx 崩溃排查
# 1. 查看崩溃前的日志
$ journalctl -u nginx -b -1 --since "30 min ago" -n 100
# 2. 查看 OOM 记录
$ journalctl -k --since "1 hour ago" | grep -i "oom\|killed"
# 3. 查看服务退出码
$ systemctl status nginx
$ journalctl -u nginx -n 50 -o json | jq '.[] | {MESSAGE: .MESSAGE, EXIT_STATUS: ._SYSTEMD_UNIT}'
# 4. 关联多个服务日志
$ journalctl -u nginx -u php-fpm --since "30 min ago" -o short-precise
案例 3:性能问题排查
# 查看内核性能相关日志
$ journalctl -k --since "1 hour ago" | grep -iE "hung|timeout|blocked|slow|delay"
# 查看 I/O 错误
$ journalctl -k --since "1 hour ago" | grep -iE "I/O error|read error|write error"
# 查看内存压力
$ journalctl -k --since "1 hour ago" | grep -iE "oom|memory|swap|out of"
# 查看网络异常
$ journalctl -k --since "1 hour ago" | grep -iE "link.*down|link.*up|carrier|timeout|reset"
案例 4:安全审计
# 查看所有 sudo 操作
$ journalctl -t sudo --since today
# 查看用户登录/登出
$ journalctl --since today | grep -E "session (opened|closed)"
# 查看文件权限变更
$ journalctl --since today | grep -i "chmod\|chown"
# 查看服务启停
$ journalctl --since today | grep -E "Started|Stopped|Failed"
# 查看所有 cron 任务执行
$ journalctl -t CRON --since today
# 导出审计日志
$ journalctl --since "2026-07-01" --until "2026-07-10" -o export > audit-july.export
案例 5:日志告警
# 使用 systemd path 监控日志文件
# /etc/systemd/system/log-alert.path
[Unit]
Description=Monitor Error Log
[Path]
PathChanged=/var/log/myapp/error.log
[Install]
WantedBy=multi-user.target
# /etc/systemd/system/log-alert.service
[Unit]
Description=Send Alert on Error Log Change
[Service]
ExecStart=/usr/local/bin/send-alert.sh
# 使用 journalctl 持续监控并发送告警
#!/bin/bash
# /usr/local/bin/error-monitor.sh
journalctl -f -p err | while read line; do
# 发送到告警系统
curl -X POST https://alerts.sre.wang/api/alert \
-H "Content-Type: application/json" \
-d "{\"message\": \"$line\"}"
done
好的实践
日志配置清单
# /etc/systemd/journald.conf
[Journal]
Storage=persistent
Compress=yes
Seal=yes
SplitMode=uid
RateLimitIntervalSec=30s
RateLimitBurst=10000
SystemMaxUse=2G
SystemKeepFree=4G
SystemMaxFileSize=100M
MaxRetentionSec=2week
MaxFileSec=1month
ForwardToSyslog=no
ForwardToWall=yes
MaxLevelStore=debug
MaxLevelSyslog=info
日志级别使用建议
| 级别 | 使用场景 | 示例 |
|---|---|---|
| emerg | 系统不可用 | 内核 panic |
| alert | 需立即处理 | 磁盘满 |
| crit | 严重错误 | 服务崩溃 |
| err | 运行错误 | 请求失败 |
| warning | 潜在问题 | 磁盘使用率 80% |
| notice | 重要事件 | 配置变更 |
| info | 正常运行 | 请求完成 |
| debug | 调试信息 | 详细参数 |
日志写入好的实践
# 1. 应用通过 systemd 日志接口写入
# 而非直接写文件(获得结构化字段、自动轮转)
# 2. 包含足够的上下文
logger -t myapp "Request completed" \
REQUEST_ID=$REQ_ID \
METHOD=$METHOD \
PATH=$PATH \
STATUS=$STATUS \
DURATION=${DURATION}ms
# 3. 不要记录敏感信息
# ❌ logger "User login: password=abc123"
# ✅ logger "User login: user=admin, method=publickey"
# 4. 合理使用日志级别
# - 生产环境默认 info 级别
# - 排查问题时临时调到 debug
# - 不在循环中打 debug 日志
# 5. 日志格式统一
# 时间戳 + 级别 + 模块 + 消息 + 上下文字段
总结
日志管理是运维的基础能力,从日志中可以发现问题、定位故障、审计行为。核心要点:
- journald 是现代 Linux 的标准日志系统:结构化存储、索引查询、自动轮转,远优于传统 syslog。
- journalctl 是强大的查询工具:支持按时间、服务、优先级、字段等多维度过滤,
-f实时跟踪,-o json适合脚本处理。 - 日志大小必须控制:
SystemMaxUse和MaxRetentionSec防止日志撑满磁盘,--vacuum-size紧急清理。 - logrotate 管理应用日志文件:Nginx、MySQL 等直接写文件的应用必须配置 logrotate。
- 结构化日志是趋势:通过 journald 结构化字段(REQUEST_ID、USER_ID 等)实现日志关联和快速查询。
- 远程日志收集是大规模运维的必需品:Fluent Bit + Loki/ELK 实现集中化日志管理。
- 日志分析需要工具链配合:
journalctl+grep/awk/jq组合可以完成大部分分析需求。 - 日志安全不容忽视:不记录敏感信息、控制访问权限、定期审计登录和操作日志。
日志管理的黄金法则:日志是有成本的数据。每条日志都消耗存储、I/O 和 CPU,只记录有价值的日志,并确保日志可搜索、可关联、可告警。