新闻详情

新闻详情

首页 / 资讯中心 / 详情

健康检查实录:一个冻住事件循环的探针、一道只查一次的存储门、一个 404 的生产探针

发布时间:2026/10/2 5:56:32来源:尧图网络
健康检查实录:一个冻住事件循环的探针、一道只查一次的存储门、一个 404 的生产探针
起因是一个老问题没有 Milvus 的时候api 容器永远unhealthy。之前我们以为这只是「readiness 慢」。这次在本机把那段探针单独拎出来跑了一遍pymilvus 连一个不可达地址要 10.1 秒而这 10.1 秒里同一个 asyncio 事件循环上每 0.5 秒一次的计时任务一次都没跑。readiness 端点是async def里面的连接却是同步的。顺着这条线又查出三件事存储那道硬门只在第一次成功前检查、生产 compose 的探针路径是 404、不加--env-file时.env里有一半的值会被默认值静默覆盖。每条附复现步骤与行号。先看结论问题api 容器显示unhealthy的时候它说的对吗它到底在检查什么一句话我们的健康检查有一处查得太重有一处查得太轻有一处查错了地方还有一处根本没收到你以为你给它的配置。太重向量库的 readiness 探针是async def里面却做了一次同步的 gRPC 连接。向量库不在时每次探针都会把整个 API 进程的事件循环冻住 10 秒以上本机实测 10.1 秒容器里之前实测整个请求 34 秒。太轻对象存储是 readiness 的硬门但它只在第一次成功之前真正检查之后结果被永久缓存。MinIO 在启动后挂掉readiness 照样报connected。查错了地方生产版 compose 的探针打的是/api/v1/health/ready而这个路径不存在返回 404。真正的端点在/health/ready。没收到配置不加--env-file .env启动时.env里凡是 compose 的environment:块也列了的键都会被 compose 里写死的默认值静默覆盖没列的键却照常生效。后面是逐层拆解最后一节是一张排障速查表。一、先把三个端点分清健康相关的路由在server/app/api/v1/health/router.py挂载时没有加前缀server/app/main.py:429app.include_router(health_router, tags[health])所以路径就是字面上的这几个端点查什么失败时/health什么都不查恒返回healthy—/health/live同上恒返回healthy—/health/ready数据库、对象存储、向量库数据库或存储失败 ⇒ 503向量库失败 ⇒ 仍 200只把vector标成unavailableliveness 什么都不查是对的它只回答「进程还活着吗」查依赖反而会让依赖抖一下就把进程重启掉。readiness 那三道检查写得很清楚router.py:82–106try: await db.execute(text(SELECT 1)) db_status connected except Exception: raise HTTPException(status_code503, detailDatabase is unavailable) try: await storage.ensure_ready() storage_status connected except Exception: raise HTTPException(status_code503, detailObject storage is unavailable) try: await vector.check_ready() vector_status connected except Exception: vector_status unavailable数据库和存储是硬门向量库是软门——docstring 给的理由是「向量库挂了非向量接口照样能用没必要把实例从流量里摘掉」router.py:74–77。这个设计本身是对的。问题出在每道门的实现细节上。二、compose 里谁在检查谁开发版docker/docker-compose.yml里各容器的健康检查是这样的服务健康检查间隔 / 超时 / 重试postgrespg_isready10s / 5s / 10redisredis-cli ping10s / 5s / 10miniocurl -f .../minio/health/live10s / 5s / 10etcdetcdctl endpoint health10s / 5s / 10milvuscurl -f .../healthz9091 端口15s / 5s / 10vaultvault status10s / 5s / 10apiPythonurlopen(/health/ready, timeout3)10s / 5s / 5webwget首页10s / 5s / 5outbox-dispatcherPythonurlopen(/metrics, timeout3)9201 端口10s / 5s / 5knowledge-ingest-worker无—scheduler无—另有三个一次性容器minio-init、migrate、bootstrap都是restart: no下游用condition: service_completed_successfully等它们以 0 退出。依赖是这样串起来的api等 postgres / redis / minio / milvus / vault 全部 healthy、等三个一次性容器成功退出docker-compose.yml:264–280web只等一件事——apihealthy:298–300。所以 api 的探针是整条链的咽喉它不过前端就永远不启动。三、症状api 一直 unhealthyweb 永远起不来这一段是上个月写最小部署拓扑时撞上的这里只简述把 milvus 和 etcd 从栈里拿掉/health/ready仍然返回正确的vector:unavailable但要等34 秒才返回。而 api 的探针自己只给 3 秒urlopen(..., timeout3)compose 再给 5 秒docker-compose.yml:282–284。34 秒对 3 秒必然失败于是 api 恒为unhealthyweb 因为depends_on: api: service_healthy永远不启动。当时给的绕法是docker compose up -d --no-deps web现在仍然有效。当时我们还写了一句「api 显示 unhealthy 是预期的服务本身是好的。」这句话只对了一半。四、那十几秒里整个 API 是冻住的看向量库探针的实现server/app/adapters/vector/milvus.py:112–115async def check_ready(self) - None: Probe vector-store connectivity; raise if unreachable (for readiness checks). self._ensure_connected() utility.get_server_version()check_ready是async def但它调用的_ensure_connected():89–99里是connections.connect(...)一个同步的 pymilvus 调用。在async def里直接做同步网络 I/O意味着这段时间事件循环什么都干不了。我在本机把这段单独拎出来跑了一遍soit/server/.venvpymilvus 2.5.11连一个没有服务监听的端口同时在同一个事件循环上挂一个每 0.5 秒记一次时间的任务probe failed: MilvusException probe 10.1s, max gap between 0.5s ticks 10.3s, ticks during probe 010.1 秒里计时任务一次都没跑。这 10.1 秒是 pymilvus 自己的默认连接超时容器里那 34 秒还叠加了 Docker 网络里对一个不存在的服务名做 DNS 解析的时间。再连续探两次看失败会不会被记住probe 1 failed: MilvusException probe 1 10.2s probe 2 failed: MilvusException probe 2 10.1s不会。连接失败时不会留下任何连接connections.has_connection(default)下次还是假每一次探针都要重新付满全价。把这个放回 compose 里开发版的 api 是单进程uvicorndocker-compose.yml:261没有--workers健康检查每 10 秒一次。以下是推论不是在容器里观测到的每次探针都会把这唯一的事件循环冻住 10 秒以上容器里按 34 秒算而 compose 的探针 3 秒就放弃、10 秒后再来一次——服务端那边上一次还没卡完下一次已经排上了。也就是说在没有向量库的拓扑里这个 API 进程大部分时间都卡在健康检查里。上个月那次登录能成功是因为登录请求要么落在两次探针之间要么排队等到了探针结束——「服务本身是好的」但它是在探针的间隙里好的。对照一下同一个仓库里另外两个适配器都做对了pgvector 后端的check_ready是await asyncio.to_thread(self._check_ready)adapters/vector/pgvector.py:100–102阻塞调用丢进线程事件循环不受影响。存储适配器的每个同步操作都经过_run_sync_operationasyncio.to_thread外面再包一层asyncio.wait_foradapters/storage/fsspec.py:23–35默认超时 10 秒settings.py:178storage_operation_timeout_seconds。所以修法也很清楚Milvus 的探针照存储那样包一层to_threadwait_for超时给一两秒就够。issue #44 里建议的正是这个写法只是 issue 当时只看到了「慢」没看到「冻」。五、反过来存储那道门只检查一次存储是硬门按说应该是最严的一道。看ensure_readyadapters/storage/fsspec.py:120–158async def ensure_ready(self) - None: if self._ready: return async with self._ready_lock: if self._ready: return ... exists await _run_sync_operation(storage_ready, ..., self.fs.exists, readiness_root) ... if not exists: raise KernelError(STORAGE_NOT_READY, Storage root is not ready, ...) self._ready True第一次成功之后_ready被置为True此后每次调用在第一行就直接返回。这个缓存能活多久取决于适配器实例活多久。readiness 用的实例来自依赖注入容器的get(storage_port)router.py:44–47而容器的get()会把工厂造出来的实例缓存成单例wiring/container.py:350–372。所以一个 API 进程的生命周期里存储这道门只真正检查到第一次成功为止。本机复现fsspec 的memory://后端不依赖任何外部服务1st probe: ok root removed, exists False 2nd probe on same instance: ok (cached) fresh instance raised: KernelError STORAGE_NOT_READY Storage root is not ready根目录已经不存在了同一个实例仍然报就绪换一个新实例才报错。这对启动阶段是有用的MinIO 没起来时api 不会被判为 ready。但启动之后 MinIO 挂了/health/ready依然会返回storage:connected而真正读写对象的请求会各自失败。ensure_ready这个名字其实说得很准——它是「确保初始化过」不是「探测现在通不通」问题在于 readiness 把它当成了后者来用。现有的单测tests/unit/test_health_readiness.py:52–57用一个直接抛异常的假存储验证了 503 分支没有覆盖「先成功、后失联」这种情况。六、生产版 compose 的探针打的是一个不存在的路径开发版的探针打/health/ready。生产版docker/docker-compose.production.yml:151–156healthcheck: test: [CMD-SHELL, python -c \import urllib.request;urllib.request.urlopen(http://localhost:9200/api/v1/health/ready)\] interval: 15s timeout: 10s retries: 5 start_period: 30s多了一个/api/v1前缀。第一节说过health 路由挂载时没有前缀。我用 Starlette 的TestClient直接打当前主干上的应用/api/v1/health/ready 404 {success:false,code:NOT_FOUND,message:Not Found,... /health/live 200 {success:true,code:OK,message:OK,data:{status:...urlopen碰到 404 会抛HTTPError探针进程以非零退出——生产栈里的 api 容器会一直是unhealthy。它之所以没把生产栈搞挂是因为生产版里没有任何服务用condition: service_healthy等 api网关和前端写的都是短格式的depends_on: - apidocker-compose.production.yml:116–118、:166–167只等容器启动、不看健康。于是这个探针失败得悄无声息只在docker ps里挂一个unhealthy以及让任何依赖容器健康状态的外部监控永远报警。两个附带问题生产探针的urlopen没有给超时开发版给了timeout3只能靠 compose 的 10 秒兜底。从外面也打不到 readiness生产网关 Caddy 只把/api/*转给 api其余一律转给前端docker/production/Caddyfile:18–19。按这个路由规则外部负载均衡器访问/health/ready会落到前端容器上。仓库里还有两份文档也写的是带前缀的路径docs/QUALITY_GATE.md:199、docs/operations/database-connections.md:74而快速开始文档写的是对的docs/quickstart.md:76。打算提一个 issue一并改掉。七、.env只生效了一半这是 issue #27 里点到的第一个坑值得单独讲清楚因为它的症状不是「启动失败」而是「启动成功了但用的不是你的配置」。开发版 compose 里每个服务端容器同时有两处配置来源docker-compose.yml:159–215env_file: - path: ../.env required: false environment: DATABASE_PASS: ${DATABASE_PASS:-soit} SECRET_KEY: ${SECRET_KEY:-change-me} ...两条 compose 规则叠在一起就出事了同一个键在environment:和env_file:里都有时environment:赢。environment:里的${DATABASE_PASS:-soit}是插值插值读的是项目目录下的.env也就是 compose 文件所在的docker/目录或者命令行--env-file指定的文件——不是env_file:指向的那个../.env。所以在仓库根目录放一份.env、不加--env-file直接启动${DATABASE_PASS:-soit}找不到值落回默认的soit然后覆盖掉env_file读进来的那一份。用docker compose config就能验证不需要起任何容器。根目录.env里放两行DATABASE_PASSfrom-root-env MY_ONLY_KEYonly-in-env-file--- without --env-file: DATABASE_PASS: soit MY_ONLY_KEY: only-in-env-file --- with --env-file .env: DATABASE_PASS: from-root-env MY_ONLY_KEY: only-in-env-file同一份文件一个键生效、一个键被静默换成了默认值。environment:块里列了四十多个键数据库、Redis、MinIO、Vault、SECRET_KEY、各家模型的 API key……它们全都有这个问题没列在里面的键比如MILVUS_MODE、VECTOR_BACKEND反而照常透传。最麻烦的是它看起来一切正常postgres 容器的POSTGRES_PASSWORD也走同一个默认值两边都是soit于是数据库连得上、服务起得来只是你设的密码和SECRET_KEY根本没用上。快速开始文档的命令里写了--env-file .envdocs/quickstart.md:13照着敲不会踩自己改命令、或者从docker/目录里直接docker compose up的人会踩。八、别拿 Milvus Lite 当容器里的绕法九月初仓库加了一个MILVUS_MODElite用嵌入式的 Milvus Lite 读写本地文件不需要 milvus / etcd 容器。看起来正好能解决第三节的问题但它不适合用在这套 compose 里它是单进程的文件库。提交说明写得很直白一个进程独占这个数据库文件别的进程读不到它。在 compose 里写向量的是knowledge-ingest-worker查向量的是api两个独立容器之间没有共享卷。按这个结构两边会各自打开自己容器里的那个文件。生产环境直接拒绝 litesettings.py:550–554。它的定位是「本机调试知识库」docs/development.md的 Milvus Lite 一节不是「砍掉 Milvus 的部署方式」。九、排障速查表症状跑这条看什么怎么办docker compose ps里 api 是unhealthydocker inspect --format {{json .State.Health}} soit-api-1Log里最近 5 次探针的Outputtimed out⇒ 探针超时HTTP Error 503⇒ 数据库或存储没过HTTP Error 404⇒ 探针路径错了分别看下面三行探针超时curl -s -m 60 -o /dev/null -w %{http_code} %{time_total}s\n http://localhost:9200/health/ready200 但远超 3 秒 ⇒ 向量库不可达第四节起 milvus etcd或接受 unhealthy用--no-deps起 web503同上去掉-o /dev/null看返回体Database is unavailable或Object storage is unavailable查 postgres / minio 容器与连接配置注意存储这道门只在启动时有效第五节404看 compose 文件里的探针路径带了/api/v1前缀改成/health/ready第六节web 一直不启动docker compose -f docker/docker-compose.yml ps -aapi 不是 healthy先解决 api临时可docker compose -f docker/docker-compose.yml up -d --no-deps webapi 根本没起来docker compose -f docker/docker-compose.yml ps -a migrate bootstrap minio-init一次性容器是不是Exited (0)非 0 就看docker compose -f docker/docker-compose.yml logs migrate bootstrap配置好像没生效docker compose --env-file .env -f docker/docker-compose.yml config api与去掉--env-file的输出对比你改的键在两份输出里是否一致启动命令一律带--env-file .env第七节worker 显示Up但不干活docker compose -f docker/docker-compose.yml logs --tail 50 knowledge-ingest-worker scheduler这两个容器没有健康检查Up只代表进程在看日志目前没有更好的信号十、现在还对不上的地方按老规矩自己查出来的先自己列。①向量库探针在事件循环里做同步连接且没有超时。每次探针冻住整个进程 10 秒以上。issue #44 已经记了「慢」打算在 #44 下补上「冻」的复现。修法to_threadwait_for与存储适配器同构。②存储这道硬门只检查到第一次成功为止。启动后存储失联readiness 不会反映。影响编排系统不会把一个存储已经断开的实例摘掉。绕法监控对象存储本身别只看api 的 readiness。打算提一个 issue。③生产版 compose 的 api 探针打到 404且没有超时。影响生产栈的 api 恒unhealthy外部监控恒报警。打算提一个 issue。④两份文档里的 readiness 路径带了不存在的前缀QUALITY_GATE.md:199、operations/database-connections.md:74。与③一起改。⑤生产网关没有把 readiness 暴露出去。按 Caddyfile 的路由规则/health/ready会落到前端。影响外部负载均衡器没有可用的就绪探测地址。⑥ReadyResponse的 docstring 说status可能是not_readyrouter.py:31–32但代码从不返回它不就绪时直接抛 503。小问题但写客户端的人会按 docstring 去判断字段。⑦knowledge-ingest-worker和scheduler没有健康检查。进程卡死时 compose 仍显示Up。⑧不加--env-file时.env里与environment:块重名的键被静默覆盖。这是 compose 的既定语义不是 bug但我们的 compose 写法让它变得很容易踩而且踩了之后没有任何报错。打算在 issue #27 的排障文档里把它列为第一条。另有一处没有核实、只是没看到的数据库引擎没有显式配置连接超时或连接池等待超时infra/db/session.py:65–73只传了pool_pre_ping、pool_size、max_overflow所以数据库那道门在「数据库主机不可达」时要等多久取决于驱动的默认值。这次没有测不下结论。坦白局这次有实跑但没有起容器。实跑的四件事都在本机pymilvus 的连接耗时与事件循环阻塞、存储适配器的缓存memory://后端、TestClient打两个路径、docker compose config验证插值。代码对应soit/主干提交8a24af4。容器里那 34 秒是上个月的实测「API 在容器里大部分时间是冻住的」是推论。依据是单进程 uvicorn 加上本机实测的事件循环阻塞我没有在容器里测请求排队的时长。本机的 10.1 秒不等于你那里的数字。它是 pymilvus 的默认连接超时服务名解析不到、端口被防火墙丢包、主机在但端口没开耗时都不一样。我只读了社区版。这篇更正了我们自己上一篇里的一句话。「服务本身是好的」写的时候是基于「登录成功了」这个观测观测本身没错推论少走了一步。利益相关我是 SOIT 的维护者。一句话结论健康检查不是样板代码。一个探针该查什么、查几次、花多久、会不会拖慢被查的服务每一条都要单独核对而healthy只是说「上一次探针的进程以 0 退出了」。来试试也来挑刺仓库在github.com/soit-ai/soit。三个最快的核对入口server/app/adapters/vector/milvus.py:112–115对照adapters/vector/pgvector.py:100–102看一个async def里有没有to_threadserver/app/adapters/storage/fsspec.py:120–158从if self._ready:那一行读下去把docker/docker-compose.production.yml:152的探针路径和server/app/main.py:429的挂载方式放在一起看。如果你在自己的部署里碰到了速查表之外的症状欢迎开 issue——issue #27 那份排障文档还没人认领你遇到的坑可能正好是下一条。本文首发于 SOIT 官网博客https://soit.ai/zh-cn/blog/what-your-healthcheck-checks
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

