
最近折腾了短生命周期容器的指标采集,踩了几个坑,这篇把问题说清楚。
这几年,这个问题我听过很多次,也实际参与过不少讨论:如果应用运行在容器里,又没有对外暴露 HTTP(超文本传输协议)的 /metrics 端点,该怎么采集它的指标?
有些应用从设计之初就没打算这么做。也有人觉得,仅仅为了监控再塞一个 HTTP 服务进应用,开销没有必要。
当应用执行的是短生命周期任务时,这个问题就更有意思了,比如 CLI(命令行界面)命令、定时任务或者批处理作业。
我碰到的一个真实案例,是一个 PHP 7.4 单体应用,里面全是 CLI 命令,我们正在逐步把它迁移到容器环境。
应用本身已经稳定运行了很多年,后面还有多年积累的业务代码。我们既需要计数器、直方图这类业务指标,也需要数据库查询耗时这类技术指标。
放在物理服务器或虚拟机上,这事相对简单:把数据写进文件,定期做聚合,再推给监控系统就行。
这种方案甚至可以稳定跑上好几年。
但迁到 Kubernetes(容器编排平台)之后,情况完全变了。
容器启动、执行任务,然后退出。没人能保证 Prometheus(监控与告警系统)的下一次抓取恰好发生在容器还活着的时候。
即使应用能生成指标,仍然有个问题:到底什么时候采集?又由谁保证最后那一份状态还能被访问到?
假设一个定时命令五秒就执行完了,而 Prometheus 的抓取间隔是十五秒。
那么这个应用实例很可能永远不会被 Prometheus 看到。
当然可以把指标发送到独立服务,但这样又会冒出一串问题:谁负责聚合数据?怎么确保最后一次更新成功送达?网络延迟怎么办?指标生命周期又由谁管理?
如果应用报错退出呢?
如果容器已经收到终止信号呢?
如果监控系统临时不可用呢?
最初只是“采集几个指标”这么简单的需求,最后却慢慢变成了另一个需要开发、部署和维护的服务。
而这一切只是为了回答几个问题:处理了多少次操作?每次用了多长时间?应用查询数据库总共花了多少时间?
当然,也可以干脆不采了。但这些指标对应用团队很重要。缺了它们,想看清应用内部到底发生了什么就会困难很多。
而且作为技术负责人,我实在没法心安理得地把这些数据丢掉。关键时刻,它们可能正好是最有价值的证据。
架构委员会最早提出的方案,是自己写一个包装器。
乍一看挺合理:启动一个小进程接收指标,再通过 HTTP 接口暴露给 Prometheus。
但问题马上跟了上来:
谁来维护它?
写个原型是一回事,让它在容器里稳定运行是另一回事。
你得处理终止信号、子进程、退出码、超时、应用异常,以及指标处理代码自身出错的情况。
更重要的是,无论发生什么,监控逻辑都不能干扰业务逻辑,更不能让应用无法完成工作。
基础设施团队同样也不太愿意再部署一个组件。
而且他们的顾虑很合理。
Kubernetes 里已经有一套能用的监控体系。Prometheus 一直在抓取微服务,配置齐全,也有人负责维护。
为了几个定时任务,再搭一层基础设施,值得吗?
讨论进行到某个阶段,结论基本变成了这样:
“单体应用先别采这些指标也行。或者花点时间,让它适配新基础设施。”
可即便改造应用,也没有解决短生命周期进程的根本问题。
你可以让 PHP 应用暴露 /metrics,但如果它在下一轮抓取前就退出了,Prometheus 依然可能什么都采不到。
当然,先得看看别人都是怎么解决的。
对长期运行的服务来说,这是最直接的办法。
应用暴露 /metrics,Prometheus 定期抓取。
可短生命周期 CLI 进程又会碰到生命周期问题:进程可能在下一轮抓取之前就退出了。
在部分批处理场景中,Pushgateway 已经是比较成熟的方案。
应用把指标推送给 Pushgateway,再由 Prometheus 从这里抓取。
不过 Pushgateway 自己也有指标生命周期。应用结束后,指标仍可能留在那里,因此必须明确管理数据的新鲜度和清理策略。
它还会引入一个新的基础设施组件,以及相应的网络通信。
对于服务级批处理任务,Pushgateway 很可能就是正确答案。但它不一定适合与短生命周期工作负载的每个实例一一关联的指标。
另一个方案是把指标写进文件,再通过 Node Exporter 暴露出来。
对某些工作负载来说,这套方法简单而且有效。
但它要求具备合适的文件共享方式和采集基础设施。在容器环境里,尤其是任务实例会被动态创建和删除时,还会引出一堆额外问题。
我们也可以在应用旁边再运行一个独立的 Exporter 容器。
这样能把应用职责和指标暴露分开。
代价则是多一个容器、多一份资源占用,还要处理两个进程之间的通信,并协调它们各自的生命周期。
OpenTelemetry 提供了更完整的遥测能力。
OpenTelemetry Collector 可以接收应用指标,再通过 HTTP 端点提供给 Prometheus。
它也可以就地运行,甚至与应用放在同一个容器里,因此不一定非要部署独立、集中式服务。
但 Collector 的核心职责还是处理和投递遥测数据,它并不是一个拿来监管任意应用进程的现成监督器。
在我们的场景里,仍然得自己实现 PHP 进程启动、信号转发、子进程清理、退出码保留,以及工作负载结束后的有限时长最终抓取等待。
换句话说,可以基于 OpenTelemetry Collector 搭建出所需行为,但包装器还是得自己写、自己维护。
后来才发现,这个包装器恰恰就是整个方案里最值得复用的部分。
这些方案本身都没错。对某一种工作负载来说,其中任何一个都可能最合适。
但我们希望同时满足几个条件:
我们考察的这些方案里,没有哪一个能在不引入额外取舍的情况下,同时满足全部条件。
后来我联系了其他公司的同事。
结果发现,他们也遇到过类似场景。
而且未必都是大型单体应用,有时就是一个普通 CLI 程序、定时任务,或者执行完任务就退出的 Worker。
工作负载不同,底层问题却差不多:应用负责把自己的事情做完,基础设施则要能按熟悉的方式采集指标。
调研过程中,我还用 ChatGPT、Gemini、DeepSeek 和 YandexGPT 讨论过备选方案,让它们质疑假设、拆解架构思路。
当然,LLM(大型语言模型)说它没见过某种方案,并不能证明这种方案不存在。最终还是得依靠现有工具的文档、能力对比,以及我们的真实需求来判断。
就这样,做一个独立工具的想法逐渐成形了。
动手写代码之前,得先明确这个工具到底解决什么,不解决什么。
没必要再造一套现有工具的替代品。
如果应用已经提供了标准的 Prometheus 端点,而且运行时间足够长,可以稳定被抓取,就没必要再引入一个组件。
如果 Pushgateway 适合某个批处理场景,那就直接用 Pushgateway。
如果基础设施本来就围绕 OpenTelemetry 建设,也没必要为了一个新产品把已有体系推翻。
我们真正感兴趣的是范围更窄的一类场景:
容器化工作负载不能或不应该负责向 Prometheus 暴露自己的指标。
不是裸机,不是通用监控系统,也不是新的中心化指标聚合平台。
就是一个容器、里面的应用,以及标准的 Prometheus 拉取模式。
我们的想法是在应用容器内部放一个小型适配器。
它在本地接收指标,通过 /metrics 暴露数据,同时负责处理工作负载的生命周期。
不强制依赖中心服务。
不需要单独的 Sidecar 容器。
也不需要围绕一套新的监控架构重写应用。
范围确定之后,基础需求也就清楚了。
第一,应用启动和结束的行为必须与没有包装器时完全一致。
退出码要保留,信号要正确转发,子进程也必须妥善处理和回收。
第二,应用必须能在本地提交指标,不需要连接外部监控基础设施。
第三,指标必须通过标准 HTTP /metrics 端点暴露,让 Prometheus 可以抓取。
第四,应用必须能够提交一份完整的指标快照。
第五,如果应用不想自己生成快照,也要提供一种简单方式,让它能够操作计数器、仪表盘和直方图。
第六,应用自然退出后,最终指标还要继续保留一段有限时间。
这里有一个必须说清楚的限制。
如果 Prometheus 不可用,或者没来得及发现这个容器,我们无法保证它一定能够采集到这些指标。
但可以再给它一次抓取机会,同时不需要让容器无限期存活。
最后,整套东西必须能运行在单个容器里,不能强制依赖额外基础设施。

