目录
Springenloud的常见组件
Nacos注册表结构
注册表结构源码分析
编辑
1.在对应的controller中找到对应的方法
2.看看ServiceManager的结构
3.然后跟进看Service(服务)
4.Cluster实例
5.总结
Nacos如何支撑十万服务注册压力
回答
添加服务源码解析
集群一致性(更新实例列表)
DelegateConsistencyServiceImpl
DistroConsistencyServiceImpl(发行一致性实现)
Nacos如何解决并发读写冲突
心跳检测
临时实例:
非临时实例:
场景:
Nacos服务发现
1.主动拉取模式:
2.订阅模式:
这个订阅模式,也就是服务变更的UDP通知(源码)
(38条消息) Nacos注册中心_Fairy要carry的博客-CSDN博客_nacos多环境注册中心
Springenloud的常见组件
从服务与服务之间进行分析:管理服务nacos,调用其他服务openFeign,拉去其他服务需要考虑到负载均衡Ribbon,还要考虑安全问题也就是网关之类的GateWay,请求服务需要限流之类的Sentinel,Hystrix熔断降级
Nacos注册表结构
首先我们服务存储就是存储到nacos的注册表结构中,这里需要从两方面入手:1.Nacos分级存储模型 2.Nacos服务端源码
1.Namespace可以将不同环境的服务隔离开,或者多租户模式,每个租户环境不一样进行隔离
2.Group将服务分组,比如一个模块:支付模块里面有多种服务,用户服务,订单服务,购物车服务等等
3.服务,一个分组里面有多个服务,Order服务等
4.集群:一个服务下可以有多个实例8001,8002等等,然后多个实例可以在不同的地方为一个集群
注册表结构源码分析
对应到Java代码中,其实nacos下由多个SpringBoot项目组成
nacos-console为nacos启动模块,配置文件下写了nacos的端口之类的
然后我们看看注册表,也就是关键的模块
最关键是下面的ServiceManager调用的方法——>引入nacos注册表
/**
* Register new instance.
*
* @param request http request
* @return 'ok' if success
* @throws Exception any error during register
*/
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
final Instance instance = parseInstance(request);
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
这里贼关键,引入了一个多层map,最外层的String指的是Namespace,然后对应的值Map就是根据不同环境的服务,再分层就是组Group——>对应的Service,Service可能包含多种服务
@Component
public class ServiceManager implements RecordListener {
/**
* Map(namespace, Map(group::serviceName, Service)).
*/
private final Map> serviceMap = new ConcurrentHashMap<>();
Service中里面用了一个HashMap,存储服务的集群,对应Cluster实例
//String集群名字,Cluster实例
private Map clusterMap = new HashMap<>();
发现最终的实例使用HashSet存储起来的,有临时和非临时两种情况,这也就是和Eureka的区别
//永久实例,Set存服务
@JsonIgnore
private Set persistentInstances = new HashSet<>();
//临时实例,体现出了和Eurka的区别
@JsonIgnore
private Set ephemeralInstances = new HashSet<>();
Nacos是多级存储模型,最外层通过namespace来实现环境隔离,然后是group分组,分组下就是服务,一个服务有可以分为不同的集群,集群中包含多个实例。因此其注册表结构为一个Map,类型是:Map
外层key是namespace_id
,内层key是group+serviceName,
然后Service内部维护一个Map,结构是:Map
,key是clusterName,值是集群信息——>Cluster内部维护一个Set集合,元素是Instance类型,代表集群中的多个实例。
Nacos如何支撑十万服务注册压力
从两方面回答,1.需要搭建nacos集群,提高并发能力,因为多个nacos实例可以负载均衡嘛;
2.那既然是多个nacos实例,那么它们是怎么保证数据的一致性呢?
Nacos内部接受到注册请求时,它不会立马写数据,而是将服务注册的任务放到一个阻塞队列中,然后就立即响应给客户端注册成功,也就是说异步的处理了服务注册的请求,然后注册请求的完成时利用了线程池读取阻塞队列的任务;那么服务的同步更新呢?也是异步的完成,提高了并发写的能力
首先尝试获取我们的namespaceId,然后尝试获取serviceName,也就是group@@那些——>2.接着我们会解析请求注册服务,解析为实例封装到Instance中——>3.serviceManager调用注册的方法注册我们的服务
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
//从请求中获取namespaceId
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
//获取服务名称
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
//封装为Instance实例,其实就是放在表中最底层Set那里
final Instance instance = parseInstance(request);
//注册服务
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
然后进入registerInstance注册实例的方法
1.如果时第一次注册实例会创建一个空的服务(下面还包含集群,只是默认没有设置),将实例注册——>2.此时还没有注册进这个空的服务,只是包含了名字之类,但是实例信息都还没有放进去——>3.调用addInstance()方法,将注册的实例信息注册大service服务中(根据是否实例还是非实例进行注册)——>体现出了装饰器模式
//将实例注册到 AP 模式的服务中。 如果服务或集群不存在,
此方法会静默创建服务或集群。 @param namespaceId 命名空间id @param serviceName 服务名称 @param instance instance to register @throws Exception 过程中发生任何错误
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
//1.创建一个空的服务,第一次才会创建
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
//2.从注册表中拿到服务
Service service = getService(namespaceId, serviceName);
if (service == null) {
throw new NacosException(NacosException.INVALID_PARAM,
"service not found, namespace: " + namespaceId + ", service: " + serviceName);
}
//3.添加实例
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
3.添加实例进服务
1.首先根据我们的id服务名称啥的创建key作为唯一标识——>2.然后获取服务——>3.通过上锁保证多个实例的情况下出现脏写现象,防止对数据并行或者并发的处理——>4.里面获取了需要更新的实例列表,并且将要更新实例放在里面(并且addIPAddress中会覆盖旧的实例表),最后调用put方法进行注册表的更新以及集群数据同步(非临时和临时是不一样的,里面用到了装饰模式)
DelegateConsistencyServiceImpl和DistroConsistencyServiceImpl两种对consistencyService
的put方法进行重写
/**
* Add instance to service.
*
* @param namespaceId namespace
* @param serviceName service name
* @param ephemeral whether instance is ephemeral
* @param ips instances
* @throws NacosException nacos exception
*/
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException {
//把我们的实例生成一个key,每个服务有唯一的key
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
Service service = getService(namespaceId, serviceName);
//当服务中的多个实例进行并行操作,所以加锁了,防止脏写,串行执行
synchronized (service) {
//得到实例列表
List instanceList = addIpAddresses(service, ephemeral, ips);
//封装实例列表到Instances对象
Instances instances = new Instances();
instances.setInstanceList(instanceList);
//更新注册表,同步数据给到集群其他节点
consistencyService.put(key, instances);
}
}
集群一致性(更新实例列表)
集群一致性的体现关键在于临时实例和非临时实例对于put方法的重写
@Override
public void put(String key, Record value) throws NacosException {
// 根据实例是否是临时实例,判断委托对象
mapConsistencyService(key).put(key, value);
}
默认情况下所有实例都是临时实例
private ConsistencyService mapConsistencyService(String key) {
// 判断是否是临时实例:
// 是,选择 ephemeralConsistencyService,也就是 DistroConsistencyServiceImpl类
// 否,选择 persistentConsistencyService,也就是PersistentConsistencyServiceDelegateImpl
return KeyBuilder.matchEphemeralKey(key) ? ephemeralConsistencyService : persistentConsistencyService;
}
看看它里面的put方法——>1.先将实例信息写入本地的实例表——>2.然后再开启集群同步
1.onPut(key, value):其中value就是Instances,要更新的服务信息。这行主要是基于线程池方式,异步的将Service信息写入注册表中(就是那个多重Map)
2.distroProtocol.sync():就是通过Distro协议将数据同步给集群中的其它Nacos节点
public void put(String key, Record value) throws NacosException {
// 先将要更新的实例信息写入本地实例列表
onPut(key, value);
// 开始集群同步
distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
globalConfig.getTaskDispatchPeriod() / 2);
}
跟进看下onPut方法
判断是否是临时实例(之前就已经判断过一次),将我们的实例封装到数据集Datum中,并且进行赋值——>然后放到阻塞队列中,给到线程池去异步执行
public void onPut(String key, Record value) {
// 判断是否是临时实例
if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
// 封装 Instances 信息到 数据集:Datum
Datum datum = new Datum<>();
datum.value = (Instances) value;
datum.key = key;
datum.timestamp.incrementAndGet();
// 放入DataStore
dataStore.put(key, datum);
}
if (!listeners.containsKey(key)) {
return;
}
// 放入阻塞队列,这里的 notifier维护了一个阻塞队列,并且基于线程池异步执行队列中的任务
notifier.addTask(key, DataOperation.CHANGE);
}
addTask将任务加入阻塞队列
// DistroConsistencyServiceImpl.Notifier类的 addTask 方法:
public void addTask(String datumKey, DataOperation action) {
if (services.containsKey(datumKey) && action == DataOperation.CHANGE) {
return;
}
if (action == DataOperation.CHANGE) {
services.put(datumKey, StringUtils.EMPTY);
}
// 任务放入阻塞队列
tasks.offer(Pair.with(datumKey, action));
}
我们可以发现Notifier是一个Runnable
public class Notifier implements Runnable {
private ConcurrentHashMap services = new ConcurrentHashMap<>(10 * 1024);
private BlockingQueue> tasks = new ArrayBlockingQueue<>(1024 * 1024);
所以说他会通过我们的线程池中不断从阻塞队列获取任务进行服务列表的更新
这里利用了一个死循环,目的是不断从阻塞队列中获取任务
// DistroConsistencyServiceImpl.Notifier类的run方法:
@Override
public void run() {
Loggers.DISTRO.info("distro notifier started");
// 死循环,不断执行任务。因为是阻塞队列,不会导致CPU负载过高
for (; ; ) {
try {
// 从阻塞队列中获取任务
Pair pair = tasks.take();
// 处理任务,更新服务列表
handle(pair);
} catch (Throwable e) {
Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
}
}
}
然后我们看看distroProtocal.sync()异步方法
作用:实现了集群同步的逻辑,
其中同步的任务封装为一个DistroDelayTask对象。
交给了distroTaskEngineHolder.getDelayTaskExecuteEngine()执行,这行代码的返回值是:
NacosDelayTaskExecuteEngine,这个类维护了一个线程池,并且接收任务,执行任务。
public void sync(DistroKey distroKey, DataOperation action, long delay) {
// 遍历 Nacos 集群中除自己以外的其它节点
for (Member each : memberManager.allMembersWithoutSelf()) {
DistroKey distroKeyWithTarget = new DistroKey(distroKey.getResourceKey(), distroKey.getResourceType(),
each.getAddress());
// 定义一个Distro的同步任务
DistroDelayTask distroDelayTask = new DistroDelayTask(distroKeyWithTarget, action, delay);
// 交给线程池去执行
distroTaskEngineHolder.getDelayTaskExecuteEngine().addTask(distroKeyWithTarget, distroDelayTask);
if (Loggers.DISTRO.isDebugEnabled()) {
Loggers.DISTRO.debug("[DISTRO-SCHEDULE] {} to {}", distroKey, each.getAddress());
}
}
}
执行任务的方法processTasks()方法
可以看出来基于Distro模式的同步是异步进行的,并且失败时会将任务重新入队并充实,因此不保证同步结果的强一致性,属于AP模式的一致性策略。
protected void processTasks() {
Collection
Nacos如何解决并发读写冲突
并发读解决:Nacos在更新实例列表中,会猎用CopyOnWrite技术:也就是先将我们的Old实例列表拷贝一份,然后再将更新拷贝的实例列表,用更新后的实例列表去覆盖这个旧的实例列表,像CopyOnWrite技术在List中的CopyOnWriteArrayList也有实现(复制一个新的容器在里面作值,然后将引用指向它)
总结:这样每次都会创建一个新的实例列表,保证了读并发的一致性(但是这里只是一个软一致),并且消耗较多资源,好处解决了脏读问题
(38条消息) 集合-实习猎杀面试管(小撕源码)_Fairy要carry的博客-CSDN博客
并发写解决问题:
在注册实例的时候,会对我们的service进行加锁,那么你不同的service自然就不存在并发写的情况;相同service是通过锁来互斥,并且最重要的是它里面是基于异步单线程SingleThreadExecutor实现的,线程池中线程数量为1
(38条消息) SingleThreadExecutor的使用简析_绅士jiejie的博客-CSDN博客
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException {
// 监听服务列表用到的key,服务唯一标识,例如:com.alibaba.nacos.naming.iplist.ephemeral.public##DEFAULT_GROUP@@order-service
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
// 获取服务
Service service = getService(namespaceId, serviceName);
// 同步锁,避免并发修改的安全问题
synchronized (service) {
// 1)获取要更新的实例列表
List instanceList = addIpAddresses(service, ephemeral, ips);
// 2)封装实例列表到Instances对象
Instances instances = new Instances();
instances.setInstanceList(instanceList);
// 3)完成 注册表更新 以及 Nacos集群的数据同步
consistencyService.put(key, instances);
}
}
心跳检测
这里直接上总结吧,源码给爷看吐了,分了临时实例和非临时实例两种;
采用nacos客户端心跳检测模式,心跳>15s则不健康,30s则从服务列表中删除
采用服务端主动检测方式,特点就是可以更好的清楚服务的状态,并且出现异常并不会删除服务
比如说在高请求场景下,流量比较高(双十一),此时服务service需要更多实例进行应对,因为这些实例在双十一后就无需使用了,那么我们可以用临时实例;而那些经常使用的比如订单服务我们可以采用非临时实例
Nacos服务发现
Nacos服务发现有两种模式
消费者定期主动从Nacos拉取服务列表并缓存起来(本地缓存),再服务调用时优先读取本地缓存中的服务列表
消费者订阅Nacos中的服务列表,并基于UDP协议来接收服务变更通知。当Nacos中的服务列表更新时,会发送UDP广播给所有订阅者
比较:与Eureka相比,Nacos的订阅模式服务状态更新更及时,消费者更容易及时发现服务列表的变化,剔除故障服务。
本质上就是将消费者UDP端口,IP信息封装起来作为一个PushClient,也就是推送的一个客户端,方便以后这个服务变更更好的推送消息,然后它又涉及两个重要接口
在上一节中,`InstanceController`中的`doSrvIpxt()`方法中,有这样一行代码:
```java
pushService.addClient(namespaceId, serviceName, clusters, agent,
new InetSocketAddress(clientIP, udpPort),
pushDataSource, tid, app);
PushService本质也就是实现了ApplicationListener接口
这个是事件监听器接口,监听的是ServiceChangeEvent(服务变更事件)。
当服务列表变化时,就会通知我们: