香港服务器的 Debian 11:我如何把 systemd + journald 调到“不丢一条日志”

上周五,推广活动 23:00 准点打爆,我们边盯 Grafana,边看 Nginx 的 5xx 抖了一下。我第一反应是翻日志,可 journalctl 里出现了一条刺眼的提示:
“Suppressed 14322 messages from PID 1227 (api.service)”
那一刻我就知道,问题不在业务先,在我这条日志链路上。
这篇就是我后来“补课”的完整记录:在 Debian 11(Bullseye,systemd v247)上,如何把 systemd 与 journald 结合起来,在高流量场景既不丢日志、又不把机器拖垮。不管你是第一次摸 journald 的新手,还是准备给线上拧螺丝的老手,我尽量把坑和细节都摊开。
现场环境与基线
机房/网络
- 机房:葵涌 KCDC,双路市电 + N+1 UPS
- 上联:10G 两路(HKIX + 运营商专线),BGP 入口限速 9.5 Gbps
- 峰值(事故当晚):7.8 Gbps,HTTP RPS 峰到 320k req/s(多机摊)
香港服务器(单台)
| 项目 | 参数 |
|---|---|
| 机型 | 1U 定制(相当于 Dell R650 级别) |
| CPU | Intel Xeon Silver 4314 × 2(32C/64T) |
| 内存 | 256GB DDR4-3200 |
| 系统盘 | 2 × 480GB SATA SSD(RAID1) |
| 日志盘 | 2 × 1.92TB NVMe U.2(RAID1,XFS) |
| 网卡 | Intel X710 10GbE × 2(LACP) |
| OS | Debian 11 (Bullseye),systemd v247 |
| 容器 | Docker 24(daemon 日志驱动改 journald) |
服务进程(与日志相关)
api.service(Go) / edge-ngx.service(OpenResty) / queue-worker.service(Rust)
集中式日志:一台“汇聚节点”,跑 systemd-journal-remote
每台业务机跑 systemd-journal-upload 主动推送
当晚症状
journald 告警:Suppressed N messages(典型 RateLimit 触发)
/run/log/journal 使用率飙升(内存环形区),持久目录未开启
rsyslog 默认装上形成双写(journald → rsyslog → 文件),I/O 抖
目标与方案
目标
- 零丢失:在极端突发时刻不压死关键业务日志
- 低开销:CPU、I/O 可控,不给 P99 延迟添堵
- 可回放/可聚合:跨机查询、追踪一条请求链路
- 可观测:丢弃/限流要可见、可报警
方案总览(文字拓扑)
应用(stdout/stderr)→ systemd(接 stdout,打上 Unit/CGroup 元数据)→ journald(持久化、限流、压缩、配额)→ journal-upload(TLS 推送)→ 汇聚机 journal-remote(集中存储/查询)
关键点:
- 把日志入口统一到 systemd(不是散落到各自文件)
- journald 持久化到 NVMe,合理限流/配额
- 只保留一种链路(禁用无意义的 syslog 双写)
- 对关键服务单独放宽/关闭限流,对“话痨服务”降噪
实操一步步
1)开启持久存储并做好权限
journald 默认可能只把日志扔在内存(/run/log/journal)。先启持久目录:
sudo mkdir -p /var/log/journal
sudo chown root:systemd-journal /var/log/journal
sudo chmod 2755 /var/log/journal
2755 和 systemd-journal 组很重要,否则你会发现普通用户/采集侧读不到。
2)为日志盘单独挂载(可选但强烈建议)
NVMe RAID1,XFS:
# 假设阵列设备是 /dev/md3
sudo mkfs.xfs -f /dev/md3
echo '/dev/md3 /var/log/journal xfs noatime,nodiratime,logbufs=8,logbsize=256k 0 2' | sudo tee -a /etc/fstab
sudo mount -a
XFS 在 journald 这种小对象写入场景通常比 ext4 更稳,noatime 能少些脏写。
3)调 journald.conf
编辑 /etc/systemd/journald.conf(没有就创建),这套是我在 300k+ RPS 下跑通的保守参数:
[Journal]
# 存储与压缩
Storage=persistent
Compress=yes
Seal=yes
SplitMode=uid
# 刷盘与速率
SyncIntervalSec=5m
RateLimitIntervalSec=5s
RateLimitBurst=20000
# 空间与单文件大小(根据你日志盘大小调整)
SystemMaxUse=8G
SystemKeepFree=5G
SystemMaxFileSize=256M
RuntimeMaxUse=1G
RuntimeKeepFree=256M
RuntimeMaxFileSize=64M
# 仅保留一条链路,避免 journald -> rsyslog -> 文件产生双写抖动
ForwardToSyslog=no
ForwardToKMsg=no
ForwardToConsole=no
ForwardToWall=no
# 限制级别:丢弃 debug 噪音(必要时再开)
MaxLevelStore=info
MaxLevelKMsg=notice
应用后重启:
sudo systemctl restart systemd-journald
sudo systemctl status systemd-journald -l
为什么不是把 RateLimit 直接关掉?
关掉(设置为 0)确实“不丢”,但极端情况下会把 journald 本身打到高 CPU。我的策略是全局放宽(5s/20000),关键服务单独放开(下一步)。
4)让服务把日志直接打进 journal,并做“分级限流”
以 api.service 为例(Go 应用,stdout 打日志):
sudo systemctl edit api.service
加入 drop-in:
[Service]
# 入口统一:把 stdout/stderr 都进 journal
StandardOutput=journal
StandardError=journal
# 便于检索
SyslogIdentifier=api
# 关键服务不做 rate limit(只在个别核心服务使用)
LogRateLimitIntervalSec=0
LogRateLimitBurst=0
# 避免进程太多 fd 打爆
LimitNOFILE=1048576
# 发生崩溃自动拉起,减少“窗口期”
Restart=always
RestartSec=0.5s
Nginx/OpenResty(edge-ngx.service)我不再让它写 access.log 文件,而是改配置把访问日志写 syslog(由 systemd 接管)。在 nginx.conf:
error_log syslog:server=unix:/dev/log,tag=edge-ngx,severity=error;
access_log syslog:server=unix:/dev/log,facility=local7,tag=ngx_access;
再在 edge-ngx.service 同样设置 StandardOutput/StandardError,确保一切归一到 journal。
容器场景:把 Docker 日志驱动也改 journald
/etc/docker/daemon.json:
{
"log-driver": "journald",
"log-opts": {
"tag": "{{.Name}}.{{.ID}}"
}
}
改完 sudo systemctl restart docker,容器 stdout/stderr 也统一走 journal 了。
5)集中式收集:journal-upload → journal-remote
汇聚机(集中节点)
sudo apt-get update
sudo apt-get install -y systemd-journal-remote
# 使用 socket 激活,监听 19532/TCP
sudo systemctl enable --now systemd-journal-remote.socket
# 也可直接启服务(需要配置 TLS 证书)
# sudo systemctl enable --now systemd-journal-remote.service
/etc/systemd/journal-remote.conf(启用 TLS,生产必须):
[Remote]
Seal=yes
SplitMode=host
ServerKeyFile=/etc/systemd/journal-remote/journal-remote.key
ServerCertificateFile=/etc/systemd/journal-remote/journal-remote.crt
TrustedCertificateFile=/etc/systemd/journal-remote/ca.crt
生成/下发证书略。目标目录默认在 /var/log/journal/remote。
业务机(每台)
sudo apt-get install -y systemd-journal-remote # 里面包含 upload
/etc/systemd/journal-upload.conf:
[Upload]
URL=https://<汇聚机IP或域名>:19532
# 双向 TLS(推荐)
ServerKeyFile=/etc/systemd/journal-upload/upload.key
ServerCertificateFile=/etc/systemd/journal-upload/upload.crt
TrustedCertificateFile=/etc/systemd/journal-upload/ca.crt
启用:
sudo systemctl enable --now systemd-journal-upload.service
sudo systemctl status systemd-journal-upload -l
验证汇聚机能看到来自各主机的日志:
# 在汇聚机上
sudo journalctl -D /var/log/journal/remote -o short-iso --since "5 min ago" | head
journal-upload 内建断点续传(基于 cursor),短线/重启不会重复泛滥或丢失。
6)监控与可见性
常用指令:
# 看磁盘占用
journalctl --disk-usage
# 校验日志完整性与损坏
sudo journalctl --verify
# 看 journald 自身是否发生抑制(RateLimit)
journalctl -u systemd-journald -b -p warning
# 过滤某服务(含元数据)
journalctl -u api.service -o json | head -n 5
我还加了两个告警:
当 journalctl -u systemd-journald | grep Suppressed 命中次数 > 0,触发告警
当 /var/log/journal 使用率 > 85% 且增长速率持续 10 分钟,提示扩容或调低日志等级
压测与对比数据
压测方法:3 台 wrk 压住边缘层,业务链路写结构化 JSON 到 stdout;每条请求 2 条日志(入/出),可控洪峰。
调优前(默认 Debian 11)
| 指标 | 数值 |
|---|---|
| RateLimit 参数 | 30s / 1000(默认) |
| 丢日志(Suppress) | 1.9% ~ 4.3%(峰值) |
| journald CPU | 60% ~ 120%(总核) |
| /run 占用 | 峰到 850MB(易顶满) |
| P99 延迟抖动 | +6.8% |
调优后(本文参数)
| 指标 | 数值 |
|---|---|
| RateLimit 参数 | 5s / 20000(关键服务 0/0) |
| 丢日志(Suppress) | 0%(连续 3 次压测) |
| journald CPU | 18% ~ 35% |
| /var/log/journal 占用 | 稳定 2.1GB(配额 8GB) |
| P99 延迟抖动 | +1.2% |
| 汇聚落盘速率 | 75–110 MB/s(峰) |
现场踩过的坑 & 修复手记
双写环路
装了 rsyslog 又让 journald ForwardToSyslog=yes,等于同时写两套,还可能 syslog 再写回 journald。表现就是 I/O 抖动 + 重复日志。
解法:关 ForwardToSyslog,保留一条通路;需要转发再让汇聚机做。
只开了 Runtime 存储
/var/log/journal 不存在,重启后“断档”。
解法:创建目录、设好权限(上文步骤 1)。
关键服务被全局 RateLimit 连坐
高峰期一些 metrics agent、健康检查刷屏,把全局突发额度吃掉,关键服务也被“连坐”。
解法:用 per-service 的 LogRateLimitIntervalSec/LogRateLimitBurst=0 单独放开核心服务;反而给 agent 类服务单独设置较低的 burst。
日志过长被截断
某些错误堆栈一次输出几十 KB,阅读困难。
策略:应用侧改成多行结构化(每行有相同 TraceID),而不是硬怼超长一行;检索时用 _PID + TRACE_ID 组合。
汇聚机时间偏移
引用时间范围查询不准。
解法:统一 NTP(systemd-timesyncd 或 chrony),并观察 _BOOT_ID 与 __REALTIME_TIMESTAMP 的一致性。
权限问题
观察/排障账号看不到完整日志。
解法:加入 systemd-journal 组:sudo usermod -aG systemd-journal <user>。
可复制的“最终配方”
journald(统一版)
# /etc/systemd/journald.conf
[Journal]
Storage=persistent
Compress=yes
Seal=yes
SplitMode=uid
SyncIntervalSec=5m
RateLimitIntervalSec=5s
RateLimitBurst=20000
SystemMaxUse=8G
SystemKeepFree=5G
SystemMaxFileSize=256M
RuntimeMaxUse=1G
RuntimeKeepFree=256M
RuntimeMaxFileSize=64M
ForwardToSyslog=no
ForwardToKMsg=no
ForwardToConsole=no
ForwardToWall=no
MaxLevelStore=info
关键服务 drop-in(示例)
# systemctl edit api.service
[Service]
StandardOutput=journal
StandardError=journal
SyslogIdentifier=api
LogRateLimitIntervalSec=0
LogRateLimitBurst=0
LimitNOFILE=1048576
Restart=always
RestartSec=0.5s
Docker 日志驱动
// /etc/docker/daemon.json
{
"log-driver": "journald",
"log-opts": {
"tag": "{{.Name}}.{{.ID}}"
}
}
汇聚机
# /etc/systemd/journal-remote.conf
[Remote]
Seal=yes
SplitMode=host
ServerKeyFile=/etc/systemd/journal-remote/journal-remote.key
ServerCertificateFile=/etc/systemd/journal-remote/journal-remote.crt
TrustedCertificateFile=/etc/systemd/journal-remote/ca.crt
业务机上传
# /etc/systemd/journal-upload.conf
[Upload]
URL=https://log-collector.internal:19532
ServerKeyFile=/etc/systemd/journal-upload/upload.key
ServerCertificateFile=/etc/systemd/journal-upload/upload.crt
TrustedCertificateFile=/etc/systemd/journal-upload/ca.crt
运维“复盘清单”(上线前逐条核对)
- /var/log/journal 存在,权限正确(2755 & systemd-journal)
- journald Storage=persistent,ForwardToSyslog=no
- 全局 RateLimitIntervalSec/RateLimitBurst 合理,关键服务 drop-in 放开
- NVMe 专盘挂载参数合理(noatime 等)
- Docker 日志驱动 journald 生效
- journal-upload / journal-remote TLS 正常,跨机可查
- 告警:检测 Suppressed 日志 & 空间水位
- 预案:磁盘满时 journalctl --vacuum-size=... / --vacuum-time=...
尾声:再回到那个周五
活动结束后,我们把全链路重放了一次。那串最先抖动的 5xx,在汇聚机上被我一条条拉了出来:从边缘层的入站头、到 API 的超时重试、再到内部服务的一处锁竞争,路径清清楚楚。第二周,新的业务线在同样的压力测试下,零丢失、延迟平稳,上线像换了辆底盘扎实的新车。
我一直觉得,日志系统对运维像黑匣子之于航空。systemd + journald 并不花哨,但只要把入口统一、限流与配额想清楚、再加一台稳当的汇聚点,在香港这种高流量、波动大的环境里,也一样能稳稳落地。如果你也在为“Suppressed N messages”头疼,不妨按这篇把螺丝拧一遍。多数时候,问题就会安静下来。