Docker 部署 SmokePing:网络延迟与丢包监控实战

1. SmokePing 测的到底是什么:从一次 ICMP 往返到一张曲线

我先说一个常被搞混的点。很多人第一次听说 SmokePing,会把它当成"网络监控系统",装上以后发现它既不看 CPU 也不看内存,只画几条歪歪扭扭的彩色线,就觉得这工具功能单一。实际上它干的事情非常垂直:持续、长期、可视化地测量一条链路的往返延迟和丢包率,然后把数据压缩成一种体积可控、查询代价极低的时间序列,让你能在半年后翻回去看"那天下午三点到底是不是网络抖了"。用 Docker 部署 SmokePing 的价值不在于省去几条安装命令,而在于把"探测进程"和"CGI 绘图前端"这两套东西连同它们各自的权限、时区、持久化需求,一次性钉在一份可复制的描述文件里。

我最早接触它的场景是一个跨机房的内网链路:每天上午十点左右业务方就喊卡,运维过去 ping 两分钟,一切正常,人一走又开始卡。这种"间歇性、低概率、短时间"的问题,靠人肉 ping 根本抓不到,因为它需要的是每五分钟固定发一组包,连续记录几十天。这就是 SmokePing 的舒适区。它的思路很朴素:按固定周期向目标发若干个探测包,统计这一组包的丢包数量和中位数延迟,写进 RRD 数据库,绘图前端再从数据库里把历史数据拉出来渲染成图。

这个模型决定了三件事,后面所有的部署配置都围绕它们展开。第一,它是无状态探测 + 有状态存储,探测进程挂掉重启没关系,但 RRD 文件丢了等于历史全废,所以存储必须落盘。第二,它的数据写入频率是固定的,叫step,默认 300 秒,改这个参数会连带影响已有数据库的兼容性,不能随手改。第三,它的探测能力由"探针"决定,探针可以是 ICMP、DNS 查询、TCP 连接、HTTP 请求,这决定了它能覆盖的场景边界。

1.1 探测循环:守护进程、探针与 RRD 数据库

SmokePing 运行起来后有两个常驻角色。一个是smokeping守护进程,它按照step的节奏醒来,读配置里定义的目标列表,为每个目标启动对应的探针子进程去发包,收回结果后按目标名把数据写进对应的.rrd文件。另一个是 CGI 前端,只有当浏览器访问时才被拉起,它的工作是从.rrd里取出指定时间段的数据,丢给绘图库算出一张 PNG。两者之间没有直接通信,唯一的耦合点就是那个数据目录。

RRD 这个存储格式老派但极其能打。它的核心设计是"固定大小的环形数据库":文件创建时就把所有需要保留的时间粒度全部预留好,写入时只做覆盖,不做追加。好处是文件大小恒定、不会随着时间膨胀、查询任意历史区间都是常数级开销。代价是建库那一刻的参数就锁死了,比如你按 5 分钟粒度建了库,之后想把step改成 60 秒,旧文件会直接报错拒绝写入,只能删库重来。这个特性在容器化场景里尤其需要注意,因为很多人会把数据目录挂载到宿主机,改配置试错时忘了清理旧的 RRD,然后对着日志里一堆"expected X, got Y"发呆。

一个目标对应一个 RRD 文件,文件名由目标在配置里的路径加上探针名拼成,比如路径是LocalMachine、探针是FPing,那文件大概就叫LocalMachine~FPing.rrd。这意味着你调整配置里的目标层级结构时,实际上是在改变文件的命名空间——把一个目标从+China挪到+Overseas下面,它会从零开始积攒新数据,旧数据还在磁盘上但图表里看不到了。这个坑我在第一次重构分组的时候踩得很实。

1.2 两张图的读法:丢包色块与中位数曲线

默认每个目标会出两张图,上下排列。上面那张是丢包图,下面那张是延迟图。这两张图的编码方式不一样,得分开理解,否则很容易误判。

丢包图用色块表示。没有丢包时是一条冷色调的基准带,随着丢包比例上升,颜色会往暖色方向走,从浅到深依次过渡;如果某一格完全没有收到回应(连探测都失败了),会用另一种颜色单独标出;白色或灰色通常代表这段时间没有数据——可能是探测进程当时没跑,也可能是容器重启导致的空档。所以看到图上出现灰色断层,别急着怀疑网络,先去看容器那段时间有没有重启过。这个判断顺序能省掉大量无谓的排查。

延迟图信息更密。图里的灰色或浅色阴影区域表示这一组探测包的分布范围,深色的那条线是中位数。有些配置下阴影还会分成几层,表示四分位区间。读这张图的诀窍是看形状不看绝对值:一条平直的中位线突然开始周期性起伏,往往说明链路上有个队列在定时堆积;中位线稳定但阴影区域突然变宽,说明抖动变大而平均延迟没变,这种情况对实时业务的影响比平均延迟上升更严重。我一般会先看阴影宽度再看中位线高度,因为平均值的欺骗性太强了。

提示:判断"是不是我这边的问题",最省钱的办法是在同一台 SmokePing 上同时监控网关、出口路由和远端目标三个层级。如果三层同时抖,问题在本地;如果只有远端抖,问题在中间链路。这个三层对照的配置方式后面会具体写。

1.3 它不擅长的事,以及什么时候该换方案

把边界说清楚,能避免大量无效投入。SmokePing 的强项是长期、低频、多目标的延迟趋势记录,它不擅长做秒级实时告警,也不擅长做应用层的事务追踪。它的最小粒度受step限制,默认五分钟一轮,就算你把step压到 60 秒,一轮探测也只有pings个包(默认 20 个),它能告诉你"这一分钟里有一半的包丢了",但没法告诉你"这一分钟里第 37 秒发生了什么"。

另外它的告警是"延迟型"而不是"事件型"。告警规则基于历史数据序列的模式匹配,不是基于某个瞬时事件。想做到"目标一不可达立刻响铃",它的响应延迟至少是一个step加上告警检查周期。真要低延迟告警,得让别的工具去做,SmokePing 留在旁边负责提供证据链——出事以后翻出那张图,比一百行日志都有说服力。我一般的分工是:实时告警交给专门的探测系统,SmokePing 负责长期取证和趋势分析,两者监控同一批目标,互不替代。

2. 上 Docker 之前得想清楚的三个边界:挂载、时区、内核权限

容器化这件事本身没有难度,真正的麻烦都在容器和宿主机交界的那一层。SmokePing 尤其明显,因为它同时需要"写数据""发原始包""被 Web 服务器调用"这三件性质完全不同的事。我见过不少人第一次跑起来顺风顺水,第二天重启容器之后历史曲线全空,或者页面能打开但图上一条线都没有,这些问题的根子基本都在下面三个边界上。

2.1 哪些目录必须落到宿主机

必须挂载的目录只有两类,但每一类下面还有细节。第一类是配置目录,SmokePing 的主配置文件通常叫Config,里面用@include引入一堆分片文件,改配置只需要改宿主机上的文件然后让容器重新加载,不用进容器操作。第二类是数据目录,也就是 RRD 文件存放的地方,这个目录丢了就等于所有历史归零。

还有第三类容易被忽略:图片缓存目录。SmokePing 的 CGI 为了减少重复渲染,会把生成的 PNG 缓存到磁盘上,下次同样的请求直接返回文件。这个目录如果放在容器可写层里,容器一重建缓存就没了,问题不大;但如果它和 RRD 目录共用同一个挂载点,权限模型就会打架——写入 RRD 的是探测进程的用户,写缓存的是 Web 服务器的用户,两个用户对同一个目录都需要写权限。

