技术实践 ·

同一个 Go 客户端,怎样同时运行在 Linux 和 OpenWrt 上?Fleetd 的部署取舍

同一个 Go 客户端怎样运行在 Linux 和 OpenWrt 上?结合 Fleetd v0.3.0 讲清 amd64/arm64、systemd/procd、状态持久化、固件升级恢复与监控能力差异。

把客户端放进一台云服务器,和放进家里的软路由,看起来都是复制一个程序、写一份配置,再让它运行起来。

真正要长期使用时,问题才会逐渐显出来:机器重启以后谁来启动程序?网络还没准备好怎么办?日志去哪里找?保存过的设备身份和命令记录,会不会跟着一次重启消失?

Fleetd 要同时管理 Linux 服务器和 OpenWrt、iStoreOS 软路由。这一篇就聊它怎样处理这些差异。前面已经讲过设备接入和浏览器命令签名,这里把视角移到机器本身:怎样让客户端成为一个能持续运行、可以排查问题的系统服务。

以下按 Fleetd v0.3.0 的实现来讲。当前安装入口支持 amd64、arm64,以及 systemd 或 OpenWrt 的 procd,不代表所有使用 Linux 内核的设备都能直接安装。

共用一套源码,按架构生成程序

OpenWrt 本身也是 Linux。对 Fleetd 来说,服务器和路由器之间,有不少可以直接复用的部分:网络连接、身份验证、命令授权,以及从内核接口读取基本指标。

因此,客户端没有分成“服务器版”和“路由器版”两套代码。构建时指定 Linux 目标,再分别生成 amd64 和 arm64 的程序。

这里的“同一个客户端”指同一套源码和协议。amd64 的可执行文件不能直接拿到 arm64 上运行,安装时仍要选择匹配的产物。

机器报告的架构 安装器选择的产物
x86_64、amd64 amd64
aarch64、arm64 arm64
其他值 当前安装器拒绝继续

这也是介绍 OpenWrt 支持时需要说清楚的地方。OpenWrt 覆盖的硬件很多,包括 MIPS 和 32 位 ARM;Fleetd 当前这条安装路径没有覆盖它们。

Go 支持交叉编译,并不自动等于项目支持所有目标架构。要增加一种架构,还要检查依赖、构建产物、安装识别、更新流程和真实设备上的行为。不能只把构建参数改一下,就宣布兼容。

安装前可以用 uname -m 查看机器报告的架构,但这也只是第一步。内核、用户空间和可用的系统功能仍然会影响运行。

单文件部署,减少的是运行环境准备

Fleetd 的客户端构建设置了 CGO_ENABLED=0。这样可以减少对目标机器上 C 动态链接环境的依赖,不必给每台设备安装 Go 编译器,也不用先准备 Node.js 或 Python 运行环境来启动客户端。

对软路由来说,这一点很实用。它的存储、软件包和系统组件往往比普通服务器精简。多安装一套运行环境,意味着更多下载、依赖和维护工作。

但单个可执行文件不等于没有运行条件。网络要可达,HTTPS 需要有效的证书信任环境,配置目录要能写,内核也要提供程序依赖的功能。

有些采集项还会调用系统工具。例如容器信息依赖 docker,WireGuard 信息依赖 wg。程序本体不需要安装一套语言运行时,和所有功能都不需要外部工具,是两回事。

因此,我更愿意把这个选择理解为:先减少部署的基础负担,再明确哪些能力有额外条件。

安装器先判断环境,再接入系统服务

当前安装器大致按这个顺序工作:

  1. 检查是否以 root 执行,识别 CPU 架构。
  2. 判断机器使用 systemd,还是具备 OpenWrt 的 procd 环境。
  3. 从自己的 Fleetd 服务端下载对应架构的客户端。
  4. 运行一次 version,确认下载的程序至少能在这台机器上启动。
  5. 创建或保留配置,安装程序,注册并启动服务。
  6. 输出设备指纹,等待管理后台核对并批准。

其中第四步很有必要。下载成功只说明拿到了文件,version 能运行,才进一步说明这个可执行文件可以在当前环境里启动。

不过,这仍然不是完整验收。它没有验证长期连接、采集、权限、开机自启或固件升级后的恢复。短暂运行一次版本命令,不能替代这些检查。

