rsyslog 集中日志服务器实战:多机日志汇聚、TLS 加密传输、磁盘防爆与告警扫描全流程

为什么个人站长也需要一台日志集中服务器

很多个人站长在一台 VPS 上跑着 Nginx、PHP-FPM、MySQL,出了故障第一反应是 SSH 上去翻 tail -f。问题是:一旦服务器被打挂、被入侵、或者干脆起不来,你连不上去,日志就全部锁在那台机器里。更麻烦的是当你手上有两三台机器(主站一台、图床一台、备份一台),排查一个跨机请求要从三个终端里来回对照时间戳,效率极低。

我之前一直用 journalctl 和本地文件日志凑合,直到有一次主站磁盘写满导致 sshd 都登不上,我才意识到日志必须离开故障机、单独存放。这篇文章讲的就是用 rsyslog 搭一台集中日志服务器:不改包、不占内存、Debian/Ubuntu 默认就装好了,几百台机器的场景也够用。

方案选型:为什么是 rsyslog 而不是 ELK

先说清楚取舍,避免你走弯路。常见的日志集中方案有三类:

  • rsyslog / syslog-ng:系统自带、纯 C 实现、内存占用几 MB、支持 TCP/TLS 转发与落盘,天然适合"把日志原样搬到一台机器上存着"这种需求。
  • Elasticsearch/Loki 全家桶:查询能力强大,但要额外吃几百 MB 到几 GB 内存,个人站的小机器上性价比极低。
  • Vector / Promtail:新一代采集器,适合结构化管道与脱敏,但需要额外的部署维护成本。

我的建议是:如果你只是要"日志不丢、能按主机和时间检索、能做告警",rsyslog 是最省心的。等你有真正的全文检索需求,再在 rsyslog 落盘的目录上叠加 Vector 或 Loki 也不迟——rsyslog 完全可以把日志同时写文件(供 Vector 读取)和转发。

架构设计:客户端、服务端与端口

一个最小可用的集中日志架构只有两个角色:

  1. 日志服务端(collector):监听 UDP/TCP 514 或自定义端口,把收到的日志按来源主机分目录落盘,配置 logrotate 轮转。
  2. 日志客户端:每台业务机上的 rsyslog 把本地日志转发过去。

端口选择上有两个坑:一是标准 514/udp 在部分云厂商那里被限速甚至封禁,且 UDP 会丢包;二是在容器或 NAT 环境里 UDP 的源 IP 容易变成网关地址,导致你分不清日志来自哪台机器。所以我强烈建议:内网用 TCP 514,公网或跨机房用 TCP 加 TLS 走 10514。

服务端配置:按主机分目录落盘

服务端假设是一台只做日志的 1 核 1G 小机器。先确认 rsyslog 已安装:

apt install -y rsyslog
rsyslogd -v | head -3

然后创建自定义配置。Debian/Ubuntu 上不要直接改 /etc/rsyslog.conf,而是放一个独立文件进 /etc/rsyslog.d/,升级时不冲突:

cat > /etc/rsyslog.d/10-remote.conf <<'EOF'
# 加载 TCP 接收模块
module(load="imtcp")
# 允许的客户端网段,防止被公网随便灌日志
input(type="imtcp" port="514" ruleset="remote")
# 基于来源 IP 做 rDNS,没有内网 DNS 时可关掉以省开销
# global(net.enableDNS="off")

# 模板:/var/log/remote/来源IP/年-月-日/程序名.log
template(name="RemotePerHost" type="string"
  string="/var/log/remote/%FROMHOST-IP%/%$YEAR%-%$MONTH%-%$DAY%/%programname%.log")

ruleset(name="remote") {
  # 过滤掉本地噪声,只收远程
  if $fromhost-ip != '127.0.0.1' then {
    action(type="omfile" dynaFile="RemotePerHost"
           dirCreateMode="0755" fileCreateMode="0640"
           fileOwner="syslog" fileGroup="adm")
    # 同时保留一份原始流,方便日后接入 Vector
    action(type="omfile" file="/var/log/remote/all.log")
  }
}
EOF
systemctl restart rsyslog
systemctl status rsyslog --no-pager

几个关键点解释一下。%programname% 会生成 sshd.log、nginx.log 之类的文件,比笼统一个大文件好定位;如果你更想要"一台机器一天一个文件",把模板换成 /var/log/remote/%FROMHOST-IP%/%$YEAR%-%$MONTH%-%$DAY%.log 即可。dirCreateMode 和 fileCreateMode 是必须显式写的,默认权限由 umask 决定,可能让日志文件对所有用户可读——生产环境里日志可能含敏感信息(登录名、路径),不该是世界可读的。

防火墙与权限:先堵住公网灌日志的口子

裸奔的 514 端口会让任何人往你服务器写日志,轻则磁盘被灌满(本质是一次 DoS),重则伪造日志干扰你排查。所以在服务端一定要先设防火墙白名单:

# 只允许业务机网段访问 TCP 514
ufw allow from 10.8.0.0/24 to any port 514 proto tcp
ufw deny 514/tcp
ufw deny 514/udp
ufw status verbose

如果走公网,务必上 TLS。rsyslog 支持基于 gtls 的加密传输,用自签 CA 即可:

# 服务端
module(load="imtcp" StreamDriver.Name="gtls"
       StreamDriver.Mode="1" StreamDriver.AuthMode="x509/name")
input(type="imtcp" port="10514" ruleset="remote")

# 生成自签 CA 与证书
openssl req -x509 -newkey rsa:4096 -days 3650 -nodes \
  -keyout ca.key -out ca.crt -subj "/CN=log-ca"
openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr \
  -subj "/CN=logserver.internal"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -days 1825 -out server.crt
cat server.crt server.key ca.crt > /etc/rsyslog.d/logserver.pem
chmod 600 /etc/rsyslog.d/logserver.pem

客户端配置:把本地日志推过去

业务机上加一段转发规则。注意要和本地落盘并存,别只转发不落盘——网络抖动的瞬间你会同时失去本地和远程两份日志:

cat > /etc/rsyslog.d/90-forward.conf <<'EOF'
# 队列参数:网络断了先缓冲在内存,满了再落盘
$ActionQueueType LinkedList
$ActionQueueFileName fwdRule1
$ActionQueueMaxDiskSpace 1g
$ActionQueueSaveOnShutdown on
$ActionResumeRetryCount -1

# TLS 客户端
$DefaultNetstreamDriverCAFile /etc/rsyslog.d/ca.crt
$ActionSendStreamDriver gtls
$ActionSendStreamDriverMode 1
$ActionSendStreamDriverAuthMode x509/name
$ActionSendStreamDriverPermittedPeer logserver.internal

# 只转发这些设施,避免把 auth 之外的全推过去
*.* @@logserver.internal:10514
EOF
systemctl restart rsyslog

这里的 @@ 表示 TCP,单写 @ 表示 UDP——这是 rsyslog 里最容易记混的一处语法。$ActionResumeRetryCount -1 表示无限重试,配合 SaveOnShutdown on,即使 logserver 重启,客户端也会把日志缓冲住,恢复后补发,这是很多人漏掉的关键配置。

磁盘保护:日志服务器最怕被写满

集中日志服务器最大的风险不是丢日志,而是被日志撑爆磁盘——一旦写满,rsyslog 自己都可能写不动,你就彻底瞎了。三件事必须做:

  1. 给 /var/log 单独一个分区或 volume,别和系统盘混在一起。
  2. 配置 logrotate,按天轮转、压缩、保留 30 天。
  3. 加一个磁盘用量告警,到了 80% 就提醒。
cat > /etc/logrotate.d/remote-logs <<'EOF'
/var/log/remote/*/*/*.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 0640 syslog adm
    sharedscripts
    postrotate
        /usr/lib/rsyslog/rsyslog-rotate
    endscript
}
EOF
# 手动演练一次,确认不报错
logrotate -d /etc/logrotate.d/remote-logs

注意 postrotate 里调用的是 rsyslog-rotate,它会给 rsyslog 发信号让它重开文件句柄。如果你漏了这一步,会出现"文件切了但磁盘没变小"——因为 rsyslog 还在往已改名(已被删除 inode)的老文件里写,这个坑我在另一篇讲 logrotate 的文章里详细写过。

验证与排查:确认日志真的到了

配完先做一次端到端验证,别等出事才发现根本没通:

# 在业务机上发一条测试日志
logger -p user.info "zz1984-connectivity-test $(date +%s)"

# 在服务端按主机找文件
find /var/log/remote -name '*.log' -newermt '-2 minutes' | head
grep -r "zz1984-connectivity-test" /var/log/remote/ | tail -3

# 看 TCP 握手与错误
ss -tnp | grep :514
journalctl -u rsyslog -n 50 --no-pager | grep -i -E 'error|queue|refused'

