一、引言

项目中的大部分服务已经容器化,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。

Prometheus架构图

(二)容器化要解决的四类访问需求

原有采集方式依赖宿主机环境,容器化后需要:读取宿主机文件、获取宿主机网络信息、获取宿主机进程信息,以及执行宿主机 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=/host

path.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,最终以实际运行时权限策略为准。

四、文献引用

最后修改:2026 年 09 月 14 日
如果觉得我的文章对你有用,请随意赞赏