一、引言
项目中的大部分服务已经容器化,agent-client却比较特殊:它需要采集宿主机及其运行组件的信息。因此,容器既要保留原有采集方式,也必须在必要范围内访问宿主机资源。本文记录agent-client容器化的实现与 v1、v2、v3 三次迭代。目标是不修改采集器业务代码、不长期维护两套程序,同时读取宿主机文件、网络和进程信息,并在采集容器化组件时执行宿主机 Docker 命令。文中的xxxx、xxx是原有占位符;脚本、Dockerfile、Docker 命令和 YAML 均用于说明部署边界,除明确完整的命令外,不应直接作为可运行配置。
二、正文
(一)采集职责决定了容器边界
本项目由 Java 实现,包含 gateway、agent(agent-server、agent-client)、task、resource 四个模块。agent-client 的职责类似 Prometheus Exporter:采集机器的操作系统、主机名、CPU、内存、网络等信息,也采集 nginx、RocketMQ、MySQL、Redis 等组件的版本、安装路径和日志目录。
机器信息通过 Shell 命令、/etc/hostname、/etc/system-release 等文件,以及 JNI 或第三方库获得。组件信息则先从 /proc/<pid>/ 文件收集进程信息,再由 Python 或 Shell 脚本解析;采集容器化组件时还需要执行 Docker 命令。新增组件时,可在管理页面添加符合约定格式的 Python 或 Shell 解析脚本,无须修改 Java 代码后重新打包。
Prometheus 通常针对不同目标部署不同 Exporter,例如 node_exporter 采集机器信息,mysqld_exporter 可按配置监控多个 MySQL 实例。相较之下,agent-client 以单实例覆盖多类采集对象,信息粒度也不同于专用 Exporter。