没收到日志时按这个顺序查:一、防火墙是否放行来源 IP;二、客户端 @@ 的目标端口和主机名是否解析正确;三、服务端 imtcp 是否真的加载(rsyslogd -N1 可做语法检查);四、SELinux/AppArmor 是否拦了 rsyslog 写新目录。AppArmor 在 Ubuntu 上默认会给 rsyslog 一个路径白名单,你新建 /var/log/remote 后如果写不进去,八成是它——在 /etc/apparmor.d/local/usr.sbin.rsyslogd 里加一行路径再 reload 即可。

一套更省事的替代思路

如果你不想在客户端装一堆配置,还有两个务实的做法:一是用 systemd-journal-upload,它把 journald 的日志直接推给远端的 systemd-journal-remote,天然带二进制索引与 HMAC 校验;二是只在客户端开 imfile 模块把指定日志文件(比如 Nginx access.log)作为"新文件"读进来再转发,比用 syslog 设施过滤更直观。rsyslog 的 imfile 写法大致是:

module(load="imfile")
input(type="imfile" File="/var/log/nginx/access.log"
      Tag="nginx-access" Severity="info" Facility="local5"
      PersistStateInterval="200")

这样 Nginx 的访问日志也会被搬走,配合服务端按 Tag 分文件,你就能在日志服务器上同时看到系统日志、SSH 登录、Nginx 访问与 PHP 错误,一台机器管全站。

进阶:把关键日志变成能告警的信号

日志存下来只是第一步,真正的价值在于让它主动告诉你出事了。个人站没必要上整套告警平台,用最土的 logcheck 或者自己写个定时脚本扫关键字就够。核心思路是:把"值得你半夜爬起来看"的日志模式列出来,用 cron 每分钟扫一遍新写入的日志,命中就发一封邮件或者推送一条消息到手机上。

cat > /usr/local/bin/logwatch.sh <<'EOF'
#!/bin/bash
PATTERNS='Failed password|authentication failure|Invalid user|segfault|OOM|out of memory|Banned|error while loading'
LOG=/var/log/remote/all.log
STATE=/var/lib/logwatch.offset
[ -f "$STATE" ] || echo 0 > "$STATE"
START=$(cat "$STATE")
TOTAL=$(wc -l < "$LOG")
if [ "$TOTAL" -lt "$START" ]; then START=0; fi
tail -n +$((START+1)) "$LOG" | grep -Ei "$PATTERNS" | \
  while read -r line; do
    logger -t logwatch "$line"
  done
echo "$TOTAL" > "$STATE"
EOF
chmod +x /usr/local/bin/logwatch.sh
# 每分钟跑一次
echo '* * * * * root /usr/local/bin/logwatch.sh' > /etc/cron.d/logwatch

这段脚本里有个容易踩的坑:不能用 grep 每次扫全文件——日志大了之后每次都读几百 MB,几分钟就把小机器的 CPU 吃满。所以要用一个 offset 文件记录"上次读到第几行",只处理新增的部分。另外注意日志轮转之后行号会重置,脚本里那句 if [ "$TOTAL" -lt "$START" ] 就是处理这种情况的兜底,不加的话轮转当天会静默漏掉所有日志。

关于保留策略与合规的几句提醒

日志里会记录访客 IP、User-Agent、登录用户名、访问路径,这些在多数司法辖区里属于个人信息。个人站不必过度紧张,但有两点建议:一是只保留你真正会看的时长,30 天对绝大多数个人站足够,保留三年既无用处又徒增风险;二是如果站点面向欧洲用户,日志里最好把完整 IP 做个末段掩码(比如 203.0.113.x),既不影响排查攻击来源,又能降低合规压力。rsyslog 可以用 mmfields 或 mmnormalize 模块在落盘前做简单的字段替换,也可以退一步,用前面提到的 Vector 管道在读取时做脱敏。

还有一个现实问题:日志服务器本身要备份吗? 我的答案是"不必全量备份,但关键日志要"。全站日志动辄几十 GB,每天备份不现实;但安全相关的日志(SSH 登录、认证失败)体积极小,单独转发到第二个位置、保留一年,是很划算的。真出事的时候,这几 MB 的认证日志往往比几十 GB 的访问日志更关键。

写在最后

集中日志的价值不在于"能查日志",而在于故障时你依然看得见。服务器被 DDoS 打挂、磁盘写满登不上去、甚至被入侵后攻击者 history -c 清痕迹——只有离开本机的日志才靠得住。这套 rsyslog 方案在 1G 内存的小机器上跑一年也不会有存在感,却能在关键时刻救你一次。建议这周就先把服务端搭起来,客户端从主站开始接,跑顺了再铺到其他机器。

Last modification:October 16th, 2026 at 01:24 pm

Leave a Comment