site logo

Marico's space

MetricShell:我们如何尝试解决短生命周期容器的指标问题

前端技术 2026-11-10 11:28:17 5

最近折腾了短生命周期容器的指标采集,踩了几个坑,这篇把问题说清楚。

问题本身

这几年,这个问题我听过很多次,也实际参与过不少讨论:如果应用运行在容器里,又没有对外暴露 HTTP(超文本传输协议)的 /metrics 端点,该怎么采集它的指标?

有些应用从设计之初就没打算这么做。也有人觉得,仅仅为了监控再塞一个 HTTP 服务进应用,开销没有必要。

当应用执行的是短生命周期任务时,这个问题就更有意思了,比如 CLI(命令行界面)命令、定时任务或者批处理作业。

我碰到的一个真实案例,是一个 PHP 7.4 单体应用,里面全是 CLI 命令,我们正在逐步把它迁移到容器环境。

应用本身已经稳定运行了很多年,后面还有多年积累的业务代码。我们既需要计数器、直方图这类业务指标,也需要数据库查询耗时这类技术指标。

放在物理服务器或虚拟机上,这事相对简单:把数据写进文件,定期做聚合,再推给监控系统就行。

这种方案甚至可以稳定跑上好几年。

但迁到 Kubernetes(容器编排平台)之后,情况完全变了。

容器启动、执行任务,然后退出。没人能保证 Prometheus(监控与告警系统)的下一次抓取恰好发生在容器还活着的时候。

即使应用能生成指标,仍然有个问题:到底什么时候采集?又由谁保证最后那一份状态还能被访问到?

假设一个定时命令五秒就执行完了,而 Prometheus 的抓取间隔是十五秒。

那么这个应用实例很可能永远不会被 Prometheus 看到。

当然可以把指标发送到独立服务,但这样又会冒出一串问题:谁负责聚合数据?怎么确保最后一次更新成功送达?网络延迟怎么办?指标生命周期又由谁管理?

如果应用报错退出呢?

如果容器已经收到终止信号呢?

如果监控系统临时不可用呢?

最初只是“采集几个指标”这么简单的需求,最后却慢慢变成了另一个需要开发、部署和维护的服务。

而这一切只是为了回答几个问题:处理了多少次操作?每次用了多长时间?应用查询数据库总共花了多少时间?

当然,也可以干脆不采了。但这些指标对应用团队很重要。缺了它们,想看清应用内部到底发生了什么就会困难很多。

而且作为技术负责人,我实在没法心安理得地把这些数据丢掉。关键时刻,它们可能正好是最有价值的证据。

寻找解决方案

架构委员会最早提出的方案,是自己写一个包装器。

乍一看挺合理:启动一个小进程接收指标,再通过 HTTP 接口暴露给 Prometheus。

但问题马上跟了上来:

谁来维护它?

写个原型是一回事,让它在容器里稳定运行是另一回事。

你得处理终止信号、子进程、退出码、超时、应用异常,以及指标处理代码自身出错的情况。

更重要的是,无论发生什么,监控逻辑都不能干扰业务逻辑,更不能让应用无法完成工作。

基础设施团队同样也不太愿意再部署一个组件。

而且他们的顾虑很合理。

Kubernetes 里已经有一套能用的监控体系。Prometheus 一直在抓取微服务,配置齐全,也有人负责维护。

为了几个定时任务,再搭一层基础设施,值得吗?

讨论进行到某个阶段,结论基本变成了这样:

“单体应用先别采这些指标也行。或者花点时间,让它适配新基础设施。”

可即便改造应用,也没有解决短生命周期进程的根本问题。

你可以让 PHP 应用暴露 /metrics,但如果它在下一轮抓取前就退出了,Prometheus 依然可能什么都采不到。

现有工具能不能用?

当然,先得看看别人都是怎么解决的。

应用自己提供 HTTP 端点

对长期运行的服务来说,这是最直接的办法。

应用暴露 /metrics,Prometheus 定期抓取。

可短生命周期 CLI 进程又会碰到生命周期问题:进程可能在下一轮抓取之前就退出了。

Prometheus Pushgateway

在部分批处理场景中,Pushgateway 已经是比较成熟的方案。

应用把指标推送给 Pushgateway,再由 Prometheus 从这里抓取。

不过 Pushgateway 自己也有指标生命周期。应用结束后,指标仍可能留在那里,因此必须明确管理数据的新鲜度和清理策略。

它还会引入一个新的基础设施组件,以及相应的网络通信。