下载工具方面,安装器优先用 curl,没有时使用 wget。这对精简环境比较方便,但仍要确认系统实际提供的工具支持相应 HTTPS 请求。这里不统一给出某一条包管理器安装命令,因为 OpenWrt 版本和固件集成的软件包可能不同。

还有一个容易误会的信任关系:客户端从自己的 Fleetd 服务端下载。服务端从已上传并通过签名验证的发布包中取出程序;初次安装脚本本身依赖你信任的服务端和 HTTPS。它没有在目标机器上独立验签下载文件。后续由客户端验证更新包,是另一条流程,留到更新篇再展开。

systemd 和 procd,接管的是同一个进程

程序安装好以后,Fleetd 把进程交给系统已有的服务管理器。两边的主要差异如下:

项目 systemd 机器 OpenWrt / procd 机器
程序位置 /usr/local/bin/fleetd-agent /usr/bin/fleetd-agent
配置位置 /etc/fleetd/agent.yaml /etc/fleetd/agent.yaml
服务定义 /etc/systemd/system/fleetd-agent.service /etc/init.d/fleetd-agent
新装时的状态目录 /var/lib/fleetd /etc/fleetd
日志入口 systemd journal OpenWrt 系统日志

systemd 服务通过 ExecStart 指向客户端,设置 Restart=always 和五秒重启间隔,同时配置 network-online.target 的启动顺序。

OpenWrt 则生成使用 USE_PROCD=1 的启动文件,设置开机启动顺序、进程命令和 respawn 参数,把标准输出、标准错误交给系统日志。

两个分支最终运行的都是同一个客户端入口,读取同一类 YAML 配置。差异主要留在安装和服务接入层,不需要在连接协议里到处判断当前是哪种发行版。

这里还要分清两种恢复:进程退出,由服务管理器处理;连接中断,由客户端自己的重连逻辑处理。

如果服务端暂时不可达,客户端进程仍然可以活着,并按退避策略继续尝试。没有必要每次网络断开都重启整个服务。反过来,进程已经退出时,内部重连代码也无从运行,就需要 systemd 或 procd 接手。

开机顺序同样不能保证远端一定可用。systemd 的网络目标和 OpenWrt 较晚的启动顺序,都无法确保此时 DNS、上游链路和服务端已经准备完成。客户端自己能处理连接失败,才不会把“启动得够晚”当成唯一保障。

在 OpenWrt 上,状态存在哪里比文件名更重要

这一处差异会直接影响上一章讲过的防重放。

普通服务器上,/var/lib/fleetd 通常适合放客户端运行状态。但在常见 OpenWrt 布局中,/var 依赖内存中的临时存储。程序重启和整台设备重启,对这些文件的影响并不一样。

Fleetd 的安装器因此在 OpenWrt 分支把 state_dir 指向 /etc/fleetd。这里需要区分三类文件:

文件 作用
agent.yaml 连接地址、受信公钥、权限和状态目录等配置
agent.key 客户端自身的身份密钥,决定设备身份
seen.json 已接受请求的 ID 记录,用于防重放

前两项默认就在 /etc/fleetd。seen.json 则跟随 state_dir:systemd 新装时放在 /var/lib/fleetd,OpenWrt 新装时放在 /etc/fleetd。

身份密钥丢失后重新生成,后台看到的就会是另一种设备身份。防重放记录丢失,也不能再声称原来那些尚在有效期内的请求一定会被识别为重复。

所以,“文件写到磁盘了”还不够,要进一步问:这个路径在当前系统上到底对应持久存储,还是重启即清空的内存盘?如果使用自定义固件、只读布局或特殊挂载,也应按实际挂载情况确认。

把状态放到闪存还有写入成本。当前实现接受新的授权请求时会更新防重放记录,并不是每十秒上报一次指标就把指标写进这个文件。两种写入不能混为一谈。若以后发展成高频远程任务执行工具,就要重新评估持久化频率和设备闪存条件;目前不能凭空给出寿命或吞吐保证。

保留配置,不等于固件升级后服务自动恢复

OpenWrt 安装分支还会把 /etc/fleetd/ 加入 /etc/sysupgrade.conf,让这部分文件进入配置保留清单。

这有助于在支持并启用保留配置的升级流程中保住身份和配置。不过,清单里保留的是这个目录,并没有同时保证 /usr/bin/fleetd-agent 和 /etc/init.d/fleetd-agent 一定随固件升级留下。