(二)容器化要解决的四类访问需求
原有采集方式依赖宿主机环境,容器化后需要:读取宿主机文件、获取宿主机网络信息、获取宿主机进程信息,以及执行宿主机 Docker 命令。
这些需求与 Docker 的 namespace、cgroup 隔离有关。本场景主要涉及 PID、net、ipc、mnt、uts 等 namespace:PID namespace 决定进程视图,net namespace 决定网络视图;不共享它们时,采集结果来自容器而不是宿主机。文件访问需要宿主机目录挂载和容器内绑定挂载,Docker 命令还依赖 Docker 二进制文件与 /run/docker.sock 可用。
(三)node_exporter 提供的挂载参考
node_exporter 的启动参数给出了参考:共享主机网络和进程空间,将宿主机根目录以只读、rslave 方式挂载到容器,再通过 --path.rootfs 指向该目录。
docker run -d \
--net="host" \
--pid="host" \
-v "/:/host:ro,rslave" \
quay.io/prometheus/node-exporter:latest \
--path.rootfs=/hostpath.rootfs 默认值是 /,上例将其改为宿主机根目录在容器中的挂载路径 /host。下面是相关参数的结构示意,省略上下文,不能直接作为源码使用:
var (
procPath = kingpin.Flag("path.procfs", "procfs mountpoint.").Default(procfs.DefaultMountPoint).String()
sysPath = kingpin.Flag("path.sysfs", "sysfs mountpoint.").Default("/sys").String()
rootfsPath = kingpin.Flag("path.rootfs", "rootfs mountpoint.").Default("/").String()
)(四)以环境变量保留原有启动入口
直接修改 bootstrap.yaml 不利于容器交付。因此,在原有启动脚本外增加 docker-startup.sh:它读取环境变量,未提供时使用默认值,替换配置后调用原始启动脚本。这样不必维护两套完整启动脚本。
以下仅为原有逻辑的结构示意;replace 的实现、变量引用、配置路径和 xxxx 必须按实际脚本确认:
#!/bin/bash
nacos_url=${NACOS_URL}
if [ -z "${NACOS_URL}" ]; then
nacos_url="http://127.0.0.1"
fi
function replace() {
# 原有替换逻辑
...
}
replace "${nacos_url}" "${old_url}" "${bootstrap_path}"
xxxx/bin/startup.sh(五)以绑定挂载兼容既有采集方式
现有采集同时使用 Shell、JNI 和本地方法,路径并不统一,且相关代码跨越多人和较长时间,直接修改业务代码成本较高。镜像先将宿主机根目录挂载到 /rootfs,再根据环境变量列出的文件、目录在容器中执行绑定挂载。
下列脚本是挂载逻辑的结构示意,展示文件与目录的处理边界;使用前需结合现场补全路径、权限和错误处理:
MOUNT_FILES="/etc/hostname /etc/system-release /usr/bin/docker /run/docker.sock"
MOUNT_DICTIONARIES="/usr/local/bin"
for f in ${MOUNT_FILES}; do
if [[ ! -f ${f} ]]; then
touch "${f}"
fi
mount -o bind "/rootfs/${f}" "${f}"
done
for d in ${MOUNT_DICTIONARIES}; do
if [[ ! -d ${d} ]]; then
mkdir -p "${d}"
fi
mount -o bind "/rootfs/${d}" "${d}"
done这与逐项使用多个 -v 的访问效果相同,但将可定制项集中到 MOUNT_FILES、MOUNT_DICTIONARIES,能缩短 Docker 命令或 Kubernetes YAML。挂载逻辑放入镜像 CMD 后,现场可在部署阶段重写启动命令,无须重新打包镜像。
以下 Dockerfile 只是结构示意:基础镜像、遗漏步骤、xxx 和完整启动路径都需由实际镜像补全。
FROM CentOS:7.9
ENV MOUNT_FILES "/etc/hostname /etc/system-release /usr/bin/docker /run/docker.sock"
ENV MOUNT_DICTIONARIES "/usr/local/bin"
CMD ["/bin/bash", "-c", "for f in ${MOUNT_FILES}; do if [[ ! -f ${f} ]]; then touch ${f}; fi; mount -o bind /rootfs/${f} ${f}; done; for d in ${MOUNT_DICTIONARIES}; do if [[ ! -d ${d} ]]; then mkdir -p ${d}; fi; mount -o bind /rootfs/${d} ${d}; done; xxx/docker-startup.sh"](六)v1:用特权模式打通采集路径
v1 使用 --privileged=true,并通过 --network="host"、--pid="host" 分别共享主机网络和进程空间。--network 与 --net 都可以表达网络模式,命令末尾还可覆盖 Dockerfile 的 CMD,用于现场定制。
docker run \
-tid \
--restart=always \
--privileged=true \
--network="host" \
--pid="host" \
-e NACOS_URL="192.168.1.30:8848" \
-e MOUNT_FILES="/etc/hostname /etc/system-release /usr/bin/docker /run/docker.sock" \
-e MOUNT_DICTIONARIES="/usr/local/bin" \
-v "/:/rootfs:ro,rslave" \
xxxx/agent-client该版本能够运行,但 --privileged=true 几乎把宿主机的大部分能力交给容器,部分现场安全策略不允许这样启动。
(七)v2:按 Capabilities 收窄权限
v2 改用 Linux Capabilities,只保留原部署实际使用的能力:
CAP_SYS_ADMIN:执行系统管理任务;缺少它时,容器不能绑定挂载宿主机映射进来的文件或目录。CAP_SYS_PTRACE:允许追踪进程,使agent-client可访问/proc/<pid>/environ并读取进程环境变量。CAP_DAC_OVERRIDE:忽略文件 DAC 访问权限,用于读、写和执行宿主机文件。
Docker 通过 --cap-add、--cap-drop 增删能力,参数值不使用 CAP_ 前缀:
docker run \
-tid \
--restart=always \
--cap-add=SYS_ADMIN \
--cap-add=SYS_PTRACE \
--cap-add=DAC_OVERRIDE \
--network="host" \
--pid="host" \
-e NACOS_URL="192.168.1.30:8848" \
-e MOUNT_FILES="/etc/hostname /etc/system-release /usr/bin/docker /run/docker.sock" \
-e MOUNT_DICTIONARIES="/usr/local/bin" \
-v "/:/rootfs:ro,rslave" \
xxxx/agent-client运行在 Kubernetes 时,可将该配置转换为 DaemonSet。下列 YAML 为结构示意:资源字段和缩进已整理,但镜像、环境变量和挂载策略仍须按现场补全并审核。
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: agent-client
namespace: test
spec:
selector:
matchLabels:
agent: client
template:
metadata:
labels:
agent: client
spec:
hostNetwork: true
hostPID: true
containers:
- name: agent-client
image: xxx/agent-client
imagePullPolicy: IfNotPresent
env:
- name: NACOS_URL
value: 192.168.1.30:8848
- name: MOUNT_FILES
value: /etc/hostname /etc/system-release /usr/bin/docker /run/docker.sock
- name: MOUNT_DICTIONARIES
value: /usr/local/bin
volumeMounts:
- mountPath: /rootfs
name: rootfs
readOnly: true
securityContext:
privileged: false
capabilities:
add: ["SYS_ADMIN", "SYS_PTRACE", "DAC_OVERRIDE"]
volumes:
- name: rootfs
hostPath:
path: /(八)v3:为 hostNetwork 补上集群 DNS
v2 的 YAML 在内网测试正常,但现场使用 https://nacos.default.svc.cluster.local:8848 作为 Nacos 地址时,agent-client 无法注册。将 hostNetwork 设为 false 后可以启动,除网卡列表外的采集属性与非容器化部署一致;为满足网络信息采集,仍需保留主机网络。
排查显示,使用主机网络时容器内 /etc/resolv.conf 为空;未使用时则包含集群 nameserver、search 域和 ndots 设置。直接写入 DNS 配置可以恢复注册和采集,但会固定 DNS 地址与搜索域,切换集群还得改脚本。
v3 最终增加 dnsPolicy: ClusterFirstWithHostNet,使共享主机网络的 Pod 使用 Kubernetes DNS 配置。以下是配置要点的结构示意,需合并到完整 DaemonSet 中:
spec:
template:
spec:
hostNetwork: true
hostPID: true
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: agent-client
image: xxx/agent-client
env:
- name: NACOS_URL
value: https://nacos.default.svc.cluster.local:8848(九)上线前逐项比对宿主机视图
启动后,应在容器与宿主机分别执行以下命令并对比结果:
ps -ef
ip addr
hostname
cat /etc/system-release
docker ps共享 PID 和网络命名空间后,进程和网卡信息才可能对应宿主机;使用 hostNetwork 时,网络采集与 Kubernetes DNS 都要验证。/rootfs 虽以只读方式挂载,但在容器中创建目标并执行绑定挂载仍需相应权限。Docker 客户端路径、/run/docker.sock 是否存在取决于宿主机实际环境;NACOS_URL 是否需要协议前缀、脚本替换逻辑和 hostPath 安全策略也应由部署方确认。
三、总结
agent-client 的容器化经历了三次迭代:v1 通过特权模式验证采集路径;v2 用 SYS_ADMIN、SYS_PTRACE、DAC_OVERRIDE 取代特权模式;v3 在保留 hostNetwork、hostPID 的前提下,使用 ClusterFirstWithHostNet 解决集群 DNS 解析。宿主机根目录挂载配合 MOUNT_FILES、MOUNT_DICTIONARIES,让既有 Shell、JNI 和本地方法得以延续,也为现场定制保留了入口。
该方案适用于确实需要访问宿主机资源,且现场允许相应能力授权与目录挂载的采集器。部署前不能只验证服务启动,还应核对进程、网络、文件、Docker Socket 和 DNS,最终以实际运行时权限策略为准。