对于服务级批处理任务,Pushgateway 很可能就是正确答案。但它不一定适合与短生命周期工作负载的每个实例一一关联的指标。

Node Exporter Textfile Collector

另一个方案是把指标写进文件,再通过 Node Exporter 暴露出来。

对某些工作负载来说,这套方法简单而且有效。

但它要求具备合适的文件共享方式和采集基础设施。在容器环境里,尤其是任务实例会被动态创建和删除时,还会引出一堆额外问题。

Sidecar Exporter

我们也可以在应用旁边再运行一个独立的 Exporter 容器。

这样能把应用职责和指标暴露分开。

代价则是多一个容器、多一份资源占用,还要处理两个进程之间的通信,并协调它们各自的生命周期。

OpenTelemetry

OpenTelemetry 提供了更完整的遥测能力。

OpenTelemetry Collector 可以接收应用指标,再通过 HTTP 端点提供给 Prometheus。

它也可以就地运行,甚至与应用放在同一个容器里,因此不一定非要部署独立、集中式服务。

但 Collector 的核心职责还是处理和投递遥测数据,它并不是一个拿来监管任意应用进程的现成监督器。

在我们的场景里,仍然得自己实现 PHP 进程启动、信号转发、子进程清理、退出码保留,以及工作负载结束后的有限时长最终抓取等待。

换句话说,可以基于 OpenTelemetry Collector 搭建出所需行为,但包装器还是得自己写、自己维护。

后来才发现,这个包装器恰恰就是整个方案里最值得复用的部分。

这些方案本身都没错。对某一种工作负载来说,其中任何一个都可能最合适。

但我们希望同时满足几个条件:

  • 保持现有 Prometheus 基础设施不变。
  • 不强制增加额外服务。
  • 不为了指标再增加一个容器。
  • 不要求应用自己提供 HTTP 端点。
  • 工作负载退出后,让最终指标继续保留有限时间。
  • 同一套方案既能用于 Docker,也能用于 Kubernetes。

我们考察的这些方案里,没有哪一个能在不引入额外取舍的情况下,同时满足全部条件。

会不会只有我们碰到这个问题?

后来我联系了其他公司的同事。

结果发现,他们也遇到过类似场景。

而且未必都是大型单体应用,有时就是一个普通 CLI 程序、定时任务,或者执行完任务就退出的 Worker。

工作负载不同,底层问题却差不多:应用负责把自己的事情做完,基础设施则要能按熟悉的方式采集指标。

调研过程中,我还用 ChatGPT、Gemini、DeepSeek 和 YandexGPT 讨论过备选方案,让它们质疑假设、拆解架构思路。

当然,LLM(大型语言模型)说它没见过某种方案,并不能证明这种方案不存在。最终还是得依靠现有工具的文档、能力对比,以及我们的真实需求来判断。

就这样,做一个独立工具的想法逐渐成形了。

划定边界

动手写代码之前,得先明确这个工具到底解决什么,不解决什么。

没必要再造一套现有工具的替代品。

如果应用已经提供了标准的 Prometheus 端点,而且运行时间足够长,可以稳定被抓取,就没必要再引入一个组件。

如果 Pushgateway 适合某个批处理场景,那就直接用 Pushgateway。

如果基础设施本来就围绕 OpenTelemetry 建设,也没必要为了一个新产品把已有体系推翻。

我们真正感兴趣的是范围更窄的一类场景:

容器化工作负载不能或不应该负责向 Prometheus 暴露自己的指标。

不是裸机,不是通用监控系统,也不是新的中心化指标聚合平台。

就是一个容器、里面的应用,以及标准的 Prometheus 拉取模式。

我们的想法是在应用容器内部放一个小型适配器。

它在本地接收指标,通过 /metrics 暴露数据,同时负责处理工作负载的生命周期。

不强制依赖中心服务。

不需要单独的 Sidecar 容器。

也不需要围绕一套新的监控架构重写应用。

需求

范围确定之后,基础需求也就清楚了。

第一,应用启动和结束的行为必须与没有包装器时完全一致。

退出码要保留,信号要正确转发,子进程也必须妥善处理和回收。

第二,应用必须能在本地提交指标,不需要连接外部监控基础设施。

第三,指标必须通过标准 HTTP /metrics 端点暴露,让 Prometheus 可以抓取。

第四,应用必须能够提交一份完整的指标快照。

第五,如果应用不想自己生成快照,也要提供一种简单方式,让它能够操作计数器、仪表盘和直方图。

第六,应用自然退出后,最终指标还要继续保留一段有限时间。