我的处理办法是给数据区和缓存区分别建目录,然后把它们的属组设成共同的一个组,目录权限用2775(那一位2是 setgid,作用是让目录里新建的文件自动继承父目录的属组)。这样探测进程和 Web 用户都能写,也不会因为某次容器重建后文件属主变成 root 而突然画不出图。这个细节在文档里基本不会提,但它是"图表莫名变空白"这个症状最常见的成因之一。

mkdir -p /srv/smokeping/{config,data,cache} chown -R 1000:1000 /srv/smokeping chmod -R 2775 /srv/smokeping/data /srv/smokeping/cache

上面这个1000:1000只是示例,实际要填容器内探测进程和 Web 用户对应的 uid/gid。做法很简单:容器跑起来之后docker exec进去id一下,把两个用户的信息看清楚,再回到宿主机对齐。

2.2 时区不对,图的时间轴会整体平移

这个坑几乎每个自己打镜像的人都会踩一次。容器默认是 UTC,SmokePing 按本地时间生成时间标签和图表刻度,如果时区没设,你在页面上看到的所有时间都会比你认知里的时间差几个小时。可怕的是它不会报错,图照常画,只是"昨天下午三点"的那次抖动,在图上显示成了上午十一点,你去查日志会发现对不上,然后开始怀疑人生。

正确的做法是双管齐下:环境变量TZ设成目标时区,同时在容器启动时把/etc/localtime和/etc/timezone一起写好。只设环境变量在某些基础镜像里不够,因为底层的 C 库在解析时间时读的是/etc/localtime。RRD 里存的是时间戳,本身不带时区信息,所以时区改错不会污染已存数据,改对之后历史曲线的时间轴会立刻正确,这一点算是好消息。

environment: - TZ=Asia/Shanghai

配合启动脚本里的这两行,基本就稳了:

ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo "$TZ" > /etc/timezone

2.3 ICMP 权限:容器里发原始包的两条路

SmokePing 最常用的探针是fping,它默认走原始套接字发 ICMP 包。这里有个容易误判的地方:标准 Docker 容器的默认能力集里是包含NET_RAW的,所以用默认参数跑通常没有问题。但一旦你在编排文件里写了cap_drop: [ALL],或者在更严格的运行环境里跑,fping就会静默失败——注意是"静默",探测结果全是丢包,图表上一条紫色横线,看起来像目标全挂了,实际上是权限不够根本发不出去包。

两条路可以解决。第一条是显式保留能力:给容器加NET_RAW。第二条是让fping走非特权 ICMP,也就是用数据报套接字而不是原始套接字,这需要在容器内放开net.ipv4.ping_group_range,把允许发包的 gid 范围设宽:

cap_add: - NET_RAW # 或者在非特权场景下: sysctls: net.ipv4.ping_group_range: "0 2147483647"

我给的建议是优先用第一种,简单直接;如果是 rootless 容器或者受限的集群环境,那第二条路更干净。判断到底走没走通,最快的验证是在容器里手动跑一次:

docker exec -it smokeping fping -c 3 1.1.1.1

如果这里能出结果,说明权限没问题,问题在别处;如果这里报socket: Operation not permitted或者直接沉默,那就是权限问题,别再去翻配置文件了。

3. 从空目录跑通第一张图:镜像、compose 与验证清单

我不太建议直接用现成的第三方镜像然后黑盒使用,理由不是不信任,而是 SmokePing 的配置文件改动频率很高,你需要清楚地知道配置在镜像里的哪个位置、CGI 脚本映射到哪个 URL、Apache 的站点配置是怎么挂进去的。自己写一份 Dockerfile,几十行,把所有东西摆在明面上,后面调优的时候省心得多。

下面这份骨架基于 Debian 系的包管理,思路是把发行版已经打包好的 SmokePing、fping、Apache 装进去,再用一个入口脚本把守护进程和 Web 服务一起拉起来。包名和路径以你实际使用的基础镜像为准,第一次构建完务必进容器手动验证一遍,不要直接上生产。

FROM debian:bookworm-slim ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y --no-install-recommends \ smokeping \ fping \ apache2 \ tzdata \ ca-certificates \ curl \ && rm -rf /var/lib/apt/lists/* RUN a2enmod cgid \ && a2enconf smokeping \ && a2dissite 000-default COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh EXPOSE 80 ENTRYPOINT ["/entrypoint.sh"]

入口脚本负责三件事:设时区、修目录权限、同时启动探测守护进程和 Apache。

#!/bin/bash set -e : "${TZ:=Asia/Shanghai}" ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo "$TZ" > /etc/timezone mkdir -p /data /var/cache/smokeping/images /var/run/smokeping chown -R smokeping:www-data /data /var/cache/smokeping/data /var/run/smokeping 2>/dev/null || true chmod -R 2775 /data /var/cache/smokeping/images 2>/dev/null || true smokeping --config=/etc/smokeping/config --nodaemon & exec apachectl -D FOREGROUND

3.1 编排文件:把配置、数据、缓存三个卷分清楚

services: smokeping: build: . image: smokeping:local container_name: smokeping restart: unless-stopped ports: - "8080:80" environment: - TZ=Asia/Shanghai volumes: - ./config:/etc/smokeping/config.d - ./data:/data - ./cache:/var/cache/smokeping/images cap_add: - NET_RAW

这里有个设计选择值得说:我把宿主机目录挂到config.d而不是整个/etc/smokeping。原因是主配置文件Config里通常是一串@include,只做编排不做内容,把它留在镜像里,把真正要改的分片文件(General、Probes、Targets、Alerts)放宿主机。这样升级镜像时不会因为挂载覆盖导致主配置丢失,改动也只集中在少数几个文件上。代价是如果你要改@include的结构就得重新构建镜像,但这个操作频率极低,可以接受。

数据目录我建议不要和配置目录放在同一个父目录下再整体挂载。分开挂载的好处是备份策略可以差异化:配置需要版本控制,数据需要定期快照,缓存可以直接丢弃。

3.2 第一次启动后的五项验证

容器起来之后不要急着开浏览器,按下面这个顺序验一遍,能提前发现八成问题。

第一项,看守护进程有没有真的在跑。docker logs里如果没有明显报错,再docker exec进去ps aux | grep smokeping确认进程存在。第二项,看数据目录里有没有开始生成.rrd文件,默认step是 300 秒,意味着至少要等一个探测周期结束才会有第一个数据点,刚启动那几分钟目录是空的很正常,别慌。第三项,容器内手动跑一次 fping 验证权限。第四项,访问 Web 首页,确认 CGI 能执行、菜单能渲染。第五项,等两个周期之后回来看图,此时应该能看到至少一根线的雏形。

注意:第一次部署时如果只是打开首页看到菜单,不要就此认为成功。菜单是静态渲染的,即使数据库全空也能显示。真正的验收标准是"某个目标下点进去能看到一张有数据的图"。

3.3 反向代理与访问控制别留到最后

SmokePing 本身不带认证,CGI 页面裸奔在公网上等于把内网拓扑和目标清单拱手送人——你的 Targets 配置里往往写着大量内网地址和业务节点名称,这些信息本身就是资产。放在反向代理后面加一层基础认证是最低要求,同时建议在代理层再做一次 IP 白名单。

代理配置里有两个点容易出问题。一是cgiurl这个配置项必须和实际的对外访问路径严格一致,否则前端生成的链接会 404,页面能打开但点任何目标都跳不过去。二是路径前缀,如果你挂在/smokeping/而不是根路径下,Apache 侧的别名映射和配置里的imgurl都得同步改,改漏一个就会出现"图裂了但文字正常"的诡异现象。

4. 三份配置文件的写法:General、Probes、Targets 逐项拆解

配置是 SmokePing 里最需要耐心的部分,好在它的语法很朴素:*** 段名 ***划分大段,@include引入分片,+的数量表示层级,key = value写参数。真正容易出错的是"哪些参数属于哪个段"和"层级继承怎么生效",下面按实际情况拆。

4.1 General 段:路径变量与全局行为

General 段里除了联系人和邮件设置,最关键的是几个路径变量,它们决定了数据写到哪里、缓存放哪里、绘图模板从哪读。

*** General *** owner = 运维组 contact = ops@example.internal mailhost = localhost cgiurl = http://monitor.example.internal:8080/smokeping.cgi charset = utf-8 datadir = /data imgcache = /var/cache/smokeping/images imgurl = /smokeping/images piddir = /var/run/smokeping

charset这一行看起来不起眼,但如果你的菜单名或者目标标题里写了中文,不设它会出乱码。datadir一定要指向你挂载的那个卷,这是数据持久化的唯一入口。imgurl是浏览器访问缓存图片时用的 URL 前缀,它必须和 Apache 里的别名对应,不对应就是图裂。

4.2 Probes 段:探针定义与参数含义

探针段定义"用什么方式探测",每个探针有个名字,Targets 里通过probe = 名字引用。

*** Probes *** + FPing binary = /usr/bin/fping packetsize = 56 step = 300 pings = 20 timeout = 1.5

pings和step是最需要想清楚的两个数。pings = 20意味着每轮发 20 个包,step = 300意味着每 5 分钟一轮,所以一格里其实包含了 20 次测量,图上画的中位数是这 20 次的统计结果而不是单次值。丢包率也是按"20 个里丢几个"算出来的,所以理论上最小可分辨的丢包粒度是 5%。如果你需要识别 1% 级别的偶发丢包,得把pings加大或者把step缩短,这两个动作都会成比例增加发包量,后面容量那一节会算这笔账。

packetsize = 56是 ICMP 载荷大小,加上 8 字节 ICMP 头和 20 字节 IP 头正好凑成常见的 84 字节报文。如果你的场景关心大包路径的 MTU 问题,可以另起一个探针把packetsize调到 1400 左右,做大小包对比,这个手法在排查分片黑洞时非常好用。

还有两个参数不常改但值得知道:offset控制探测周期在时间轴上的相位,多实例部署时错开可以避免所有探针同时发包;mininterval限制同一探针对多个目标的发包间隔,防止一瞬间打出大量包。

4.3 Targets 段:层级结构决定了图表的组织方式

Targets 段是日常改动最多的地方。它的语法核心就一条:+的数量表示层级深度,一级菜单一个加号,二级两个,以此类推。

*** Targets *** probe = FPing menu = 总览 title = 网络延迟总览 + 内网 menu = 内网 title = 内网节点 ++ 核心网关 menu = 核心网关 title = 核心网关延迟 host = 10.0.0.1 alerts = lossdetect, rttdetect ++ 出口链路 menu = 出口链路 title = 出口链路延迟 host = 10.0.0.254 + 外部 menu = 外部 title = 外部目标 ++ 公共DNS menu = 公共DNS title = 公共DNS延迟 host = 1.1.1.1 probe = FPing

这里有两点经验。第一,属性是向后继承的。顶层的probe = FPing会往下传,所以子节点不写probe也用 FPing;同理alerts写在父节点上,整个子树都会继承。这个特性很好用,但也容易造成"我在子节点上写了覆盖,结果发现没生效"的困惑,原因是覆盖位置写错了层级。第二,menu是菜单上显示的文字,title是页面上的大标题,两个都可以写中文,但都要注意charset。host才是真正被探测的东西,它可以是 IP 也可以是域名;如果写域名,每一轮都要做一次解析,对 DNS 有额外压力,长期监控建议直接用 IP,把域名解析这件事单独用 DNS 探针去测。

提示:配置里不要给同一个目标起两个不同的路径名。有两个路径就等于两套 RRD 文件,等于两倍的探测发包量,而且两张图永远对不上。重构分组之前先想清楚,改完之后旧数据不会跟着迁移。

4.4 Alerts 段:pattern 的匹配方向容易搞反

告警段的语法是整份配置里最反直觉的。它用一套类似正则的表达式去匹配历史数据序列,匹配方向是从最新一格往回退。

*** Alerts *** to = ops@example.internal from = smokeping@example.internal + lossdetect type = loss pattern = >0%,*12*,>0%,*12*,>0% comment = 12 个周期内出现三次丢包 + rttdetect type = rtt pattern = >200,*6*,>200 comment = 连续两个周期中位数延迟超过 200ms

pattern里*12*的含义是"允许往回跳过最多 12 个数据点",而不是"第 12 个点"。所以上面第一行的意思是:最近一格丢包率大于 0,往回最多 12 格找到第二个大于 0 的,再往回最多 12 格找到第三个大于 0 的,三个都满足就触发。这个语义如果不搞清楚,写出来的规则要么永远不响,要么一响就刷屏。

type常见的有loss(按丢包率匹配)、rtt(按延迟值匹配)、matcher(结合两者做更复杂的模式)、baseline(和同期历史基线比较)。新上手建议只用loss和rtt这两种,逻辑简单、行为可预期。baseline那种自适应告警虽然聪明,但在数据量少的头几天会给出非常诡异的判断。

5. 探针选型:除了 fping 还能测什么

默认大家都用 ICMP 探针,因为它最通用、开销最小。但网络问题的形态很多,只用 ICMP 相当于只用体温计看病。SmokePing 内置了若干探针,覆盖的层次不同,混着用能让故障定位快很多。

5.1 从 ICMP 到应用层:四类探针的适用边界

探针测的是什么典型用途需要注意
FPingICMP 往返延迟与丢包链路质量基线,几乎所有目标都该配部分主机限速 ICMP,会误报丢包
DNS解析耗时与结果排查解析慢、解析结果漂移需要指定查询名和解析服务器
CurlHTTP 请求整体耗时与状态码应用层可用性、TLS 握手耗时频率别太高,会刷业务日志
EchoPing自己实现协议交互内网私有协议探测需要自己写模块,成本较高

一个很实用的组合是:对同一个业务入口同时配 ICMP 和 Curl 两个探针。如果 ICMP 正常但 Curl 曲线抖,问题在应用层或者 TLS 握手;如果两个一起抖,问题在网络路径。这种二选一的判断在有图的情况下是几秒钟的事,没图的时候可能要查半天。

DNS 探针我要单独说一句。它测的是"从 SmokePing 所在机器去问指定解析服务器,拿到答案要多久",不是"用户解析要多久"。如果你要做的是用户体验视角的解析监控,得把 SmokePing 部署在接近用户的位置,或者用多探针分布式部署,否则测出来的是机房视角,参考价值有限。

5.2 采样密度与目标数量的容量账

这部分是唯一需要动手算的。发包速率等于目标数乘以每轮包数除以周期秒数:

总发包速率 = 目标数量 × pings ÷ step

按默认值算,100 个目标、pings = 20、step = 300,得到大约每秒 6.7 个 ICMP 包。这个量级对任何正经网络设备都是毛毛雨,可以放心堆目标。但如果为了抓偶发丢包把pings抬到 100,或者把step压到 60,同样的目标数会变成每秒 166 个包,这时候就要考虑探测机自身的网卡中断和 CPU 开销了,而且在低端交换机上可能触发 ICMP 限速,反而制造假丢包。

存储这边反而不用太担心。每个目标一个 RRD 文件,大小主要由保留的时间粒度和精度决定,通常几十 KB 到一两百 KB 之间。1000 个目标大概也就是一百多 MB 的量级,随便一块盘都装得下。真正的开销在绘图:每打开一次页面,CGI 就要读取若干个 RRD 并渲染 PNG,目标多了之后首次访问会明显变慢,这也是为什么缓存目录必须可写——缓存命中率直接决定了这个面板好不好用。

5.3 一个目标该配几个探针

我的原则是:核心节点配两个,边缘节点配一个。核心节点用 ICMP 打底看链路,再加一个应用层探针看业务;边缘节点只配 ICMP,够用就行。配太多探针的副作用不只是资源消耗,还有图表的可读性——一个目标下面挂四五张图,排查的时候反而不知道该看哪张。

另外一个细节:如果某个目标本身会限制 ICMP 频率(不少云厂商的网关都这么干),你会看到 ICMP 图上有规律性的丢包而业务完全正常。这种时候不要急着去调网络,先在配置里把这个目标的pings降下来,或者干脆换成 TCP 类探针,避免被自己的监控数据误导。

6. 图表空白的排查链路:按顺序过六个检查点

"页面能打开但是图上没线"是 SmokePing 最高频的问题,没有之一。这个症状背后可能有十几种原因,但它们的排查顺序是固定的,从外到内一层层收窄,比随机尝试快得多。下面这个链路是我自己遇到问题时会走的顺序,基本能在十分钟内定位。

6.1 第一层:探测到底有没有在发

先确认最外层的现实:容器能不能把包发出去。进容器手动跑 fping,这是唯一可靠的判断依据。

docker exec -it smokeping fping -c 3 10.0.0.1 docker exec -it smokeping fping -c 3 1.1.1.1

如果内网通、外网不通,那是网络出口或者 DNS 的问题,跟 SmokePing 无关。如果两个都不通但容器能 ping 宿主,那就是权限问题,看前面NET_RAW那一节。这一步没通过,后面所有检查都是白费。

6.2 第二层:数据目录里有没有文件

ls -lh ./data/*.rrd | head

目录为空说明探测进程根本没写数据。这时候分两种情况:如果容器刚启动不到一个step,正常,再等等;如果跑了一小时还是空的,去看docker logs,十有八九是配置语法错误导致守护进程启动即退出。SmokePing 的配置错误日志有时候很隐晦,习惯做法是进容器手动跑一次前台模式,把错误直接打在屏幕上:

smokeping --config=/etc/smokeping/config --nodaemon --debug

有文件但是图表还是空,那就进第三层。

6.3 第三层:RRD 文件是不是在正常更新

docker exec -it smokeping rrdtool lastupdate /data/内网/核心网关~FPing.rrd

这条命令会输出最后一次写入的时间戳和数值。如果时间戳停在一小时前,说明写入侧卡住了;如果时间戳是新的但数值全是NaN或者-1,说明探测发了但没有回包,回到第一层重新查。这一步能明确区分"写不进去"和"写进去了但值是空的",这两个问题的处理方式完全不同。

6.4 第四层:CGI 能不能读到数据

用命令行模拟一次 CGI 请求,看返回的是 PNG 还是错误信息:

curl -sI "http://localhost:8080/smokeping.cgi?target=内网.核心网关&displaymode=normal"

如果返回的是 500 而不是image/png,去看 Apache 的错误日志。最常见的两条是"权限不足读不了 RRD"和"找不到绘图库"。前者回到 2.1 节检查属组和 setgid;后者说明镜像里少装了绘图相关依赖。

6.5 第五层:时间范围选错了

这个说起来有点蠢但确实常见。SmokePing 的图表有多种时间范围视图,从几小时到一年。如果你刚部署两小时,切到"一年"视图,图上当然是空的——因为数据根本没那么长。默认视图设置不当会让人误以为没数据。检查一下 Presentation 段里配置的默认展示范围,新部署时先看小时级视图。

6.6 第六层:缓存里躺着的是旧的坏图

前面五层都通了,浏览器还是显示空白或者旧图,那就清缓存。因为缓存文件名通常由目标名和参数拼成,配置改了但缓存键没变,你会一直看到那张生成失败的图。

rm -rf ./cache/*

清完之后刷新页面,如果图出来了,说明问题已经解决,只是被缓存挡住了。这也是为什么我建议在排查期间临时把缓存目录挂成可随时清空的独立卷——省得每次都要进容器操作。

7. 分布式探针与长期维护:slave 架构、备份与升级

单实例跑到几十个目标很轻松,但一旦需要"从多个地理位置同时测同一个目标",就得用 master/slave 架构。这个架构的原理很朴素:master 负责配置和绘图,slave 只负责按 master 下发的指令干活,把探测结果回传给 master 写库。

7.1 master 与 slave 的配置对齐要点

master 侧要开一个 slaves 段,声明有哪些 slave、各自的显示名和颜色,并且指定一个共享密钥文件:

*** Slaves *** secrets = /etc/smokeping/smokeping_secrets + 机房A display_name = 机房A color = 0060a0

密钥文件里一行一个,格式是"slave 名 + 密钥"。slave 侧的 General 段里要填master_url和shared_secret,然后用--master-url参数启动。目标节点上用slaves = 机房A指定由哪个 slave 探测。

这里最容易出问题的是配置同步。slave 启动时会从 master 拉取 Targets 配置,但它只认自己的名字,如果一个目标没指定slaves或者指定的名字拼错了,这个目标就不会被任何 slave 探测,而 master 自己的本地探针也可能不接这个活,最终结果就是图一直是空的。排查方法是看 master 日志里有没有"slave 请求配置"的记录,以及 slave 侧有没有成功建立连接。

网络方面,master 和 slave 之间是 slave 主动连 master 的 HTTP 接口,所以只要 slave 能出网访问 master 就行,不需要在 master 侧做端口映射。这个设计对内网穿透很友好,但也意味着 slave 的密钥一旦泄露,任何人都能伪装成 slave 往你的数据库里灌假数据,密钥文件权限记得收紧。

7.2 备份与迁移:备份什么、怎么验

备份清单只有三样:配置目录、数据目录、镜像的构建文件。缓存不用备。数据目录的备份有个技巧——不要在探测写入的同时直接拷文件,RRD 文件在写入瞬间是不一致的,拷出来的副本可能损坏。稳妥做法是先停容器再拷,或者用文件系统快照。如果实在不能停,就用rrdtool dump导出成 XML 再压缩,代价是体积会大不少。恢复的时候用rrdtool restore导回去,比直接拷文件更安全。

迁移到新机器时有个坑:如果新机器的路径、用户名、uid 和旧机器不一致,挂载上去之后文件属主会变成一串数字,探测进程写不进去。迁移完第一件事是chown -R对齐属主,然后跑一次写入验证,别等第二天才发现数据断了。

7.3 版本停滞与升级策略

SmokePing 这类工具更新频率不高,很多人装完就再也没动过。这在小规模场景下没什么问题,但长期不升级会带来两个麻烦:一是基础镜像里的依赖逐渐过时,出安全问题没法快速修补;二是某天想用新特性的时候发现配置格式已经变了,改动成本陡增。

我的策略是把它当成一个"半年动一次"的服务。每次动的时候按这个顺序:先在另一台机器上用同样的配置起一个新实例,确认能正常出图并且历史数据能挂载进去,再切换流量。因为配置和数据都在宿主机卷上,容器本身是无状态的,所以这种"新旧并行、切换卷"的方式几乎零风险。唯一要小心的是不要让两个实例同时往同一个数据目录写——RRD 不是并发安全的,两个进程同时更新一个文件会直接损坏它。切换前先停旧的,等它把当前周期写完,再起新的。

注意:升级或者更换基础镜像之后,第一件事是进容器fping --version和rrdtool --version各看一眼。曾经出现过新镜像里 fping 参数变化导致packetsize不生效的情况,图上延迟突然整体抬高,查了半天才发现是探测报文变大了。

最后分享一个我自己用了很久的小习惯:在 Targets 里专门留一个指向本机网关的"基准目标",简单、稳定、几乎不会出问题。当别的目标曲线出现异常时,先看这个基准目标——它平你就往远端查,它抖你就往本地查。这个小小的参照物,比任何复杂的排查流程图都管用。