报文修改的本质:从物理层校验到eBPF内核级篡改 2026/10/2 7:30:24

报文修改的本质:从物理层校验到eBPF内核级篡改

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
雷达FPGA信号处理核心:DDC数字下变频原理与实战 2026/10/2 7:30:23

雷达FPGA信号处理核心:DDC数字下变频原理与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
基于大语言模型构建医疗AI Agent:从架构设计到代码实现 2026/10/2 7:30:16

基于大语言模型构建医疗AI Agent:从架构设计到代码实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
充电桩继电器选型指南:核心参数、供应商梯队与失效排查 2026/10/2 7:30:16

充电桩继电器选型指南:核心参数、供应商梯队与失效排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
PyQt5桌面应用现代化改造:qfluentwidgets实战与避坑指南 2026/10/2 7:30:16

PyQt5桌面应用现代化改造:qfluentwidgets实战与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
空间面板杜宾模型实战:New Elhorst Panel Code 从跑通到避坑 2026/10/2 7:30:09

空间面板杜宾模型实战:New Elhorst Panel Code 从跑通到避坑

简介:这份资源是面向空间计量经济学研究者与高年级学生的MATLAB代码包,聚焦空间杜宾模型、空间滞后模型与空间误差模型在面板数据中的实现,可帮助解决模型设定、参数估计与代码报错等实际问题。压缩包共57个文件,以53个m脚本为核心…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