docker compose 写了 depends_on,服务起来还是连不上数据库:healthcheck 与 conditio
现象: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 负责别让应用起太早,应用负责别一失败就退出。
- 点赞
- 收藏
- 关注作者
评论(0)