健康探针、就绪门禁与等待循环
实现依赖项等待循环和存活探针,让容器化服务可靠启动。
健康探针、就绪门禁与等待循环 是 CoddyKit 上的免费 Linux Command Line & Bash Scripting Mastery 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Linux Command Line & Bash Scripting Mastery 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。
服务为何会在启动时失败
在容器化环境中,服务很少会独立启动。Web 应用需要数据库准备就绪,微服务需要消息代理可用,而工作进程需要缓存准备就绪后才能处理任务。
缺少协调时,容器会并行启动,第一批请求可能在依赖项健康之前到达。结果就是:连接被拒绝错误、空指针异常以及需要手动重启的损坏的启动状态。
- 存活性探针——进程是否仍在运行且未陷入死锁?
- 就绪性探针——服务是否已准备好接收流量?
- 等待循环——一种会阻塞启动过程、直到依赖项可访问的脚本
Kubernetes 提供了内置的探针机制,但驱动 initContainers、entrypoint.sh 包装器以及独立健康检查的 Shell 脚本都是用 Bash 编写的。掌握这些内容是 DevOps 的核心技能。
等待模式
最简单的依赖等待循环会按固定间隔轮询目标,直到目标变得可访问。典型结构使用 while 循环,并通过 nc(网络连接工具)或 curl 探测 TCP 端口或 HTTP 端点。
关键设计决策:
- 超时——N 秒后退出,避免故障依赖项使容器组永久挂起
- 退避间隔——在探测之间休眠,避免不断冲击正在恢复的服务
- 退出代码——超时时退出并返回 1,使容器重启(或使初始化容器明确失败)
下面的代码片段最多等待 60 秒,直到 TCP 端口接受连接,然后才启动主进程。
#!/usr/bin/env bash
set -euo pipefail
HOST="${DB_HOST:-postgres}"
PORT="${DB_PORT:-5432}"
TIMEOUT=60
INTERVAL=2
ELAPSED=0
echo "[wait-for] Waiting for ${HOST}:${PORT}..."
until nc -z "$HOST" "$PORT" 2>/dev/null; do
if (( ELAPSED >= TIMEOUT )); then
echo "[wait-for] Timed out after ${TIMEOUT}s waiting for ${HOST}:${PORT}" >&2
exit 1
fi
echo "[wait-for] ${HOST}:${PORT} not ready — retrying in ${INTERVAL}s (${ELAPSED}s elapsed)"
sleep "$INTERVAL"
(( ELAPSED += INTERVAL ))
done
echo "[wait-for] ${HOST}:${PORT} is up — continuing"
exec "$@"使用 curl 进行 HTTP 就绪性探针
TCP 连接只能说明端口已打开,不能说明其后的应用已准备好处理请求。许多服务提供专用的 /health 或 /readyz 端点,只有在所有内部子系统初始化完成后才会返回 HTTP 200。
使用 curl --fail --silent --output /dev/null 探测 HTTP 就绪性端点。--fail 标志会使 curl 在收到 4xx/5xx 响应时以代码 22 退出,从而简洁地驱动重试逻辑。
需要了解的重要标志:
--fail——将 HTTP 错误视为 curl 错误(退出代码非零)--silent——禁止显示进度输出--max-time N——每个请求的超时时间(秒)--retry N --retry-delay S——curl 自带的重试层(适用于简单场景)
#!/usr/bin/env bash
set -euo pipefail
HEALTH_URL="${HEALTH_URL:-http://localhost:8080/healthz}"
TIMEOUT=90
INTERVAL=3
ELAPSED=0
echo "[probe] Polling readiness at ${HEALTH_URL}"
until curl --fail --silent --output /dev/null \
--max-time 2 "${HEALTH_URL}"; do
if (( ELAPSED >= TIMEOUT )); then
echo "[probe] Service not ready after ${TIMEOUT}s" >&2
exit 1
fi
printf '[probe] Not ready yet (%ds elapsed)\n' "$ELAPSED"
sleep "$INTERVAL"
(( ELAPSED += INTERVAL ))
done
echo "[probe] Service is ready"
exec "$@"等待循环中的指数退避
固定间隔的重试循环会以恒定速率不断冲击正在恢复的服务。指数退避会在每次尝试时将等待时间加倍,在恢复期间降低负载,同时在服务快速恢复时仍能迅速完成重试。
标准公式为:sleep_time = min(base * 2^attempt, max_sleep)。加入抖动部分(随机的分数偏移量)可以避免许多容器同时重启时出现惊群问题。
生产工具(如 wait-for-it)、AWS SDK 的重试机制以及 Kubernetes 控制器协调循环都使用了这种模式。
#!/usr/bin/env bash
set -euo pipefail
HOST="${HOST:-redis}"
PORT="${PORT:-6379}"
MAX_ATTEMPTS=8
BASE_SLEEP=1
MAX_SLEEP=30
for attempt in $(seq 1 "$MAX_ATTEMPTS"); do
if nc -z "$HOST" "$PORT" 2>/dev/null; then
echo "[backoff] Connected to ${HOST}:${PORT} on attempt ${attempt}"
exec "$@"
fi
# Exponential backoff with jitter
raw=$(( BASE_SLEEP * (2 ** (attempt - 1)) ))
capped=$(( raw < MAX_SLEEP ? raw : MAX_SLEEP ))
jitter=$(( RANDOM % 3 ))
sleep_time=$(( capped + jitter ))
echo "[backoff] Attempt ${attempt}/${MAX_ATTEMPTS} failed — sleeping ${sleep_time}s"
sleep "$sleep_time"
done
echo "[backoff] ${HOST}:${PORT} unreachable after ${MAX_ATTEMPTS} attempts" >&2
exit 1探测多个依赖项
实际应用通常有多个依赖项:数据库、缓存、消息代理,以及可能存在的外部接口。按顺序探测它们会浪费启动时间。更好的方法是并行探测所有依赖项,并等待它们全部成功。
Bash 的 & 运算符会将每个探测放到后台运行,而 wait 会收集它们的退出代码。如果任何探测失败,入口脚本就会以非零状态退出,从而触发容器重启。
关键技巧:使用 $! 捕获后台 PID,并将它们显式传递给 wait,这样就可以检查每个探测的退出代码。
#!/usr/bin/env bash
set -euo pipefail
wait_tcp() {
local host="$1" port="$2" timeout="${3:-30}"
local elapsed=0
until nc -z "$host" "$port" 2>/dev/null; do
(( elapsed >= timeout )) && { echo "TIMEOUT ${host}:${port}" >&2; return 1; }
sleep 2; (( elapsed += 2 ))
done
echo "[ok] ${host}:${port}"
}
# Launch all probes in parallel
wait_tcp postgres 5432 60 & PID_PG=$!
wait_tcp redis 6379 30 & PID_RD=$!
wait_tcp rabbitmq 5672 45 & PID_RQ=$!
# Collect results — fail fast if any probe failed
FAILED=0
for pid in $PID_PG $PID_RD $PID_RQ; do
wait "$pid" || (( FAILED++ ))
done
if (( FAILED > 0 )); then
echo "[entrypoint] ${FAILED} dependency probe(s) failed — aborting" >&2
exit 1
fi
echo "[entrypoint] All dependencies ready"
exec "$@"存活性与就绪性:针对不同探针使用不同脚本
Kubernetes 区分存活性探针和就绪性探针,二者应执行不同的操作:
- 存活性探针——回答的问题是:进程是否仍然存活且未陷入死锁?它应该快速执行,并且只检查内部状态(例如,进程 PID 文件是否存在,或本地健康端点是否返回 200)。存活性探针失败会终止并重启容器。
- 就绪性探针——回答的问题是:这个容器组是否应该接收流量?它可以检查下游依赖项。就绪性探针失败会将容器组从服务负载均衡器中移除,但不会重启容器组。
不要将缓慢的依赖项检查放入存活性探针。短暂的数据库中断可能会错误地重启所有应用容器组,使问题进一步恶化。
#!/usr/bin/env bash
# liveness.sh — fast local-only check
# Used in: livenessProbe.exec.command
set -euo pipefail
PID_FILE="/var/run/app/app.pid"
HEALTH_URL="http://127.0.0.1:8080/internal/live"
# Check 1: process is running
[[ -f "$PID_FILE" ]] || { echo "PID file missing" >&2; exit 1; }
kill -0 "$(cat "$PID_FILE")" 2>/dev/null || { echo "Process dead" >&2; exit 1; }
# Check 2: local endpoint responds (2s timeout — never block)
curl --fail --silent --max-time 2 --output /dev/null "$HEALTH_URL" || {
echo "Liveness endpoint unresponsive" >&2
exit 1
}
echo "live"
exit 0启动探针和 initContainers
Kubernetes 提供了第三种探针类型:startupProbe。在它首次成功之前,系统会暂不运行存活性探针和就绪性探针,从而为启动缓慢的应用(JVM 预热、数据库迁移)提供初始化所需的时间,避免触发错误的存活性失败。
对于依赖项等待,initContainers 通常比入口脚本更简洁。它们会在应用容器启动前运行,而 Kubernetes 会自动处理重试和重启逻辑。初始化容器镜像只需要 sh、nc 或 curl;您可以使用精简的 busybox 或 alpine 镜像。
Pod 清单中的 initContainer 规范示例:
# kubernetes/pod-with-init.yaml (illustrative — not runnable as bash)
# initContainers run sequentially before app containers
initContainers:
- name: wait-for-postgres
image: busybox:1.36
command:
- sh
- -c
- |
set -e
echo 'Waiting for postgres...'
until nc -z postgres 5432; do
echo 'postgres not ready — sleeping 2s'
sleep 2
done
echo 'postgres is up'
- name: run-migrations
image: myapp:latest
command: ['python', 'manage.py', 'migrate', '--noinput']
envFrom:
- secretRef:
name: app-secrets带 JSON 输出的健康检查脚本
生产系统通常会汇总多个子系统的健康状态,并将其以结构化 JSON 负载的形式公开。负载均衡器、编排器和监控面板都会使用这些数据。
Bash 健康检查脚本可以直接使用 printf 或 jq 构建 JSON 输出。退出代码仍然负责驱动自动化;JSON 正文则供操作人员和监控系统使用。
约定:健康时返回 HTTP 200 和 {"status":"ok"},不健康时返回 HTTP 503 和 {"status":"degraded", "checks":{...}}。下面的脚本旨在由轻量级 HTTP 包装器(例如 socat)提供服务,或由 Kubernetes 执行探针直接调用。
#!/usr/bin/env bash
# health_check.sh — composite health with JSON output
set -uo pipefail
check_postgres() {
pg_isready -h "${DB_HOST:-postgres}" -p "${DB_PORT:-5432}" \
-U "${DB_USER:-app}" -t 2 &>/dev/null
}
check_redis() {
redis-cli -h "${REDIS_HOST:-redis}" -p "${REDIS_PORT:-6379}" \
PING 2>/dev/null | grep -q PONG
}
check_disk() {
local usage
usage=$(df / | awk 'NR==2{gsub(/%/,"",$5); print $5}')
(( usage < 90 ))
}
PG_OK=0; RD_OK=0; DSK_OK=0
check_postgres && PG_OK=1
check_redis && RD_OK=1
check_disk && DSK_OK=1
OVERALL=$(( PG_OK && RD_OK && DSK_OK ))
STATUS=$( (( OVERALL )) && echo 'ok' || echo 'degraded' )
printf '{"status":"%s","checks":{"postgres":%s,"redis":%s,"disk":%s}}\n' \
"$STATUS" "$PG_OK" "$RD_OK" "$DSK_OK"
(( OVERALL )) && exit 0 || exit 1使用截止时间模式的超时工具
GNU timeout 命令可以包装任意命令,并在命令未能于指定时长内完成时将其终止。这是在无需手动管理后台任务的情况下,为等待循环或健康探针设置硬截止时间的最简洁方法。
timeout DURATION COMMAND [ARGS...]
timeout 的退出代码:
- 0——命令在截止时间内成功完成
- 命令的退出代码——命令已运行,但返回了非零代码
- 124——命令超时(已发送 SIGTERM)
- 137——命令被 SIGKILL 终止(在
--kill-after之后)
检测退出代码 124 后,您可以打印清晰的超时消息,而不是笼统的错误消息。
#!/usr/bin/env bash
set -euo pipefail
HOST="${DB_HOST:-postgres}"
PORT="${DB_PORT:-5432}"
DEADLINE=60 # seconds
# Inline poll loop, wrapped by timeout
timeout "$DEADLINE" bash -c "
until nc -z '$HOST' '$PORT' 2>/dev/null; do
echo '[wait] ${HOST}:${PORT} not ready...'
sleep 2
done
" && echo "[wait] ${HOST}:${PORT} is up" || {
code=$?
if (( code == 124 )); then
echo "[wait] Timed out after ${DEADLINE}s waiting for ${HOST}:${PORT}" >&2
else
echo "[wait] Probe failed with exit code ${code}" >&2
fi
exit "$code"
}
exec "$@"纯 Bash 实现的自包含等待脚本
许多精简的容器镜像没有 nc(网络连接工具)。纯 Bash 可以通过对 /dev/tcp 使用进程替换来建立 TCP 连接。这是一项非标准但得到广泛支持的 Bash 内置功能,不需要任何外部工具。
语法:/dev/tcp/HOST/PORT——当您向此路径重定向数据或从此路径重定向数据时,Bash 会打开 TCP 连接。如果连接被拒绝或超时,它会引发错误(退出状态非零)。
许多 Docker Compose 配置中都附带的热门 wait-for-it.sh 脚本使用了这项技术。下面的脚本是一个完整的独立实现,您可以将其 COPY 到任何 Dockerfile 中。
#!/usr/bin/env bash
# wait-for-it.sh (pure bash, no nc/curl required)
set -uo pipefail
usage() { echo "Usage: $0 HOST:PORT [-t TIMEOUT] [-- COMMAND]"; exit 1; }
parse_hostport() {
HOST="${1%%:*}"
PORT="${1##*:}"
[[ -n "$HOST" && "$PORT" =~ ^[0-9]+$ ]] || usage
}
[[ $# -ge 1 ]] || usage
parse_hostport "$1"; shift
TIMEOUT=30
[[ "${1:-}" == "-t" ]] && { TIMEOUT="$2"; shift 2; }
[[ "${1:-}" == "--" ]] && shift
wait_for() {
local elapsed=0
while (( elapsed < TIMEOUT )); do
# Pure bash TCP probe — no nc, no curl
if (exec 3<>"/dev/tcp/${HOST}/${PORT}") 2>/dev/null; then
exec 3>&-
return 0
fi
sleep 1
(( elapsed++ ))
done
return 1
}
echo "Waiting for ${HOST}:${PORT} (timeout ${TIMEOUT}s)..."
if wait_for; then
echo "${HOST}:${PORT} is available"
[[ $# -gt 0 ]] && exec "$@"
else
echo "Timed out waiting for ${HOST}:${PORT}" >&2
exit 1
fi将探针集成到 entrypoint.sh
入口脚本模式是在一个易于维护的脚本中组合等待循环、环境验证和进程启动的标准方式。Docker 的 ENTRYPOINT 会调用此脚本,而脚本最后使用 exec "$@" 将控制权交给 CMD,并保持相同的 PID(从而实现信号的正常转发)。
生产级 entrypoint.sh 通常会:
- 尽早验证必需的环境变量(快速失败)
- 运行依赖项等待循环
- 执行数据库迁移(如果适用)
- 运行最终自检
- 使用
exec "$@"转移控制权
使用 exec 至关重要:它会替换 Shell 进程,使应用成为 PID 1,并直接接收 Docker/Kubernetes 在正常关闭时发送的 SIGTERM。
#!/usr/bin/env bash
# docker/entrypoint.sh
set -euo pipefail
# ── 1. Validate required env vars ────────────────────────────────────
for var in DATABASE_URL REDIS_URL SECRET_KEY; do
[[ -n "${!var:-}" ]] || { echo "FATAL: ${var} is not set" >&2; exit 1; }
done
# ── 2. Parse DB host/port from DATABASE_URL ──────────────────────────
DB_HOST=$(echo "$DATABASE_URL" | sed -E 's|.*@([^:/]+).*|\1|')
DB_PORT=$(echo "$DATABASE_URL" | sed -E 's|.*:([0-9]+)/.*|\1|')
# ── 3. Wait for dependencies ─────────────────────────────────────────
timeout 60 bash -c "
until nc -z '${DB_HOST}' '${DB_PORT}' 2>/dev/null; do sleep 2; done
" || { echo "Database unreachable" >&2; exit 1; }
# ── 4. Run migrations ────────────────────────────────────────────────
echo "[entrypoint] Running migrations..."
python manage.py migrate --noinput
# ── 5. Hand off to CMD (exec preserves PID 1 for signal handling) ────
echo "[entrypoint] Starting application: $*"
exec "$@"知识检查:存活性与就绪性探针
检验您对本课所涵盖关键概念的理解。
回顾:健康探针、就绪门控与等待循环
在本课中,您构建了用于可靠协调容器启动的 Bash 工具集。您学习了以下内容:
- 等待模式——带有硬超时和已用时间保护的
until nc -z HOST PORT循环,可以避免因故障依赖项而无限阻塞。 - HTTP 就绪性探针——
curl --fail --max-time可以验证应用是否真正就绪,而不仅仅是端口是否打开。 - 指数退避——在重试之间逐渐加倍休眠间隔,可以减少惊群负载,并让正在恢复的服务稳定下来。
- 并行依赖项探测——使用
&将探测放到后台运行,并使用wait $PID收集结果;存在多个依赖项时,这可以缩短启动延迟。 - 存活性与就绪性——存活性探针必须快速执行且只检查本地状态;就绪性探针可以检查下游系统。绝不能混淆二者。
- startupProbe——在应用初始化期间保护启动缓慢的应用,避免错误的存活性失败。
- /dev/tcp 探针——纯 Bash TCP 检查不需要外部工具,非常适合精简的容器镜像。
- entrypoint.sh——环境验证、等待循环、迁移以及
exec "$@"构成了容器化服务的标准启动模式。
这些模式构成了 Docker Compose、Kubernetes 和 ECS 中自愈型生产级容器部署的基础。
常见问题解答
「健康探针、就绪门禁与等待循环」课时是免费的吗?
是的 — 「健康探针、就绪门禁与等待循环」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Linux Command Line & Bash Scripting Mastery 课程的其余内容,请升级到 CoddyKit PRO。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。
「健康探针、就绪门禁与等待循环」这节课中我会学到什么?
实现依赖项等待循环和存活探针,让容器化服务可靠启动。 你通过在浏览器中直接运行的动手代码来练习 Linux Command Line & Bash Scripting Mastery,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Linux Command Line & Bash Scripting Mastery 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Linux Command Line & Bash Scripting Mastery 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「健康探针、就绪门禁与等待循环」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Linux Command Line & Bash Scripting Mastery 课中编写并运行代码吗?
能。每节 Linux Command Line & Bash Scripting Mastery 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。