Docker 容器启动了,不代表服务真的能用:Healthcheck 实战

很多刚接触 Docker 的人都会看这个命令:

bash 复制代码
docker ps

只要看到:

text 复制代码
STATUS
Up 10 seconds

就觉得:

服务启动成功了。

但实际上:

容器启动成功,不代表容器里的服务已经可以正常使用。

这也是 Docker 项目里一个非常常见的问题。

最近我在维护开源项目 ShiyuAdmin:

text 复制代码
https://github.com/Rodert/ShiyuAdmin

里面就用到了一个非常实用的 Docker 功能:

text 复制代码
Healthcheck

今天只讲这一个知识点。


一、先看一个真实问题

假设我们的系统有三个服务:

text 复制代码
Go Backend
PostgreSQL
Redis

通过 Docker Compose 一起启动:

bash 复制代码
docker compose up -d

Docker 可能按照下面的顺序启动:

text 复制代码
PostgreSQL 容器启动
Redis 容器启动
Go 容器启动

看起来没有问题。

但是这里有一个坑。

PostgreSQL 容器虽然已经启动了:

text 复制代码
Container Running

但 PostgreSQL 自己可能还在:

text 复制代码
初始化数据库
创建用户
加载数据文件
启动数据库服务

这个时候 Go 后端马上连接:

text 复制代码
PostgreSQL:5432

就可能直接报:

text 复制代码
connection refused

所以我们需要区分两个概念。

text 复制代码
容器启动成功

和:

text 复制代码
容器里的服务已经准备好

完全不是一回事。


二、Healthcheck 是干什么的?

Docker Healthcheck 就是用来检查:

容器里面的服务到底还能不能正常工作。

比如 PostgreSQL。

我们可以这样配置:

yaml 复制代码
services:
  postgres:
    image: postgres:15

    healthcheck:
      test:
        [
          "CMD-SHELL",
          "pg_isready -U postgres"
        ]

      interval: 10s
      timeout: 5s
      retries: 5

Docker 会定期在容器里面执行:

bash 复制代码
pg_isready -U postgres

如果 PostgreSQL 已经可以接受连接:

text 复制代码
healthy

如果还没有准备好:

text 复制代码
starting

如果连续多次检查失败:

text 复制代码
unhealthy

三、Docker 容器其实有三种健康状态

配置 Healthcheck 以后,我们执行:

bash 复制代码
docker ps

可能看到:

text 复制代码
Up 10 seconds (health: starting)

过一会:

text 复制代码
Up 30 seconds (healthy)

如果服务异常:

text 复制代码
Up 2 minutes (unhealthy)

所以容器状态不再只是:

text 复制代码
Running

还多了一层:

text 复制代码
starting
healthy
unhealthy

这就比单纯判断进程是否存在准确很多。


四、ShiyuAdmin 是怎么做的?

以 PostgreSQL 为例:

yaml 复制代码
shiyu-postgres:
  image: postgres:15

  healthcheck:
    test:
      [
        "CMD-SHELL",
        "pg_isready -U shiyu -d shiyu_admin_scaffold"
      ]

    interval: 10s
    timeout: 5s
    retries: 5

这里:

yaml 复制代码
interval: 10s

表示:

text 复制代码
每隔 10 秒检查一次
yaml 复制代码
timeout: 5s

表示:

text 复制代码
一次检查超过 5 秒算失败
yaml 复制代码
retries: 5

表示:

text 复制代码
连续失败 5 次以后认为 unhealthy

整体逻辑就是:

text 复制代码
Docker
  ↓
每 10 秒
  ↓
执行 pg_isready
  ↓
成功?
 ┌───────┴───────┐
 是              否
 ↓               ↓
healthy       继续重试

五、Redis 更简单

Redis 可以直接使用:

bash 复制代码
redis-cli ping

正常情况下会返回:

text 复制代码
PONG

因此 Docker Compose 可以写:

yaml 复制代码
shiyu-redis:
  image: redis:7

  healthcheck:
    test:
      [
        "CMD",
        "redis-cli",
        "ping"
      ]

    interval: 10s
    timeout: 5s
    retries: 5

