【深入理解SpringCloud微服务】深入理解nacos
- Nacos服务注册
- 内存注册表
- 内存注册表的更新
- 通知客户端服务变更、服务同步、健康检查
- 2.x版本nacos的变化
Nacos服务注册
spring-cloud-alibaba-nacos-discovery通过实现spring-cloud-commons规范定义的接口,完成nacos接入spring-cloud后的客户端自动注册,我们不研究Nacos如何实现spring-cloud-commons规范,只要知道最终是利用Spring的事件监听机制触发服务注册。
然后就会调用nacos的NamingService进行服务注册,所以真正的入口是这个NamingService。
nacos总体上也是CS结构:
有一个nacos服务端接收http请求,然后我们的微服务集成了一个nacos客户端,发送http请求给nacos服务端,这个nacos客户端就是NamingService。通过Spring的事件监听机制触发NamingService的调用,而NamingService通过HttpURLConnection发送http请求。
AbstractAutoServiceRegistration#onApplicationEvent:
public void onApplicationEvent(WebServerInitializedEvent event) {
this.bind(event);
}
public void bind(WebServerInitializedEvent event) {
...
this.start();
...
}
public void start() {
...
this.register();
...
}
protected void register() {
this.serviceRegistry.register(this.getRegistration());
}
上面的this.serviceRegistry就是NacosServiceRegistry,this.getRegistration()返回的就是NacosRegistration,然后this.serviceRegistry.register(this.getRegistration())就会进入NacosServiceRegistry的register方法。
NacosServiceRegistry#register
@Override
public void register(Registration registration) {
...
namingService.registerInstance(serviceId, group, instance);
...
}
调用NamingService的registerInstance方法进行服务注册,里面就是向nacos服务端发起服务注册的http请求了。
然后nacos的InstanceController接收到http请求,调用ServiceManager更新内存注册表。
nacos使用了一个ServiceManager保存了内存注册表,当接收到服务注册请求时,nacos服务端会把服务实例信息写入到ServiceManager里面的内存注册表。
@RestController
@RequestMapping(UtilsAndCommons.NACOS_NAMING_CONTEXT + "/instance")
public class InstanceController {
...
@Autowired
private ServiceManager serviceManager;
...
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
...
//请求发送的是Instance信息,接收请求的时解析出来的也是Instance
final Instance instance = parseInstance(request);
//保存客户端注册信息
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
...
}
内存注册表
Nacos的ServiceManager保存了一个内存注册表,是一个双层的ConcurrentHashMap,外层的key是namespace(nacos的命名空间),内层的key是group拼上serviceName(服务名),value则是一个Service对象。
/**
* Map(namespace, Map(group::serviceName, Service)).
*/
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
Service里面并不是直接存放注册上来的服务实例信息,而是一个Map<String, Cluster>类型的clusterMap,Cluster代表集群,nacos这里把不同集群下的实例分组,把同一集群下的实例分到同一个Cluster里面,这样我们就可以就近选取同一集群下的实例。
Service#clusterMap
//Service里面保存的又是一个Map,集群名称和Cluster实例的映射,Cluster里面才保存真正的服务实例
private Map<String, Cluster> clusterMap = new HashMap<>();
Cluster里面才是真正存放注册上来的服务实例信息,有两个Set集合:
- Set<Instance> persistentInstances: 持久化实例信息集合
- Set<Instance> ephemeralInstances:非持久化实例信息集合
@JsonIgnore
private Set<Instance> persistentInstances = new HashSet<>(); //持久实例集合
@JsonIgnore
private Set<Instance> ephemeralInstances = new HashSet<>(); //临时实例集合
Instance里面存储的就是服务实例信息,什么ip地址端口号之类的。
内存注册表的更新
Eureka通过三级缓存解决了读写并发冲突,而Nacos则是通过写时复制(copy on write)解决读写并发冲突。
当有服务实例注册上来时,nacos会检查ServiceManger中的内存注册表是否有对应服务的Service对象,如果没有会创建一个空的Service对象放入到内存注册表,如果已经有对应服务的Service对象,则不需要创建。nacos不会马上去修改这个Service对象里面的服务实例信息,而是把该Service对象里面的所有Instance对象,连同新注册上来的Instance对象,收集到一个List中
这里只会收集所有临时实例,或者所有非临时实例,不会临时和非临时都全部收集上来。然后根据一定的规则生成一个key,把该key与前面的List存入一个内存的临时map中。然后把这个生成的key丢到一个tasks队列中。
后台会有一个线程轮询这个tasks队列,从队列中取得key,然后根据key从map中取出List,然后把List中的Instance按所属cluster进行分组。分好组后再逐一处理每一组中的Instance,同一组的Instance都是统一集群下的服务实例,再经过一定的处理后,最后会赋值到Cluster对象里面的两个Instance类型的Set集合中的其中一个(临时实例会赋值到ephemeralInstances,非临时实例赋值到persistentInstances),完成写时复制操作。
值得注意的是,这里nacos使用的了异步写内存注册表,性能是比Eureka要高的。
通知客户端服务变更、服务同步、健康检查
通知客户端服务变更这里与Eureka不一样,Eureka是开启了一个定时任务进行增量更新来感知注册中心服务端的变化。而nacos则是当有服务注册上来,会发送一个UDP请求,通知客户端有服务变更。
服务同步这nacos与Eureka做法差不多,也是采用了异步同步,不过nacos做了优化,它会攒够一批再一次性同步,也就是做了批量同步的优化。但是超过一定的期限,还是没有攒够一个批次的大小,还是会进行同步。
健康检查的定时任务是在创建Service的时候启动的,会定时对该Service里面的每个Instance实例进行检查,检查他们的客户端最后一次发送心跳的时间是否超过15s,如果是,则标记为不健康,如果超过30s,则进行服务下线(从内存注册表中剔除,并通知客户端)。
心跳也是由NamingService开启定时任务定时发送的,这个会在发送服务注册的http请求之前就会开启。
2.x版本nacos的变化
在nacos2.x以上的版本,如果是临时实例的注册,会走GRPC协议,如果是非临时实例的注册,会走http协议。
内存注册表的结构也发生了变化,那么与之对应的内存注册表读写的流程(也就是服务注册与服务发现的流程)也发生了变化。
ServiceManager里面不再直接存放双层内存注册表,而是放了一个Service自己与自己对于的map:ConcurrentHashMap<Service, Service> singletonRepository。
然后每一个连接上来的客户端,都会为它创建一个Client对象,有一个ClientManager管理这些Client对象。
然后Client里面有一个ConcurrentHashMap<Service, InstancePublishInfo>的map,key是注册上来的服务实例所属的Service,而value则是服务实例信息。
还有一个ClientServiceIndexesManager存放Service与所有对应的clientId之间的映射。
因为一个Service可能会有多个服务实例,每个服务实例注册上来时,都会创建一个Client,那么这里记录一个Service对应的有哪些服务实例,Service就可以拿到对应的clientId集合,进而拿到所有的Client对象,而Client对象中有存放了对应的服务实例信息,这样就可以拿到对应的服务实例信息。
于是服务发现的流程就是这样:
除了内存注册表以及服务注册与发现的变化外,服务同步、通知客户端服务变更、以及健康检查也发生了变化。
服务同步和通知客户端服务变更这两个操作也是异步的,这一点和1.x版本一样。由于2.x的nacos已经换成了GRPC协议,所以这里也是通过发送GRPC协议的请求进行处理,因为GRPC协议底层是长连接的,因此这里不需要再重复建立请求,只需要拿到对应的Connection连接对象,发送一个GRPC请求就可以。
1.x版本的nacos通过定时任务定时检测服务实例的最后心跳时间,如果超期了就进行服务下线。而2.x版本的nacos由于换成了GRPC协议的长连接,服务端和客户端之间没有再次建立请求的额外开销,因此增加了一个服务端主动探活的操作,如果客户端有响应,那么还是不会进行服务下线的。
2.x版本的nacos还对异步处理进行了封装,把事件监听机制封装到NotifyCenter中,把一些定时任务、延时任务、异步任务等的处理封装到了NacosTaskExecuteEngine中,这里就不细说了。