uRick's PKM uRick's PKM
首页
导航
  • Java
  • 数据库
书单
  • 优质好文
  • 有趣的工具
  • 分类
  • 标签
  • 归档
关于
首页
导航
  • Java
  • 数据库
书单
  • 优质好文
  • 有趣的工具
  • 分类
  • 标签
  • 归档
关于
  • Mybatis基础实战
  • Mybatis的运作机制探索
  • 深入探索XXL-Job
  • 深入Nacos
    • 1. 特性
    • 2. 应用
    • 3. 原理解析
      • 3.1. 服务发现&注册
      • 3.2. 动态配置
    • 4. 服务监控
    • 5. 技巧总结
  • freemwork
uRick
2021-03-22
目录

深入Nacos

随着微服务的盛行,许多项目架构均采用服务化架构。随着业务需求不断的变化,服务也在演进迭代,前期也就几个服务,在许多小单位手动维护(😨很鸡肋,服务化了为啥还要了手动呢)服务配置,自己埋的坑总是要填的。随着服务实例越来越多后,实例环境维护成本一天天增加,费时费力,想早点下班陪媳妇又泡汤了,又得跪榴莲咯!那么这时候迫切需要自动化、可视化、统一维护环境信息,有没有什么通用的解决方案呢?

既然想要统一维护,那么可以抽离各个服务实例中的配置信息,通过网络通讯连接集中管理维护配置的中间件;这个过程也就把服务实例本地配置放到远端,需要网络通讯来传递配置信息数据,那么这时候性能相对于本地化配置性能肯定有所损耗的。面对各种又多又复杂到难以维护的实例,这都是小巫见大巫,一点点损耗也非常有价值。既然配置信息需要通过网络传递,那么设计一款这样的中间件必然绕不开网络异常,如何保证网络不可用,依然提供高效服务;这样的中间件,在社区中已经有很多非常成熟生态,也不需要重复造轮子,那来即用,如Nacos、Consul、Spring Cloud Config等。

Nacos是集服务注册与发现、配置管理为一体的中间件,它能够快速实现服务发现注册、服务配置集中维护,服务元数据以及流量管理。

# 1. 特性

  1. 服务注册发现 Nacos支持基于DNS或RPC的服务发现,服务消费方可以通过HTTP&API查找服务和发现服务应用,服务提供者可以通过Nacos Client & Open API完成服务的注册。
  2. 动态配置中心 传统项目开发修改或者调整配置信息都需要重启应用或重新打包非常不方便,而动态配置化中心,可以实现配置中心化、外部化、配置动态应用到服务中,无需重启或重新部署应用程序;而且提供集中维护管理配置信息,降低维护多个服务配置信息的成本。
  3. 服务及元数据管理&监控 Nacos替康简洁的UI管理控制中心,能够很好的维护监控服务元数据,监控服务健康状况;而且可以控制服务上下线,阻止不健康的服务接收和处理其请求。

# 2. 应用

目前Nacos生态非常丰富,官方提供很多框架接入组件,通过也可以通过Open API自行根据公司业务需求接入Naco,非常简单。

Nacos支持三种集群模式:单机(测试)、集群(高可用)、多集群(多数据中心),操作可查看官方文档:

  1. https://nacos.io/zh-cn/docs/cluster-mode-quick-start.html (opens new window)
  2. https://nacos.io/zh-cn/docs/deployment.html (opens new window)

在生产环境使用Naco建议通过VIP(Vitrual IP Address)来对外统一提供服务,便于应用IP变更或后期维护

集群实例搭建架构图

# 3. 原理解析

Nacos 提供了服务注册&发现、动态外部化配置独立模块,都可以分开独立使用,通过源码阅读Nacos源码实现的细节,对掌握它的运行机制以及底层采用的基础理论,以及后期使用有很大帮助,下面主要基于Spring Cloud体系从服务注册发现、外部化配置来探讨。

# 3.1. 服务发现&注册

如图服务提供应用启动后,向Nacos服务实例中心注册服务,注册成功并创建心跳检测任务(BeatTask),定时向注册中心发起心跳检测,续约应用状态,也就是保持服务活跃状态,便于注册中心判断注册的服务不可用自动下线;而服务消费方启动后根据配置的订阅服务从注册中心获取服务实例,若本地缓存中存在对应的服务则优先使用本地缓存服务实例。

服务发现注册交互图

在Nacos 在接入Spring Cloud 体系规范,通过Spring Cloud Alibaba 组件引用Nacos组件即可集成Nacos 服务注册能力:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
1
2
3
4

Nacos 集成Spring Cloud 生态完全遵守Spring Cloud 规范,Spring Cloud中服务注册和发现通过一下几大组件完成; ServiceInstance是一个标记接口,Registraction是注册服务实例抽象;具体的服务注册细节通过ServiceRegistry完成,Nacos组件已经实现了接口;而应用注册时机以及什么时候触发注册应用则是通过AbstractAutoServiceRegistration实现,AbstractAutoServiceRegistration同时实现了ApplicationListener监听器接口,用于监听WebServerInitializedEvent事件来完成服务注册。

Spring  Cloud 自动注册UML

  1. 服务发现

而服务发现则通过DiscoveryClient查找服务实例,在Spring Cloud中服务发现有还有ReactiveReactiveDiscouveryClient的实现。Spring Cloud原生有SimpleDiscoveryClient、CompositeDiscoveryClient实现,前者是基于Properties,Spring Cloud 服务发现提供了NacosDiscoveryClient实现。

Spring Cloud 服务发现UML

NacosDiscoveryClient在获取服务实例是通过定时任务定时拉取和启动UDP服务,将端口和地址传递至Nacos服务端,服务端通过监听事件探测服务实例的变化及时推送至对应的客户端中;通过主动推和定时拉取模式解决服务实例状态变化客户端更新不及时问题,Nacos获取服务实例流程如下:

服务发现流程

  • 基于Spring Cloud 规范,应用在获取服务实例前获取服务唯一标识(服务唯一ID),然后在根据ID获取具体的服务实例信息;当然在获取实例时优先检测本地缓存中是否存在服务,若存在则直接是用,Nacos中通过GET:/nacos/v1/ns/service/list获取ID;
  • 在初次查找订阅服务时,需要通过接口GET:/nacos/v2/ns/instance/list获取服务并放入本地缓存中;
  • 然后添加定时查询服务的定时任务UpdateTask(缺省查询间隔:1000ms),定时任务在每次执行结束后会再次触发重复调度逻辑(任务重新放入线程池中);
  • UpdateTask任务在自动获取实例更新实例信息时也需要检测缓存是已经存在,其次检测服务实例和任务执行的lastRefTime时间,当serviceObj.getLastRefTime() <= lastRefTime时说明本地服务状态未更新过,则主动Nacos注册中心服务状态,否则认为服务以及通过UPD监听更新了服务信息,仅刷新一下服务即可。UpdateTask核心源码如下:
public class UpdateTask implements Runnable {
    long lastRefTime = Long.MAX_VALUE;
    private final String clusters;
    private final String serviceName;
    public UpdateTask(String serviceName, String clusters) {
        this.serviceName = serviceName;
        this.clusters = clusters;
    }
    @Override
    public void run() {
        long delayTime = -1;
        try {
            ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
            if (serviceObj == null) {
                updateServiceNow(serviceName, clusters);
                delayTime = DEFAULT_DELAY;
                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;
            }
            delayTime = serviceObj.getCacheMillis();
        } catch (Throwable e) {
            NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e);
        } finally {
            if (delayTime > 0) {
                executor.schedule(this, delayTime, TimeUnit.MILLISECONDS);
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

上文中提到Nacos注册中心通过监听客户端的UPD端口,当发生服务实例状态变化后主动推送;那么客户端是怎么建立UPD端口的呢?其实在客户端初始化UpdateTask任务时也创建了UPD监听服务PushReceiver,它实现了Runnable接口,其核心处理逻辑源码如下:

public class PushReceiver implements Runnable, Closeable {
    private static final Charset UTF_8 = Charset.forName("UTF-8");
    private static final int UDP_MSS = 64 * 1024;
    private ScheduledExecutorService executorService;
    private DatagramSocket udpSocket;
    private HostReactor hostReactor;
    private volatile boolean closed = false;
    public PushReceiver(HostReactor hostReactor) {
        try {
            this.hostReactor = hostReactor;
            this.udpSocket = new DatagramSocket();
            this.executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                    Thread thread = new Thread(r);
                    thread.setDaemon(true);
                    thread.setName("com.alibaba.nacos.naming.push.receiver");
                    return thread;
                }
            });
            this.executorService.execute(this);
        } catch (Exception e) {
            NAMING_LOGGER.error("[NA] init udp socket failed", e);
        }
    }
    
    @Override
    public void run() {
        while (!closed) {
            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 = JacksonUtils.toObj(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(JacksonUtils.toJson(hostReactor.getServiceInfoMap()))+ "\"}";
                } else {
                    // do nothing send ack only
                    ack = "{\"type\": \"unknown-ack\"" + ", \"lastRefTime\":\"" + pushPacket.lastRefTime+ "\", \"data\":" + "\"\"}";
                }
                udpSocket.send(new DatagramPacket(ack.getBytes(UTF_8), ack.getBytes(UTF_8).length,packet.getSocketAddress()));
            } catch (Exception e) {
                NAMING_LOGGER.error("[NA] error while receiving push data", e);
            }
        }
    }
    @Override
    public void shutdown() throws NacosException {
        String className = this.getClass().getName();
        NAMING_LOGGER.info("{} do shutdown begin", className);
        ThreadUtils.shutdownThreadPool(executorService, NAMING_LOGGER);
        closed = true;
        udpSocket.close();
        NAMING_LOGGER.info("{} do shutdown stop", className);
    }
    public static class PushPacket {
        public String type;
        public long lastRefTime;
        public String data;
    }
    public int getUdpPort() {
        return this.udpSocket.getLocalPort();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
  1. 服务注册 在Spring Boot 中服务注册时通过Spirng 监听器监听Web应用启动自动完成服务注册的,Nacos同样也遵守了这一规范,其中入口在AbstractAutoServiceRegistration中,Nacos中服务注册流程如下:

服务注册流程

  1. 首先由于AbstractAutoServiceRegistration实现了ApplicationListener,并监听WebServerInitializedEvent事件,所以当应用启动WebServer初始化时会触发初始化事件,完成服务的注册;
  2. Nacos判断当前服务是否是ephemeral服务,若不是,则通过serverProxy.registerService(groupedServiceName, groupName, instance)完成注册(POST:/nacos/v1/ns/instance);
  3. 否则创建定时心跳检测任务,定时向Nacos服务管理中心请求心跳检测,续约当前服务的状态是否健康,BeatTask任务执行心跳检测的频率根据服务端返回clientBeatInterval参数决定(缺省值为:5000Lms),BeatTask核心实现源码如下:
class BeatTask implements Runnable {
    BeatInfo beatInfo;
    public BeatTask(BeatInfo beatInfo) {
        this.beatInfo = beatInfo;
    }
    
    @Override
    public void run() {
        if (beatInfo.isStopped()) {return;}
        long nextTime = beatInfo.getPeriod();//任务执行间隔默认:5000ms
        try {
            //通过接口POST:/nacos/v1/ns/instance/beat发起心跳续约服务
            JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
            //服务端返回的心跳检测间隔
            long interval = result.get("clientBeatInterval").asLong();
            boolean lightBeatEnabled = false;
            if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) {
                lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean();
            }
            BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
            if (interval > 0) {
                nextTime = interval;
            }
            int code = NamingResponseCode.OK;
            if (result.has(CommonParams.CODE)) {//心跳检测成功
                code = result.get(CommonParams.CODE).asInt();
            }
            //服务中心返回20404,未找到服务,则通过POST:/nacos/v1/ns/instance接口注册
            if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
                Instance instance = new Instance();
                instance.setPort(beatInfo.getPort());
                instance.setIp(beatInfo.getIp());
                instance.setWeight(beatInfo.getWeight());
                instance.setMetadata(beatInfo.getMetadata());
                instance.setClusterName(beatInfo.getCluster());
                instance.setServiceName(beatInfo.getServiceName());
                instance.setInstanceId(instance.getInstanceId());
                instance.setEphemeral(true);
                try {
                    serverProxy.registerService(beatInfo.getServiceName(),NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
                } catch (Exception ignore) {}
            }
        } catch (NacosException ex) {
            NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg());
            
        }
        //继续把当前任务放入任务调度线程池中
        executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 3.2. 动态配置

Nacos Spring Cloud 中实现动态配置,是通过Spring Cloud RefreshScop特性基于上下文环境Enviroment体系完成自动刷新、自动配置的,其原理详见Enviroment章节,Nacos为了规避PULL、PUSH获取数据模式的缺陷,采用HTTP长轮询^1 (opens new window)方式实现配置动态获取更新的,其交互流程如下:

Nacos动态配置交互流程

Spring Cloud体系本身就是建立在Spring Boot之上,而Nacos自动装载配置是通过Spring Boot启动是基于初始化器完成配置初始化的,PropertySourceBootstrapConfiguration就是一个初始化器接口的实现,内部逻辑时序图如下:

Spring Cloud 加载配置源码时序图

  1. initialize 初始化根据PropertySourceLocator获取所有的环境配置源,初始化日志框架合并配置属性源信息;
  2. locateCollection 解析PropertySource集合,包括CompositePropertySource;
  3. NacosPropertySourceLocator#locate Nacos加载配置核心的实现,加载所有配置信息,目前Nacos支持三种类型的配置:共享配置、拓展配置、应用程序性配置。共享配置是为了提供多个应用间共用同一套外部配置,降低维护多份的成本,则在Nacos中仅需维护一份即可,多个应用仅需要指定配置data-id即可。
@Override
	public PropertySource<?> locate(Environment env) {
		nacosConfigProperties.setEnvironment(env);
		ConfigService configService = nacosConfigManager.getConfigService();

		if (null == configService) {
			log.warn("no instance of config service found, can't load config from nacos");
			return null;
		}
		long timeout = nacosConfigProperties.getTimeout();
		nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
				timeout);
		String name = nacosConfigProperties.getName();

		String dataIdPrefix = nacosConfigProperties.getPrefix();
		if (StringUtils.isEmpty(dataIdPrefix)) {
			dataIdPrefix = name;
		}

		if (StringUtils.isEmpty(dataIdPrefix)) {
			dataIdPrefix = env.getProperty("spring.application.name");
		}

		CompositePropertySource composite = new CompositePropertySource(
				NACOS_PROPERTY_SOURCE_NAME);

		loadSharedConfiguration(composite);//加载共享配置
		loadExtConfiguration(composite);//加载拓展配置
		loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);//加载应用配置

		return composite;
	}

// 一次根据三种文件格式加载配置:dataIdPrefix,dataIdPrefix + DOT + fileExtension, dataIdPrefix + SEP1 + profile + DOT + fileExtension
private void loadApplicationConfiguration(CompositePropertySource compositePropertySource, String dataIdPrefix,NacosConfigProperties properties, Environment environment) {
		String fileExtension = properties.getFileExtension();
		String nacosGroup = properties.getGroup();
		// load directly once by default,根据应用名称加载配置
		loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,fileExtension, true);
		// load with suffix, which have a higher priority than the default,根据文件拓展名加载配置
		loadNacosDataIfPresent(compositePropertySource,dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
		// Loaded with profile, which have a higher priority than the suffix
		for (String profile : environment.getActiveProfiles()) {//根据active profiles加载配置信息
			String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
			loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,fileExtension, true);
		}
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
spring.application.name=service-provider
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
# config external configuration
# 1、data-id 在默认的组 DEFAULT_GROUP,不支持配置的动态刷新
spring.cloud.nacos.config.extension-configs[0].data-id=ext-configcommon01.properties
# 2、data-id 不在默认的组,不支持动态刷新
spring.cloud.nacos.config.extension-configs[1].data-id=ext-configcommon02.properties
spring.cloud.nacos.config.extension-configs[1].group=GLOBALE_GROUP
# 3、data-id 既不在默认的组,也支持动态刷新
spring.cloud.nacos.config.extension-configs[2].data-id=ext-configcommon03.properties
spring.cloud.nacos.config.extension-configs[2].group=REFRESH_GROUP
spring.cloud.nacos.config.extension-configs[2].refresh=true
1
2
3
4
5
6
7
8
9
10
11
12

由上配置可知:

  • 通过 spring.cloud.nacos.config.extension-configs[n].data-id 的配置方式来支持多个data-id的配置;
  • 通过 spring.cloud.nacos.config.extension-configs[n].group 的配置方式自定义data-id所在的组,不明确配置的话,默认是 DEFAULT_GROUP;
  • 通过 spring.cloud.nacos.config.extension-configs[n].refresh 的配置方式来控制该data-id在配置变更时,是否支持应用中可动态刷新, 感知到最新的配置值。默认是不支持的;
  • 多个 data-id同时配置时,他的优先级关系是 spring.cloud.nacos.config.extensionconfigs[n].data-id 其中 n 的值越大,优先级越高;

注意: spring.cloud.nacos.config.extension-configs[n].data-id 的值必须带文件扩展名,文件扩展名既可支持 properties,又可以支持 yaml/yml。 此时spring.cloud.nacos.config.file-extension 的配置对自定义扩展配置的data-id文件扩展名没有影响。

对于应用程序配置(非拓展、共享配置),支持格式:${dataIdPrefix}-${spring.profiles.active}.${file-extension},其中${dataIdPrefix}默认为spring.application.name参数,也可以通过spring.cloud.nacos.config.prefix来配置;${spring.profiles.active}就是Spring EnviromentProfile,${file-extension}是文件拓展名,当前仅支持.properties、yaml/yml格式。

而且应用加载配置有3中类型文件,默认一次加载${dataIdPrefix}、${dataIdPrefix}.${file-extension}、${dataIdPrefix}-${spring.profiles.active}.${file-extension}。

对于以上3种类型配置优先级关系遵守:共享配置 < 拓展配置 < 应用程序性配置^2 (opens new window)

  1. loadNacosData 通过GET:/v1/cs/configs获取配置,流程如图。

获取配置

Naco获取配置和监听配置中心配置变化动态更新本地服务配置,通过长轮询机制实现,核心实现在类LongPollingRunnable中,默认2000ms触发一次轮询线程,首次应用启动时根据监听配置文件来创建长轮询线程,创建逻辑如下:

public void checkConfigInfo() {
 // 监听器数量(每个配置一个监听器,有几个配置则有几个长轮询)
 int listenerSize = cacheMap.get().size();
 // 长轮询任务数(向上取整)
 int longingTaskCount = (int) Math.ceil(listenerSize /ParamUtil.getPerTaskConfigSize());
 if (longingTaskCount > currentLongingTaskCount) {
    for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
    executorService.execute(new LongPollingRunnable(i));
    }
 currentLongingTaskCount = longingTaskCount;
 }
}
1
2
3
4
5
6
7
8
9
10
11
12