这里有一个必须说清楚的限制。

如果 Prometheus 不可用,或者没来得及发现这个容器,我们无法保证它一定能够采集到这些指标。

但可以再给它一次抓取机会,同时不需要让容器无限期存活。

最后,整套东西必须能运行在单个容器里,不能强制依赖额外基础设施。

MetricShell 是怎么来的

经过这轮调研和实现,最后做出来的是 MetricShell。

它的思路并不复杂。

我们不让应用自己管理 HTTP 端点,而是把这部分工作交给一个小型包装器进程。

MetricShell 作为容器里的 PID(进程标识符)1 启动,再拉起用户真正要执行的命令。

这个命令可能是 PHP CLI 应用、定时任务,也可能是一个 Worker。

MetricShell 负责监督进程、接收指标,并通过 /metrics 暴露数据。

与此同时,应用照常完成自己的业务。

我原以为这很简单

最初的计划看起来非常直接:启动进程,接收一些指标,通过 HTTP 提供出去,然后退出。

软件开发有条不知道算不算定律的规律:你以为很快能做完的事情,往往会拖很久。

这次也一样,原本以为两小时能搞定,最后悄悄变成了两个月。

各种问题很快堆了起来:

  • 如果应用已经退出,但最后一批指标还在处理,该怎么办?
  • 究竟什么才算最终状态?
  • 如果 SIGTERM 恰好在同一时刻到达呢?
  • 如果 Prometheus 正好在快照更新时开始读取呢?
  • 如果应用生出了一整群无翼龙,而且它们也在发送指标呢?

然后还有在 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 │ │
│ └──────────────────────────┘ │
└────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

工作负载自然退出后,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
Enter fullscreen mode Exit fullscreen mode

再打开一个终端:

curl http://127.0.0.1:9090/metrics
Enter fullscreen mode Exit fullscreen mode

这时就能看到 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"]
Enter fullscreen mode Exit fullscreen mode

这个示例只演示了如何集成包装器。应用仍然需要通过支持的本地接口之一提交指标,Prometheus 也必须配置为能够发现并抓取这个容器。

生产环境部署时,文档建议按 Digest 固定镜像版本,并使用文档中给出的配置。

仓库里提供了 Docker 和 Kubernetes 示例、协议说明、配置选项以及运行限制。

限制与取舍

任何架构决策都有代价。

MetricShell 也不例外。

它会在容器里增加一个进程,并承担正确监督工作负载的责任。

应用还是得自己生成或发布指标。MetricShell 不可能凭空判断哪些业务指标值得监控。

最终快照只在有限时间内可用。如果 Prometheus 错过这个窗口,指标仍然可能丢失。

指标注册表只存在于某个 MetricShell 实例的内存中,实例重启后不会恢复之前的状态。

MetricShell 也不负责 Prometheus 目标发现时序数据存储,以及监控基础设施配置。

这些都是刻意划定的边界。

MetricShell 只解决一个具体问题,并不想长成另一套完整的监控系统。

接下来做什么

到目前为止,MetricShell 的核心功能已经实现。

源代码、文档、架构决策和发布产物都放在 GitHub 上。

但还有一个问题没有答案:这套方案对其他工程师到底有多大价值?

我现在还不知道。

也许对大多数团队来说,现有工具已经完全够用。

也许这个问题出现的频率,比大家想象得更高。

也可能早就有人用更简单、更优雅的方式解决了它。

如果有三位开发者或 DevOps(开发运维)工程师开始使用 MetricShell,或者把它推荐给合适的场景,我就认为这个项目值得继续做下去。

毕竟,三个人里已经算上一个了:我。

我也非常希望看到针对架构的技术批评,特别是那些我可能遗漏的限制。

欢迎讨论技术方案、指出错误,或者提出完全不同的实现路径。

GitHub: https://github.com/Denki77/metricshell

致谢

在整个调研和实现过程中,同事和 AI 工具都帮了我很多。

特别感谢:

  • Leonid(SuperJob)——正是他的热情和支持,让这次工程探索一直推进下去。
  • Alexey(OpticCity)——在算法决策方面给出了可靠建议。
  • Dima(Tile.Expert)——不断提出让人不太舒服的问题,并和我讨论各种可能的解法。
  • ChatGPT 和 Codex——协助调研、实现以及评审技术决策。
  • Google Gemini——提出了恰到好处的问题,即便答案一开始并不明显。

当然,也要感谢所有和我讨论过容器化工作负载监控的人。

很多新的工程方案,起点往往就是这样的讨论。