因此,升级固件以后可能出现一种状态:身份和配置还在,但程序或启动文件需要重新安装。恢复出厂、明确不保留配置的刷机方式,也不在这个保留机制的保证范围里。

这里不能把“写进 sysupgrade 清单”写成“升级以后肯定自动上线”。真正需要确认的是:

  • 保留的配置和身份是否还在。
  • 程序和启动服务是否存在,是否仍能运行。
  • 客户端恢复后,设备指纹是否与升级前一致。
  • 如果上报暂时没有回来,是服务未启动、网络未通,还是需要重新批准。

这些是升级前后应该做的检查,不是本文已经完成的一轮刷机测试。

再次运行当前安装器,会保留已经存在的 agent.yaml,更新程序并重新写入服务定义。这个行为适合恢复安装,但也有另一面:你重新传入 --permission、--server 或 --signer,并不会自动覆盖已有配置。

同理,旧配置如果把 OpenWrt 的状态目录写错了,重跑安装器也不会顺便替你纠正。需要检查配置本身,再重启客户端使修改生效。

能运行同一个客户端,不代表监控能力完全相同

Fleetd 的基础指标主要来自 Linux 的 /proc:CPU、内存、负载、运行时间和网卡累计流量等。这部分接口相对统一,所以可以在服务器和软路由之间复用。

网络速率和 CPU 使用率需要前后两次采样。刚启动时只有第一份计数,不能把这时候的速率值当成一段完整采样区间的结果。网卡重建导致计数器回退时,当前实现也会跳过那次速率计算,避免直接算出不合理的差值。

扩展采集则有明显的环境差异:

采集项 当前依赖
失败服务 systemctl list-units --state=failed
容器状态 docker ps -a
WireGuard 对端 wg show all dump

客户端能由 procd 托管,不意味着已经实现了 procd 服务健康状态采集。当前失败服务的采集路径仍然使用 systemd 命令。

当外部工具不存在或调用失败时,相应采集会返回无数据,基础监控仍可继续。这个行为让精简环境不必为缺少 Docker 等组件而完全退出,但也留下一个必须正确理解的结果:没有采集到异常,不等于已经证明所有服务健康。

当前实现也没有为每一种缺失原因都提供完整的能力状态说明。如果页面没有相关数据,排查时要区分功能不适用、工具不存在、调用失败和确实没有对象,而不能一律理解成零异常。

基础指标按十秒节奏采集上报,磁盘、服务、容器和 WireGuard 这组较慢的信息每六轮采集一次。这是调度设置,不是严格的实时承诺。外部命令耗时、网络异常和机器负载都可能影响实际间隔。

装好以后,按运行链路验收

对一个常驻客户端,我更关心下面这条链路是否完整:程序能启动,服务管理器能接管,网络恢复后能连上,设备重启以后身份和配置仍然正确。

在 systemd 机器上,先看服务状态和近期日志:

systemctl status fleetd-agent
journalctl -u fleetd-agent -n 50 --no-pager

OpenWrt 上可以从系统日志查起:

logread -e fleetd

这几条是诊断命令,不会替你重启设备。需要重启客户端使配置生效时,systemd 使用 systemctl restart fleetd-agent,OpenWrt 使用 /etc/init.d/fleetd-agent restart。

随后在后台确认设备已批准、数据时间在更新,观察 CPU 和网卡是否经过了后续采样。如果设备只需要监控,保留 monitor 即可,不必为了验收先开放 root 命令权限。

整机重启和固件升级的验证应安排在维护窗口,尤其是承担上网或远程接入的路由器。一次受控重启后确认客户端自动上线、指纹不变,比连续启动几次 version 更接近长期运行需要的证据。

项目的 CI 包含 amd64、arm64 构建和测试,但交叉编译通过不等于每种 OpenWrt 固件都通过了实机验收。目前也没有在本文给出内存占用、CPU 开销或最低闪存容量的实测结论。选择设备时,仍应检查剩余空间,并观察自己的使用负载。

Fleetd 这部分实现的取舍,是把协议和客户端逻辑共用,把服务管理与目录布局的差异留在明确的适配位置。这样既能复用代码,也不会把路由器当成一台目录结构完全相同的小服务器。

下一篇继续聊监控之后的事:采集到了指标,怎样让告警真正值得看,而不是每天多收到几封通知邮件。

项目与相关资料: