Nacos&Ribbon&Feign核心微服务架构图
架构原理
1、微服务系统在启动时将自己注册到服务注册中心,同时外发布 Http 接口供其它系统调用(一般都是基于SpringMVC)
2、服务消费者基于 Feign 调用服务提供者对外发布的接口,先对调用的本地接口加上注解@FeignClient,Feign会针对加了该注解的接口生成动态代理,服务消费者针对 Feign 生成的动态代理去调用方法时,会在底层生成Http协议格式的请求,类似/stock/deduct?productId=100
3、Feign 最终会调用Ribbon从本地的Nacos注册表的缓存里根据服务名取出服务提供在机器的列表,然后进行负载均衡并选择一台机器出来,对选出来的机器IP和端口拼接之前生成的url请求,生成调用的Http接口地址http://192.168.0.60:9000/stock/deduct?productId=100,最后基于HTTPClient调用请求。
Nacos架构图
Nacos核心功能点
服务注册:Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如ip地址、端口等信息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。
服务心跳:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。
服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)
服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存
服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。
Nacos服务注册表结构:Map<namespace, Map<group::serviceName, Service>>
举例说明:
Nacos实现注册源码
1:首先会将传进来的实例参数放入一个队列当中去
2:程序后台会有一个线程异步的去读取这个队列,并将里面的数据放入双层map结构中的一个Set集合当中去,双层Map结构如下
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
Service中有一个
private Map<String, Cluster> clusterMap = new HashMap<>();
Cluster中就有Set结构,具体的是数据就放在Set中
@JsonIgnore
private Set<Instance> persistentInstances = new HashSet<>();
@JsonIgnore
private Set<Instance> ephemeralInstances = new HashSet<>();
3:为什么使用队列去异步注册呢
为了提高性能。满足高并发。注册是在客户端去调用的,服务端去执行的,如果搞同步的话,就会比较慢。系统启动时间就会更长。
Nacos注册表如何防止多节点读写并发冲突
首先平时我们可能会在注册时候会加排它锁,注册完后解锁,
但是在nacos中会去搞一个注册表的副本,注册或者修改的时候是修改的副本,然后再去替换真正的注册表内存数据。也就是copy on write机制。可能会出现读取到的数据不是最新数据的问题,但是性能更高。
oldMap就是副本。
如果是新增的话就会直接新增在副本当中,如果是修改的话就会想把副本中的数据删除掉,然后再新增到副本当中去,对注册中心中的老的数据没有影响,删除的话就会直接删掉,最后再去覆盖老的注册中心中的数据。
Nacos注册表为什么搞个双层map
首先第一层key为nameSpace可以实现不同环境的隔离。
第二层map的key为group,可以实现不同服务的之间的隔离。
同时也可以实现一个高可扩展。
Nacos服务发现源码
通过getAlllInstance去本地缓存Map查询服务列表获取调用远程接口去获取,
首先会先去本地缓存中获取服务列表,如果为空就会udateServiceNow,调用远程接口去获取服务,
客户端还会定时的去服务端拉去最新的服务列表,
远程接口主要就通过doSrvIpxt方法会去获取服务,通过调用service.srvIPs,最终去获取Set结构中的实例数据。返回的就是注册时写入的实例属性。
ephemeralInstances或者persistentInstances
nacos心跳机制源码
如果是临时实例,在调用注册实例的时候,会调用addBeatInfo,在这个方法中会起线程,并且执行这个线程,这个线程就会sendBeat,调用远程服务端的心跳接口。并且每隔五秒执行一次。
在这个接口中就会开启processClientBeat线程任务去更新最后的心跳时间。
nacos健康检查源码
当第一次注册服务时,就会去开启延时任务,一个健康检查的线程定时去执行。在createEmptyService中会调用createServiceIfAbsent,在这个方法中会调用初始化init方法,在这个init方法中就会开启健康检查的线程。
在这个线程中会allIPs拿到所有的实例,然后对这些实例进行for循环遍历一个一个的去比较最新心跳时间与当前时间差,超过15秒健康状态就会置为false,instance.setHealthy(false);,超过30秒就会删除这个实例。deleteIp(instance);。
Nacos服务变动事件发布源码
在服务端注册实力发生变化时,通过spring的事件发布与监听去实现
getPushService().serviceChanged(this);
this.applicationContext.publishEvent(new ServiceChangeEvent(this, service));
在pushService中的onApplicationEvent方法就会去监听这个事件,对其进行处理,这个方法中就会启动一个线程池,线程就会调用udpPush(ackEntry);方法,将这些信息发送给客服端。客户端收到数据后就会对数据进行相应的处理。
upd不像tcp那样会三次握手,可能会丢包。轻量传输协议。对于zookeeper那种通过tcp长链接来说会节约很多性能,就算丢包了,也还有客户端通过定时拉取的方式兜底。
Nacos心跳在集群架构下的设计原理
一个客户端的心跳检查只会在nacos集群中的一台服务上去做检查,如果某个客户端的状态发生变化后才会再去同步给集群中的其他服务。
int target = distroHash(serviceName) % servers.size();
return target >= index && target <= lastIndex;
具体选择那一台机器去负责心跳检查,其实就是根据服务的名称做一次hash运算,然后对集群中的服务数量取模,这样就会只返回一台机器为true,继续去做后面的具体的心跳检查的判断业务。
nacos集群节点状态同步源码
集群节点之间也会互相同步节点状态,如果感知到有节点宕机了,几圈其他的节点就会感知到并且更新集群节点状态,这个会影响到心跳任务机器的选择。
在ServerListManager类中(这个部类在启动的时候就会加载)的init方法中,
@PostConstruct
public void init() {
GlobalExecutor.registerServerStatusReporter(new ServerStatusReporter(), 2000);
GlobalExecutor.registerServerInfoUpdater(new ServerInfoUpdater());
}
会调用new ServerStatusReporter,每隔两秒调用一次,这个内部类会启动一个线程,在这个线程中会从配置文件中获取所有的集群实例信息,然后for循环调用send方法区通知集群中的其他节点,告知他们我还活着。
synchronizer.send(server.getAddress(), msg);
Nacos集群服务新增数据同步源码
在DistroConsistencyServiceImpl的put方法中,就会调用distroProtocol.sync方法去同步新增的数据实例,在这个方法中就是对所有的除去自己的member进行for循环去做同步的操作。最终通过 tasks.put(key, newTask);把数据放入一个map中,
distroTaskEngineHolder.getDelayTaskExecuteEngine().addTask(distroKeyWithTarget, distroDelayTask);
tasks.put(key, newTask);
然后异步的去取出这个map中的数据,然后会将其放入一个队列当中,然后会有一个线程InnerWorker异步的去取这个队列中的数据进行处理。通过http请求同步到其他节点当中去。
Nacos集群服务状态变动同步源码
在ServiceManager类中(在启动的时候就会加载这个类),init方法中,也会new ServiceReporter,这个会启动一个线程,在这个线程中,会获取到所有的serviceName,对其for循环调用send方法,去同步状态。其实就是通过http去请求其他服务的接口去实现的。
GlobalExecutor.scheduleServiceReporter(new ServiceReporter(), 60000, TimeUnit.MILLISECONDS);
synchronizer.send(server.getAddress(), msg);
新的节点加入集群后怎么去获取集群中已有的数据
在DistroProtocol这个类中(这个类在启动时就会加载),在调用构造方法的时候就会启动一个定时任务,
startDistroTask();
在这个任务中,就会startLoadTask();,这个任务就会调用DistroLoadDataTask,
这个任务就会启动线程,这个线程的run方法就会从远端机器拉取所有的数据快照,
loadCompletedMap.put(each, loadAllDataSnapshotFromRemote(each));
在这个loadAllDataSnapshotFromRemote方法中呢就会获取所有的member出去自己的然后进行for循环,调用一台机器后就会return,不用调用每台机器都去操作,调用getDatumSnapshot,然后通过getAllData通话http请求去获取远程数据。
总体流程如下