上述cacheMap.get().size()是监听器的个数,通常每个监听器会创建一个长轮询(一个监听器监听一个配置),监听的创建在NacosContextRefresher#registerNacosListener,监听器会根据NacosPropertySourceRepository缓存创建配置文件监听,一旦配置发生变更则通过事件RefreshEvent触发Spring Cloud应用清理环境缓存信息。

private void registerNacosListener(final String groupKey, final String dataKey) {
    String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
    Listener listener = listenerMap.computeIfAbsent(key,lst -> new AbstractSharedListener() {
                @Override
                public void innerReceive(String dataId, String group,String configInfo) {
                    refreshCountIncrement();
                    nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
                    // todo feature: support single refresh for listening
                    applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config"));
                    if (log.isDebugEnabled()) {
                        log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s",group, dataId, configInfo));
                    }
                }
            });
    try {configService.addListener(dataKey, groupKey, listener);}
    catch (NacosException e) {log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,groupKey), e);}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在LongPollingRunnable#run中通过读取检查本地缓存配置是否发生变化而对变更进行通知,当线程需要从Nacos配置中心获取变化的配置时,首先通过GET:/v1/cs/configs/listener请求你获取变化的Key信息,也就是下图流程的checkUpdateDataIds,获取到变更的Keys后,依次遍历变更的Keys通过接口(GET:/v1/cs/configs)获取变更的配置,然后根据变更配置与本地缓存配置对比,当发生变更后通过checkListenerMD5流程通知监听器。

长轮询流程

# 4. 服务监控

Nacos 也提供了丰富的Dashboard监控,可以通过运维平台管理服务元数据,维护服务手动上下线以及动态配置变更发布通知;同时自带租户权限、集群管理,使用非常简单便捷直观。

监控

# 5. 技巧总结

  1. 通过长轮询机制解决常用PULL和PUSH模式下获取数据的缺陷,能够很好的解决PUSH模式下消息积压、客户端性能瓶颈以及PULL模式下数据获取不及时的问题;
  2. Naco端大量使用了订阅/发布事件监听模式完成服务&配置的动态监听与发布,日常业务开发过程中也可以借鉴解决一些问题,非常适用;
  3. 同时Nacos中大量使用了异步多线程处理机制,有很多值得学习研究的地方,对日后高并发多线处理业务问题提供帮助;
  4. 通过Java SPI机制加载初始化AbstractPublisher发布器,提供了很好的拓展和可伸缩性,很值得学习,SPI机制在Java已经并大量使用,很好的实现顶层设计,能为自身业务或者组件提供更多的可能性;
  5. 2.0.x新版本Nacos客户端已经采用Grpc实现,不在使用长轮询机制实现配置变更信息;
  6. Nacos集群选举采用Raft^3 (opens new window)算法实现,最开始是框架内部的一个简单实现,现在采用SOFAJRaft^4 (opens new window)实现,无论哪一种实现,其实基本原理一样。

Raft算法中,起根本源于Paxos,Raft采用模块化实现,在Raft中集群中节点主要三种角色:

  • Leader:集中管理者,主要负责接收客户端的请求,集群中其他节点不参与处理请求,若其他非Leader节点收到请求,则转发到Leader进行处理;负责向Follower发起心跳检测,通知Follower同步数据;
  • Candiate: 用于参与选举成为Leader的候选者,当Follower参与选举,具备选举能力时则成为Candiate;
  • Follower:负责响应Leader和Candiate请求;

在Raft一致算法中,为了保证Leader高可用,当Leader节点故障或者启动是进行选举,选出Leader主持工作;Leader的选举主要在集群服务启动和Leader服务挂掉后,选举方式。

#Nacos#阿里生态
上次更新: 2024/03/02, 14:21:03
深入探索XXL-Job

← 深入探索XXL-Job

最近更新
01
从0到1:开启商业与未来的秘密
11-26
02
如何阅读一本书: 读懂一本书,精于一件事
10-25
03
深入理解Lambda
06-27
更多文章>
Theme by Vdoing | Copyright © 2019-2024 uRick | CC BY 4.0
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式