一、从一次"服务雪崩"说起
那是2019年的双十一,我负责的电商系统迎来了流量洪峰。系统架构是典型的微服务:用户请求 → 网关 → 订单服务 → 库存服务 → 支付服务。
凌晨零点刚过,监控系统报警此起彼伏。我发现一个致命问题:库存服务的一台机器CPU满了,但负载均衡器不知道,还在往它分发请求。
结果那台机器扛不住,开始响应超时;上游的订单服务等啊等,终于超时了;订单服务也开始堆积请求;然后订单服务也扛不住了......
整个链路像多米诺骨牌一样,一个接一个倒下。
后来复盘发现,如果我们用了服务注册与发现机制,健康检查功能会自动把那台故障机器从服务列表中剔除,流量就不会打到它身上了。
二、为什么需要服务注册与发现
2.1 传统方式的问题
方式1:配置文件硬编码
yaml
# 订单服务配置
inventory:
hosts:
- 192.168.1.10:8080
- 192.168.1.11:8080
- 192.168.1.12:8080
问题:服务器IP变了要改配置,扩容要改配置,缩容要改配置------改到怀疑人生。
方式2:DNS服务发现
bash
# 通过DNS解析服务地址
nslookup inventory-service
# 输出:
inventory-service canonical = inventory-1.default.svc.cluster.local
inventory-service canonical = inventory-2.default.svc.cluster.local
问题:DNS没有健康检查,挂了的服务IP还在DNS记录里。
2.2 服务注册与发现的原理
┌─────────────────────────────────────────────────────────────┐
│ │
│ 服务启动: │
│ ┌──────────────┐ │
│ │ 库存服务实例 │ → 注册到注册中心 │
│ │ 192.168.1.10 │ → 心跳:I'm alive! │
│ └──────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 注册中心 │ │
│ │ │ │
│ │ 服务名: inventory-service │ │
│ │ 实例1: 192.168.1.10:8080 (健康) │ │
│ │ 实例2: 192.168.1.11:8080 (健康) │ │
│ │ 实例3: 192.168.1.12:8080 (不健康,已移除) │ │
│ └──────────────────────────────────────────────────┘ │
│ ↓ │
│ 服务查询: │
│ ┌──────────────┐ │
│ │ 订单服务 │ ← 查询注册中心:我需要调库存服务! │
│ │ │ ← 获取实例列表:[10, 11](自动排除12) │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
2.3 核心概念
| 概念 | 说明 |
|---|---|
| 服务(Service) | 一组提供相同功能的服务实例集合,如"用户服务" |
| 服务实例(Instance) | 服务的具体运行实例,每个实例有唯一的IP:Port |
| 服务注册(Register) | 服务启动时向注册中心注册自己的地址 |
| 服务注销(Deregister) | 服务停止时从注册中心删除自己的信息 |
| 心跳(Heartbeat) | 服务定期向注册中心发送心跳,证明自己还活着 |
| 服务发现(Discovery) | 调用方从注册中心获取服务实例列表 |
| 健康检查(Health Check) | 注册中心检测服务是否还存活 |
三、Nacos是什么
3.1 Nacos概览
Nacos(Naming and Configuration Service)是阿里巴巴开源的服务注册与配置管理中心,是Spring Cloud Alibaba生态的核心组件。
Nacos核心能力:
┌────────────────────────────────────────────┐
│ Nacos │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 服务注册与发现 │ │ 配置中心 │ │
│ │ │ │ │ │
│ │ • 注册服务实例 │ │ • 配置管理 │ │
│ │ • 心跳检测 │ │ • 配置热更新 │ │
│ │ • 健康检查 │ │ • 配置共享 │ │
│ │ • 动态权重 │ │ • 配置加密 │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
└────────────────────────────────────────────┘
3.2 Nacos与Spring Cloud集成
xml
<!-- Spring Boot依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2023.0.1.2</version>
</dependency>
yaml
# application.yml - 服务提供者
spring:
application:
name: inventory-service # 服务名
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # Nacos地址
namespace: prod # 命名空间
group: DEFAULT_GROUP # 分组
cluster-name: BJ-GZ01 # 集群
# 实例元数据
cloud:
nacos:
discovery:
instance:
ip: ${NACOS_INSTANCE_IP:${spring.cloud.client.ip-address}}
port: ${SERVER_PORT:8080}
weight: 1 # 权重
enabled: true
healthy: true
ephemeral: false # false=持久化实例,true=临时实例
yaml
# application.yml - 服务消费者
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# RestTemplate + LoadBalancer配置
cloud:
loadbalancer:
ribbon:
enabled: false # 使用Spring Cloud LoadBalancer
3.3 服务注册
java
// 方式1:自动注册(通过依赖自动生效)
// 只需引入依赖,配置好Nacos地址,服务自动注册
// 方式2:手动注册(更精细的控制)
@RestController
public class NacosRegistrationController {
@Autowired
private NamingService namingService;
@GetMapping("/register")
public String register() throws NacosException {
// 手动注册服务实例
namingService.registerInstance(
"inventory-service", // 服务名
"192.168.1.10", // IP
8080, // 端口
"DEFAULT" // 分组
);
return "注册成功";
}
@GetMapping("/deregister")
public String deregister() throws NacosException {
// 手动注销
namingService.deregisterInstance(
"inventory-service",
"192.168.1.10",
8080
);
return "注销成功";
}
@GetMapping("/heartbeat")
public String heartbeat() throws NacosException {
// 发送心跳
namingService.sendHeartbeat(
"inventory-service",
"192.168.1.10",
8080
);
return "心跳发送成功";
}
}
四、服务发现:找到你想调用的服务
4.1 RestTemplate + 服务名调用
java
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced // 开启负载均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
public Order createOrder(Long productId, Long userId) {
// 直接用服务名调用,像调用本地服务一样
String url = "http://inventory-service/api/inventory/" + productId;
// Nacos + Ribbon会自动:
// 1. 从Nacos获取inventory-service的实例列表
// 2. 根据负载均衡策略选择一个实例
// 3. 替换url中的服务名为实际IP
// 4. 发起调用
ResponseEntity<Inventory> resp = restTemplate.getForEntity(
url,
Inventory.class
);
Inventory inventory = resp.getBody();
// 创建订单...
Order order = new Order();
order.setProductId(productId);
order.setUserId(userId);
return order;
}
}
4.2 Feign客户端调用
java
// 定义Feign接口
@FeignClient(name = "inventory-service", path = "/api")
public interface InventoryClient {
// 调用库存扣减
@PostMapping("/inventory/deduct")
Result<Void> deduct(@RequestBody DeductRequest request);
// 查询库存
@GetMapping("/inventory/{productId}")
Result<Inventory> getInventory(@PathVariable Long productId);
}
// 使用Feign客户端
@Service
public class OrderService {
@Autowired
private InventoryClient inventoryClient;
public void createOrder(Long productId, Integer count) {
// 像调用本地方法一样调用远程服务
inventoryClient.deduct(new DeductRequest(productId, count));
}
}
4.3 编程式服务发现
java
@Service
public class DynamicServiceDiscovery {
@Autowired
private NamingService namingService;
// 获取所有健康实例
public List<Instance> getHealthyInstances(String serviceName) throws NacosException {
// selectInstances自动过滤不健康实例
return namingService.selectInstances(serviceName, true);
}
// 获取所有实例
public List<Instance> getAllInstances(String serviceName) throws NacosException {
return namingService.selectInstances(serviceName, false);
}
// 获取一个健康实例(负载均衡)
public Instance getOneHealthyInstance(String serviceName) throws NacosException {
return namingService.selectOneHealthyInstance(serviceName);
}
// 订阅服务变化
public void subscribeService(String serviceName) throws NacosException {
namingService.subscribe(serviceName, event -> {
// 服务列表变化时的回调
Instances instances = (Instances) event.getContent();
System.out.println("服务实例列表变化: " + instances.getHosts().size());
for (Instance instance : instances.getHosts()) {
System.out.println(" - " + instance.getIp() + ":" + instance.getPort()
+ " (健康:" + instance.isHealthy() + ")");
}
});
}
}
五、健康检查:保障服务可用性
5.1 Nacos健康检查原理
临时实例(ephemeral=true):
实例注册后,Nacos不会主动检查健康状态
依赖客户端心跳维持
客户端心跳停止 → Nacos自动剔除实例
持久化实例(ephemeral=false):
Nacos服务端主动检查健康状态
通过TCP/HTTP方式探测
探测失败 → Nacos标记为不健康
5.2 客户端心跳配置
yaml
spring:
cloud:
nacos:
discovery:
# 心跳间隔(默认5秒)
heart-beat-interval: 5000
# 心跳超时时间(默认15秒)
heart-beat-timeout: 15000
# 不健康后多少秒剔除(默认30秒)
ip-delete-timeout: 30000
java
// 自定义心跳续约
@RestController
@RequestMapping("/health")
public class HealthController {
@Autowired
private HeartBeatManager heartBeatManager;
@GetMapping("/status")
public Result<String> status() {
// 业务健康检查
boolean serviceHealthy = checkBusinessHealth();
if (!serviceHealthy) {
// 通知Nacos当前实例不健康
heartBeatManager.markUnhealthy();
} else {
heartBeatManager.markHealthy();
}
return Result.ok("OK");
}
private boolean checkBusinessHealth() {
// 检查数据库连接
// 检查Redis连接
// 检查消息队列
// ...
return true;
}
}
5.3 服务端主动探测
yaml
# Nacos服务端健康检查配置(standalone-mysql.yaml)
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/nacos?characterEncoding=utf8
username: nacos
password: nacos
# 健康检查配置
nacos:
health-checker:
# HTTP健康检查
http:
max-timeout: 2000 # 超时时间ms
path: /actuator/health
# TCP健康检查
tcp:
max-timeout: 2000
# MySQL健康检查
mysql:
max-timeout: 2000
5.4 分区容灾:同机房优先调用
yaml
spring:
cloud:
nacos:
discovery:
cluster-name: BJ-GZ01 # 北京、广州01机房
java
// 优先调用同机房实例
@Service
public class SameClusterLoadBalancer implements LoadBalancer {
@Autowired
private NamingService namingService;
@Override
public ServiceInstance choose(String serviceId) {
try {
// 1. 先尝试获取同机房实例
List<Instance> sameClusterInstances = namingService.selectInstances(
serviceId,
true, // 只选健康的
"BJ-GZ01" // 当前机房
);
if (!sameClusterInstances.isEmpty()) {
// 同机房有实例,随机选一个
return randomPick(sameClusterInstances);
}
// 2. 同机房没有,使用任意健康实例
List<Instance> allInstances = namingService.selectInstances(serviceId, true);
return randomPick(allInstances);
} catch (NacosException e) {
// 异常情况下fallback
return fallbackChoose(serviceId);
}
}
}
六、集群部署:保障高可用
6.1 Nacos集群架构
┌──────────────────┐
│ 负载均衡器 │
│ (Nginx/云LB) │
└────────┬─────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Nacos-1 │◄────►│ Nacos-2 │◄────►│ Nacos-3 │
│ (Leader) │ │ (Follower) │ │ (Follower) │
│ 192.168.1.1│ │ 192.168.1.2 │ │ 192.168.1.3 │
│ :8848 │ │ :8848 │ │ :8848 │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
└────────────────────┼────────────────────┘
▼
┌──────────────┐
│ MySQL │
│ (主从复制) │
└──────────────┘
6.2 集群配置
yaml
# cluster.conf - 节点配置
# 在每个Nacos节点的conf目录下配置
192.168.1.1:8848
192.168.1.2:8848
192.168.1.3:8848
yaml
# application.properties - 应用配置
server.port=8848
# 数据源配置(使用MySQL持久化)
spring.datasource.platform=mysql
db.num=2
db.url.0=jdbc:mysql://192.168.1.10:3306/nacos?characterEncoding=utf8&connectTimeout=1000
db.url.1=jdbc:mysql://192.168.1.11:3306/nacos?characterEncoding=utf8&connectTimeout=1000
db.user=nacos
db.password=nacos
# 集群节点配置
nacos.inetutils.prefer-hostname-over-ip=false
nacos.inetutils.ip-address=192.168.1.1
# Raft协议配置
nacos.core.protocol.raft.data=file
nacos.core.protocol.raft.min-election-timeout=10000
nacos.core.protocol.raft.max-election-timeout=15000
6.3 客户端接入集群
yaml
# 服务端接入Nacos集群
spring:
cloud:
nacos:
discovery:
# 配置所有Nacos节点地址
server-addr: 192.168.1.1:8848,192.168.1.2:8848,192.168.1.3:8848
# Nacos控制台集群管理页面查看状态:
# http://192.168.1.1:8848/nacos/#/clusterManagement
6.4 健康检查机制
bash
# 检查Nacos集群健康状态
curl http://192.168.1.1:8848/nacos/v1/console/health/readiness
# 返回:
{
"status": "UP"
}
# 检查leader节点
curl http://192.168.1.1:8848/nacos/v1/ns/raft/leader
# 返回:
{
"leader": "192.168.1.1:7848"
}
七、配置中心:应用的"中央控制室"
7.1 Nacos配置管理模型
Nacos配置层次:
命名空间(Namespace)
│
└── 分组(Group)
│
└── Data ID
│
└── 配置内容(JSON/YAML/Properties)
命名空间场景:
- dev(开发环境)
- test(测试环境)
- uat(预发环境)
- prod(生产环境)
分组场景:
- DEFAULT_GROUP(默认分组)
- ORDER_GROUP(订单相关配置)
- INVENTORY_GROUP(库存相关配置)
7.2 配置发布
java
// 方式1:控制台发布
# 在Nacos控制台 → 配置管理 → 配置列表 → 新建配置
# Data ID: order-service.yaml
# Group: DEFAULT_GROUP
# Namespace: prod
# 配置内容:
order:
timeout: 5000
max-retry: 3
payment-url: http://payment-service/api
java
// 方式2:OpenAPI发布
@RestController
public class ConfigController {
@Autowired
private ConfigService configService;
@GetMapping("/publish-config")
public String publishConfig() throws NacosException {
String dataId = "order-service.yaml";
String group = "DEFAULT_GROUP";
String content = "order:\n timeout: 5000";
configService.publishConfig(
dataId,
group,
content
);
return "配置发布成功";
}
@GetMapping("/get-config")
public String getConfig() throws NacosException {
String config = configService.getConfig(
"order-service.yaml",
"DEFAULT_GROUP",
5000
);
return config;
}
}
7.3 动态配置监听
java
// @RefreshScope实现配置热更新
@RestController
@RefreshScope // 当Nacos配置变化时,Bean自动重建
public class OrderConfigController {
// 配置变化时,这个值会自动更新
@Value("${order.timeout:3000}")
private int orderTimeout;
@Value("${order.max-retry:3}")
private int maxRetry;
@GetMapping("/config")
public Map<String, Object> getConfig() {
Map<String, Object> config = new HashMap<>();
config.put("timeout", orderTimeout);
config.put("maxRetry", maxRetry);
config.put("refreshTime", new Date());
return config;
}
}
7.4 多环境配置共享
yaml
# 共享配置
spring:
application:
name: order-service
cloud:
nacos:
config:
# 共享配置(所有环境共享)
shared-configs:
- data-id: common.yaml
group: DEFAULT_GROUP
refresh: true
- data-id: log.yaml
group: DEFAULT_GROUP
refresh: true
# 扩展配置(当前环境私有)
# - data-id: ${spring.application.name}-${spring.profiles.active}.yaml
# group: ${spring.cloud.nacos.config.group}
# refresh: true
八、生产环境最佳实践
8.1 保护注册中心
yaml
# Nacos访问控制
spring:
cloud:
nacos:
discovery:
# 命名空间隔离(不同环境用不同命名空间)
namespace: ${NACOS_NAMESPACE:prod}
config:
namespace: ${NACOS_NAMESPACE:prod}
# 开启认证
nacos.core.auth.enabled=true
nacos.core.auth.server.identity.key=${NACOS_AUTH_KEY:yourKey}
nacos.core.auth.server.identity.value=${NACOS_AUTH_VALUE:yourValue}
yaml
# 服务端连接认证
spring:
cloud:
nacos:
discovery:
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
config:
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
8.2 优雅下线
java
// 应用关闭时先从Nacos注销
@Component
public class NacosGracefulShutdown implements DisposableBean {
@Autowired
private NamingService namingService;
@Value("${spring.cloud.client.ip-address}")
private String ip;
@Value("${server.port}")
private int port;
@Value("${spring.application.name}")
private String serviceName;
@PreDestroy
public void deregister() {
try {
// 1. 从注册中心注销
namingService.deregisterInstance(serviceName, ip, port);
// 2. 等待一下,确保Nacos已处理
Thread.sleep(3000);
System.out.println("服务已从Nacos注销: " + serviceName);
} catch (Exception e) {
System.err.println("注销服务失败: " + e.getMessage());
}
}
}
8.3 监控告警
yaml
# Nacos监控指标(Prometheus)
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
yaml
# Prometheus告警规则
groups:
- name: nacos-alerts
rules:
- alert: NacosInstanceDown
expr: up{job="nacos"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Nacos节点不可用"
- alert: NacosServiceDown
expr: nacos_service_count == 0
for: 2m
labels:
severity: warning
annotations:
summary: "注册服务数量为0,可能有问题"
九、踩坑实录
坑1:Nacos注册的服务名带了下划线
某次部署,发现服务注册到Nacos后,消费者死活调用不到。
原因 :Spring Boot的application.name是
order_service(带下划线),Nacos注册后消费者找不到。教训:服务名统一使用横杠(order-service),不要用下划线。Ribbon和Feign默认用横杠匹配。
坑2:Nacos集群脑裂
三节点Nacos集群,网络抖动后,出现两个Leader,数据不一致。
解决:使用大于3的奇数节点(3、5、7),配置合理的Raft超时时间。
教训:Nacos使用Raft协议,需要合理的选举超时配置。
坑3:配置热更新没生效
修改了Nacos配置,控制台显示已更新,但应用没生效。
原因:Bean没有加@RefreshScope注解,或者@Value注入的字段是static的。
教训:@RefreshScope只能作用域实例化的Bean,static字段不会热更新。
坑4:临时实例被误删
某次发布,服务重启时Nacos心跳断了,旧实例被删,新实例还没注册完成,出现短暂的调用失败。
解决:提高心跳超时时间,或使用持久化实例。
教训:发布时先注册新实例,等新实例就绪后再下线旧实例(滚动发布)。
十、总结
服务注册与发现是微服务架构的基础:
- Nacos:集服务注册、配置中心于一身,阿里开源,Spring Cloud Alibaba默认集成
- 服务注册:心跳机制保障服务可用性
- 服务发现:动态获取服务实例列表,无需硬编码
- 健康检查:自动剔除故障实例,防止调用失败
- 配置中心:配置集中管理,热更新无需重启
- 集群部署:Nacos本身也需要高可用部署
最佳实践:
- 生产环境Nacos集群至少3节点
- 使用命名空间隔离不同环境
- 服务下线前先从注册中心注销
- 配置使用@RefreshScope实现热更新
- 开启Nacos认证,防止未授权访问
- 监控Nacos健康状态和注册实例数
血的教训:
不要以为服务注册后就万事大吉了。注册中心本身就是单点,需要集群保障高可用。同时,客户端要有容错机制(重试、熔断),不能完全依赖注册中心的健康检查。
思考题: 你们的微服务用的是Nacos、Eureka还是Consul?为什么?服务下线时的优雅注销做了吗?
个人观点,仅供参考