0%

Linux 日志系统的历史与使用

Linux 日志系统的历史与使用

接手一台刚交付的服务器,服务起不来。你按习惯打开 /var/log/messages,发现文件是空的,甚至根本不存在。换成 journalctl 一查,启动失败的记录却明明就在那里。

日志并没有丢。你熟悉的那个文本文件,可能根本没有程序负责写入,而系统的日志服务一直在正常工作。

Linux 的日志之所以有这么多入口,要从它的历史说起。早期 Unix 留下了 syslog 的接口和分类规则。后来,rsyslog 扩展了收集和转发能力。再后来,systemd 带来了 journald。每一代新工具进入系统时,旧的接口和配置都没有随之退场,于是它们一层层叠在了一起。

顺着这段历史看下来,journalctl、rsyslog 和 logrotate 就不再是三套需要分别死记的命令。它们合起来回答的是四个问题:谁接收消息,怎样保存和查询,如何送往别处,什么时候清理。

一、从 syslog 到 journald

1. syslog:统一收集,按类别分发

20 世纪 80 年代初,Eric Allman 在加州大学伯克利分校开发 sendmail 时编写了 syslog。他需要知道邮件系统在做什么:邮件何时投递成功,何时失败,为什么失败。syslog 的思路是让程序把消息交给一个公共服务,再由管理员统一决定这些消息存到哪里。sendmail 随伯克利的 BSD 发行版分发,syslog 也跟着进入了 BSD Unix。

syslog 起初只供 sendmail 使用,后来被越来越多的程序采用,逐渐成为 Unix 上事实标准的日志机制。应用只需调用 syslog() 提交消息,不用自己管理日志文件。管理员则按两个维度编写处理规则:消息来自哪类程序(facility,来源类别),以及事情有多严重(severity,严重级别)。比如,邮件日志单独存一个文件,认证记录写到另一个文件,出现紧急事件时向所有登录用户的终端广播。日志都是纯文本,用 cat、tail、grep 就能看。

不过,syslog 统一的只是收集方式,消息格式并没有统一。传统 BSD syslog 的时间戳只有月、日和时分秒,既没有年份,也没有时区。程序名、进程号和正文的写法也因程序而异。人工翻看少量日志时,结合上下文通常能看懂;一旦要用脚本批量处理,就得为每种格式单独写解析规则。

合并多台机器的日志时,问题更明显。没有时区,不同时区的主机就无法对齐到同一条时间轴上;没有年份,跨年的记录就可能顺序错乱。

在此后近二十年里,syslog 一直没有正式的协议规范,各家实现互不完全兼容。2001 年 8 月发布的 RFC 3164 以信息性文档(Informational)的形式,整理了当时通行的 BSD syslog 协议。它记录的是既有实现的做法,不是新定的标准,所以这些局限也被原样保留了下来。

2. syslog-ng 与 rsyslog:增强收集和转发

早期 Linux 发行版默认使用 sysklogd。它由两部分组成:syslogd 负责常规日志,klogd 负责读取内核消息。它的功能与 BSD syslog 基本一致:按 facility 和 severity 把消息写入文件,或者通过 UDP 发给其他主机。

服务器越来越多以后,集中收集日志成了刚需,这套实现开始不够用了。

1998 年,Balázs Scheidler 开始开发 syslog-ng,它加入了 TCP 传输和按消息内容过滤。

2004 年,Rainer Gerhards 在 sysklogd 的基础上开发了 rsyslog。它保留了原有的配置写法,又陆续加入了 TCP 与 TLS 传输、可靠传输协议 RELP、磁盘队列、数据库输出,以及表达力更强的配置语言。

此后,主流发行版先后把 rsyslog 设为默认的 syslog 实现:Fedora 8(2007 年)最先切换,Debian 5(Lenny,2009 年)随后跟进,RHEL 则从 RHEL 6(2010 年)开始默认使用 rsyslog。

rsyslog 主要解决的是”消息收上来以后怎么处理”:按规则分流、改写格式、可靠地送到远端。至于消息本身的格式能不能统一,还得靠协议来规定。

3. RFC 5424:给消息定下统一格式

2009 年 3 月发布的 RFC 5424 为 syslog 规定了统一的消息格式,在标准层面取代了 RFC 3164。这份标准的作者,正是 rsyslog 的开发者 Rainer Gerhards。

新格式有几处关键改进:

  • 时间戳采用 RFC 3339 格式。只要给出时间,就必须带完整的年份和时区偏移,还可以带最多 6 位小数秒。只有在完全无法获取系统时间时,才允许用 - 占位。
  • 应用名、进程标识和消息标识各有独立的字段。
  • 额外信息可以放进 STRUCTURED-DATA,按规定的键值对格式记录。

