nacos服务注册与发现设计图
为什么需要使用服务发现 ?
我们假设你正在编写某些代码,这些代码调用了有 REST API 或 Thrift API 的服务。为了发送一个请求,你的代码需要知道服务实例的网络位置 (IP 地址与端口)。在运行于物理硬件上的传统应用中,服务实例的网络位置是相对静态的。例如,你的代码可以从偶尔更新的配置文件中读取网络位置。 然而,在现代基于云的微服务应用中,这是一个更难解决的问题。服务实例有动态分配的网络位置。此外,由于自动扩缩、故障与升级,整组服务实例会动态变更。 因此,你的客户端代码需要使用更精确的服务发现机制。 有两种主要的服务发现模式:客户端发现(client-side discovery)与服务端发现(server-side discovery)。让我们先来看看客户端发现。
客户端发现模式是怎么样的?
当使用客户端发现模式时,客户端负责确定可用服务实例的网络位置和请求负载均衡。客户端查询服务注册中心(service registry),它是可用服务实例的数据库。 之后,客户端利用负载均衡算法选择一个可用的服务实例并发出请求。 服务实例的网络位置在服务注册中心启动时被注册。当实例终止时,它将从服务注册中心中移除。通常使用心跳机制周期性地刷新服务实例的注册信息。 Netflix OSS 提供了一个很好的客户端发现模式示例。Netflix Eureka 是一个服务注册中心,它提供了一组用于管理服务实例注册和查询可用实例的 REST API。Netflix Ribbon 是一个 IPC 客户端,可与 Eureka 一起使用,用于在可用服务实例之间使请求负载均衡。本章稍后将讨论 Eureka。 客户端发现模式存在各种优点与缺点。该模式相对比较简单,除了服务注册中心,没有其他移动部件。此外,由于客户端能发现可用的服务实例, 因此可以实现智能的、特定于应用的负载均衡决策,比如使用一致性哈希。该模式的一个重要缺点是它将客户端与服务注册中心耦合在一起。 你必须为你使用的每种编程语言和框架实现客户端服务发现逻辑。 现在我们已经了解了客户端发现,接下来让我们看看服务端发现。
服务发现原理:
服务发现由NacosWatch完成,它实现了Spring的Lifecycle接口,容器启动和销毁时会调用对应的start()和stop()方法
来看对应源码
@Override
public void start() {
// cas设置运行状态为true
if (this.running.compareAndSet(false, true)) {
// 延时执行一个服务发现任务
this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(
this::nacosServicesWatch, this.properties.getWatchDelay());
}
}
@Override
public void stop() {
// 设置运行状态为false 然后取消正在执行的任务
if (this.running.compareAndSet(true, false) && this.watchFuture != null) {
this.watchFuture.cancel(true);
}
}
public void nacosServicesWatch() {
try {
boolean changed = false;
NamingService namingService = properties.namingServiceInstance();
// 获取nacos server上最新的服务提供者们
ListView<String> listView = properties.namingServiceInstance()
.getServicesOfServer(1, Integer.MAX_VALUE);
List<String> serviceList = listView.getData();
// 有新的订阅产生 订阅完后发布事件
Set<String> currentServices = new HashSet<>(serviceList);
currentServices.removeAll(cacheServices);
if (currentServices.size() > 0) {
changed = true;
}
// 取消已经下线的服务订阅,发起取消订阅操作并删除订阅监听
if (cacheServices.removeAll(new HashSet<>(serviceList))
&& cacheServices.size() > 0) {
changed = true;
for (String serviceName : cacheServices) {
namingService.unsubscribe(serviceName,
subscribeListeners.get(serviceName));
subscribeListeners.remove(serviceName);
}
}
cacheServices = new HashSet<>(serviceList);
// 订阅服务 并对每个服务都添加一个心跳检测监听
for (String serviceName : cacheServices) {
if (!subscribeListeners.containsKey(serviceName)) {
EventListener eventListener = event -> NacosWatch.this.publisher
.publishEvent(new HeartbeatEvent(NacosWatch.this,
nacosWatchIndex.getAndIncrement()));
subscribeListeners.put(serviceName, eventListener);
namingService.subscribe(serviceName, eventListener);
}
}
// 有服务变化 发布事件
if (changed) {
this.publisher.publishEvent(
new HeartbeatEvent(this, nacosWatchIndex.getAndIncrement()));
}
}
catch (Exception e) {
log.error("Error watching Nacos Service change", e);
}
}
大致流程:nacos client这边在spring容器启动后执行一个服务订阅操作的延时任务,这个任务执行时先拉取nacos server那边最新的服务列表,然后与本地缓存的服务列表进行比较,取消订阅下线的服务,然后向nacos server发起订阅操作,订阅所有服务
那么服务消费者如何实时感知服务提供者的状态信息呢
1、服务消费者订阅后会执行一个轮询任务(每1s执行一次)用来拉取最新的服务提供者信息并实时更新,实现在HostReactor中的UpdateTask完成,下面来看代码
public class UpdateTask implements Runnable {
long lastRefTime = Long.MAX_VALUE;
private String clusters;
private String serviceName;
public UpdateTask(String serviceName, String clusters) {
this.serviceName = serviceName;
this.clusters = clusters;
}
@Override
public void run() {
try {
// 拿到当前的服务信息
ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
// 为空 拉取最新的服务列表随后更新
if (serviceObj == null) {
updateServiceNow(serviceName, clusters);
// 继续轮询
executor.schedule(this, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
return;
}
if (serviceObj.getLastRefTime() <= lastRefTime) {
// 当前服务未及时更新 进行更新操作
updateServiceNow(serviceName, clusters);
serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
} else {
// if serviceName already updated by push, we should not override it
// since the push data may be different from pull through force push
refreshOnly(serviceName, clusters);
}
// 设置服务最新的更新时间
lastRefTime = serviceObj.getLastRefTime();
// 订阅被取消
if (!eventDispatcher.isSubscribed(serviceName, clusters) &&
!futureMap.containsKey(ServiceInfo.getKey(serviceName, clusters))) {
// abort the update task:
NAMING_LOGGER.info("update task is stopped, service:" + serviceName + ", clusters:" + clusters);
return;
}
// 继续下一次轮询
executor.schedule(this, serviceObj.getCacheMillis(), TimeUnit.MILLISECONDS);
} catch (Throwable e) {
NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e);
}
}
}
2、上面服务注册时我们说过,服务提供者注册时nacos服务端也有一个相应的心跳检测,当心跳检测超时也就是未及时收到服务提供者的心跳包,nacos server判定该服务状态异常 随后通过UDP推送服务信息用来告知对应服务消费者,服务消费者通过PushReceiver来处理udp协议,HostReactor.processServiceJson(String json)来更新本地服务列表
/********************************PushReceiver*****************************/
public void run() {
while (true) {
try {
// byte[] is initialized with 0 full filled by default
byte[] buffer = new byte[UDP_MSS];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
udpSocket.receive(packet);
String json = new String(IoUtils.tryDecompress(packet.getData()), "UTF-8").trim();
NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());
PushPacket pushPacket = JSON.parseObject(json, PushPacket.class);
String ack;
if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {
// 处理变更信息
hostReactor.processServiceJSON(pushPacket.data);
// send ack to server
ack = "{\"type\": \"push-ack\""
+ ", \"lastRefTime\":\"" + pushPacket.lastRefTime
+ "\", \"data\":" + "\"\"}";
} else if ("dump".equals(pushPacket.type)) {
// dump data to server
ack = "{\"type\": \"dump-ack\""
+ ", \"lastRefTime\": \"" + pushPacket.lastRefTime
+ "\", \"data\":" + "\""
+ StringUtils.escapeJavaScript(JSON.toJSONString(hostReactor.getServiceInfoMap()))
+ "\"}";
} else {
// do nothing send ack only
ack = "{\"type\": \"unknown-ack\""
+ ", \"lastRefTime\":\"" + pushPacket.lastRefTime
+ "\", \"data\":" + "\"\"}";
}
udpSocket.send(new DatagramPacket(ack.getBytes(Charset.forName("UTF-8")),
ack.getBytes(Charset.forName("UTF-8")).length, packet.getSocketAddress()));
} catch (Exception e) {
NAMING_LOGGER.error("[NA] error while receiving push data", e);
}
}
}
服务注册和订阅我只讲解了主要流程,nacos server那边处理源码太多就不一一贴出来了,根据对应的api接口进去一看便知,nacos源码比较好理解,没有什么特别难读懂的地方,这边只是提供给大家一个看源码的思路,具体详细流程还需要读者自己去细读
实际使用步骤:
1,添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
2,配置服务提供者
spring.application.name=zuul-gateway
server.port=19997
spring.cloud.nacos.discovery.server-addr=localhost:8848
3,注解开启服务注册发现功能
@EnableDiscoveryClient
public class ZuulApplication {
...
}