docker-compose 里 depends_on 写了不管用,启动顺序得靠 healthcheck
家里那台小机上跑了几个自建服务,其中一个 Python 应用要连 PostgreSQL。每次重启机器之后,应用容器总是起不来,日志第一行就是连不上数据库:
psycopg2.OperationalError: could not connect to server: Connection refused
Is the server running on host "db" and accepting TCP/IP connections?
但神奇的是,我手动 docker compose restart app 一次就好了。一开始我以为是玄学,后来发现是我把 depends_on 理解错了。
我的 compose 文件里明明写了依赖:
services:
app:
build: .
depends_on:
- db
db:
image: postgres:16
depends_on 到底保证了什么
查了一圈才反应过来:depends_on 只保证容器被创建并启动的顺序,也就是 compose 先去 start db 这个容器,再去 start app。它完全不关心 db 里面的 PostgreSQL 进程有没有初始化完、有没有开始监听端口。
而 PostgreSQL 首次启动要跑 initdb,数据目录大的话还要做一轮恢复,十几二十秒很常见。这段时间里容器已经是 running 状态了,但 5432 端口还没人接。app 恰好在这时候去连,自然被拒。
我手动 restart 一次能好,就是因为那时候数据库已经就绪了。
顺带一提,短语法 - db 和长语法写法不一样,只有长语法能加条件:
depends_on:
db:
condition: service_started # 默认就是这个,等价于短语法
service_started 是默认行为,所以它跟我原来写的完全等价,加了也没用。
让它真的等:healthcheck
要真正等到"服务可用",得让 compose 知道 db 什么时候算健康。这就是 healthcheck 干的事。
数据库这类官方镜像,一般都有自带的探测命令:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s
pg_isready 是 PostgreSQL 自带的小工具,只做连接性检查,很轻。test 用上面这种数组形式是最稳的,不会踩到 shell 引号的坑。
MySQL / MariaDB 对应的是:
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -p$$MYSQL_ROOT_PASSWORD --silent"]
注意 compose 文件里写 $$ 才是字面的 $,不然会被 compose 自己先做一轮变量替换。这个我第一次写的时候漏了,healthcheck 一直失败,查了半天才发现密码根本没传进去。
Redis 更简单:
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
然后把 depends_on 的条件改掉
光有 healthcheck 还不够,依赖方那边要跟着改:
services:
app:
build: .
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s
service_healthy 的意思是:等 db 的健康检查通过之后,才启动 app。改完之后我连续重启了几次机器,应用再没报过 connection refused。
几个参数的含义顺便记一下,调的时候心里有数:
interval:两次检查之间的间隔timeout:单次检查超过这个时间就算失败retries:连续失败几次才判定为 unhealthystart_period:启动初期的宽限期,这段时间里的失败不计入 retries,专门给初始化慢的服务用的
start_period 是我最后才加的。之前数据库恢复数据比较慢,开头几次检查全失败,retries 很快用完被判 unhealthy,结果 app 反而永远起不来了。加上宽限期之后就正常了。
自己写的应用怎么暴露健康状态
如果是自己 build 的镜像,最好在 Dockerfile 里就带上:
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -f http://localhost:8000/healthz || exit 1
应用那边加一个 /healthz 接口,别只返回 200,顺手查一下数据库还能不能连:
@app.get("/healthz")
def healthz():
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
return {"ok": True}
这样上游依赖挂掉的时候,这个容器自己也会变成 unhealthy,别的服务就不会再往它身上发请求了。
这里有个坑:很多精简镜像(尤其是 alpine 系)没有装 curl,CMD curl -f ... 会直接 command not found,然后一直失败。要么在镜像里装一下,要么换成镜像里本来就有的 python:
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/healthz').status==200 else 1)"
我就在这上面栽过一次,健康检查永远 unhealthy,但应用本身跑得好好的。
怎么确认它到底健康不健康
不要靠猜,直接看:
docker compose ps
输出里 db 那行会带状态,像 Up 2 minutes (healthy) 或者 (health: starting)。一直卡在 starting 就说明检查没过。
想看每次检查的具体输出:
docker inspect --format '{{json .State.Health}}' <容器名> | python -m json.tool
里面的 Log 数组会列出最近几次检查的 ExitCode 和 Output,命令写错了、密码不对、端口不通,在这里全都看得见。前面那个 $$ 的问题就是靠这条命令定位到的。
小结
现在我的习惯是:compose 里两个服务只要有依赖关系,就给被依赖的那个配 healthcheck,再把依赖方写成 condition: service_healthy。多写五六行,换来的是重启之后不用再手动补一次 restart。
depends_on 本身没毛病,只是它管的是容器启动顺序,不是服务的就绪状态——把这两件事分开想,这个问题就不容易再犯了。
- 点赞
- 收藏
- 关注作者
评论(0)