过去的日志,人能读懂,程序却很难稳定地解析。RFC 5424 要解决的就是这个问题:只要发送方遵守协议,接收方就能用同一套规则提取各个字段,不必再为每个程序单独编写解析逻辑。完整的时间信息,也让不同年份、不同时区的日志能够准确对齐。

不过,现实中仍有大量设备和程序在发送 RFC 3164 风格的消息,所以接收端通常两种格式都要能处理。

到这里,syslog 这条线已经补上了两块:rsyslog 管好了消息的处理和转发,RFC 5424 统一了消息格式。但还有一个问题始终没有解决:消息是谁发的,全靠发送方自己填写。程序名、进程号都写在消息里,填错了,甚至有意冒充,接收端都无从核实。

解决这个问题的思路来自 systemd。systemd 负责启动和管理系统上的所有服务,本来就知道每个进程属于哪个服务。既然如此,与其相信程序的自述,不如让日志服务直接把消息来源记下来。journald 就是按这个思路设计的。

4. journald:由日志服务记下消息来源

journald 是 systemd 自带的日志服务。2011 年 11 月,Lennart Poettering 等人公布了它的设计;2012 年 1 月发布的 systemd 38 是第一个包含 journal 的版本。

它为本机日志提供了另一套方案,接收的消息来源包括:

  • 内核消息;
  • 通过传统 syslog 套接字 /dev/log 提交的消息;
  • 通过原生 journal API(如 sd_journal_send())提交的消息;
  • 服务的标准输出和标准错误;
  • 内核审计(audit)记录。

这些消息统一保存在 journal 中。在使用 systemd 的系统上,连 /dev/log 都是由 journald 监听的。也就是说,应用调用 syslog() 时,消息的第一站是 journald,而不是 rsyslog。

journald 最大的变化在于:消息从哪里来,不再只靠程序在正文里自报家门,而是由 journald 自己记下来。每条消息除了正文,还附带进程 ID、用户 ID、可执行文件路径、所属的 systemd unit,以及本次启动的标识。排查服务故障时,这些信息比正文里自称的程序名可靠得多。为了能按这些字段快速查询,journald 采用带索引的二进制格式存储,查询则通过 journalctl 完成。

代价是底层文件不再是普通文本,读取和恢复都离不开能解析 journal 格式的工具。Linux 社区为此争论了很久。

这场争论常被概括为”文本与结构化之争”,但其实混在一起的是两个问题:

  • 数据有没有结构。 RFC 5424 用纯文本就能表达结构化字段,rsyslog 也能处理结构化数据。结构化并不要求二进制。
  • 文件用什么格式存储。 journald 选择二进制,是为了建立索引,加快按字段查询。

真正有争议的是后者。它是存储方式上的权衡(trade-off),而不是要不要结构化的分歧。

二、日志链路

上面几代工具,今天常常同时运行在同一台机器上。以 RHEL 8 的默认安装为例,执行下面的命令,可以看到 journald 和 rsyslog 都在运行:

在这类系统上,journald 负责本机日志的收集、保存和查询,rsyslog 负责按规则处理、写入文本文件和远程转发,两者的能力有一部分重叠。rsyslog 和许多应用写出的文本日志都会不断增长,所以还需要 logrotate 定期轮转和清理。三者的常见分工如下:

工具 常见职责 典型问题
journald / journalctl 收集、保存和查询本机 journal 这个服务什么时候报错?重启前发生了什么?
rsyslog 接收消息,按规则处理、写文件或转发 认证日志写到哪里?怎样送到中央服务器?
logrotate 定期轮转、压缩和删除文本日志 文件会不会一直增长?轮转后应用是否继续正常写入?

看这张图时,有几点值得特别注意:

  1. 本机日志的第一站是 journald。 程序通过 syslog 接口、标准输出或 journal 接口输出的日志,以及内核消息,都会先进入 journald。程序只决定用哪种方式输出,最终由谁接收是系统决定的。

  2. rsyslog 拿到的是 journald 的”二手消息”。 rsyslog 不直接接收本机程序的日志,而是从 journald 获取:RHEL 上由 rsyslog 通过 imjournal 主动读取 journal,Debian 上由 journald 开启 ForwardToSyslog=yes 后推送一份副本过来。

  3. 同一批日志,可能各存一份。 journald 把日志写进自己的 journal:开启持久化时存在 /var/log/journal,否则存在内存中的 /run/log/journal,重启即丢失。rsyslog 则把日志写成 /var/log/messages、/var/log/secure(Debian 上是 /var/log/syslog、/var/log/auth.log)等文本文件。两份数据来源相同,这也是图中二者”能力部分重叠”的原因。

  4. /var/log 下的文件并非都出自 rsyslog。 图中的”文本文件”指的是 rsyslog 写出的那几个系统日志。nginx、dnf、apt 等应用会直接写自己的日志文件,auditd 写 /var/log/audit/audit.log,登录记录则保存在 wtmp、btmp 等二进制文件中(用 last、lastb 查看),这些都与 rsyslog 无关。

  5. logrotate 只管文本文件,不管 journal。 rsyslog 写的文件和应用自己写的文件都由 logrotate 轮转;journal 则由 journald 按容量参数自行清理,不需要也不应该配置 logrotate。

在使用 systemd 的系统上,程序只决定用什么方式输出日志:自己写文件、调用 syslog 接口、打印到标准输出,或者调用 journal 接口。除了自己写文件,其余几种方式最终都会汇入 journald;装了 rsyslog 的系统,再由 rsyslog 从 journald 获取消息,写成文本文件。反过来,没装 rsyslog 的系统不会生成这些文本文件,依赖它们的工具就可能失效,第四章会讲到一个真实的例子。

journald 是 systemd 的组成部分,在使用 systemd 的系统上无法单独去掉。如果希望持久保存只交给 rsyslog,RHEL 7、8 的默认配置其实就是这样:journald 只把日志放在内存里,写到磁盘的工作由 rsyslog 完成。真正没有 journald 的,是不使用 systemd 的系统,比如 RHEL 6 及更早的版本,以及 Devuan 等发行版。在这些系统上,由 rsyslog 这类 syslog 服务直接监听 /dev/log、读取内核消息,也就是第一章介绍的传统 syslog 架构;程序打印到标准输出的内容,则需要由启动脚本自行重定向到文件。

所以,要看懂一台机器的日志配置,先要弄清消息走的是哪条路。同一条消息还可能既保存在本机,又被转发到远程服务器。/var/log 里的某个文件,只是这条链路上的一个去向。

现在可以回答开头的问题了。journal 里有记录,/var/log/messages 却是空的,最常见的原因有三个:

  • 系统里根本没有运行 rsyslog。 例如 Debian 12 起默认就不再安装 rsyslog,日志只存在于 journal 中。
  • rsyslog 在运行,但没有拿到消息,或者没有写这个文件的规则。 详见第四章第 4 节。
  • 文件名本来就不对。 Debian 和 Ubuntu 上的综合日志文件叫 /var/log/syslog,不叫 messages。

三、journal 的查询与存储管理

1. 用 journalctl 排查服务故障

直接执行 journalctl,会从最早的一条记录开始,按时间顺序列出当前账号有权查看的日志,并进入分页界面(空格翻页,G 跳到末尾,q 退出)。这里混着内核和所有服务的消息,排查具体故障时,要先缩小范围。

journalctl 分页界面

服务启动失败时,第一条命令是:

1
journalctl -xeu myapp

较新版本的 systemd 在 systemctl start 失败时,提示信息里推荐的就是这条命令。三个选项的含义如下:

选项 含义
-x 在日志条目旁附加 catalog 说明(解释性文字),例如服务启动失败的常见原因。只有在 journal catalog 中登记过的消息才有说明
-e 直接跳到日志末尾(--pager-end),从最新的内容看起
-u myapp 只显示指定 unit 的日志,等价于 --unit=myapp。myapp 对应 myapp.service,后缀可以省略

这些筛选条件可以自由组合,同时生效。下面是最常用的几种,完整用法见 man journalctl。

实时跟踪新日志,类似 tail -f:

1
journalctl -u nginx -f

查看最近 50 条,不进入分页:

1
journalctl -u nginx -n 50 --no-pager

按时间范围查看,可配合 --until 指定结束时间:

1
journalctl -u nginx --since "30 min ago"

--since 的短选项是 -S,--until 的短选项是 -U;--no-pager 没有短选项。注意大写的 -U 和小写的 -u(--unit)含义完全不同。

只看 err 及更严重的级别:

1
journalctl -u nginx -p err

按正文匹配(需要 systemd 237 及以上版本,可用 systemctl --version 查看):

1
journalctl -u nginx -g 'timeout|refused'

查看上一次启动的末尾。-b 指定看哪一次启动,后面跟的数字就是它的参数:0 为本次,-1 为上一次,以此类推。-e 表示直接跳到末尾:

1
journalctl -b -1 -e

-b -1 只能看上一次启动。如果要找的是更早的某次启动,先列出 journal 里保存的所有启动记录:

1
journalctl --list-boots

输出中每行是一次启动,第一列是编号,最后两列是这次启动期间第一条和最后一条日志的时间。根据时间找到目标那次启动,再把它的编号交给 -b。例如编号为 -2,就执行 journalctl -b -2 -e。

注意,--list-boots 能列出多少次启动,取决于 journal 是否持久化。如果用的是易失存储,重启后旧记录就没了,这里只会看到本次启动一行,详见本章第 3 节。

下图中只有一条启动记录,一种可能是 journal 没有持久化,之前的启动记录在重启时都丢了。

只看内核消息,默认只显示本次启动:

1
journalctl -k

从上图第一行可以看出:journal 最早的记录正是本次启动的时刻。这说明这台机器从 9 月 18 日启动后一直没有重启,已经运行了 16 天左右,而且 journal 里没有比本次启动更早的记录。

使用这些命令时,有三点需要注意:

  • 它们只能查到已经进入 journal 的记录。应用如果直接写自己的文件(如 /var/log/myapp/error.log),要到对应的文件里去看。
  • -b -1 能否查到上一次启动,取决于 journal 是否持久化,详见本章第 3 节。
  • 服务名因发行版而异。比如 SSH 服务在 RHEL 上叫 sshd,在 Debian 和 Ubuntu 上叫 ssh。本文示例统一用 sshd,在 Debian 系上请自行替换。

还有一个小技巧:对齐不同来源的时间线时,可以加上 --utc,让 journal 的时间戳统一按 UTC 显示。它只改变 journal 自身时间戳的显示,应用写在正文里的时间字符串不受影响。

2. journal 字段与来源信息

默认输出只显示了每条记录的一小部分。换成 verbose 格式,就能看到全部字段:

1
journalctl -u sshd -n 1 -o verbose

下面是一条记录的示意:

其中,MESSAGE、SYSLOG_IDENTIFIER 等字段由发送方提供。以下划线开头的字段则由 journald 自己补充,官方文档称为可信字段(trusted fields),客户端无法像提交普通字段那样填写或覆盖它们。

但”可信”的范围是有限的。可信字段能说明消息来自哪个进程、哪个服务,却不能保证正文内容属实,也不能保证主机被入侵后日志依然完整。

还有一个细节要注意。服务的标准输出和标准错误会通过一条连接接入 journald,从这条连接写入的所有内容,_PID 记录的都是最初建立连接的那个进程,通常就是服务的主进程。如果主进程又启动了子进程,子进程沿用同一个标准输出,它写入的内容也会记在主进程名下。

以下划线开头的这些字段,都可以直接作为查询条件。例如,journalctl _UID=1000 查看某个用户的进程产生的日志,journalctl _EXE=/usr/bin/python3 查看某个程序产生的日志。如果要交给脚本处理,加上 -o json 以 JSON 格式输出即可。

不过,日常查看服务日志时,还是优先使用 -u。它除了匹配服务自身写入的记录,还会带上 systemd 关于这个服务的消息,比如何时启动、何时停止、为什么失败,比单独匹配 _SYSTEMD_UNIT= 更完整。

3. journal 持久化

journald 一直在记录日志,但这些记录不一定写到了磁盘上。

在易失存储模式下,日志保存在 /run/log/journal 中。/run 是内存文件系统,只要机器不重启,日志就一直可以查询;一旦重启,内存清空,日志也随之消失。所以在这种模式下,journalctl -b -1 查不到上一次启动的记录。

换句话说,持久化解决的不是”能不能记录”,而是”重启后还在不在”。要让日志跨重启保留,就要启用持久化存储,把记录写到磁盘上的 /var/log/journal。

采用哪种存储方式,由 journald 配置中的 Storage= 参数决定。它有四个可选值,对应的保存位置和行为如下表:

设置 行为
volatile 易失存储,保存到 /run/log/journal
persistent 优先保存到 /var/log/journal;启动早期或磁盘不可写时,暂存到 /run/log/journal
auto 默认值。/var/log/journal 存在时持久化,否则使用易失存储
none 不保存 journal 记录,但向 syslog、控制台等的转发仍可能继续

journald 的配置由主配置文件和配置片段两部分组成。

主配置文件是 /etc/systemd/journald.conf,文件里被注释掉的各行就是默认值。

配置片段是放在 /etc/systemd/journald.conf.d/ 等目录下、以 .conf 结尾的文件,会覆盖主配置文件中的同名参数;多个片段按文件名顺序读取,后读取的生效。

发行版和软件包有时会在 /usr/lib/systemd/journald.conf.d/ 下放入自己的片段,所以只看主配置文件,未必能看到真正生效的值。

要查看全部配置,可以用下面的命令。它会按读取顺序依次列出主配置文件和所有配置片段的内容:

1
systemd-analyze cat-config systemd/journald.conf

配置写对了,不等于已经生效。修改后需要重启 journald,并确认 journal 文件实际写在哪里:

1
journalctl --header | grep -i "file path"

路径在 /run/log/journal 下,说明当前是易失存储;在 /var/log/journal 下,说明已经持久化。

发行版和安装镜像都可能修改默认配置。要判断一台麒麟、统信、RHEL 或 Debian 系机器是否已经持久化,要看两件事:实际生效的配置,以及 /var/log/journal 里是否真的有记录。只凭发行版家族判断,或者只看目录在不在,都可能出错。

推荐用配置片段的方式修改 journald 配置,明确指定持久化:

1
2
3
4
5
6
7
sudo mkdir -p /etc/systemd/journald.conf.d
sudo tee /etc/systemd/journald.conf.d/persistent.conf <<'EOF'
[Journal]
Storage=persistent
SystemMaxUse=2G
SystemKeepFree=1G
EOF

生产环境中,建议把容量限制和持久化写在同一个片段里。上面的数值仅为示例,具体取值方法见本章第 4 节。

然后重启 journald,再刷新一次:

1
2
sudo systemctl restart systemd-journald
sudo journalctl --flush

这里一定要用 restart,不要拆成 stop 和 start 两步。按 systemd 文档的说明,restart 会保留各服务连到 journald 的日志流;单独执行 stop 则会断开这些连接,之后这些服务的标准输出就不再进入 journal。

配置完成后,先确认关键服务的新记录仍在正常进入 journal;等下一次计划内重启后,再确认重启前的记录可以查到。持久化只对此后的记录生效,已经随重启消失的记录无法找回。

4. 限流、容量与清理

开启持久化之后,journald 仍然会限制写入速率和磁盘占用。这两项限制都可能让你以为日志都在,其实少了一部分。

限流由 RateLimitIntervalSec= 和 RateLimitBurst= 控制。较新版本 systemd 的默认值如下;早期版本的 RateLimitBurst 是 1000,具体以本机的 man journald.conf 为准:

1
2
RateLimitIntervalSec=30s
RateLimitBurst=10000

限流按服务分别计算,实际阈值还会随可用磁盘空间自动调整。日志中出现 Suppressed N messages 时,说明有 N 条消息被丢弃了,手里的记录并不完整。如果某个服务确实需要高频输出,可以在它的 unit 文件里用 LogRateLimitIntervalSec=、LogRateLimitBurst= 单独放宽(systemd 240 起支持),不要直接取消全局限流。

容量方面,持久化 journal 由两个参数控制:SystemMaxUse= 规定最多占用多少空间,SystemKeepFree= 规定至少给磁盘留出多少空闲。例如红帽为 OpenShift 集群节点给出的 journald 示例配置是:

1
2
SystemMaxUse=8G
SystemKeepFree=20%

数值仅为示例。实际取值可按”日均日志量 × 本机保留天数 × 1.5”估算,日均日志量可以用 journalctl --disk-usage 除以已记录的天数粗略得出。

易失存储有一组对应的 Runtime* 参数。

journald 按容量删除旧文件,所以日志能保留多少天并不固定:日志量越大,同样的空间能覆盖的时间就越短。MaxRetentionSec= 可以按时间删除旧记录,但它设定的是保留时间的上限,并不能保证最短保留多久。

查看当前占用:

1
journalctl --disk-usage

需要手工清理时,可以按空间或按时间清理:

1
2
sudo journalctl --vacuum-size=500M
sudo journalctl --vacuum-time=30d

vacuum 只清理已归档的 journal 文件,正在写入的活动文件不受影响,所以执行后总占用可能仍高于指定值。如果想先把当前文件归档再一起清理,可以加上 --rotate。清理之前,记得先把故障调查需要的记录导出保存。

四、rsyslog:接收、处理与转发

前一章讲的 journal,解决的是本机日志的收集、保存和查询。

读到这里,你可能会有一个疑问:journal 已经能保存和查询日志,为什么 RHEL 等发行版还要默认运行 rsyslog?反过来,Debian 12 为什么又不再默认安装它?

要回答这个问题,关键是弄清 rsyslog 在什么情况下是必要的。

本章的核心观点是:rsyslog 的价值在于”按需”。

1. rsyslog 的定位变了

在很长一段时间里,rsyslog 是每台 Linux 机器上默认运行的必需组件。原因很简单:它是把日志保存到磁盘上的那个程序。