这其实就是 Docker Healthcheck 最核心的思想:

找一个真正能代表服务正常工作的命令。


六、Web 服务怎么检查?

假设我们的 Go 后端提供:

text 复制代码
/api/v1/system/health

这个接口正常时返回:

json 复制代码
{
  "status": "ok"
}

我们就可以直接访问这个接口判断服务状态。

比如:

yaml 复制代码
healthcheck:
  test:
    [
      "CMD-SHELL",
      "wget --quiet --tries=1 --spider http://127.0.0.1/api/v1/system/health || exit 1"
    ]

  interval: 30s
  timeout: 10s
  retries: 3

这比判断:

text 复制代码
Go 进程是否存在

靠谱得多。

因为下面这种情况非常常见:

text 复制代码
Go 进程存在
数据库连接挂了
Redis 挂了
接口已经不能正常访问

从操作系统角度:

text 复制代码
Process Running

但是从用户角度:

text 复制代码
Service Down

Healthcheck 应该尽可能检查:

text 复制代码
服务能力

而不仅仅是:

text 复制代码
进程能力

七、Healthcheck 最大的搭档:depends_on

Healthcheck 单独使用已经很有价值。

但是在 Docker Compose 里,它还有一个经典搭配:

yaml 复制代码
depends_on

比如:

yaml 复制代码
shiyu-app:

  depends_on:
    shiyu-postgres:
      condition: service_healthy

    shiyu-redis:
      condition: service_healthy

什么意思?

Go 后端不会简单地等:

text 复制代码
PostgreSQL 容器启动

而是等:

text 复制代码
PostgreSQL 健康检查通过

Redis 同理。

于是启动顺序变成:

text 复制代码
PostgreSQL 启动
      ↓
Healthcheck
      ↓
PostgreSQL healthy

Redis 启动
      ↓
Healthcheck
      ↓
Redis healthy

      ↓

Go Backend 启动

这就可以减少很多:

text 复制代码
connection refused

之类的问题。


八、没有 Healthcheck 会发生什么?

如果只写:

yaml 复制代码
depends_on:
  - postgres
  - redis

它通常只能保证:

text 复制代码
postgres 容器先启动
redis 容器先启动

但是不能保证:

text 复制代码
PostgreSQL 已经可以连接
Redis 已经可以访问

比如:

text 复制代码
00:00 PostgreSQL 容器启动
00:01 Go Backend 启动
00:02 Go 尝试连接数据库
00:03 PostgreSQL 还在初始化
00:03 connection refused
00:05 PostgreSQL 才真正 Ready

这就是为什么:

text 复制代码
启动顺序

和:

text 复制代码
服务就绪顺序

不是一个概念。


九、自己写一个健康检查接口

如果你是 Java、Go、Node.js 开发者,我非常建议自己的项目提供一个:

text 复制代码
/health

接口。

最简单的 Go 示例:

go 复制代码
package main

import (
	"encoding/json"
	"net/http"
)

func healthHandler(w http.ResponseWriter, r *http.Request) {

	w.Header().Set("Content-Type", "application/json")

	json.NewEncoder(w).Encode(map[string]string{
		"status": "ok",
	})
}

func main() {

	http.HandleFunc("/health", healthHandler)

	http.ListenAndServe(":8080", nil)
}

然后 Docker:

yaml 复制代码
healthcheck:
  test:
    [
      "CMD-SHELL",
      "wget --quiet --spider http://127.0.0.1:8080/health || exit 1"
    ]

  interval: 30s
  timeout: 5s
  retries: 3

这样就完成了一个最简单的服务健康检查。


十、再进一步:不要只返回 ok

真正生产环境里,可以检查:

text 复制代码
数据库
Redis
磁盘
关键依赖

比如:

go 复制代码
func healthHandler(w http.ResponseWriter, r *http.Request) {

	if err := db.Ping(); err != nil {

		w.WriteHeader(http.StatusServiceUnavailable)

		json.NewEncoder(w).Encode(map[string]string{
			"status": "error",
			"database": "down",
		})

		return
	}

	json.NewEncoder(w).Encode(map[string]string{
		"status": "ok",
		"database": "ok",
	})
}

这样:

text 复制代码
Go 进程活着

但是数据库挂了的时候:

text 复制代码
/health

依然会返回失败。

Docker 就能检测出来:

text 复制代码
unhealthy

十一、怎么查看具体健康检查结果?

先执行:

bash 复制代码
docker ps

如果看到:

text 复制代码
unhealthy

可以进一步执行:

bash 复制代码
docker inspect 容器名

或者:

bash 复制代码
docker inspect \
  --format='{{json .State.Health}}' \
  容器名

里面可以看到最近几次 Healthcheck 的:

text 复制代码
执行时间
退出码
输出结果

排查起来非常方便。


十二、一个完整例子

下面是一个简化版 Docker Compose:

yaml 复制代码
services:

  postgres:

    image: postgres:15

    environment:
      POSTGRES_PASSWORD: 123456

    healthcheck:
      test:
        [
          "CMD-SHELL",
          "pg_isready -U postgres"
        ]

      interval: 10s
      timeout: 5s
      retries: 5


  redis:

    image: redis:7

    healthcheck:
      test:
        [
          "CMD",
          "redis-cli",
          "ping"
        ]

      interval: 10s
      timeout: 5s
      retries: 5


  app:

    image: my-app:latest

    depends_on:

      postgres:
        condition: service_healthy

      redis:
        condition: service_healthy

    healthcheck:
      test:
        [
          "CMD-SHELL",
          "wget --quiet --spider http://127.0.0.1:8080/health || exit 1"
        ]

      interval: 30s
      timeout: 5s
      retries: 3

整个启动逻辑非常清晰:

text 复制代码
PostgreSQL
    ↓
healthy

Redis
    ↓
healthy

    ↓

App
    ↓
/health
    ↓
healthy

这已经是一套非常标准的 Docker 服务启动方案。


十三、Healthcheck 解决的其实不是 Docker 问题

表面上看:

text 复制代码
Healthcheck

是一个 Docker 配置。

但它解决的实际上是一个非常重要的分布式系统问题:

text 复制代码
服务存在

不等于:

text 复制代码
服务可用

以后学 Kubernetes,你还会看到类似的东西:

text 复制代码
Liveness Probe
Readiness Probe
Startup Probe

其实思路都是一样的:

text 复制代码
你不能只判断一个进程有没有启动。

你还要知道:

它能不能真正提供服务。

所以如果你的 Docker Compose 里现在只有:

yaml 复制代码
image:
ports:
environment:
volumes:

可以看看自己的项目是不是应该补一个:

yaml 复制代码
healthcheck:

这个配置看起来只有几行。

但对于数据库、Redis、后端服务这些存在启动依赖的系统来说,往往非常有价值。

Docker 容器 Running,只能证明容器还活着。

Healthcheck Healthy,才更接近我们真正想知道的事情:服务能不能用。

相关推荐
正仪1 小时前
systemd 是什么?
linux·运维·windows
暖核1 小时前
华为云运维三件套实战:云审计 + 云监控 + 云日志完整操作指南
linux·运维·华为云
hhb_6181 小时前
智能任务自动化协同AI工作流技术文档
运维·人工智能·自动化
虚无的纽扣1 小时前
【Linux】进程第二课:进程优先级和环境变量
linux·运维·服务器
maosheng11462 小时前
【无标题】linux中企业容易考的的小知识点1(rhcsa)
linux·运维·服务器
牢姐与蒯2 小时前
Linux进程(九).自定义shell——深度理解命令行和shell
linux·运维·服务器
snow@li2 小时前
服务器运维:mysql 安装笔记
运维·服务器·mysql
天道jimmy2 小时前
VulnHub 系列:View2aKill,1
linux·运维·服务器
荀彧原名苟或2 小时前
SAP 执行 CMD命令(GUI_EXEC 执行电脑本机命令、SXPG_COMMAND_EXECUTE函数执行服务器上的外部程序)<转载>
运维·服务器·sap·abap