ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PostgreSQL 就绪检查实战:从 pg_isready 到 K8s 探针

PostgreSQL 就绪检查实战:从 pg_isready 到 K8s 探针 先讲一个我经常遇到的场面估计你也碰到过docker-compose 里定义了 postgres 和 web 两个服务web 的 depends_on 写了 db结果容器启动顺序看着没问题web 的日志却一行could not connect to server: Connection refused。新手第一反应是 sleep 10 再启动仿佛多睡几秒世界就安静了。其实问题不在顺序而在“容器起来了”和“PostgreSQL 能接受查询”是两回事。这次聊的就是标题里那件事——Check Wait For PostgreSQL To Become Ready也就是怎么正确、靠谱地等待 PostgreSQL 真正就绪。这篇文章适合容器编排、CI/CD、裸机部署脚本里被数据库启动顺序折磨过的人看完你就能写出一套可复用的等待逻辑而不是靠猜时间。1. 为什么“启动”不等于“就绪”1.1 先弄清 PostgreSQL 实例的启动流程很多人以为 PostgreSQL 启动就是“进程跑起来”这一下子实际上一个实例从初始化到对外可服务经历了好几个阶段。首先是初始化数据目录也就是 initdb 干的事它负责生成 pg_control、配置文件、默认数据库模板这一步通常在安装或首次运行时完成耗时看你磁盘速度。接着才是真正启动 postmaster 主进程它会读取 postgresql.conf 和 pg_hba.conf分配共享内存然后拉起一系列后台进程比如 checkpointer、bgwriter、walwriter、autovacuum launcher、stats collector 等。最后是预写式日志恢复也就是 WAL replay如果上次崩溃或者断电这一步会把未完成的事务重新处理完。只有这些全部弄完日志里才会出现那句经典标志database system is ready to accept connections。在这个流程里监听端口打开得其实相当早。postmaster 在完成共享内存初始化、但还没结束 WAL recovery 的时候就可能已经 bind 了 5432 端口。也就是说你用nc -zv去探测端口基本上是通的但这时真的拿 psql 去连多半会得到一句the database system is starting up。这就是最容易踩的第一个坑TCP 端口通不代表实例 ready。我见过不止一个团队把“端口探测成功”当成就绪信号然后应用连上去运气好遇到的是连接被拒运气不好遇到的是恢复中业务直接报 “the database system is starting up”反而比连接被拒更难排查。1.2 “容器状态是 up”也不等于就绪容器化和 K8s 普及之后又多了一层误判docker ps看到 ContainerStatus 是 running或者 Kubernetes 里 Pod 是 Running就以为数据库可以用了。实际上容器是 running 只说明容器主进程还活着和 PostgreSQL 有没有完成启动是两个概念。官方 postgres 镜像启动时如果发现数据目录未初始化会先跑 entrypoint 脚本调用 initdb再 exec postgres 主进程。这段时间里容器状态一样是 up但 database 完全连不上。同样的道理也适用于裸机环境里的 systemd。你systemctl start postgresql之后systemctl 会在进程退出码非零时才认为失败只要 postmaster fork 成功后没立刻退出service 就显示 active哪怕后端还在做 WAL recovery。所以“服务状态 active”和“数据库 ready”之间的时间差少则几百毫秒多则几分钟——取决于数据量、上次崩溃点和 IO 速度。理解了这一点下面的所有检查手段才有的放矢。2. 常见的就绪检查手段怎么选才不坑2.1 pg_isready官方给的“一刀”PostgreSQL 官方自带一个命令行工具名字就直白得很叫pg_isready。它专门用来检测一个 PostgreSQL 服务器是否接受连接语法大致是pg_isready -h 127.0.0.1 -p 5432 -U postgres -d postgres这个工具的特点是轻量、快而且不需要输入密码。它只在 TCP 层完成一次身份握手能判断出 postmaster 是否已经处于可以接受新连接的状态。退出码有三个含义0 表示服务器接受连接1 表示服务器拒绝连接2 表示没有响应。注意“拒绝连接”和“没有响应”不一样前者通常说明服务器已经启动、正在运行但可能因为连接数满、pg_hba.conf 配置、SSL 参数等原因把你的探针拒了后者一般说明端口没监听或者进程还没起来。实际写脚本时很多人只用 pg_isready 就够了尤其是 Docker 官方 postgres 镜像里已经内置了这个二进制直接用pg_isready -U postgres就能探测。但它有一个边界pg_isready 只验证“PostgreSQL 接受连接”这一层不验证下面的具体查询能力。如果你对它要求更高比如要等到某个数据库创建完成、或者某个插件装好单靠 pg_isready 是不够的。2.2 psql SELECT 1最接近业务视角的探测比 pg_isready 更进一步的做法是用 psql 真的发一条 SQL最常见的哨兵语句是SELECT 1。这条语句成本极低却能验证完整的链路TCP 握手、postmaster 接受连接、认证通过、然后能执行查询。实践里我通常这样写psql -h $HOST -p $PORT -U $USER -d $DB -Atqc SELECT 1几个参数解释一下-A关闭对齐输出-t只输出元组-q安静模式-c执行单条命令。输出是1就说明数据库真能干活了。如果有问题你往往能看到比“连接被拒”更有诊断价值的报错比如password authentication failed、FATAL: sorry, too many clients already、或者the database system is starting up。这三种错误分别指向认证配置、连接池容量、WAL 恢复进度每一个都值得单独处理。不过要小心psql 是客户端工具并不是所有环境都装了。最典型的就是 Alpine 瘦身镜像里面可能只有内核没有 psql这时候要么装postgresql-client要么就退而求其次只做上游探测。另一个坑是如果服务器要求密码而你在脚本里没有设置PGPASSWORD环境变量psql 会进入交互式输入密码状态直接把你整个等待脚本卡死。后面第 5 章我会专门讲怎么绕过这几个坑。2.3 纯 TCP 端口探测快速但只能判断“通不通”还有一种非常常见的做法就是用 bash 的/dev/tcp、nc、或者 Python 的socket去探测端口timeout 3 bash -c /dev/tcp/$HOST/$PORT echo port open相比 pg_isready 和 psql这种办法胜在零依赖任何环境都能跑。但它的语义也很弱“端口能连上”离“PostgreSQL 就绪”差得有点远。原因前面说了postmaster 可能在 WAL recovery 阶段就已经监听端口另外如果前面有一个 HAProxy、PgBouncer、或者云数据库的负载均衡器端口通只说明代理活着不代表后面真实实例可用。所以我的建议是端口探测只适合做“快速失败”判断比如判断网络不通、防火墙没放行、宿主机没起来真正决定“继续让应用启动”的哨兵应当用 pg_isready 和 psql 至少二选一。3. 手写一个健壮的 wait-for-postgres 脚本3.1 脚本设计一个脚本应对容器、CI、服务器聊完原理和工具这里给出一套我自己在项目里反复用的方案。目标很明确支持超时、支持可配置间隔、能区分“还没起来”和“真出问题”并且尽量只在标准 Bash 环境跑不引入新依赖。适用场景包括docker-compose 里 web 服务启动前等待、CI 流水线里等待测试库、以及服务器上由 systemd 拉起应用前的预检。设计上有几个关键决定第一用“超时截止时间”而不是“最大重试次数”。原因很简单重试次数 x 间隔算出来的总时长会因为 sleep 不精确而漂移用date %s计算 deadline到点就退出逻辑清晰。第二优先用 pg_isready然后立刻用 psql 再验证一次。两个都过才算 ready如果环境里没有这两个工具最后才退化为 TCP 端口探测。第三所有中间日志都打印到 stderr方便和应用的 stdout 区分而在 CI 里也不会污染业务输出。3.2 完整脚本与逐段解析下面是我精简过的一个版本直接复制就能用。需要说明的是为了让脚本可以在没有 psql 的容器里勉强工作我加了一层 fallback如果你希望更严格把 fallback 删掉即可。#!/usr/bin/env bash set -euo pipefail HOST${POSTGRES_HOST:-127.0.0.1} PORT${POSTGRES_PORT:-5432} USER${POSTGRES_USER:-postgres} DB${POSTGRES_DB:-postgres} TIMEOUT${WAIT_TIMEOUT:-60} INTERVAL${WAIT_INTERVAL:-2} PGPASSWORD${PGPASSWORD:-} deadline$(( $(date %s) TIMEOUT )) log() { echo [wait-for-postgres] $* 2; } while :; do now$(date %s) if [ $now -ge $deadline ]; then log timeout after ${TIMEOUT}s: PostgreSQL at ${HOST}:${PORT} is still not ready exit 1 fi if command -v pg_isready /dev/null 21; then if pg_isready -h $HOST -p $PORT -U $USER -d $DB /dev/null 21; then if command -v psql /dev/null 21; then if PGCONNECT_TIMEOUT$INTERVAL psql -w -h $HOST -p $PORT -U $USER -d $DB -Atqc SELECT 1 /dev/null 21; then log PostgreSQL at ${HOST}:${PORT} is ready exit 0 fi else log fallback: pg_isready ok, but psql not found, treating as ready exit 0 fi fi else if (exec 3/dev/tcp/$HOST/$PORT) 2/dev/null; then exec 3- 3- log fallback: port is open, but pg_isready/psql not found, treating as ready exit 0 fi fi log still waiting for PostgreSQL at ${HOST}:${PORT} ... sleep $INTERVAL done几个细节值得单独说明。第一exec 3/dev/tcp这种写法依赖 Bash 的/dev/tcp虚拟文件如果你的 shell 不是 Bash或者编译 Bash 时禁用了--enable-net-redirections它会失效。所以我把这一段放在if条件里就算失败也不会触发set -e退出。第二psql 调用里加了-w也就是 never prompt for password配合PGPASSWORD环境变量。这样即便密码错了psql 也会立即失败而不是卡在那里等你输入。第三PGCONNECT_TIMEOUT设成和重试间隔一样是为了避免 psql 在极端情况下自己卡很久导致整体等待时间超出预期。3.3 在 docker-compose 里直接换掉“假 depends_on”脚本写完之后最常见的用法就是把它挂进 docker-compose。早期版本的 compose 只支持depends_on: - db它只能确保 db 容器先启动完全不管 db 是不是 ready这就是很多人踩坑的根源。新版本 compose 支持了condition: service_healthy配合一个健康检查就好用多了。下面是我常用的片段services: db: image: postgres:16 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: app healthcheck: test: [CMD-SHELL, pg_isready -U postgres -d app] interval: 5s timeout: 3s retries: 5 start_period: 10s web: build: . environment: DATABASE_URL: postgres://postgres:postgresdb:5432/app depends_on: db: condition: service_healthy重点解释一下 healthcheck 的几个参数interval是两次探测之间的间隔timeout是单次探测的超时上限retries是连续失败多少次后判定 unhealthystart_period是最初的一段宽限期这段期间探测失败不会计入 retries。start_period对数据库这种启动开销大的服务特别有用比如第一次挂载数据卷、初始化数据库可能要 30 秒这时候你给它 10 秒的 start_period 显然不够得根据真实启动时间调大一点。另外如果连 pg_isready 都没有也可以用pg_isready的全路径或者干脆用psql -U postgres -d app -Atqc SELECT 1作为 test 命令语义更强只是要多吃点资源。4. Kubernetes 里的就绪检查从探针说起4.1 为什么容器探针不解决全部问题进了 Kubernetes 之后等待问题有了更高层的抽象探针。但探针只是把“检查逻辑”交给了 Kubelet底层用什么命令、什么参数你依然得自己想清楚。很多人在 PostgreSQL 的 deployment YAML 里只写一个livenessProbe用 pg_isready发现 Pod 偶尔被重启日志显示数据库正在启动。原因在于Kubelet 按周期跑 liveness 探针如果探针连续失败达到failureThreshold就会 kill 容器。数据库实例在冷启动、崩溃恢复、或者数据量很大的初始化阶段往往探针就是会失败于是陷入“NotReady - 重启 - 又 NotReady - 又重启”的循环。正确的思路是把“启动慢但迟早会好”和“真的起不来”区分开。这就是startupProbe的存在意义。startupProbe 专治启动期它的failureThreshold可以设得比较大让数据库有足够时间完成恢复只有 startup 探针成功后kubelet 才会开始执行 readiness 和 liveness 探针。所以对 PostgreSQL 来说一个常见的组合就是startupProbe用宽松阈值保护启动阶段readinessProbe用严格 SQL 验证业务可用性livenessProbe甚至可以不配或者只做最低限度的检查减少误杀。4.2 startupProbe 与 readinessProbe 的配置实例下面是我在 deployment 里常用的一段配置关键点都在注释里containers: - name: postgres image: postgres:16 ports: - containerPort: 5432 startupProbe: exec: command: - /bin/sh - -c - pg_isready -U postgres -d postgres initialDelaySeconds: 0 periodSeconds: 5 failureThreshold: 60 timeoutSeconds: 2 readinessProbe: exec: command: - /bin/sh - -c - psql -U postgres -d postgres -Atqc SELECT 1 initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3注意 K8s 的 exec 探针不像 shell 那样直接解析命令行里的管道和重定向所以pg_isready -U postgres -d postgres psql ...这种命令必须用[/bin/sh, -c, 整条命令]包一层否则探针会失败。startupProbe 的failureThreshold: 60配合periodSeconds: 5意味着最多允许 300 秒内都失败对绝大多数数据库冷启动都够用了。如果你用的是超大实例恢复可能超过 5 分钟就把 failureThreshold 再调大。readinessProbe 里我直接用SELECT 1因为经过 startupProbe 之后数据库已经 readyreadiness 阶段主要是确认它能执行查询这时候再失败就是真实可用性问题应该被暴露出来。4.3 探针参数调优别让等待变成“重启循环”探针参数没有万能答案但有几个经验值可以分享。第一不要把timeoutSeconds设得比单次查询可能耗时还小比如 PostgreSQL 正在跑一个大 checkpointSELECT 1也可能排队几秒。我通常给 readinessProbe 3-5 秒的 timeout而 startupProbe 反而可以收紧到 2 秒因为 startup 阶段只需要判断端口和握手pg_isready 很快。第二initialDelaySeconds不要盲目设大。很多模板喜欢写 30 秒但如果数据目录初始化快了Pod 会多等 30 秒才变得 ready反过来如果初始化很慢initial delay 又没什么用真正管用的是 failureThreshold。所以我一般让 initialDelaySeconds 保持 0把时间完全交给 startupProbe 的阈值去控制。第三当你有多个副本时readinessProbe 失败会造成 Service 摘掉流量应用侧看到连接池断连这是正常现象不是 bug。配合合理的 preStop hook 和优雅停机数据库滚动升级基本上可以做到不丢请求。5. 真实排障等待脚本里最容易踩的几个坑5.1 你以为的“就绪”其实还差一层实际项目里等 PostgreSQL 就绪这件事坑远比想象中多。一个是 PgBouncer 这种连接池前置的情况。你写的等待脚本可能指向的是 PgBouncer 的端口虽然pg_isready返回 0但 PgBouncer 是 TSAM 模式下验证后端连接可能失败真正的 PostgreSQL 实例可能还 hang 在恢复里。这时候应用连上池子执行第一条 SQL 才发现后端没起来。解决方式是等待脚本直接指向 PostgreSQL 真实实例或者在脚本里加入对业务库的SELECT 1让连接池和实例的链路一起被验证。另一个常见状况是 autovacuum 或其他后台进程把 CPU 占满导致即便 PostgreSQL 已经 ready一次SELECT 1也可能响应很慢。等待脚本只要定义清晰“超时时间内跑通 SQL 就算 ready”这种慢响应只会表现为多等几秒不会误报成功。反倒是盲目信任 pg_isready 的返回码容易在后台重负载时提前放应用进来。根据我的经验检查项宁可严一点也不要让应用带着“数据库还没 ready”的隐患上线。5.2 不同平台和版本下的工具路径差异PostgreSQL 的客户端工具路径在不同安装方式下差别很大。用官方 apt/yum 安装通常在/usr/bin/pg_isready用官方二进制 tar 包解压到/usr/local/pgsql或/opt/pgsql工具在bin/目录下CentOS 7.9 上用 yum 安装 PostgreSQL 16默认路径往往是/usr/pgsql-16/bin/pg_isready。Windows 上更麻烦安装目录是C:\Program Files\PostgreSQL\17\bin\pg_isready.exePowerShell 脚本里如果没把路径加进 PATH直接调pg_isready会找不到命令。我建议在所有等待脚本里先做一次 command -v 探测找不到就给出明确错误而不是让用户面对一堆奇奇怪怪的空输出。PostgreSQL 17 和 16 在等待就绪这个场景下基本没有区别客户端工具的用法和返回码语义也一致。只是要注意官方便携版、绿色版、离线安装包这些非标准安装方式经常会漏掉 pg_isready 等辅助工具只有 psql 和 postgres 主程序。遇到这种情况等待脚本就不得不退化到 TCP 端口探测或者在脚本里额外依赖一个准备好的 psql 二进制。所以我在离线部署项目里通常会把bin目录下的全套工具原样打包进发布目录而不是只拷核心服务文件这样后面排查问题时事半功倍。5.3 多服务同时等待时怎样避免把数据库打爆一个容易被忽略的问题是并发等待。比如你在 docker-compose 里一次性起了 8 个应用服务每个服务都在启动脚本里跑等待循环每个循环每 2 秒用 psql 打一次 PG。数据库刚 ready 的瞬间8 个进程同时连上来虽然只是SELECT 1也可能瞬间把连接数推到峰值。如果 max_connections 默认值是 100倒是没危险但如果应用连接池也同时建立几十个连接叠加起来就会踩到too many clients already。这个报错会让一部分等待脚本误判为“数据库没就绪”然后一直重试反而加剧连接压力。我的解决办法有三层第一等待间隔不要设成 1 秒至少 2 秒起步给连接从容纳时间第二在等待脚本里不维护连接每次 psql 连完立即断开避免长连接堆积第三如果系统里并发等待的进程非常多可以用一个最小的 PgBouncer 实例挡在前面让所有探针走池子后端 PostgreSQL 只保留一份连接。性能影响可以忽略但连接数波动会平滑很多。另外再说一个和日志有关的细节我习惯在等待脚本里把每次失败的原因分类打印出来比如connection refused、starting up、auth failed、too many clients而不是笼统地输出waiting...。原因很简单当超时发生你只看最后几条日志就能知道数据库卡在哪个阶段是网络没通、还在恢复、还是认证配置错了。这个习惯帮我排查过很多次“为什么等了 60 秒还没就绪”的问题排查时间基本能缩短一半。写在最后我现在默认怎么用这套 wait-for-postgres 逻辑我现在基本会固化到每个项目的入口脚本里。如果是 docker-compose我优先用 healthcheck service_healthy不再依赖任何外部脚本如果是 CI我直接用独立的wait-for-postgres.sh并且把超时控制在 60 秒左右如果是 K8s我坚持用 startupProbe psql 版 readinessProbe。每个方案背后的唯一思路就是别用“容器状态”或“端口状态”当就绪标准用“PG 真的能接受查询”来当标准。至于版本PostgreSQL 16、17、甚至更早的版本这套逻辑完全通用真正的变量只有路径和连接参数。等你把等待这层地基打好后面应用侧的启动逻辑会省心非常多。
返回列表