即使到了 systemd 时代,RHEL 7、8 仍然沿用这种分工。journald 的默认设置是 Storage=auto,只有 /var/log/journal 目录存在时才把日志写到磁盘,而 RHEL 默认没有创建这个目录。于是 journal 只保存在内存里,一重启就没了。真正跨重启保留日志的,仍然是 rsyslog:它从 journal 读取消息,写进 /var/log/messages、/var/log/secure 等文本文件,再由 logrotate 定期轮转。

可以在一台默认安装的 RHEL 8 上验证这一点:

1
2
3
ls -ld /var/log/journal                   # 目录不存在,说明 journal 没有持久化
journalctl --list-boots # 只有一行,即本次启动
sudo grep "Linux version" /var/log/messages*

如果第三条命令能找到多次开机时的内核版本记录,而 --list-boots 只有一行,就说明:重启前的 journal 已经随内存清空,但 rsyslog 写下的文本日志还在。

Debian 12 走了另一条路:默认开启 journal 持久化。journal 自己就能跨重启保存日志,rsyslog 再写一份文本文件,同一份日志就会在磁盘上存两遍。所以 Debian 把 rsyslog 改为可选组件,不再默认安装。

这两种做法对比下来,结论很清楚:rsyslog 在单机上最主要的作用,是在 journal 不持久化时承担持久保存。journal 持久化以后,这个作用就重复了。 rsyslog 的定位也随之变化,从”每台机器都默认运行的必需组件”,变成了”有特定需求时再使用的工具”。

2. 什么时候需要 rsyslog

判断要不要 rsyslog,不看发行版默认装没装,而看有没有下面这些需求。每一种需求,都是 journal 单独解决不了的。

场景一:journal 不持久化,需要日志跨重启保存。
这就是 RHEL 7、8 的默认情况。如果不开 journal 持久化,又停掉 rsyslog,机器一重启,所有日志都会消失。

场景二:工具和规范依赖传统文本文件。
许多监控代理、安全审计工具、运维脚本和检查清单,都默认读取 /var/log/secure、/var/log/auth.log 这样的文件。文本文件用 grep、tail 就能直接查看,不依赖 journalctl;即使 journal 文件损坏,文本日志也不受影响。只要环境里有这类依赖,就需要 rsyslog 生成这些文件。

一个真实的例子是 fail2ban,这是一个通过分析登录日志来封禁暴力破解 IP 的安全工具。Debian 12 不再默认安装 rsyslog 后,fail2ban 默认仍去读取 /var/log/auth.log,而这个文件已经不存在。从旧版本升级上来的系统情况更隐蔽:如果卸载了 rsyslog 却保留了旧文件,文件还在,但不再更新,防护就这样悄无声息地失效了。解决办法有两种:让 fail2ban 改为直接读取 journal,或者重新安装 rsyslog。到了 Debian 13,fail2ban 已默认改为读取 journal。

这个例子说明,去掉 rsyslog 之前,要先确认有没有工具依赖它生成的文件。

场景三:按 syslog 协议把日志送到别处。
需要把日志集中到中央服务器或日志平台时,rsyslog 是现成的转发器:多数 Linux 主机本来就装着它,加一条转发规则即可,不必另装采集代理。journald 自带的 systemd-journal-upload 虽然也能转发,但用的是 systemd 自己的协议,接收端也必须是 systemd 的组件。要对接只认 syslog 协议的服务器或平台,仍然要靠 rsyslog 这类工具。

场景四:接收其他设备发来的日志。
防火墙、交换机、负载均衡器等网络设备,通常只能通过 syslog 协议发送日志。业内常见的做法,是部署专门的 syslog 服务器(rsyslog 或 syslog-ng),先把这些设备的日志稳定地接收、落盘、分类,再交给后端的日志平台。Splunk 等平台也把专用 syslog 服务器作为接收 syslog 的推荐做法,而不是让平台自己直接监听端口。

需要说明的是,rsyslog 能把多台机器的日志收集到一处、按主机分目录保存,但它不提供索引和检索界面。今天主流的集中日志平台,比如 Elasticsearch/OpenSearch、Grafana Loki、Splunk、Graylog,都不是基于 rsyslog 构建的;在容器和云原生环境中,主机上的采集工作也越来越多地交给 Fluent Bit、Vector、OpenTelemetry Collector 这类新一代采集器。rsyslog 在多机环境中的位置,是负责接收和转发的那一层,而不是整个日志平台。

反过来,如果 journal 已经持久化,又没有文本文件依赖,也不需要 syslog 转发或接收,那就可以不装 rsyslog,Debian 12 默认就是这样。

3. 单机上怎么搭配

落到单机上,journal 和 rsyslog 常见的搭配有三种:

组合 代表 优点 代价
journal 易失 + rsyslog 文本持久化 RHEL 7、8 默认 不重复写盘;文本日志兼容性好 重启后丢失结构化字段,无法用 -b -1 回看
journal 持久化 + rsyslog 很多生产环境的选择 排障能力最强,兼容性也最好 同一份日志存两份,要分别控制容量
只用持久化 journal Debian 12 默认 结构简单,没有重复存储 没有传统文本文件;依赖它们的工具要调整

第一种组合的代价值得多说一句:journal 里的可信字段,比如 _PID、_SYSTEMD_UNIT、_BOOT_ID,写进文本文件后大多丢失了,只剩时间、主机名、程序标签和正文。

对于 RHEL 生产服务器,第二种组合往往最稳妥:磁盘成本不高,而意外重启后能用 journalctl -b -1 查看重启前的完整记录,在排查宕机时非常有价值。磁盘紧张、又不需要回看历史启动的机器,保持默认也可以。新部署、没有历史包袱的系统,可以考虑第三种。

想知道自己的机器属于哪一种,看两件事就够了:

1
2
systemctl is-active rsyslog               # rsyslog 是否在运行
ls -ld /var/log/journal # journal 是否持久化

4. rsyslog 的工作原理

可以把 rsyslog 想象成一个邮件分拣中心:信件从几个收件口进来,按分拣规则分好类,再从不同的发件口送出去。对应到 rsyslog,流程如下图所示:

输入模块:消息从哪里来。 相当于分拣中心的收件口,名字以 im(input module)开头。常见的有:

模块 作用 对应场景
imjournal 直接从 journal 读取消息,RHEL 7、8 默认使用 场景一、二
imuxsock 通过本机的 syslog 套接字接收消息,Debian 系通常用它接收 journald 转发的消息 场景一、二
imtcp、imudp 在网络端口上接收其他主机或设备发来的日志 场景四
imfile 读取应用自己写的文本日志文件 场景三

使用 imuxsock 时有个前提:journald 必须开启 ForwardToSyslog=yes,否则 rsyslog 收不到消息,自然也写不出文件。

规则:消息该去哪里。 相当于分拣规则。每条规则由”条件”和”动作”两部分组成,最常见的条件是来源类别和严重级别。例如 RHEL 默认配置中的这一行:

1
authpriv.*    /var/log/secure

左边的 authpriv.* 是条件,表示认证类消息的所有级别;右边是动作,表示写入 /var/log/secure。一条消息可以同时匹配多条规则,所以它可以既写进本地文件,又转发到远端。

输出模块:消息送到哪里。 相当于分拣中心的发件口,名字以 om(output module)开头。常见的有:

模块 作用 对应场景
omfile 写入本地文件 场景一、二
omfwd 通过 UDP 或 TCP 转发到另一台主机 场景三
omrelp 用 RELP 协议转发,接收端会回复确认,比普通 TCP 更可靠 场景三
omelasticsearch、omkafka 等 把日志直接写入 Elasticsearch、Kafka 等系统 场景三、四

把三个环节串起来,RHEL 上 /var/log/messages 的来历就清楚了:journald 收集消息,imjournal 把消息读进 rsyslog,规则挑出该写进这个文件的消息,最后由 omfile 写入磁盘。

除此之外,rsyslog 还提供一些进阶功能,比如用模板决定写出的每一行长什么样,用磁盘队列在网络中断时缓存待转发的消息。需要时查阅 rsyslog 官方文档即可。

这些都写在配置文件里。 主配置文件是 /etc/rsyslog.conf,自定义配置一般放在 /etc/rsyslog.d/ 下、以 .conf 结尾的文件中。RHEL 的默认规则大多直接写在 /etc/rsyslog.conf 里,Debian 和 Ubuntu 则放在 /etc/rsyslog.d/50-default.conf 中。

要注意的是,rsyslog 的规则按顺序执行,/etc/rsyslog.d/ 下的文件会按文件名顺序,插入到主配置文件中 include 语句所在的位置。这和 journald 的配置片段”覆盖同名参数”不同:自定义规则放在哪个文件、文件叫什么名字,都可能影响结果。修改配置后,先检查语法,再重启服务:

1
2
sudo rsyslogd -N1
sudo systemctl restart rsyslog

rsyslog 和各类应用写出的文本文件会不断增长,需要定期轮转和清理,这就是下一章 logrotate 的工作。

五、logrotate:文本日志的轮转

1. 为什么需要 logrotate

journal 的容量由 journald 自己管理,而 /var/log 下的文本日志没有这种机制:它们只会追加,不会自动缩小,旧日志也不会自己消失。放任不管,轻则文件越来越大,查看和排障都很吃力;重则写满磁盘,连累同一分区上的其他服务。logrotate 就是用来解决这个问题的。

logrotate 的做法是”轮转”:定期把当前的日志文件改名归档,换一个新文件继续写;归档的旧文件压缩存放,超过保留份数的就删除。这样,日志始终分成”正在写的一个”和”归档的若干个”,总占用可控,查看也方便。

关于 logrotate,有三点需要先明确:

  • 它管所有文本日志,不只是 rsyslog 的。 rsyslog 写的 /var/log/messages,以及 nginx、dnf、apt 等应用自己写的日志,都靠 logrotate 轮转。
  • 它不管 journal。 journal 由 journald 按容量参数自行清理(见第三章第 4 节),不要给 /var/log/journal 配置 logrotate 规则。
  • 它不是常驻服务。 logrotate 在主流发行版中通常默认安装,但它并不在后台持续运行,而是由 systemd timer(logrotate.timer)或 cron 每天调用一次,执行完就退出。

2. 一次轮转做了什么

logrotate 每次被调用时,大致会经历以下步骤:

  1. 判断是否需要轮转。 读取配置,再查看自己的状态文件,里面记录着每个日志上一次轮转的时间,据此判断是否满足条件。
  2. 把当前文件改名归档。 例如 app.log 改名为 app.log.1,已有的归档依次后移,超过保留份数的最旧一份被删除。
  3. 新建一个空文件。 用原来的文件名新建 app.log,供程序继续写入。
  4. 通知应用重新打开日志。 让正在写日志的程序改为写入新文件。
  5. 压缩归档文件,并更新状态文件。

其中第 4 步最容易被忽视,却必不可少。原因在于,进程是通过文件描述符写日志的,描述符指向的是文件的 inode,而不是文件名。轮转只是改了文件名,inode 没变,如果不通知,进程会继续往改名后的 app.log.1 里写,新建的 app.log 一直是空的。同样的道理,删除一个仍被进程打开的日志文件,磁盘空间也不会释放,可以用 sudo lsof +L1 找出这类文件。

所以,轮转后要让应用重新打开日志文件,具体用什么命令或信号,以应用文档为准。例如,RHEL 上 rsyslog 的轮转规则会在轮转后给 rsyslog 发送 HUP 信号。对于不支持重新打开日志的应用,可以改用 copytruncate:先复制一份内容作为归档,再把原文件清空,这样 inode 不变,代价是复制和清空之间写入的内容会丢失。

另外有两个细节:

  • 归档文件的命名因发行版而异。 RHEL 默认启用了日期后缀,所以 /var/log 下会出现 messages-20261004 这样的文件;Debian 默认使用数字后缀,比如 syslog.1、syslog.2.gz。
  • 轮转条件只在 logrotate 被调用时才检查。 因为 logrotate 每天只运行一次,size 100M 的含义是:下次运行时,如果文件超过了 100 MB 就轮转,而不是一涨到 100 MB 就立刻轮转。

3. 一个最简单的配置

logrotate 的全局默认设置在 /etc/logrotate.conf 中,各软件的轮转规则放在 /etc/logrotate.d/ 下,一个软件一个文件。

假设有一个应用 myapp,把日志写在 /var/log/myapp/ 下,并且支持通过 reload 重新打开日志文件。新建 /etc/logrotate.d/myapp,写入:

1
2
3
4
5
6
7
8
9
/var/log/myapp/*.log {
daily
rotate 7
compress
missingok
postrotate
systemctl reload myapp
endscript
}

这几行的意思是:每天检查一次,保留 7 份归档,归档文件压缩存放,日志文件不存在时不报错;轮转完成后执行 systemctl reload myapp,通知应用重新打开日志。

写好后,先用 -d(调试模式)空跑一次。它只显示 logrotate 将要执行的操作,不会真正改动任何文件:

1
sudo logrotate -d /etc/logrotate.d/myapp

小结

syslog 最早解决了日志的统一收集和分类;syslog-ng 和 rsyslog 补上了可靠传输和灵活处理;RFC 5424 统一了消息格式;journald 带来了可信的来源信息和按字段查询。这些组件如今大多同时存在于一台机器上,各自承担接收、保存、转发、清理中的一部分。其中,rsyslog 的角色正在从默认必需的组件,变成按需使用的工具。

下次再遇到 /var/log/messages 是空的、journalctl 里却有记录,先别急着找丢失的日志。先看清这台机器的日志实际走了哪条路:谁接收,谁保存,谁转发,谁清理。