1. 为什么要在容器里跑日志服务
把 Syslog-ng 塞进 Docker 这件事,我最早是在一个混合环境里被逼出来的。当时手头有几台不同发行版的主机,外加两个边缘节点,每台机器上装 Syslog-ng 的版本、依赖库、配置文件路径全都不一样,改一次采集规则要在四五个地方重复劳动,出问题还得逐台排查。后来索性把 Syslog-ng 做成容器镜像,配置统一挂载,日志目录统一映射,世界一下就清净了。
Syslog-ng 本身是个老牌的日志管理守护进程,负责接收、解析、过滤、转发和落盘各类系统日志与网络日志。它支持 syslog 协议、JSON 格式、文件读取等多种来源,能按规则分流到文件、远程服务器、消息队列等目的地。把它容器化,核心价值在于环境一致性和配置可移植性:镜像里锁死了版本和依赖,宿主机只需要有 Docker 就能跑,配置文件通过 volume 挂进去,改完重启容器即可生效,不用碰宿主机的包管理器。
这篇内容适合两类人看:一类是刚开始接触集中式日志管理、想快速搭一套能用的采集链路的新手;另一类是在多主机环境里被版本碎片化折磨过、想用容器统一运维口径的老手。下面我会从设计思路、镜像选型、配置细节、实操流程到踩坑排查,把整套东西拆开讲清楚,参数和步骤都给到能直接抄的程度。
2. 整体设计与方案选型拆解
2.1 容器化日志服务的核心思路
日志服务的容器化和普通无状态应用不太一样,它有两个硬约束:网络端口要稳定可达,数据落盘不能丢。Syslog-ng 默认监听 514 端口(UDP 和 TCP),如果容器网络用 bridge 模式,外部设备要发日志进来就得做端口映射;如果直接用 host 网络模式,端口和宿主机共享,省去映射但牺牲了隔离性。我一般选 bridge 加显式端口映射,原因是端口占用一目了然,迁移时改个映射就行,不会和宿主机上其他服务抢端口。
数据落盘这块更关键。容器内的文件系统是临时的,容器一删日志全没,所以必须把日志输出目录通过 volume 挂到宿主机上。这里有个容易忽略的点:Syslog-ng 进程在容器里以某个用户身份运行,挂载目录的权限必须和这个用户匹配,否则会出现“容器起来了但日志写不进去”的静默故障。我后面会专门讲权限怎么对齐。
另一个设计决策是配置文件的组织方式。Syslog-ng 的主配置通常在/etc/syslog-ng/syslog-ng.conf,里面用source、destination、log三个块描述数据流。我习惯把配置拆成主配置加若干conf.d片段,挂载整个配置目录而不是单个文件,这样加一条采集规则只需要新增一个片段文件,不用动主配置,回滚也方便。
2.2 镜像选型:官方镜像还是自建
镜像来源上有两条路。第一条是用社区维护的 Syslog-ng 镜像,优点是开箱即用、体积经过优化,缺点是版本更新节奏不受自己控制,某些编译选项可能和你的需求不匹配。第二条是基于 Alpine 或 Debian 自己写 Dockerfile 构建,优点是完全可控,能按需裁剪模块,缺点是构建和维护成本高。
我的实际选择是:生产环境用自建镜像,测试和快速验证用现成镜像。自建镜像的 Dockerfile 其实很短,核心就是装包、建目录、设入口。用 Alpine 的话基础镜像小,但要注意 Syslog-ng 的某些模块(比如连消息队列的)在 Alpine 的软件源里可能拆成了独立包,得显式装上。用 Debian 系则包更全,代价是镜像大一些。下面给一个我常用的 Debian 系构建思路,参数都是实测能跑通的:
FROM debian:bookworm-slim RUN apt-get update && apt-get install -y --no-install-recommends \ syslog-ng \ syslog-ng-mod-json \ syslog-ng-mod-journal \ && rm -rf /var/lib/apt/lists/* RUN mkdir -p /var/log/syslog-ng /etc/syslog-ng/conf.d COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf EXPOSE 514/udp 514/tcp 601/tcp ENTRYPOINT ["/usr/sbin/syslog-ng", "-F", "-e"]这里-F是前台运行(容器里必须前台,否则容器会立刻退出),-e是把错误输出到 stderr,方便用docker logs看启动阶段的报错。601/tcp是 syslog 的可靠传输端口,如果你要做跨主机转发可以留着。
2.3 网络模式与端口规划
端口规划我踩过一次坑,值得单独说。514 是 syslog 的标准端口,但它是特权端口(小于 1024),容器内进程如果不是 root 就跑不起来。有两种解法:一是容器内以 root 运行(简单但不够干净),二是容器内换用高位端口(比如 5514),宿主机映射时再转回 514。我倾向第二种,映射写成-p 514:5514/udp -p 514:5514/tcp,容器内配置监听 5514,外部设备仍然按标准 514 发送,两边都舒服。
如果日志来源是同一宿主机上的其他容器,那更推荐用自定义 bridge 网络,让日志容器和业务容器加入同一网络,业务容器直接用容器名加内部端口发送,完全不走宿主机端口,隔离性和可读性都更好。跨主机采集则建议走 TCP 601 或自定义高位端口,UDP 在跨网络时丢包不可控,重要日志别用 UDP。
3. 核心配置细节与实操要点
3.1 syslog-ng.conf 的数据流骨架
Syslog-ng 的配置本质是搭管道:source定义从哪进,destination定义往哪出,log把两者连起来,中间可以插filter和parser。理解这个三段式,后面所有花活都是在这个骨架上加零件。一个最小可用的主配置长这样:
@version: 4.0 @include "scl.conf" source s_network { syslog(ip(0.0.0.0) port(5514) transport("tcp")); syslog(ip(0.0.0.0) port(5514) transport("udp")); }; destination d_file { file("/var/log/syslog-ng/all.log" create-dirs(yes) owner("syslog") group("syslog") perm(0640)); }; log { source(s_network); destination(d_file); };@version必须写,且要和镜像里的 Syslog-ng 大版本匹配,写错了启动直接报错。@include "scl.conf"引入标准配置库,很多高级 source 和 destination 驱动依赖它。create-dirs(yes)让 Syslog-ng 自动建目录,省得手动 mkdir;owner和group指定落盘文件属主,这一步和前面说的挂载权限强相关,必须和容器内运行用户一致。
3.2 按来源和级别分流的关键写法
实际场景里,所有日志堆一个文件是没法用的,得按来源主机、按设施(facility)、按级别(severity)分流。Syslog-ng 的filter表达式很灵活,支持布尔组合。比如我想把错误级别以上的日志单独抽出来告警用,同时按主机名分目录存:
filter f_err { level(err..emerg); }; destination d_byhost { file("/var/log/syslog-ng/hosts/${HOST}/messages.log" create-dirs(yes) perm(0640)); }; log { source(s_network); filter { level(err..emerg); }; destination(d_file); }; log { source(s_network); destination(d_byhost); };${HOST}是宏,运行时替换成日志里的主机名字段,这样每台来源主机自动一个目录,不用为每台机器写一条规则。这里有个细节:如果来源日志里没有主机名字段,${HOST}会是空值,路径就变成hosts//messages.log,虽然能写但很别扭。稳妥做法是在 source 里加default-facility和default-priority,或者用$(if ...)做兜底。我一般直接给个默认值,避免空目录。
3.3 挂载目录与权限对齐的实操要点
权限问题是容器化日志服务最高频的故障源,没有之一。容器内 Syslog-ng 默认以syslog用户运行(Debian 系),UID 通常是 102 左右,但不同基础镜像可能不同。宿主机上你挂载的目录如果属主是 root 且权限 755,容器内非 root 用户就写不进去,表现是容器正常运行、docker logs也没报错,但目标文件就是不出现。
我的处理流程是固定的:先在容器里查实际运行用户的 UID,命令是docker exec <容器名> id syslog,拿到 UID 和 GID;然后在宿主机上把挂载目录的属主改成这个 UID,或者反过来在 Dockerfile 里把用户 UID 固定成宿主机上某个已知值。后者更可控,我通常在 Dockerfile 里加一句RUN usermod -u 1000 syslog && groupmod -g 1000 syslog,把 UID 对齐到宿主机常用值,挂载目录属主设成 1000 即可。
注意:改 UID 要在装完包之后、启动之前做,且挂载目录的权限建议 0750 起步,别图省事给 0777,日志里可能有敏感信息。
3.4 日志轮转不能只靠容器
很多人以为日志落盘就完事了,忘了轮转。容器本身不会帮你切日志,Syslog-ng 也不自动轮转,得靠外部工具。两种做法:一是宿主机上用 logrotate 定时切挂载目录里的文件,二是 Syslog-ng 配置里用file()的log-rotate相关参数配合外部信号。我推荐第一种,因为 logrotate 是宿主机成熟工具,配置直观,还能压缩归档。
logrotate 配置片段大概这样,放在/etc/logrotate.d/syslog-ng-container:
/var/log/syslog-ng/*.log /var/log/syslog-ng/hosts/*/*.log { daily rotate 14 compress delaycompress missingok notifempty create 0640 1000 1000 }create那行的 UID/GID 要和前面权限对齐的值一致,否则轮转后新文件属主变 root,Syslog-ng 又写不进去了。这个坑我见过不止一次,轮转前一切正常,轮转后日志断流,查半天才发现是新文件权限问题。
4. 完整实操流程与关键环节
4.1 从零搭建的完整命令序列
假设宿主机已经装好 Docker,目标是把 Syslog-ng 跑起来,接收 514 端口的日志,落盘到/data/syslog。完整流程如下,命令都是可直接执行的:
第一步,准备目录和配置。建三个目录,分别放配置、日志、以及一个可选的持久化缓冲:
mkdir -p /data/syslog/{conf,logs,buffer}把前面写的syslog-ng.conf放进/data/syslog/conf/,注意容器内监听端口改成 5514(对应前面说的特权端口规避方案)。
第二步,处理权限。假设容器内 syslog 用户 UID 是 1000:
chown -R 1000:1000 /data/syslog/logs /data/syslog/buffer chmod -R 0750 /data/syslog/logs第三步,启动容器。用自建镜像my-syslog-ng:latest:
docker run -d \ --name syslog-ng \ --restart unless-stopped \ -p 514:5514/udp \ -p 514:5514/tcp \ -v /data/syslog/conf:/etc/syslog-ng/conf.d:ro \ -v /data/syslog/logs:/var/log/syslog-ng \ -v /data/syslog/buffer:/var/lib/syslog-ng \ my-syslog-ng:latest--restart unless-stopped保证宿主机重启后容器自动拉起;配置目录用:ro只读挂载,防止容器内进程误改配置;/var/lib/syslog-ng是 Syslog-ng 的持久化状态目录,磁盘缓冲模式会用到,挂出来避免容器重建丢状态。
第四步,验证。先看容器状态docker ps,再看启动日志docker logs syslog-ng,确认没有配置解析错误。然后从另一台机器发一条测试日志:
logger -n <宿主机IP> -P 514 -d "test message from remote"去/data/syslog/logs/all.log里看有没有这条记录,有就说明链路通了。
4.2 磁盘缓冲模式的关键参数
网络日志最怕的是下游不可达时丢数据。Syslog-ng 的disk-buffer能把待发送的日志先写本地磁盘,下游恢复后补发,这对转发到远程服务器的场景几乎是必配。配置写法是在 destination 里加disk-buffer()块:
destination d_remote { syslog("log-server.example" port(601) disk-buffer( mem-buf-size(16384) disk-buf-size(1073741824) reliable(yes) dir("/var/lib/syslog-ng/buffer") )); };mem-buf-size是内存缓冲条数,disk-buf-size是磁盘缓冲字节数,1GB 大概能存几十万条中等长度的日志,按你的日志量和下游故障恢复时间估算。reliable(yes)开启可靠模式,保证进程崩溃后缓冲不丢,代价是写盘更频繁。dir必须指向挂载出来的持久化目录,否则容器重建缓冲就没了。
这里有个参数选择的经验:mem-buf-size别设太大,内存缓冲在进程异常时可能丢,重要日志宁可多走磁盘;disk-buf-size要结合磁盘剩余空间和日志增速算,比如每天 2GB 日志、下游最长可能挂 2 小时,那缓冲至少留 200MB 余量,我一般按日量的 20% 到 30% 给。
4.3 用 docker-compose 固化整套环境
命令行启动参数一多就容易记错,我习惯用 compose 文件把整套环境固化下来,迁移时复制文件即可。对应的docker-compose.yml:
services: syslog-ng: image: my-syslog-ng:latest container_name: syslog-ng restart: unless-stopped ports: - "514:5514/udp" - "514:5514/tcp" volumes: - ./conf:/etc/syslog-ng/conf.d:ro - ./logs:/var/log/syslog-ng - ./buffer:/var/lib/syslog-ng healthcheck: test: ["CMD", "syslog-ng-ctl", "stats"] interval: 30s timeout: 5s retries: 3healthcheck用syslog-ng-ctl stats探活,这个命令能连上控制接口就说明进程活着,比单纯看进程存在更准。interval30 秒是折中值,太频繁没必要,太稀疏故障发现慢。
4.4 配置热加载与灰度验证
改配置不一定要重启容器。Syslog-ng 支持SIGHUP信号触发配置重载,命令是docker kill -s HUP syslog-ng。重载的好处是不中断现有连接,坏处是配置有语法错误时旧配置继续跑、新配置不生效,容易误以为改成功了。所以我的习惯是:改完配置先跑一次语法检查,容器内执行syslog-ng -s -f /etc/syslog-ng/syslog-ng.conf,-s是 syntax check 模式,只校验不启动,通过了再发 HUP。
灰度验证的思路是:新规则先只对一台测试来源主机生效,用 filter 限定${HOST}等于测试机名,观察输出文件内容正确后再放开。直接全量上规则,一旦 filter 写错把日志全导到一个文件里,清理起来很麻烦。
5. 常见问题与排查技巧实录
5.1 容器起来了但收不到日志
这是最高频的问题,排查要按链路从外到内走。先确认端口通不通,在来源机器上用nc -vzu <宿主机IP> 514测 UDP,或用nc -vz <宿主机IP> 514测 TCP。不通的话查宿主机防火墙和云环境的安全组,514 端口是否放行。通了但没日志,进容器看监听状态,docker exec syslog-ng ss -lunp看 5514 是否在监听,没监听说明配置里 source 没生效,回去查@version和端口写法。
还有一种隐蔽情况:UDP 收到了但被 filter 过滤掉了。临时把 filter 去掉,只留 source 直连 destination,看日志出不出来,能出来就是 filter 表达式写错了。level(err..emerg)这种范围写法,注意级别顺序是从低到高,写反了会一条都匹配不上。
5.2 日志文件权限反复出问题
前面讲过权限对齐,但实际还会遇到轮转后失效、SELinux 拦截、挂载目录属主被其他进程改动等情况。排查顺序是:先ls -ln看文件实际 UID/GID,和容器内id syslog对比;再看宿主机是否开了 SELinux 或 AppArmor,开了的话挂载要加:z或:Z标签,compose 里写成./logs:/var/log/syslog-ng:z;最后检查 logrotate 的create参数是否和实际 UID 一致。
提示:如果宿主机是某些强制访问控制开启的发行版,volume 挂载不加标签会表现为“目录存在但容器内看不到内容”,这个现象和权限不足很像,别混淆。
5.3 磁盘缓冲导致容器磁盘暴涨
开了 disk-buffer 后,如果下游长期不可达,缓冲文件会一直涨到disk-buf-size上限。上限设太大加上日志量大,可能把宿主机磁盘写满。我的做法是给缓冲目录单独挂一块盘或单独配额,同时在监控里盯du -sh /data/syslog/buffer,超过阈值告警。另外disk-buf-size别设成无限,Syslog-ng 虽然会按上限截断,但截断意味着丢日志,宁可早点告警让人介入。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查命令 | 处理方式 |
|---|---|---|---|
| 容器启动即退出 | 配置语法错误或@version不匹配 | docker logs <容器> | 用syslog-ng -s校验配置 |
| 端口不通 | 防火墙/安全组未放行 | nc -vz <IP> 514 | 放行对应端口 |
| 收不到日志但端口通 | source 未监听或 filter 误杀 | ss -lunp、临时去 filter | 修正 source 端口或 filter 表达式 |
| 日志文件不生成 | 挂载目录权限或属主不符 | ls -ln、id syslog | 对齐 UID/GID,检查 SELinux 标签 |
| 轮转后日志断流 | 新文件属主变 root | 查 logrotatecreate参数 | 修正 create 的 UID/GID |
| 缓冲目录暴涨 | 下游不可达且上限过大 | du -sh缓冲目录 | 调小上限并加监控告警 |
| 重载配置不生效 | 新配置有语法错误 | syslog-ng -s | 修正后重新发 HUP |
5.5 几个只有踩过才知道的细节
第一个细节:@include "scl.conf"这行如果漏了,某些 source 驱动会报“unknown plugin”,报错信息不会直接指向这行,容易绕远路。第二个细节:容器内时区默认是 UTC,日志时间戳和宿主机本地时间对不上,排查时容易误判先后顺序,解决办法是启动时加-e TZ=Asia/Shanghai或挂载/etc/localtime。第三个细节:docker logs只显示 stderr 的启动信息,运行期的统计和错误要看 Syslog-ng 自己的内部日志,配置里加一个internal()source 指向文件,才能看到运行期的告警。
我个人在实际操作中的体会是,容器化日志服务最难的不是把容器跑起来,而是把权限、轮转、缓冲这三件事的边界理清楚。跑起来只要十分钟,跑稳需要把上面这些细节都过一遍。最后分享一个小技巧:把整套 compose 文件、配置片段、logrotate 配置放在同一个版本库目录里,换机器时整个目录拷过去,改一下挂载路径就能起,比记一堆命令行参数靠谱得多。