0Pricing
Linux Command Line & Bash Scripting Mastery · 课时

健康探针、就绪门禁与等待循环

实现依赖项等待循环和存活探针,让容器化服务可靠启动。

健康探针、就绪门禁与等待循环 是 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. 编写精简的 Dockerfile 与 Shell 入口点
  2. 使用 envsubst 和 heredoc 创建配置模板
  3. 通过 CLI 和 jq 编写云资源脚本
  4. 健康探针、就绪门禁与等待循环
← 返回 Linux Command Line & Bash Scripting Mastery