docker compose 写了 depends_on,服务起来还是连不上数据库:healthcheck 与 conditio

举报
yd_271851511 发表于 2026/11/01 10:11:06 2026/11/01
【摘要】 compose 里 depends_on 配好了,启动顺序也对,应用还是报连接被拒。原因是 depends_on 默认只保证「容器启动了」,不保证「服务就绪了」。本文讲清三个 condition 的语义差别、healthcheck 怎么写才真的有用,以及在应用侧该做的兜底。

现象:docker compose up,日志里应用先起来,报 `Connection refused` 连不上 postgres,然后自己退出。可 compose 文件里明明写了:


  depends_on:

    - postgres


顺序看起来是对的,postgres 容器也确实先创建了。问题在于 depends_on 的默认语义是"启动顺序",不是"就绪顺序"。


两条时间线


容器"启动"和"能接受连接"之间有一段空白。postgres 官方镜像的启动过程是:初始化数据目录(首次启动要跑 initdb,几秒到几十秒)→ 启动 postmaster → 开始监听 5432。而 `docker start` 在容器进程被拉起来的瞬间就返回了。compose 拿到这个返回,就认为依赖满足了,接着启动下一个服务。


于是应用在 3 秒后开始连数据库,而 postgres 还在 initdb,端口根本没开。Connection refused 就是这么来的。


depends_on 的三种 condition


compose 的长时间格式支持三种写法,语义差别很大:


  depends_on:

    postgres:

      condition: service_started # 默认值,只保证容器已启动

    redis:

      condition: service_healthy # 等到 healthcheck 通过

    migrate:

      condition: service_completed_successfully # 等到该容器正常退出(exit 0)


`service_started` 就是我上面说的坑:容器进程拉起来就算满足。

`service_healthy` 依赖目标容器自己声明了 healthcheck。

`service_completed_successfully` 适合一次性任务,比如数据库迁移、初始化脚本。


绝大多数人写的 depends_on 都是第一种,因为短格式(列表形式)只能表达这一种。


healthcheck 怎么写才真的有用


关键是 healthcheck 的命令必须真实反映"服务可用",而不是"进程还在"。


对 postgres:


  healthcheck:

    test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]

    interval: 5s

    timeout: 3s

    retries: 10

    start_period: 30s


几个参数的含义值得说清楚,因为它们决定了你等的到底是多久:


- `interval`:两次检查的间隔。

- `retries`:连续失败几次才判定 unhealthy。注意是"连续",中间成功一次就重置。

- `start_period`:容器启动后的宽限期,这段时间内失败**不计入** retries。这个参数是专门为"启动慢"设计的,很多人不知道,结果把 retries 调得很大来硬扛,不如设一个合理的 start_period。


总等待时间 ≈ start_period + interval × retries。上例是 30 + 5×10 = 80 秒,够 initdb 用了。


几个常见的错误 healthcheck:


- 用 `test: ["CMD", "curl", "localhost:5432"]` —— postgres 不是 HTTP 服务,curl 会失败,healthcheck 永远 unhealthy,反而卡死启动。

- 用 `test: ["CMD", "ps", "aux"]` —— 只证明进程在,不证明端口能连。initdb 阶段进程也在。

- 忘了镜像里没有 curl / pg_isready。slim 镜像尤其容易踩,healthcheck 直接 command not found。

- `interval` 设成 1s、`retries` 设成 3 —— 总窗口只有 3 秒,比不检查还糟——会把慢启动误判成 unhealthy,然后 compose 直接放弃。


注意 `$$` 是 compose 的转义,表示传给容器的是 `$POSTGRES_USER` 而不是在宿主机上展开。这个细节写错的话,pg_isready 会因为用户名是空而失败。


还有一点:`service_healthy` 只保证依赖方**在那一刻**健康。如果 postgres 起来之后又挂了(比如磁盘满),compose 不会帮你重启应用。它是启动编排工具,不是进程守护。


应用侧该做的事


即使 compose 配对了,应用侧也必须有重试。理由有两个:


一是 healthcheck 通过之后到应用真正发第一个请求,中间还有时间。二是生产环境你不用 compose(用 K8s),那套健康检查的语义又不一样。所以连接重试应该是应用的内建能力,而不是靠编排工具兜底。


对连接池(以 HikariCP 为例)关键是两个参数:


  initializationFailTimeout: 60000 # 启动时等多久才放弃

  connectionTimeout: 3000 # 单次获取连接的超时


`initializationFailTimeout` 默认是 1ms,意思是启动时拿不到连接立刻抛异常。设成 60 秒,或者设成负数(永不失败,后台持续重试)都能解决问题。这个默认值是"应用起来连不上数据库就退出"这种行为的主要来源。


K8s 环境下的对应做法


到了 K8s 就没有 depends_on 了。可用的手段是:


- initContainer 做等待,但要注意它只能等"能连通",判断逻辑要自己写。

- readinessProbe 让 Pod 在依赖没就绪时不接收流量,但不会阻止应用自身启动失败退出。

- 最实用的其实还是应用侧的启动重试——因为 K8s 的世界里,依赖随时可能短暂不可用,指望启动顺序是不现实的。


顺便说一句,K8s 里用 `kubectl apply` 同时创建 Deployment 和 Service,Service 的 Endpoint 也是异步填充的,所以"依赖服务已存在"和"能访问到它"之间的空白比 compose 里更大。


小结


depends_on 不等于等待就绪。要么用长格式的 `condition: service_healthy` 配合一个真实的 healthcheck(命令要能验证服务可用,别用 ps aux),要么干脆在应用侧做启动重试——后者才是真正跨环境通用的解法。两者一起做最稳:compose 负责别让应用起太早,应用负责别一失败就退出。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。