经过这轮调研和实现,最后做出来的是 MetricShell。
它的思路并不复杂。
我们不让应用自己管理 HTTP 端点,而是把这部分工作交给一个小型包装器进程。
MetricShell 作为容器里的 PID(进程标识符)1 启动,再拉起用户真正要执行的命令。
这个命令可能是 PHP CLI 应用、定时任务,也可能是一个 Worker。
MetricShell 负责监督进程、接收指标,并通过 /metrics 暴露数据。
与此同时,应用照常完成自己的业务。
最初的计划看起来非常直接:启动进程,接收一些指标,通过 HTTP 提供出去,然后退出。
软件开发有条不知道算不算定律的规律:你以为很快能做完的事情,往往会拖很久。
这次也一样,原本以为两小时能搞定,最后悄悄变成了两个月。
各种问题很快堆了起来:
然后还有在 Linux 上以 PID 1 运行时必须处理的细节:转发信号、管理子进程、确保它们被正确清理,同时保留应用真正的退出码。
最后,我们不得不设计一套完整的状态机,定义事件顺序,还要处理进程终止、指标写入和 HTTP 请求之间的竞态。
有意思的是,做一个指标工具,最难的部分居然和指标本身关系不大。
真正麻烦的是如何把进程终止处理正确。
目前的实现支持两种模式。
应用先生成一份完整的指标快照,再提交给 MetricShell。
每当收到一份有效的新快照,它都会原子替换上一份快照。
如果应用本身已经会收集指标,而且能够提供完整状态,这种模式很合适。
在这个模式下,MetricShell 会维护一个本地指标注册表。
应用只需发送操作,例如增加计数器、设置仪表盘,或者向直方图记录一次观测值。
通信通过本地 Unix Socket 完成。
这样一来,每个应用都不必自己实现一套完整的 Prometheus 注册表。
无论采用哪种模式,指标暴露给 Prometheus 的机制完全相同。
假设我们的 PHP 命令已经处理完数据。
正常情况下,主进程一退出,容器也很快就会结束。
但 MetricShell 是 PID 1,它会持续监督应用进程。
工作负载自然退出后,MetricShell 会结束指标接收、冻结最后一份有效状态,并继续通过 /metrics 提供它。
接下来怎么退出,取决于具体配置。
MetricShell 可以立即退出,也可以等待一段配置时长,或者等待指定数量的有效抓取响应,但整个过程受有限超时约束。
最终退出时,它会保留工作负载的退出状态,除非 MetricShell 自身发生了优先级更高的故障。
这样一来,Prometheus 就多了一次采集最终指标的机会。
这不是可靠投递保证,也不能替代持久化存储。
但对这类工作负载来说,它让我们可以继续使用熟悉的 Prometheus 拉取模式。
应用运行期间:
┌────────────────────────────────┐
│ Container │
│ │
│ ┌──────────────────────────┐ │
│ │ MetricShell (PID 1) │ │
│ │ │ │
│ │ Snapshot / Registry │◄─┼── Prometheus
│ │ GET /metrics │ │ GET /metrics
│ └────────────▲─────────────┘ │
│ │ │
│ Local metrics input │
│ │ │
│ ┌────────────┴─────────────┐ │
│ │ PHP CLI / Cron / │ │
│ │ Worker workload │ │
│ └──────────────────────────┘ │
└────────────────────────────────┘
工作负载自然退出后,MetricShell 可以继续提供冻结的最终快照,直到配置的等待条件满足或者超时。
如果 Prometheus 没能在时限内抓取到指标,MetricShell 仍会在配置范围内退出。
有个关键细节:这种等待最终抓取的行为只适用于工作负载自然结束。如果容器从外部被终止,MetricShell 会执行有限时长的关闭流程,最终等待过程可能被中断。
截至本文写作时,当前发布的版本是 v0.2.2。
MetricShell 使用 Go 编写,采用 MIT 许可证,支持 Linux 平台的 amd64 和 arm64 架构。
想看它怎么工作,不需要专门写一个应用。
GitHub Container Registry 上已经发布了可直接运行的示例:
docker run --rm \ --name metricshell-example \ -p 127.0.0.1:9090:9090 \ ghcr.io/denki77/metricshell-example:0.2.2
再打开一个终端:
curl http://127.0.0.1:9090/metrics
这时就能看到 MetricShell 暴露出来的指标。
如果想接入自己的应用,也可以使用包含 MetricShell 二进制文件的 Artifact 镜像,再把它复制进应用镜像。
简化示例如下:
FROM ghcr.io/denki77/metricshell-artifact:0.2.2 AS metricshell FROM my-application COPY --from=metricshell /metricshell /usr/local/bin/metricshell ENTRYPOINT ["/usr/local/bin/metricshell", "--"]
CMD ["my-worker"]
这个示例只演示了如何集成包装器。应用仍然需要通过支持的本地接口之一提交指标,Prometheus 也必须配置为能够发现并抓取这个容器。
生产环境部署时,文档建议按 Digest 固定镜像版本,并使用文档中给出的配置。
仓库里提供了 Docker 和 Kubernetes 示例、协议说明、配置选项以及运行限制。
任何架构决策都有代价。
MetricShell 也不例外。
它会在容器里增加一个进程,并承担正确监督工作负载的责任。
应用还是得自己生成或发布指标。MetricShell 不可能凭空判断哪些业务指标值得监控。
最终快照只在有限时间内可用。如果 Prometheus 错过这个窗口,指标仍然可能丢失。
指标注册表只存在于某个 MetricShell 实例的内存中,实例重启后不会恢复之前的状态。
MetricShell 也不负责 Prometheus 目标发现时序数据存储,以及监控基础设施配置。
这些都是刻意划定的边界。
MetricShell 只解决一个具体问题,并不想长成另一套完整的监控系统。
到目前为止,MetricShell 的核心功能已经实现。
源代码、文档、架构决策和发布产物都放在 GitHub 上。
但还有一个问题没有答案:这套方案对其他工程师到底有多大价值?
我现在还不知道。
也许对大多数团队来说,现有工具已经完全够用。
也许这个问题出现的频率,比大家想象得更高。
也可能早就有人用更简单、更优雅的方式解决了它。
如果有三位开发者或 DevOps(开发运维)工程师开始使用 MetricShell,或者把它推荐给合适的场景,我就认为这个项目值得继续做下去。
毕竟,三个人里已经算上一个了:我。
我也非常希望看到针对架构的技术批评,特别是那些我可能遗漏的限制。
欢迎讨论技术方案、指出错误,或者提出完全不同的实现路径。
GitHub: https://github.com/Denki77/metricshell
在整个调研和实现过程中,同事和 AI 工具都帮了我很多。
特别感谢:
当然,也要感谢所有和我讨论过容器化工作负载监控的人。
很多新的工程方案,起点往往就是这样的讨论。