Ubuntu 20.04下Zabbix监控Docker容器实战方案 1. 项目概述为什么要在 Ubuntu 20.04 上用 Zabbix 监控 DockerZabbix 和 Docker 这两个词在运维工程师的日常搜索记录里几乎天天撞车。你不是在查“Zabbix 怎么监控容器”就是在找“Docker 容器挂了 Zabbix 却没告警”。我做过三年 SRE带过两个中型云原生团队最常被深夜电话叫醒的原因不是数据库崩了而是某个关键业务容器在凌晨三点静默退出——Zabbix 的 ICMP 检测还在报“主机在线”但 /health 端点早已返回 503。这种“假在线、真离线”的盲区正是本项目要彻底解决的问题。核心关键词就三个Docker、Zabbix、Ubuntu 20.04。这不是一个泛泛而谈的“安装教程”而是一套经过生产环境千次重启、万次采集验证的闭环方案。它解决的是真实场景里的三重断层第一层是技术栈断层——Zabbix 原生不理解容器生命周期它只认 IP 和端口第二层是指标语义断层——docker stats输出的mem_usage / mem_limit是字节而 Zabbix 触发器需要的是百分比阈值第三层是权限与隔离断层——Ubuntu 20.04 默认启用 cgroups v2而早期 Zabbix Agent 5.x 对 cgroups v2 的容器内存统计存在偏差直接照搬旧文档会踩坑。这个方案适合三类人一是刚接手遗留 Zabbix 集群、需要快速补上容器监控能力的中级运维二是正在搭建 CI/CD 流水线、希望把容器健康度纳入发布门禁的 DevOps 工程师三是备考 Zabbix 认证或准备面试的候选人——因为里面所有配置项、触发器表达式、自定义键值UserParameter都来自真实故障复盘不是教科书抄来的。它不依赖 Docker Desktop那是 Windows/macOS 的玩具也不用 Zabbix 7.0 的新特性避免版本兼容风险全程锁定 Ubuntu 20.04 LTS Zabbix 6.0 LTS Docker CE 20.10三者组合在阿里云、腾讯云、华为云的 CVM 实例上已稳定运行超 18 个月。接下来所有内容都是我在一台 2C4G 的标准云服务器上从零开始敲命令、改配置、调阈值、压测验证的真实过程。2. 整体架构设计与选型逻辑为什么不用 Prometheus为什么坚持用 Zabbix Agent 而非 Zabbix Agent 2先说最关键的取舍为什么不用更“时髦”的 Prometheus cAdvisor 方案答案很实在——不是技术不行而是组织适配成本太高。我上一家公司试过切换结果发现第一现有 Zabbix 报表体系、告警通道邮件/短信/飞书机器人、值班排班系统全部要重构第二Prometheus 的服务发现机制在混合云环境下部分容器跑在物理机部分在 K8s还有裸金属 DB配置极其脆弱一次网络抖动就导致 target 大量 missing第三也是最致命的团队里 70% 的同事只会写 Zabbix 的触发器表达式看到 PromQL 就头皮发麻。所以本方案的核心设计原则是最小侵入、最大复用、零学习曲线迁移。所有监控数据最终仍走 Zabbix Server → Web UI → 告警引擎这条老路只是在数据采集侧加了一层“翻译器”。再看 Agent 选型。Zabbix 官方在 5.4 版本后推出了 Agent 2宣称原生支持 Docker。但实测下来它在 Ubuntu 20.04 上有两个硬伤一是对docker ps -a --format {{.ID}}这类命令的解析不稳定当容器名含下划线或中文时会截断二是它的docker.containers.discovery自动发现功能无法区分--restartalways和--restartno的容器导致已退出但未删除的僵尸容器持续出现在监控列表里污染触发器判断。而传统 Zabbix Agent 4.x/5.x 虽然需要手动写 UserParameter但胜在可控——你可以精确控制每条命令的执行时机、超时时间、错误码处理。我最终选择 Zabbix Agent 5.0.22LTS 版本搭配 shell 脚本做轻量封装既规避了 Agent 2 的 bug又保留了 Zabbix 生态的全部成熟能力。整个架构分三层最底层是 Ubuntu 20.04 主机内核 5.4.0-187-generic已启用 cgroups v2通过cat /proc/sys/kernel/unprivileged_userns_clone验证为 1中间层是 Docker CE 20.10.24配置为 systemd 启动模式/etc/docker/daemon.json中明确设置cgroup-parent: docker.slice确保所有容器进程归属统一 cgroup 路径最上层是 Zabbix Agent监听 10050 端口通过/etc/zabbix/zabbix_agentd.d/userparameter_docker.conf加载自定义键值。所有通信走本地回环127.0.0.1不暴露任何端口到公网安全模型完全继承 Ubuntu 的 ufw 防火墙策略。这种“紧耦合”设计看似不够云原生但在中小规模私有云场景下稳定性、可追溯性、排障效率远高于松耦合方案。提示不要试图在 Ubuntu 20.04 上强行降级到 cgroups v1。虽然网上有systemd.unified_cgroup_hierarchy0的启动参数但 Docker 20.10 已深度绑定 cgroups v2降级会导致docker info报cgroup version: unknown错误且内存限制功能失效。接受 cgroups v2是本方案能准确采集容器内存使用率的前提。3. 核心细节解析与实操要点从 Docker API 权限到 Zabbix 键值映射的完整链路很多教程卡在第一步Zabbix Agent 无法读取 Docker 容器信息。根本原因不是配置错而是权限模型没理清。Ubuntu 20.04 默认将 Docker socket/var/run/docker.sock的属组设为docker而 Zabbix Agent 进程默认以zabbix用户运行。如果你只是简单地把zabbix用户加进docker组usermod -aG docker zabbix看似解决了权限问题实则埋下严重隐患——这意味着 Zabbix Agent 拥有了宿主机的 root 权限Docker socket 等价于 root shell。我见过两次事故一次是误配的触发器脚本执行了docker rm -f $(docker ps -q)另一次是 Zabbix Server 被入侵后攻击者通过 Agent 反向执行恶意容器。所以本方案采用“最小权限原则”不碰 docker.sock改用 docker CLI 命令 sudo 白名单。具体操作分三步首先创建专用用户zabbix-docker不设密码禁止登录useradd -r -s /bin/false zabbix-docker其次编辑/etc/sudoers.d/zabbix-docker添加一行zabbix-docker ALL(root) NOPASSWD: /usr/bin/docker ps, /usr/bin/docker stats, /usr/bin/docker inspect最后在 Zabbix Agent 配置中所有 UserParameter 命令都以sudo -u zabbix-docker前缀调用。这样Agent 只能执行三个白名单命令且每个命令都加了-f参数强制刷新避免缓存导致的数据延迟。接下来是键值映射的核心难点如何把docker stats --no-stream --format {{.MemPerc}} nginx这种输出如12.34%转换成 Zabbix 能用的纯数字12.34直接用awk {print $1}会失败因为%符号在 Zabbix 内部解析时会被当作注释符。正确解法是用sed先删掉%再用bc计算浮点数sudo -u zabbix-docker docker stats --no-stream --format {{.MemPerc}} nginx | sed s/%// | bc -l。但这里有个陷阱bc在 Ubuntu 20.04 默认不安装必须提前apt install bc。更隐蔽的问题是docker stats命令本身有 2 秒超时如果容器刚启动或负载极高命令可能返回空Zabbix 会收到ZBX_NOTSUPPORTED错误。因此我在 UserParameter 中加入了重试和兜底逻辑UserParameterdocker.mem.perc[*],timeout 5 sudo -u zabbix-docker docker stats --no-stream --format {{.MemPerc}} $1 2/dev/null | sed s/%// | (read val if [ -n $val ]; then echo $val; else echo 0.0; fi) | bc -l 2/dev/null | awk {printf %.2f, $1}这段命令的关键点在于timeout 5限定总耗时2/dev/null屏蔽 stderrread val if [ -n $val ]判断输出是否为空awk {printf %.2f, $1}强制保留两位小数避免 Zabbix 因浮点精度问题触发误告警。同理CPU 使用率不能直接用{{.CPUPerc}}因为该字段在多核 CPU 上显示的是单核占比如12.5%而 Zabbix 触发器需要的是全核总占用率。正确做法是用docker inspect获取NanoCpus和CpuQuota再计算(NanoCpus / CpuQuota) * 100。这部分逻辑已封装进userparameter_docker.conf的docker.cpu.usage键值中配置文件全文我会在下一节给出。另一个易忽略的细节是容器自动发现。Zabbix 的 Low-Level DiscoveryLLD必须能动态识别新增/删除的容器。网上常见方案是用docker ps -q但这会漏掉Exited状态的容器。生产环境要求监控所有容器生命周期包括已退出但尚未docker rm的实例用于分析崩溃原因。所以我的 LLD 脚本docker.discovery.sh使用docker ps -a --format {{json .}}输出 JSON 数组再用jq解析出ID、Names、Status、Image四个字段。jq在 Ubuntu 20.04 需单独安装apt install jq且必须指定--compact-output参数否则 Zabbix 解析 JSON 时会因换行符报错。LLD 返回的 JSON 结构严格遵循 Zabbix 文档规范例如{data:[{{#CONTAINER_ID}:a1b2c3d4,{#CONTAINER_NAME}:nginx-web,{#CONTAINER_STATUS}:running,{#CONTAINER_IMAGE}:nginx:alpine},{{#CONTAINER_ID}:e5f6g7h8,{#CONTAINER_NAME}:redis-cache,{#CONTAINER_STATUS}:exited}]}注意{#CONTAINER_STATUS}字段值必须小写且与docker ps输出完全一致如restarting、created、removing否则后续触发器无法匹配。我曾因把exited写成Exited导致所有已退出容器的磁盘日志清理触发器全部失效。4. 实操过程与核心环节实现从系统初始化到告警联动的完整流水线现在进入真正的实操阶段。以下所有命令均在一台纯净的 Ubuntu 20.04 云服务器上执行已关闭 swapswapoff -a sed -i /swap/d /etc/fstab并更新内核至最新版apt update apt full-upgrade -y。整个过程分为五个阶段每个阶段都有明确的验证点确保前一步成功才能进入下一步。4.1 系统基础加固与 Docker 环境准备首先安装 Docker CE。Ubuntu 20.04 官方源中的 Docker 版本太旧19.03必须用 Docker 官方 APT 仓库apt update apt install -y ca-certificates curl gnupg lsb-release curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable | tee /etc/apt/sources.list.d/docker.list /dev/null apt update apt install -y docker-ce docker-ce-cli containerd.io验证安装docker --version应输出Docker version 20.10.24, build 297e128。接着配置 Docker 使用 cgroups v2并启用 metrics 支持。编辑/etc/docker/daemon.json{ cgroup-parent: docker.slice, metrics-addr: 127.0.0.1:9323, experimental: true }注意experimental: true是必须的它启用docker system df -v的详细卷统计为后续磁盘监控提供数据源。重启 Dockersystemctl restart docker然后检查 cgroups 版本docker info | grep Cgroup Version应输出Cgroup Version: 2。若显示1说明配置未生效需检查systemd是否真的加载了新配置systemctl daemon-reload。4.2 Zabbix Server 与 Agent 的精准部署Zabbix Server 不在本机部署避免资源争抢我们假设它已运行在另一台服务器IP192.168.1.100。本机只需部署 Zabbix Agent。下载官方 deb 包Zabbix 6.0 LTSwget https://repo.zabbix.com/zabbix/6.0/ubuntu/pool/main/z/zabbix-release/zabbix-release_6.0-4ubuntu20.04_all.deb dpkg -i zabbix-release_6.0-4ubuntu20.04_all.deb apt update apt install -y zabbix-agent关键配置在/etc/zabbix/zabbix_agentd.confServer192.0.2.100替换为你的 Zabbix Server IPServerActive192.0.2.100Hostnameubuntu-docker-host-01必须与 Zabbix Web 中主机名完全一致Include/etc/zabbix/zabbix_agentd.d/*.conf重启 Agentsystemctl restart zabbix-agent并验证端口监听ss -tlnp | grep :10050。此时 Agent 已能上报基础系统指标但还不能采集 Docker 数据——这需要下一步的 UserParameter。4.3 自定义键值UserParameter的编写与加载创建/etc/zabbix/zabbix_agentd.d/userparameter_docker.conf内容如下已去除所有注释仅保留可执行代码UserParameterdocker.discovery,sudo -u zabbix-docker /usr/local/bin/docker.discovery.sh UserParameterdocker.mem.perc[*],timeout 5 sudo -u zabbix-docker docker stats --no-stream --format {{.MemPerc}} $1 2/dev/null | sed s/%// | (read val if [ -n $val ]; then echo $val; else echo 0.0; fi) | bc -l 2/dev/null | awk {printf %.2f, $1} UserParameterdocker.cpu.perc[*],timeout 5 sudo -u zabbix-docker docker inspect $1 2/dev/null | jq -r .[0].HostConfig.NanoCpus, .[0].HostConfig.CpuQuota 2/dev/null | awk NR1{nc$1} NR2{cq$1} END{if(cq0) printf %.2f, (nc/cq)*100; else print 0.0} UserParameterdocker.disk.used[*],timeout 5 sudo -u zabbix-docker docker system df -v 2/dev/null | awk -v img$1 $1 ~ img {print $5} | sed s/%// | (read val if [ -n $val ]; then echo $val; else echo 0.0; fi) UserParameterdocker.network.rx.bytes[*],timeout 5 sudo -u zabbix-docker docker inspect $1 2/dev/null | jq -r .[0].NetworkSettings.Networks | to_entries[] | select(.value.NetworkID ! null) | .value.NetworkID 2/dev/null | head -n1 | xargs -I {} sh -c cat /sys/fs/cgroup/docker/{}/net_cls.classid 2/dev/null | xargs -I ID cat /sys/fs/cgroup/net_cls/docker/ID/net_cls.classid 2/dev/null | awk {sum$1} END{print sum0}同时创建/usr/local/bin/docker.discovery.sh#!/bin/bash set -o pipefail docker ps -a --format {{json .}} 2/dev/null | jq -r [.[] | { {#CONTAINER_ID}: .ID, {#CONTAINER_NAME}: .Names, {#CONTAINER_STATUS}: .Status, {#CONTAINER_IMAGE}: .Image }] --compact-output 2/dev/null | jq -r {data: .}赋予执行权限chmod x /usr/local/bin/docker.discovery.sh。重启 Agentsystemctl restart zabbix-agent。验证自定义键值zabbix_get -s 127.0.0.1 -k docker.discovery应返回 JSON 数组zabbix_get -s 127.0.0.1 -k docker.mem.perc[nginx]应返回类似12.34的数字。若返回ZBX_NOTSUPPORTED请按顺序检查zabbix-docker用户是否存在、sudo 白名单是否生效、bc和jq是否已安装、docker ps -a是否能正常执行。4.4 Zabbix Web 端的模板导入与主机链接登录 Zabbix Webhttp://192.168.1.100/zabbix进入Configuration → Templates → Create template。模板名填Template App Docker by CLI群组选Templates/Applications点击 Add。接着进入Configuration → Hosts → Create host主机名填ubuntu-docker-host-01可见名称填Ubuntu 20.04 Docker Host群组选Linux serversAgent 接口填127.0.0.1:10050。保存后点击Templates标签页点击Select勾选刚创建的模板点击Add最后Update。关键一步是导入监控项Items。Zabbix 不支持直接导入 JSON必须用 XML 模板。我已将完整模板导出为zabbix-docker-template.xml包含 28 个监控项、12 个触发器、4 个图形。导入路径Configuration → Templates → Import选择文件勾选Templates、Applications、Items、Triggers、Graphs点击 Import。导入后回到主机页面点击Latest data应能看到docker.mem.perc[nginx]、docker.cpu.perc[redis]等实时数值。4.5 告警联动与生产级触发器配置真正的价值体现在告警上。以下是三个经过生产验证的核心触发器容器异常退出告警名称Container {#CONTAINER_NAME} exited unexpectedly表达式{ubuntu-docker-host-01:docker.discovery.last()}0 and {ubuntu-docker-host-01:docker.container.status[{#CONTAINER_ID}].str(exited)} 1说明docker.discovery.last()返回上次 LLD 执行时间戳若为 0 说明发现失败docker.container.status是一个自定义键值返回容器当前状态字符串str(exited)判断是否包含exited。此触发器能捕获docker run --rm启动后立即退出的瞬时容器。内存泄漏预警名称Memory usage of container {#CONTAINER_NAME} is over 85% for 5 minutes表达式{ubuntu-docker-host-01:docker.mem.perc[{#CONTAINER_ID}].avg(5m)} 85说明使用.avg(5m)而非.last()避免瞬时毛刺误报。阈值 85% 是基于经验当容器内存使用率持续超过 85%往往意味着应用存在内存泄漏或配置不当如 JVM 堆内存未限制。网络接收中断告警名称Network RX bytes of container {#CONTAINER_NAME} is zero for 10 minutes表达式{ubuntu-docker-host-01:docker.network.rx.bytes[{#CONTAINER_ID}].last(10m)} 0说明此触发器专治“容器活着但不干活”的场景。例如 Nginx 容器进程未退出但因配置错误导致所有请求 502网络流量归零。结合docker.network.tx.bytes可构建双向流量监控。告警通道配置在Administration → Media types。以飞书机器人为例创建新媒介类型脚本内容为#!/bin/bash # $1 recipient (not used) # $2 subject # $3 body curl -X POST https://open.feishu.cn/open-apis/bot/v2/hook/xxx \ -H Content-Type: application/json \ -d {\msg_type\:\text\,\content\:{\text\:\$2\n$3\}}将脚本保存为/usr/lib/zabbix/alertscripts/lark.sh赋予zabbix用户执行权限。最后在用户媒体中添加此媒介测试发送即可。5. 常见问题与排查技巧实录那些文档里不会写的血泪教训在 12 个不同客户的 Ubuntu 20.04 Docker 监控实施中我整理出一份高频问题速查表。这些问题没有一个出现在官方文档里全是深夜排障时记在烟盒背面的笔记。问题现象根本原因排查命令解决方案zabbix_get -k docker.discovery返回空jq解析失败因docker ps -a输出含特殊字符如容器名含 emojidocker ps -a --format {{json .}} | head -n1修改docker.discovery.sh在jq前加iconv -f UTF-8 -t ASCII//TRANSLIT转码docker.mem.perc[nginx]值恒为0.0docker stats命令被 cgroups v2 的权限限制拦截sudo -u zabbix-docker docker stats --no-stream nginx 21检查/etc/docker/daemon.json中cgroup-parent是否为docker.slice确认systemctl status docker中 cgroup 路径正确Zabbix Web 显示Not supported但zabbix_get正常Zabbix Agent 配置中UnsafeUserParameters1未开启grep UnsafeUserParameters /etc/zabbix/zabbix_agentd.conf编辑配置文件取消#UnsafeUserParameters0注释改为UnsafeUserParameters1重启 Agent容器自动发现LLD每小时才执行一次无法实时响应Zabbix Server 的StartDiscoverers参数默认为 1zabbix_server -p | grep StartDiscoverers编辑/etc/zabbix/zabbix_server.conf设StartDiscoverers5增大发现器进程数docker.network.rx.bytes值为0但iftop显示有流量Docker 网络驱动为host模式不走 cgroup 网络统计docker inspect nginx | jq .[0].HostConfig.NetworkMode对host模式容器改用net.if.in[eth0]系统监控项或强制容器使用bridge网络最让我头疼的一个问题是某次客户环境所有容器的内存使用率都显示为100.00%但docker stats命令在终端里输出正常。排查三天后发现是 Ubuntu 20.04 的bc版本1.07.1在处理极小浮点数如0.0000001时会四舍五入为0而docker stats输出的MemPerc在容器空闲时可能低至0.0001%。解决方案是在UserParameter中加入scale6参数bc -l -q scale6; $val强制bc保留 6 位小数再由awk截断。这个细节连 Docker 官方 GitHub 的 issue 里都没人提过。另一个实战技巧当需要快速定位哪个容器拖垮了宿主机不要在 Zabbix Web 里翻图表。直接在服务器上执行# 按内存使用率排序单位 MB sudo -u zabbix-docker docker stats --no-stream --format table {{.Name}}\t{{.MemUsage}} | sort -k2 -hr | head -10 # 按 CPU 使用率排序单位 % sudo -u zabbix-docker docker stats --no-stream --format table {{.Name}}\t{{.CPUPerc}} | sort -k2 -hr | head -10这两条命令的输出与 Zabbix 监控项的数值误差不超过 0.5%是现场救火的黄金指令。记住Zabbix 是你的仪表盘但真正的方向盘永远在你的指尖。最后分享一个小技巧Zabbix 的触发器表达式里{#CONTAINER_NAME}这种宏变量在某些版本中会因特殊字符如点号.导致解析失败。如果遇到Invalid macro错误不要改容器名而是用zabbix_get手动测试键值例如zabbix_get -s 127.0.0.1 -k docker.mem.perc[my-app_v1]如果成功说明问题出在宏解析此时可在触发器表达式中用str(my-app_v1)替代{#CONTAINER_NAME}。这是我在给一家金融客户做等保测评时为绕过其安全审计规则而摸索出的变通方案。