健康检查模式:让系统自己告诉你它还好吗

说实话,我做了十几年分布式系统,最怕的就是半夜被电话吵醒——「服务挂了!」。你火急火燎地登录服务器,发现进程还在,但就是不给响应。这种「僵尸服务」比直接挂掉更可怕。后来我学乖了,给每个服务都装上健康检查,说白了就是让系统自己告诉你:「我还活着,而且能干活」。

健康检查模式,是微服务和容器化时代的必备技能。Kubernetes 用它来决定要不要重启你的 Pod,负载均衡器用它来决定要不要把流量打过来。今天我们就把它彻底讲透。

三个核心概念:健康端点、存活探针、就绪探针

这三个东西经常被搞混,我刚开始也分不清。咱们一个一个说。

健康端点(Health Endpoint)

就是一个 HTTP 接口,比如 /health。你访问它,它返回一个 JSON,告诉你服务状态。最简单的实现就是返回 {"status": "UP"}。但实际项目中,我习惯让它检查更多东西——数据库能不能连上、Redis 通不通、下游服务是否可达。

核心原则:健康端点应该轻量、快速。别在里面做复杂计算,否则健康检查本身就成了性能瓶颈。

存活探针(Liveness Probe)

这个探针问的是:「进程还活着吗?」如果挂了,Kubernetes 会重启 Pod。我遇到过一种情况:服务因为死锁导致线程全部阻塞,进程还在,但啥也干不了。存活探针就能发现这种问题。

就绪探针(Readiness Probe)

这个探针问的是:「准备好接收流量了吗?」启动时可能需要加载数据、预热缓存,这时候虽然进程活着,但还不能对外服务。就绪探针返回失败,负载均衡器就不会把流量打过来。

嗯,这里要注意:存活探针和就绪探针的失败处理方式完全不同。一个重启,一个只是摘掉流量。千万别搞混。

Spring Boot Actuator:开箱即用的健康检查

Spring Boot 提供了 Actuator 模块,内置了健康检查功能。我个人觉得这是 Spring Boot 最实用的模块之一,没有它,你就要自己写一堆重复代码。

快速集成

加个依赖就行:

// Maven
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

然后在 application.yml 里配置:

management:
  endpoints:
    web:
      exposure:
        include: health,info
  endpoint:
    health:
      show-details: always

启动后访问 /actuator/health,你会看到类似这样的响应:

{
  "status": "UP",
  "components": {
    "db": {
      "status": "UP",
      "details": {
        "database": "H2",
        "validationQuery": "isValid()"
      }
    },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 499963170816,
        "free": 327680000000,
        "threshold": 10485760
      }
    },
    "ping": {
      "status": "UP"
    }
  }
}

你看,它自动检查了数据库连接、磁盘空间。如果你用了 Redis、Elasticsearch 等,它也会自动检测。

自定义健康指示器

我在项目中遇到过需要检查一个第三方支付接口是否可达的情况。Actuator 允许你自定义健康指示器:

@Component
public class PaymentServiceHealthIndicator implements HealthIndicator {
    
    @Override
    public Health health() {
        try {
            // 模拟检查支付服务
            boolean isReachable = checkPaymentService();
            if (isReachable) {
                return Health.up()
                    .withDetail("paymentService", "available")
                    .build();
            } else {
                return Health.down()
                    .withDetail("paymentService", "unreachable")
                    .build();
            }
        } catch (Exception e) {
            return Health.down(e)
                .withDetail("paymentService", "error")
                .build();
        }
    }
    
    private boolean checkPaymentService() {
        // 实际项目中这里会发一个轻量级的请求
        return true;
    }
}

这样,/actuator/health 返回的结果里就会多一个 paymentService 的状态。

Kubernetes 中的配置示例

有了健康端点,Kubernetes 的配置就很简单了:

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
  - name: my-app
    image: my-app:latest
    livenessProbe:
      httpGet:
        path: /actuator/health/liveness
        port: 8080
      initialDelaySeconds: 30
      periodSeconds: 10
    readinessProbe:
      httpGet:
        path: /actuator/health/readiness
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5

注意这里我用了两个不同的路径。Spring Boot 2.3+ 支持将存活和就绪探针分开:

management:
  endpoint:
    health:
      probes:
        enabled: true
      group:
        liveness:
          include: livenessState
        readiness:
          include: readinessState,db

这样 /actuator/health/liveness 只检查进程是否存活,/actuator/health/readiness 会额外检查数据库是否就绪。

避坑指南

我曾经踩过的坑:

  • 把健康检查做得太重。有一次我在健康端点里做了全量数据校验,结果每次检查耗时 5 秒,导致 Kubernetes 频繁重启 Pod。记住:健康检查要轻量。
  • 存活探针和就绪探针用同一个端点。如果数据库挂了,存活探针也失败,Pod 会被重启。但有时候你只是想暂时摘掉流量,等数据库恢复。所以存活探针只检查进程状态,就绪探针才检查依赖。
  • 忘记设置 initialDelaySeconds。服务启动需要时间,如果探针在启动完成前就开始检查,会导致反复重启。

知识体系总览

下面这张图总结了健康检查模式的核心逻辑:

健康检查模式核心架构 健康端点 /health 返回状态 存活探针 进程是否活着? 就绪探针 能否接收流量? Spring Boot Actuator Kubernetes HTTP Get Kubernetes HTTP Get 返回状态码/JSON 失败 → 重启 Pod 失败 → 摘掉流量 最佳实践:轻量检查 + 分开配置 + 设置初始延迟

Java 原生实现(不依赖 Spring Boot)

如果你不用 Spring Boot,自己实现也不复杂。我习惯用 Java 的 HttpServer 快速搭一个:

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

public class HealthCheckServer {
    
    private static boolean isHealthy = true;
    private static boolean isReady = false;
    
    public static void main(String[] args) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        
        // 健康端点
        server.createContext("/health", exchange -> {
            String response = "{\"status\":\"" + (isHealthy ? "UP" : "DOWN") + "\"}";
            exchange.getResponseHeaders().set("Content-Type", "application/json");
            exchange.sendResponseHeaders(200, response.length());
            OutputStream os = exchange.getResponseBody();
            os.write(response.getBytes());
            os.close();
        });
        
        // 存活探针
        server.createContext("/liveness", exchange -> {
            String response = isHealthy ? "OK" : "FAIL";
            int code = isHealthy ? 200 : 503;
            exchange.sendResponseHeaders(code, response.length());
            OutputStream os = exchange.getResponseBody();
            os.write(response.getBytes());
            os.close();
        });
        
        // 就绪探针
        server.createContext("/readiness", exchange -> {
            String response = isReady ? "OK" : "NOT_READY";
            int code = isReady ? 200 : 503;
            exchange.sendResponseHeaders(code, response.length());
            OutputStream os = exchange.getResponseBody();
            os.write(response.getBytes());
            os.close();
        });
        
        server.setExecutor(Executors.newFixedThreadPool(4));
        server.start();
        System.out.println("Health check server started on port 8080");
    }
}

这个例子虽然简单,但核心逻辑都在了。实际项目中,你会在 isHealthyisReady 背后做真正的检查——数据库连接池状态、队列积压情况、缓存是否预热完成等等。

C++ 实现思路

C++ 里实现健康检查,我一般用 libmicrohttpd 或者 cpp-httplib。这里用 cpp-httplib 演示:

#include <httplib.h>
#include <iostream>
#include <atomic>

std::atomic<bool> isHealthy{true};
std::atomic<bool> isReady{false};

int main() {
    httplib::Server svr;
    
    // 健康端点
    svr.Get("/health", [](const httplib::Request& req, httplib::Response& res) {
        res.set_content(
            "{\"status\":\"" + std::string(isHealthy ? "UP" : "DOWN") + "\"}",
            "application/json"
        );
    });
    
    // 存活探针
    svr.Get("/liveness", [](const httplib::Request& req, httplib::Response& res) {
        if (isHealthy) {
            res.set_content("OK", "text/plain");
        } else {
            res.status = 503;
            res.set_content("FAIL", "text/plain");
        }
    });
    
    // 就绪探针
    svr.Get("/readiness", [](const httplib::Request& req, httplib::Response& res) {
        if (isReady) {
            res.set_content("OK", "text/plain");
        } else {
            res.status = 503;
            res.set_content("NOT_READY", "text/plain");
        }
    });
    
    std::cout << "Health check server started on port 8080" << std::endl;
    svr.listen("0.0.0.0", 8080);
    
    return 0;
}

C++ 版本要注意线程安全,std::atomic 在这里就派上用场了。我在一个高并发网关项目里就用过这种方案,性能完全没问题。

总结一下

健康检查模式说白了就三件事:告诉别人你还活着、告诉别人你准备好了、告诉别人你具体哪里有问题。Spring Boot Actuator 帮你做了 80% 的工作,剩下的 20% 就是根据业务自定义检查逻辑。

我个人建议,不管项目大小,都加上健康检查。它就像汽车的仪表盘,平时你可能不看,但出问题的时候,它是你第一个求助的地方。

小技巧:健康检查的日志级别可以设高一点,比如 WARN 或 ERROR。否则每次 Kubernetes 几秒就扫一次,日志会刷屏。我曾经就被这个坑过,一天产生了 10GB 的日志。


公众号:蓝海资料掘金营,微信deep3321