
Yeet 推出 CI 运行器监控工具,可识别卡慢步骤与残留进程
来源标题:Who broke CI this time?
Yeet 发布了一款 CI 运行器监控工具,能自动标记耗时异常变长的步骤,并发现上一次任务遗留的容器或进程,在它们拖垮机器前清理掉。该工具每秒查询一次系统图,从进程树中直接得出当前步骤与仓库信息,并通过 /metrics 暴露 Prometheus 指标供告警使用。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
历史记录还发现了几个我本来可能会漏掉的额外问题。某个步骤如果耗时达到该仓库正常水平的两倍,就会被标记为慢;上一个作业遗留的容器或进程会作为残留物显示出来,以便在它们拖垮机器之前清理掉。
每个 agent 还暴露 /metrics 供 Prometheus 抓取,把健康原因作为带标签的 gauge 暴露出来,这样你就可以用现有的告警系统针对 runner_watch_listener_connected == 0 或 runner_watch_health_level >= 2 告警。agent 上和 hub 上的 /llms.txt 提供同样的纯文本信息,如果你更愿意把 LLM 指向它而不是自己看仪表盘的话(毕竟我们偶尔都难免 vibe coding)。
runner 上的所有数据都来自每秒一次对 yeet 系统图的查询:进程表、CPU 计数器、内存、负载以及 docker 容器列表。这就是全部的数据来源。GitHub runner 就是一个 Runner.Listener 进程,有作业时带一个 Runner.Worker 子进程,每个步骤又是 worker 的子进程。所以无论忙还是闲,当前步骤和仓库都能直接从进程树里读出来。另一个每五秒一次的较慢查询,会通过 socket inode 把相关进程的打开 fd 与 TCP 表关联起来。仪表盘就是靠这个知道 listener 和当前步骤连到了什么、以及某个连接是否还挂着。作业内部不运行任何东西,runner 软件也没有被改动。进程树早就知道这一切,只是在等人来问。
agent 是一个 yeet service,实践中就是在仓库顶层放一个 service.toml。它列出各个 unit:isolate,就是守护进程运行的脚本;以及一个带路由的 web server,负责把连接交给它们。
alias = "runner-watch" restart = "always" working-directory = "." [units.app] isolate = "./agent.js" [units.metrics] isolate = "./metrics.js" lazy = true [units.web] web-server = "http://0.0.0.0:9420/" [units.web.routes."/tty"] target = "app" [units.web.routes."/metrics"] target = "metrics" portal = "console" tenancy = "per-connection" app 永久运行,每个 tick 往它的 tty 写一帧 JSON。web server 把这个 tty 挂载在 /tty 上,所以任何连接上来的人每秒都会收到一帧。metrics 是 lazy 且 per-connection 的:一个 GET 会启动一个新的 isolate,采一次样本,打印 Prometheus 格式并退出,退出即结束响应。/llms.txt 用的是同样的技巧,只是输出 markdown。
这个文件就是全部的部署产物。我有一堆 ansible role 的坟场,那些在上一版 Ubuntu LTS 上跑得很好,而这是很久以来我第一次往机群里部署东西却没往坟场里添新成员。
这也是为什么安装只有一行。yeet service apply 会克隆仓库、读取顶层的 service.toml,并让那台主机上的服务与之匹配。push 之后再跑同一行就是升级。它只重启有变化的 isolate,web server 保持运行,所以仪表盘连眼都不眨一下。这是我最满意的部分,因为往十六台 runner 上部署以前是我一直拖着不做的苦差事,直到有人问为什么修复还没上线。
hub 不过又是一个 service 文件:一条给仪表盘的静态路由,以及每台 runner 一条 upstream 路由,/r/<name>/ 经 Tailscale 转发到那台 runner,包含 websocket。它是生成的,因为名册来自 tailscale status,但形状是一样的。hub 从不与 GitHub 通信。它只知道十六个主机名并转发流量,我对它的信任就这么多。
[units.web.routes."/"] static = "./public" [units.web.routes."/r/github-actions-runner-1"] upstream = "http://github-actions-runner-1:9420/" 这里是 runner 让自己可见的地方,而方向和我预期的正好相反。每个 yeet web server 都在 /.well-known/yeet/manifest.json 发布自己的路由清单。当 hub 获得一条 upstream 路由时,它会拨通那台 runner 的 manifest 流并保持 websocket 打开,runner 每次有变化就沿着它推送完整的路由列表。hub 把这些合并进自己的 manifest,所以只要那台 runner 还在响应,hub 的清单里就会列出它的 /tty 和 /metrics。如果某台 runner 失联,它的路由就会消失,hub 每隔几秒重拨直到它回来,没有状态需要清理,因为每次推送都是完整快照。
仪表盘是故意做笨的。它获取 hub 的 manifest,找到里面每个 /r/<name>/tty,通过 hub 向每一个打开 websocket,然后在页面里做聚合。没人告诉它有多少台 runner。加一台 runner 就是多一条路由加一次 apply。我在一次故障期间做过这件事。一次就成功了,这条流水线里的其他东西我可不敢这么说。
代码在 github.com/yeet-src/runner-watch,hub 和 job hook 都包含在 README 里。
哦对了,我打开仪表盘时它告诉我的第一件事,就是我有一台 runner VM 上根本没有 actions-runner 进程。所以,是的。那台是我干的。
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。