健康检查机制
健康检查是 Nacos 保证服务可用性的核心机制。本章深入介绍 Nacos 的健康检查原理和配置方法。
健康检查概述
在微服务架构中,服务实例可能因为各种原因变得不可用:进程崩溃、网络故障、资源耗尽等。健康检查机制能够及时发现这些问题,将不健康的实例从服务列表中剔除,防止请求被发送到不可用的实例。
Nacos 健康检查模式
Nacos 支持两种健康检查模式,分别对应两种实例类型:
| 模式 | 实例类型 | 检查方式 | 适用场景 |
|---|---|---|---|
| 客户端心跳 | 临时实例(Ephemeral) | 客户端主动发送心跳 | 微服务实例 |
| 服务端探测 | 持久实例(Persistent) | 服务端主动探测 | 数据库、中间件等 |
临时实例健康检查
临时实例采用客户端心跳模式,这是微服务场景下最常用的模式。
工作原理
┌──────────────────────────────────────────────────────────────────┐
│ 临时实例健康检查流程 │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 服务实例 │ │ Nacos │ │
│ │ (客户端) │ │ Server │ │
│ └──────────────┘ └──────────────┘ │
│ │ │ │
│ │ 1. 注册实例 │ │
│ │ ─────────────────────────────────>│ │
│ │ │ │
│ │ 2. 启动心跳任务 │ │
│ │ (每 5 秒发送一次) │ │
│ │ ─────────────────────────────────>│ │
│ │ │ │
│ │ 3. 心跳响应 │ │
│ │<───────────────────────────────── │ │
│ │ │ │
│ │ │ 15 秒无心跳 │
│ │ │ → 标记为不健康 │
│ │ │ │
│ │ │ 30 秒无心跳 │
│ │ │ → 剔除实例 │
└──────────────────────────────────────────────────────────────────┘
心跳时间配置
spring:
cloud:
nacos:
discovery:
# 心跳间隔(毫秒),默认 5000
heart-beat-interval: 5000
# 心跳超时时间(毫秒),默认 15000
heart-beat-timeout: 15000
# IP 删除超时时间(毫秒),默认 30000
ip-delete-timeout: 30000
时间参数关系
心跳间隔 < 心跳超时 < IP 删除超时
心跳间隔:5 秒 → 客户端每 5 秒发送一次心跳
心跳超时:15 秒 → 15 秒未收到心跳,标记为不健康
IP 删除超时:30 秒 → 30 秒未收到心跳,从列表中剔除
实例状态流转
┌─────────────┐ 注册成功 ┌─────────────┐
│ 未注册 │ ───────────────> │ 健康 │
└─────────────┘ └─────────────┘
│
│ 15 秒无心跳
▼
┌─────────────┐
│ 不健康 │
└─────────────┘
│
│ 30 秒无心跳
▼
┌─────────────┐
│ 已剔除 │
└─────────────┘
心跳实现原理
Spring Cloud Alibaba Nacos Discovery 使用定时任务发送心跳:
// 简化的心跳逻辑
public class BeatTask implements Runnable {
@Override
public void run() {
// 构建心跳请求
BeatInfo beatInfo = new BeatInfo();
beatInfo.setServiceName(serviceName);
beatInfo.setIp(ip);
beatInfo.setPort(port);
// 发送心跳
JSONObject result = namingService.sendBeat(beatInfo);
// 处理响应
if (result != null) {
long nextBeatDelay = result.getLong("clientBeatInterval");
// 重新调度下一次心跳
executorService.schedule(new BeatTask(), nextBeatDelay, TimeUnit.MILLISECONDS);
}
}
}
持久实例健康检查
持久实例由 Nacos Server 主动探测,适合需要长期稳定存在的服务。
支持的探测方式
| 探测方式 | 说明 | 配置参数 |
|---|---|---|
| TCP | 检测端口是否可连接 | 默认方式 |
| HTTP | 发送 HTTP 请求检查响应 | 配置健康检查 URL |
| MySQL | 检测 MySQL 是否可用 | 自动识别 |
| 自定义 | 用户自定义探测逻辑 | 实现 HealthCheckProcessor |
TCP 探测
TCP 探测是最简单的方式,只需检测端口是否可连接:
# 注册持久实例
curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance' \
-d 'serviceName=mysql-service' \
-d 'ip=192.168.1.200' \
-d 'port=3306' \
-d 'ephemeral=false'
Nacos 会定期尝试连接 192.168.1.200:3306,如果连接失败则标记为不健康。
HTTP 探测
通过配置健康检查 URL,Nacos 会发送 HTTP 请求进行探测:
# 注册时配置健康检查 URL
curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance' \
-d 'serviceName=web-service' \
-d 'ip=192.168.1.100' \
-d 'port=8080' \
-d 'ephemeral=false' \
-d 'metadata={"preserved.register.source":"SPRING_CLOUD"}'
在 Spring Boot 应用中实现健康检查端点:
@RestController
public class HealthController {
@GetMapping("/actuator/health")
public Map<String, Object> health() {
Map<String, Object> result = new HashMap<>();
// 检查各项健康指标
boolean dbHealthy = checkDatabase();
boolean redisHealthy = checkRedis();
if (dbHealthy && redisHealthy) {
result.put("status", "UP");
} else {
result.put("status", "DOWN");
result.put("details", Map.of(
"database", dbHealthy ? "UP" : "DOWN",
"redis", redisHealthy ? "UP" : "DOWN"
));
}
return result;
}
private boolean checkDatabase() {
// 检查数据库连接
try {
// 执行简单查询
return true;
} catch (Exception e) {
return false;
}
}
private boolean checkRedis() {
// 检查 Redis 连接
try {
// 执行 PING 命令
return true;
} catch (Exception e) {
return false;
}
}
}
探测配置
在 Nacos Server 端配置探测参数:
# 健康检查间隔(毫秒)
nacos.naming.health-check.interval=5000
# 健康检查超时时间(毫秒)
nacos.naming.health-check.timeout=3000
# 健康检查重试次数
nacos.naming.health-check.retry=3
健康检查 API
查询实例健康状态
# 查询服务实例列表(只返回健康实例)
curl 'http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=user-service'
# 查询所有实例(包括不健康实例)
curl 'http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=user-service&healthyOnly=false'
# 查询指定实例详情
curl 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=user-service&ip=192.168.1.100&port=8080'
手动更新实例健康状态
# 标记实例为不健康
curl -X PUT 'http://127.0.0.1:8848/nacos/v1/ns/instance' \
-d 'serviceName=user-service' \
-d 'ip=192.168.1.100' \
-d 'port=8080' \
-d 'healthy=false'
发送心跳
# 手动发送心跳
curl -X PUT 'http://127.0.0.1:8848/nacos/v1/ns/instance/beat' \
-d 'serviceName=user-service' \
-d 'ip=192.168.1.100' \
-d 'port=8080' \
-d 'beat={"cluster":"DEFAULT","ip":"192.168.1.100","port":8080,"serviceName":"user-service"}'
保护阈值
保护阈值是防止服务雪崩的重要机制。
雪崩问题
假设某服务有 10 个实例,其中 8 个因故障变为不健康状态。如果没有保护机制,所有请求都会涌向剩余的 2 个健康实例,可能导致这 2 个实例也被压垮,形成雪崩效应。
保护阈值原理
健康实例比例 = 健康实例数 / 总实例数
如果 健康实例比例 < 保护阈值:
返回所有实例(包括不健康的)
否则:
只返回健康实例
配置保护阈值
spring:
cloud:
nacos:
discovery:
# 保护阈值,范围 0-1
protect-threshold: 0.5
或在控制台配置:
- 进入「服务管理」->「服务列表」
- 点击服务名进入详情
- 设置「保护阈值」
保护阈值示例
假设服务有 10 个实例,保护阈值设为 0.5:
| 健康实例数 | 健康比例 | 保护阈值 | 返回结果 |
|---|---|---|---|
| 10 | 100% | 50% | 10 个健康实例 |
| 6 | 60% | 50% | 6 个健康实例 |
| 4 | 40% | 50% | 所有 10 个实例 |
| 2 | 20% | 50% | 所有 10 个实例 |
当健康比例低于阈值时,返回所有实例,让消费者自行决定如何处理。
健康检查最佳实践
1. 合理设置心跳参数
根据网络环境和业务特点调整心跳参数:
# 网络稳定的环境
spring:
cloud:
nacos:
discovery:
heart-beat-interval: 5000
heart-beat-timeout: 15000
ip-delete-timeout: 30000
# 网络不稳定的环境
spring:
cloud:
nacos:
discovery:
heart-beat-interval: 10000
heart-beat-timeout: 30000
ip-delete-timeout: 60000
2. 实现完善的健康检查端点
健康检查端点应该检查所有关键依赖:
@RestController
public class HealthController {
@Autowired
private DataSource dataSource;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> health() {
Map<String, Object> health = new LinkedHashMap<>();
Map<String, Object> components = new LinkedHashMap<>();
// 检查数据库
components.put("database", checkDatabase());
// 检查 Redis
components.put("redis", checkRedis());
// 检查磁盘空间
components.put("diskSpace", checkDiskSpace());
// 判断整体状态
boolean allHealthy = components.values().stream()
.allMatch(status -> "UP".equals(((Map<?, ?>) status).get("status")));
health.put("status", allHealthy ? "UP" : "DOWN");
health.put("components", components);
health.put("timestamp", Instant.now());
return allHealthy
? ResponseEntity.ok(health)
: ResponseEntity.status(503).body(health);
}
private Map<String, Object> checkDatabase() {
Map<String, Object> result = new HashMap<>();
try (Connection conn = dataSource.getConnection()) {
boolean valid = conn.isValid(3);
result.put("status", valid ? "UP" : "DOWN");
} catch (Exception e) {
result.put("status", "DOWN");
result.put("error", e.getMessage());
}
return result;
}
private Map<String, Object> checkRedis() {
Map<String, Object> result = new HashMap<>();
try {
String pong = redisTemplate.getConnectionFactory()
.getConnection().ping();
result.put("status", "PONG".equals(pong) ? "UP" : "DOWN");
} catch (Exception e) {
result.put("status", "DOWN");
result.put("error", e.getMessage());
}
return result;
}
private Map<String, Object> checkDiskSpace() {
Map<String, Object> result = new HashMap<>();
File file = new File("/");
long freeSpace = file.getFreeSpace();
long totalSpace = file.getTotalSpace();
double freePercent = (double) freeSpace / totalSpace * 100;
result.put("status", freePercent > 10 ? "UP" : "DOWN");
result.put("freePercent", String.format("%.2f%%", freePercent));
return result;
}
}
3. 使用 Spring Boot Actuator
Spring Boot Actuator 提供了完善的健康检查支持:
# application.yml
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always
添加依赖后,Actuator 会自动检查常见组件的健康状态:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
4. 合理设置保护阈值
根据业务特点设置保护阈值:
- 高可用服务:设置较低的阈值(如 0.3),优先保证服务可用
- 数据一致性要求高的服务:设置较高的阈值(如 0.7),优先保证数据正确
5. 监控告警
配置健康状态监控告警:
@Component
public class HealthStatusMonitor {
@Autowired
private NamingService namingService;
@Scheduled(fixedRate = 60000)
public void checkServiceHealth() {
try {
List<Instance> instances = namingService.selectInstances(
"user-service",
false // 获取所有实例
);
long unhealthyCount = instances.stream()
.filter(i -> !i.isHealthy())
.count();
if (unhealthyCount > 0) {
// 发送告警
sendAlert(String.format(
"服务 user-service 有 %d 个不健康实例",
unhealthyCount
));
}
} catch (NacosException e) {
log.error("检查服务健康状态失败", e);
}
}
private void sendAlert(String message) {
// 实现告警逻辑:邮件、短信、钉钉等
}
}
常见问题
1. 实例频繁标记为不健康
可能原因:
- 网络延迟高,心跳超时
- 应用 GC 时间过长,阻塞心跳线程
- Nacos Server 负载过高
解决方案:
- 增加心跳超时时间
- 优化 JVM 参数减少 GC 时间
- 扩容 Nacos 集群
2. 实例被错误剔除
可能原因:
- 应用启动慢,心跳未及时发送
- 网络抖动导致心跳丢失
解决方案:
- 增加删除超时时间
- 应用启动完成后延迟注册
3. 健康检查端点响应慢
可能原因:
- 健康检查逻辑复杂
- 依赖服务响应慢
解决方案:
- 简化健康检查逻辑
- 设置超时时间
- 使用缓存避免频繁检查