概述

日志是系统运维的眼睛。从内核消息到应用日志、从安全审计到性能分析,日志贯穿了故障排查的每个环节。现代 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
指定 PIDjournalctl _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 + TCPTCP传统方案
journald → rsyslogTCP/UDP过渡方案
Fluentd/Fluent BitTCP/HTTP容器化/云原生
Promtail + LokiHTTPGrafana 生态
Filebeat + ELKTCPELK 生态

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. 日志格式统一
#    时间戳 + 级别 + 模块 + 消息 + 上下文字段

总结

日志管理是运维的基础能力,从日志中可以发现问题、定位故障、审计行为。核心要点:

  1. journald 是现代 Linux 的标准日志系统:结构化存储、索引查询、自动轮转,远优于传统 syslog。
  2. journalctl 是强大的查询工具:支持按时间、服务、优先级、字段等多维度过滤,-f 实时跟踪,-o json 适合脚本处理。
  3. 日志大小必须控制SystemMaxUseMaxRetentionSec 防止日志撑满磁盘,--vacuum-size 紧急清理。
  4. logrotate 管理应用日志文件:Nginx、MySQL 等直接写文件的应用必须配置 logrotate。
  5. 结构化日志是趋势:通过 journald 结构化字段(REQUEST_ID、USER_ID 等)实现日志关联和快速查询。
  6. 远程日志收集是大规模运维的必需品:Fluent Bit + Loki/ELK 实现集中化日志管理。
  7. 日志分析需要工具链配合journalctl + grep/awk/jq 组合可以完成大部分分析需求。
  8. 日志安全不容忽视:不记录敏感信息、控制访问权限、定期审计登录和操作日志。

日志管理的黄金法则:日志是有成本的数据。每条日志都消耗存储、I/O 和 CPU,只记录有价值的日志,并确保日志可搜索、可关联、可告警。