用 HealthCheckProcessor 写了一个 Redis 探活插件,3 小时省掉了 1 个运维脚本:自定义健康检查实战
文章目录
- [用 HealthCheckProcessor 写了一个 Redis 探活插件,3 小时省掉了 1 个运维脚本:自定义健康检查实战](#用 HealthCheckProcessor 写了一个 Redis 探活插件,3 小时省掉了 1 个运维脚本:自定义健康检查实战)
-
- [Nacos 默认只能检查 TCP 和 HTTP,但你的服务不止这两种](#Nacos 默认只能检查 TCP 和 HTTP,但你的服务不止这两种)
- [HealthCheckProcessor 接口定义](#HealthCheckProcessor 接口定义)
- [完整实现:Redis 探活插件](#完整实现:Redis 探活插件)
- 打包、部署、验证
- [扩展:HTTP 接口探活插件](#扩展:HTTP 接口探活插件)
- 一张图带走:健康检查插件开发全流程
Nacos 默认只能检查 TCP 和 HTTP,但你的服务不止这两种
Nacos 内置的健康检查支持两种协议:TCP 端口探测和 HTTP 接口探活。大部分微服务用这两种就够了。
但你有 Redis 实例、MySQL 实例、RabbitMQ 实例也在 Nacos 里注册了。它们的"健康"不是一个 TCP 端口通了就算------TCP 通了不代表 Redis 能读写,不代表 MySQL 连接池没满。
你需要一种自定义的健康检查方式:连上 Redis 发个 PING,收到 PONG 才算健康。
这篇文章用一个完整的例子走一遍 HealthCheckProcessor 插件的开发、打包、部署、验证全流程。代码可以直接拿去改。
⚠️ 本文基于 Nacos Server 2.3.x。HealthCheckProcessor 接口在 2.0.0 后稳定。如果你用的是 1.4.x,接口签名略有不同(返回值是
HealthCheckResult而非CheckResult)。
HealthCheckProcessor 接口定义
java
// com.alibaba.nacos.naming.healthcheck.extend.HealthCheckProcessor
public interface HealthCheckProcessor extends SpiExtension {
/**
* 执行健康检查
* @param task 健康检查任务,包含实例信息
* @param cluster 集群信息
*/
void process(HealthCheckTask task, Cluster cluster);
/**
* 返回这个 Processor 处理的检查类型名称
* 必须和实例注册时指定的 healthCheckType 一致
*/
String getType();
}
你需要实现 3 个方法:
| 方法 | 作用 | 你的实现 |
|---|---|---|
process(task, cluster) |
执行实际的健康检查逻辑 | 连 Redis 发 PING |
getType() |
返回检查类型标识 | "REDIS" |
getOrder()(可选) |
排序优先级 | 默认 1000 |
完整实现:Redis 探活插件
项目结构
nacos-healthcheck-redis/
├── pom.xml
└── src/main/
├── java/com/example/nacos/plugin/
│ └── RedisHealthCheckProcessor.java
└── resources/META-INF/services/
└── com.alibaba.nacos.naming.healthcheck.extend.HealthCheckProcessor
pom.xml
xml
<project>
<groupId>com.example</groupId>
<artifactId>nacos-healthcheck-redis</artifactId>
<version>1.0.0</version>
<dependencies>
<!-- Nacos Naming 接口(provided,不打包进去) -->
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-naming</artifactId>
<version>2.3.0</version>
<scope>provided</scope>
</dependency>
<!-- Jedis(轻量 Redis 客户端) -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.4.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包成 fat jar,包含 Jedis 依赖 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
核心实现类
java
package com.example.nacos.plugin;
import com.alibaba.nacos.api.naming.pojo.healthcheck.AbstractHealthChecker;
import com.alibaba.nacos.naming.healthcheck.extend.HealthCheckProcessor;
import com.alibaba.nacos.naming.healthcheck.extend.HealthCheckTask;
import com.alibaba.nacos.naming.pojo.Cluster;
import redis.clients.jedis.Jedis;
public class RedisHealthCheckProcessor implements HealthCheckProcessor {
@Override
public String getType() {
return "REDIS"; // 必须和注册实例时的 healthCheckType 一致
}
@Override
public void process(HealthCheckTask task, Cluster cluster) {
// 从实例元数据中获取 Redis 地址
String ip = task.getIp();
int port = task.getPort();
// 从 metadata 读取密码(可选)
String password = task.getMetadata().getOrDefault("redis.password", "");
int timeout = Integer.parseInt(
task.getMetadata().getOrDefault("redis.timeout", "2000"));
try (Jedis jedis = new Jedis(ip, port, timeout)) {
if (!password.isEmpty()) {
jedis.auth(password);
}
String pong = jedis.ping();
if ("PONG".equals(pong)) {
// 健康检查通过
task.setCheckResult(true);
} else {
// 响应异常
task.setCheckResult(false);
}
} catch (Exception e) {
// 连接失败或超时
task.setCheckResult(false);
}
}
@Override
public void initialize() {
// 插件初始化:可以预热连接池
}
@Override
public void shutdown() {
// 插件销毁:关闭连接池
}
}
SPI 配置文件
# src/main/resources/META-INF/services/com.alibaba.nacos.naming.healthcheck.extend.HealthCheckProcessor
com.example.nacos.plugin.RedisHealthCheckProcessor
打包、部署、验证
打包
bash
cd nacos-healthcheck-redis
mvn clean package -DskipTests
# 产出:target/nacos-healthcheck-redis-1.0.0.jar
部署
bash
# 把 jar 包复制到 Nacos Server 的 plugins 目录
cp target/nacos-healthcheck-redis-1.0.0.jar /home/nacos/plugins/
# 重启 Nacos(集群模式下逐台重启)
cd /home/nacos/bin
sh shutdown.sh
sh startup.sh -m cluster
注册实例时指定检查类型
bash
# 注册一个 Redis 实例到 Nacos,指定 healthCheckType 为 REDIS
curl -X POST "http://nacos:8848/nacos/v1/ns/instance" \
-d "serviceName=redis-cluster" \
-d "ip=10.0.1.50" \
-d "port=6379" \
-d "ephemeral=false" \
-d "metadata=healthCheckType=REDIS,redis.password=mypassword" \
-d "namespaceId=prod"
关键点:
ephemeral=false:持久化实例(服务端主动检查健康状态,不走客户端心跳)metadata.healthCheckType=REDIS:告诉 Nacos 用你的 REDIS 检查器metadata.redis.password:你的插件从 metadata 读取密码
验证
bash
# 查看实例健康状态
curl -s "http://nacos:8848/nacos/v1/ns/instance" \
-d "serviceName=redis-cluster" \
-d "ip=10.0.1.50" \
-d "port=6379" \
-d "namespaceId=prod" | jq '.healthy'
# 期望:true(如果 Redis 在线且 PING 成功)
# 停掉 Redis 后再次检查
# 期望:false(30 秒内 Nacos 会检测到健康检查失败)
扩展:HTTP 接口探活插件
如果你的服务有一个自定义的 /health 端点,返回的 JSON 里包含多个组件的状态:
json
{
"status": "UP",
"components": {
"db": {"status": "UP"},
"redis": {"status": "DOWN"},
"mq": {"status": "UP"}
}
}
你可以写一个 HTTP 探活插件,解析这个 JSON,任何一个组件 DOWN 就认为不健康:
java
public class DeepHealthCheckProcessor implements HealthCheckProcessor {
@Override
public String getType() {
return "DEEP_HTTP";
}
@Override
public void process(HealthCheckTask task, Cluster cluster) {
String url = "http://" + task.getIp() + ":" + task.getPort() + "/health";
try {
// HTTP GET /health
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setConnectTimeout(3000);
conn.setReadTimeout(3000);
if (conn.getResponseCode() == 200) {
String body = new String(conn.getInputStream().readAllBytes());
// 检查所有组件状态
boolean allUp = !body.contains("\"DOWN\"");
task.setCheckResult(allUp);
} else {
task.setCheckResult(false);
}
} catch (Exception e) {
task.setCheckResult(false);
}
}
}
一张图带走:健康检查插件开发全流程
#mermaid-svg-HM6h435vbhwQFHKO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HM6h435vbhwQFHKO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HM6h435vbhwQFHKO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HM6h435vbhwQFHKO .error-icon{fill:#552222;}#mermaid-svg-HM6h435vbhwQFHKO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HM6h435vbhwQFHKO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HM6h435vbhwQFHKO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HM6h435vbhwQFHKO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HM6h435vbhwQFHKO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HM6h435vbhwQFHKO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HM6h435vbhwQFHKO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HM6h435vbhwQFHKO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HM6h435vbhwQFHKO .marker.cross{stroke:#333333;}#mermaid-svg-HM6h435vbhwQFHKO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HM6h435vbhwQFHKO p{margin:0;}#mermaid-svg-HM6h435vbhwQFHKO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HM6h435vbhwQFHKO .cluster-label text{fill:#333;}#mermaid-svg-HM6h435vbhwQFHKO .cluster-label span{color:#333;}#mermaid-svg-HM6h435vbhwQFHKO .cluster-label span p{background-color:transparent;}#mermaid-svg-HM6h435vbhwQFHKO .label text,#mermaid-svg-HM6h435vbhwQFHKO span{fill:#333;color:#333;}#mermaid-svg-HM6h435vbhwQFHKO .node rect,#mermaid-svg-HM6h435vbhwQFHKO .node circle,#mermaid-svg-HM6h435vbhwQFHKO .node ellipse,#mermaid-svg-HM6h435vbhwQFHKO .node polygon,#mermaid-svg-HM6h435vbhwQFHKO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HM6h435vbhwQFHKO .rough-node .label text,#mermaid-svg-HM6h435vbhwQFHKO .node .label text,#mermaid-svg-HM6h435vbhwQFHKO .image-shape .label,#mermaid-svg-HM6h435vbhwQFHKO .icon-shape .label{text-anchor:middle;}#mermaid-svg-HM6h435vbhwQFHKO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HM6h435vbhwQFHKO .rough-node .label,#mermaid-svg-HM6h435vbhwQFHKO .node .label,#mermaid-svg-HM6h435vbhwQFHKO .image-shape .label,#mermaid-svg-HM6h435vbhwQFHKO .icon-shape .label{text-align:center;}#mermaid-svg-HM6h435vbhwQFHKO .node.clickable{cursor:pointer;}#mermaid-svg-HM6h435vbhwQFHKO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HM6h435vbhwQFHKO .arrowheadPath{fill:#333333;}#mermaid-svg-HM6h435vbhwQFHKO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HM6h435vbhwQFHKO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HM6h435vbhwQFHKO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HM6h435vbhwQFHKO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HM6h435vbhwQFHKO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HM6h435vbhwQFHKO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HM6h435vbhwQFHKO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HM6h435vbhwQFHKO .cluster text{fill:#333;}#mermaid-svg-HM6h435vbhwQFHKO .cluster span{color:#333;}#mermaid-svg-HM6h435vbhwQFHKO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HM6h435vbhwQFHKO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HM6h435vbhwQFHKO rect.text{fill:none;stroke-width:0;}#mermaid-svg-HM6h435vbhwQFHKO .icon-shape,#mermaid-svg-HM6h435vbhwQFHKO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HM6h435vbhwQFHKO .icon-shape p,#mermaid-svg-HM6h435vbhwQFHKO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HM6h435vbhwQFHKO .icon-shape .label rect,#mermaid-svg-HM6h435vbhwQFHKO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HM6h435vbhwQFHKO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HM6h435vbhwQFHKO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HM6h435vbhwQFHKO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1. 开发
实现 HealthCheckProcessor
- getType 返回标识
- 配置 SPI
META-INF/services/
写接口→实现类
3. 打包
mvn package
fat jar 含依赖
4. 部署
cp jar → plugins/
重启 Nacos
5. 注册实例
ephemeral=false
metadata.healthCheckType=REDIS
6. 验证
查实例健康状态
停服务看是否变 false
⚠️ nacos-naming
用 provided scope
⚠️ shade 插件
打包依赖
⚠️ 集群模式
逐台重启
6 步走完全流程。每步下面有注意事项:provided scope 不打包 Nacos 依赖、shade 插件打 fat jar、集群模式逐台重启。
你们在 Nacos 里注册过非微服务的组件(Redis、MySQL、MQ)吗?评论区留数字:1=注册过,用 TCP 检查 2=注册过,写了自定义检查插件 3=没注册过,只用 Nacos 管微服务 4=有这个需求,先收藏