Nacos是什么
an easy-to-use dynamic service discovery, configuration and service management platform for building cloud native applications
一个更易于构建云原生应用的 动态服务发现、配置管理和服务管理平台。
动态配置服务
就是通过一个系统,管理系统中的配置项,在配置项需要更新的时候,可以通过管理系统去操作更新,更新完了之后,会主动推送到订阅了这个配置的客户端。
具体的使用场景,例如,在系统中,会有数据库的链接串,账号密码等配置信息,常规的做法是写在配置文件里面,如果需要修改更新,需要重新打包编译,如果你是分布式集群,那成本太大了,通常我们都会将它抽取出来,存放到db,或者一个管理系统,Nacos,就是这个管理系统,Nacos还提供主动通知的能力,DB是没有的,在自己的系统代码里面,可以监听某个配置,如果在管理系统上修改了配置项,客户端的监听函数,会立刻执行,在里面,你可以拿到最新的配置,执行自己的业务逻辑。
服务发现及管理
这个主要是针对分布式的微服务集群系统,某A集群提供服务出去,其他应用集群,需要消费到A集群的服务,需要一个系统去管理A集群的ip列表,其他应用集群,去这个系统才能获取到A集群的ip列表,进行调用,同时该系统需要能够自动将A集群中无法工作的ip进行去除掉,这样才能保证调用方调用成功,Nacos就是提供这种能力的一个系统。
动态DNS服务
这个理解起来也简单,我们平常在代码里面 ,访问一个http的api,通常是带一个域名的,请求的时候,一般会先去DNS域名服务器上面寻找该域名对应的ip,再发起http请求,Nacos可以充当这个DNS域名服务器的角色的,优点是什么呢?Nacos提供了比DNS域名服务器更方便的管理能力,新增一个域名,只需要在Nacos的控制台上面配置一下,同时它还提供了包括权重,健康检查,属性,路由等DNS服务器不具备的能力,比DNS的产品功能,稳定性,灵活性,超出太多了。
快速开始
启动服务
下面,我尝试从Nacos的官方源码拉一份下来,进行一个服务的发布,订阅的简单流程,感受下Nacos的功能。
- 从以上 GitHub 链接中的 Release 中下载 nacos-server-2.0.1.zip
- 在win环境下进入下载的服务软件包下的bin目录敲
startup.cmd -m standalone
运行单机模式的NacosServer,这边不要直接双击startup.cmd,因为Nacos 1.3.2版本后默认是以集群方式启动。想双击启动可以修改cmd文件里面的set MODE="cluster"
为set MODE="standalone"
。
将会看到运行成功的启动图。再通过netstat -ano|findstr 8848
查看下server的端口是否监听成功。
发布服务
使用IDEA创建空的Maven工程,导入Nacos和日志(必要)坐标。
<dependencies>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.7</version>
</dependency>
</dependencies>
放log4j.properties到resources
# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE debug info warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE
# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE
# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n
# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=c:\\nacos.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n
新建Pub类并启动main()
public class Pub {
public static void main(String[] args) throws NacosException, InterruptedException {
//发布的服务名
String serviceName = "helloworld.services";
//构造一个nacos实例,入参是nacos server的ip和服务端口
NamingService naming = NamingFactory.createNamingService("192.168.136.1:8848");
//发布一个服务,该服务对外提供的ip为127.0.0.1,端口为8888
naming.registerInstance(serviceName, "127.0.0.1", 8888);
Thread.sleep(Integer.MAX_VALUE);
}
}
新建Sub类并启动main()
public class Sub {
public static void main(String[] args) throws NacosException, InterruptedException {
//订阅的服务名
String serviceName = "helloworld.services";
//创建一个nacos实例
NamingService naming = NamingFactory.createNamingService("192.168.136.1:8848");
//订阅一个服务
naming.subscribe(serviceName, event -> {
if (event instanceof NamingEvent) {
System.out.println("订阅到数据");
System.out.println(((NamingEvent) event).getInstances());
}
});
System.out.println("订阅完成,准备等数来");
Thread.sleep(Integer.MAX_VALUE);
}
}
订阅到刚才发布的数据:
订阅完成,准备等数来
订阅到数据
[Instance{instanceId='null', ip='127.0.0.1', port=8888, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='DEFAULT', serviceName='DEFAULT_GROUP@@helloworld.services', metadata={}}]
2021-05-03 12:18:20,436 5854 [ent-executor-11] DEBUG aba.nacos.common.remote.client - [1620015498786_192.168.136.1_1973]Stream server request receive, original info: metadata {
type: "NotifySubscriberRequest"
clientIp: "192.168.136.1"
}
body {
value: "{\"headers\":{},\"requestId\":\"1\",\"serviceInfo\":{\"name\":\"helloworld.services\",\"groupName\":\"DEFAULT_GROUP\",\"clusters\":\"\",\"cacheMillis\":10000,\"hosts\":[{\"ip\":\"127.0.0.1\",\"port\":8888,\"weight\":1.0,\"healthy\":true,\"enabled\":true,\"ephemeral\":true,\"clusterName\":\"DEFAULT\",\"serviceName\":\"DEFAULT_GROUP@@helloworld.services\",\"metadata\":{},\"instanceIdGenerator\":\"simple\",\"instanceHeartBeatTimeOut\":15000,\"ipDeleteTimeout\":30000,\"instanceHeartBeatInterval\":5000}],\"lastRefTime\":1620015499519,\"checksum\":\"\",\"allIPs\":false,\"reachProtectionThreshold\":false,\"valid\":true},\"module\":\"naming\"}"
}
快速开始2
看得出来,第一种方式是非常麻烦的,第二种方式使用Spring Cloud Alibaba做简单的单体微服务。
父pom.xml
<!-- spring boot 2.2.2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- spring cloud alibaba 2.1.0.RELEASE -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
子pom.xml
<!-- SpringCloud alibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
PaymentMain9002.java
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9002 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9002.class, args);
}
}
PaymentController.java写不写都可以
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id) {
return "nacos registry, serverPort: " + serverPort + ", id: " + id;
}
}
application.yml
server:
port: 9002
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
开启Nacos服务,运行PaymentMain9002类,访问http://localhost:9002/payment/nacos/123 便可正常使用controller接口。
访问http://localhost:8848/nacos,输入nacos,nacos进入。服务菜单就可以看到注册进去的服务nacos-payment-provider。
例子总结
上面流程,就是一个helloworld的服务发布和订阅,够简单吧,在实际的生产环境中,一个服务下面往往通过集群挂有好几个ip提供服务,在订阅端,拿到服务下面的所有地址列表后,通过负载均衡策略,例如随机策略,选择到一个ip进行调用,这样就完成了一次远程调用。
说明
Nacos默认使用嵌入式数据库derby,切换MySQL数据库可以在conf/application.properties中添加相关db代码,具体参考Nacos官方文档:https://nacos.io/zh-cn/docs/deployment.html
集群
涉及多端口,Nginx代理以及Nacos在Linux中集群,比较复杂,因此就看看图就行,具体想了解可以去b站看周阳讲SpringCloud。
Nacos项目结构介绍
接着分析一下Nacos工程的包模块结构,都是负责什么功能的,从全局看一下整个工程,从整体到细节。源码地址已在顶部给出。
新开源版迭代会比较频繁,很可能某个类,或者模块依赖关系下个版本就不一样了,请不要疑惑,该源码为0.2.0版。
通过分析Nacos源码工程中的pom模块依赖,画出了如下每个模块的依赖关系,每个component就对应者工程里面的一个maven module,我们重点关注一下工程代码相关的模块的依赖关系,依赖关系从上往下,下图右边部分:
接下来,将介绍一下每个模块在Nacos工程中的职责及依赖关系。
console模块
控制台相关代码,及前端资源,它依赖config和naming模块,由此可以看出,consule前端依赖的api,将直接由这2个模块进行提供。
config模块
大家知道,Nacos目前有3大核心功能,其中一个是动态配置发现,这个包里面,放的就是配置中心的代码,打开模块,内部的包结构如下图:
基本上从名字都能猜出个大概,工程是基于spring boot构建的,所以会有一个入口类Config,里面打开就是Spring boot玩家熟悉的spring boot加载入口方法了,SpringApplication.run(Config.class, args)
naming
这个是Nacos的另外一个核心功能的代码模块,动态服务发现,内部的包结构如下:
和config模块类似,NamingApp是spring boot的入口类,其他的都通过包隔离了,各个包的名字,大家也可以猜出大概是承担什么功能的。
core模块
从上图可以看出,由config直接依赖,目前里面是空的,看来还是属于规划之中,从core上里面,这里可能会抽取出一些核心服务,供多个功能模块使用,例如数据层,一致性协议等。
common模块
目前由config通过core间接依赖到,翻开了里面的代码,是一些公用的工具类,如下图所示:
目前只有这些工具类,naming目前没有依赖到,但是后续和config模块进行融合时,这里面后续会放入共同依赖的一些类,例如请求对象,异常,协议结构等。
client模块
这个里面放的是Nacos客户端的代码,服务发现和配置管理2个功能的客户端,目前在工程模块上已经做了统一,但是,在代码包上面,还是分离到,可以从包结构里面看出,如下:
很明显分了2个模块,配置和naming,后续随着整合到深度,这些都会陆续合并掉,真正做到统一。
api模块
这个里面,主要是把naming和config的api进行了抽取,从结构上看更清晰一些,api的具体实现,都还在client模块里面,它包结构如下:
在Nacos的使用手册中,使用到的工厂类,NacosFactory在构建对应实例时,调用的就是api里面的,根据不同的方法,再通过反射构造出对应的实例:
public static ConfigService createConfigService(String serverAddr) throws NacosException {
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
try {
Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
Constructor constructor = driverImplClass.getConstructor(Properties.class);
ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
return vendorImpl;
} catch (Throwable e) {
throw new NacosException(-400, e.getMessage());
}
}
其他非工程源码的模块,是用来放测试用例,文档,打包文件,启动脚本的,这个就不详细讲了,大家有兴趣可以翻开来看看。
项目结构总结
Nacos的工程,包含了Nacos Server和Nacos Client 2个工程的代码,后续会再console的代码也放入总工程中,这些代码都放在一起,对使用和学习者来说,是非常方便的,其中,Nacos Server,由配置和服务发现2个大功能,分别由config和naming承载,client和naming模块的代码,都没有共同依赖的类,由此可以看出,client与server之间的协议,是和语言没有关系,没有涉及到自己的私有应用层协议,这样的话,多语言客户端的接入,会简单很多,在客户端的模块里面,这2个功能就统一到了NacosClient模块里面,server目前还是不同的模块,后续这块需要看Nacos的架构融合演进。
总的来说,目前Nacos的工程,将框架都搭好了,但是一些整合的实现和优化,都还未清晰,好几个包都是空的,功能和系统架构,还在规划之中。
客户端初始化
现在,我们去逐步去挖掘里面的代码细节,很多人在学习开源的时候,无从下手,代码那么多,从哪个地方开始看呢?我们可以从一个接口开始入手,这个接口是你使用过的,知道它大概做什么事,有体感的,大家还记得第一章时,我们写的HelloWorld吗,对,就从里面的接口开始剥洋葱。
快速开始中第一步,是构造一个Nacos服务实例,构造实例的入参,是一个String,值的规范为ip:port,这个ip,就是我们任意一台Nacos server的地址,我们点进去看这个方法:
public static NamingService createNamingService(String serverAddr) throws NacosException {
return NamingFactory.createNamingService(serverAddr);
}
同时我们看下创建配置服务实例的代码:
public static ConfigService createConfigService(String serverAddr) throws NacosException {
return ConfigFactory.createConfigService(serverAddr);
}
我们可以看到,NacosFactory实际上是一个服务发现和配置管理接口的统一入口,再由它不通的方法,创建不同服务的实例,我们可以直接使用NamingFactory,或者ConfigFactory直接创建Nacos的服务实例,也能work。
接下来,看一下,是如何构造出这个Nacos naming实例的:
public static NamingService createNamingService(String serverList) throws NacosException {
try {
Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.naming.NacosNamingService");
Constructor constructor = driverImplClass.getConstructor(String.class);
NamingService vendorImpl = (NamingService) constructor.newInstance(serverList);
return vendorImpl;
} catch (Throwable e) {
throw new NacosException(-400, e.getMessage());
}
}
通过反射实例化出了一个NamingService的实例NacosNamingService,构造器是一个带String入参的,我们顺着往下看,构造函数里面做了哪些事情:
public NacosNamingService(String serverList) {
this.serverList = serverList;
init();
eventDispatcher = new EventDispatcher();
serverProxy = new NamingProxy(namespace, endpoint, serverList);
beatReactor = new BeatReactor(serverProxy);
hostReactor = new HostReactor(eventDispatcher, serverProxy, cacheDir);
}
入参serverList就是我们刚才传入的服务端地址,值赋给了实例的serverList字段,接下来调用了一个init方法,这个方法里面如下:
private void init() {
namespace = System.getProperty(PropertyKeyConst.NAMESPACE);
if (StringUtils.isEmpty(namespace)) {
namespace = UtilAndComs.DEFAULT_NAMESPACE_ID;
}
logName = System.getProperty(UtilAndComs.NACOS_NAMING_LOG_NAME);
if (StringUtils.isEmpty(logName)) {
logName = "naming.log";
}
cacheDir = System.getProperty("com.alibaba.nacos.naming.cache.dir");
if (StringUtils.isEmpty(cacheDir)) {
cacheDir = System.getProperty("user.home") + "/nacos/naming/" + namespace;
}
}
这面做了3件事,给namespace,logName,cacheDir赋值,namespace我们么有传入,默认是default,namespace在Nacos里面的作用,是用来进行本地缓存隔离的,一台机器上,启动一个Nacos的客户端进程,默认的本地缓存路径是default,如果再启动一个,需要重新设置一个namespace,否则就会复用之前的缓存,造成冲突;logName和cacheDir,这2个字段就不解释了,字面理解。这里多说一句,这些值的设置,可以在java启动时,通过系统参数的形式传入,并且是第一优先级的。
init方法执行完之后,接下来是实例化一些框架组件,EventDispatcher,这个是一个经典的事件分发组件,它的工作模式如下:
会有一个单独线程从blockQueue中获取事件,这个事件在Nacos这里, 就是服务端推送下来的数据,listener在我们订阅一条数据时,会从生成一个listener实例,在事件到了队列中,找到对应的listener,去执行里面listener的回调函数onEvent。如果对这个模式不熟悉的同学,可以再翻看下EventDispatcher的代码,这个属于基础知识了,和业务没有关系,这里就不过多详细讲解,篇幅太长。
接下来,实例化了一个NameProxy的组件,这个东西是干嘛的呢?我们看下里面代码:
public NamingProxy(String namespace, String endpoint, String serverList) {
this.namespace = namespace;
this.endpoint = endpoint;
if (StringUtils.isNotEmpty(serverList)) {
this.serverList = Arrays.asList(serverList.split(","));
if (this.serverList.size() == 1) {
this.nacosDomain = serverList;
}
}
executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.taobao.vipserver.serverlist.updater");
t.setDaemon(true);
return t;
}
});
executorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
refreshSrvIfNeed();
}
}, 0, vipSrvRefInterMillis, TimeUnit.MILLISECONDS);
refreshSrvIfNeed();
}
这里面逻辑有些多,我总结下,主要是启动了一个线程,每隔30s,去执行refreshSrvIfNeed()这个方法,
refreshSrvIfNeed()这个方法里面,做的事情,是通过一个http请求,去Nacos server获取一串Nacos server集群的地址列表,具体代码如下:
private void refreshSrvIfNeed() {
try {
if (!CollectionUtils.isEmpty(serverList)) {
LogUtils.LOG.info("server list provided by user: " + serverList);
return;
}
if (System.currentTimeMillis() - lastSrvRefTime < vipSrvRefInterMillis) {
return;
}
List<String> list = getServerListFromEndpoint();
if (list.isEmpty()) {
throw new Exception("Can not acquire vipserver list");
}
if (!CollectionUtils.isEqualCollection(list, serversFromEndpoint)) {
LogUtils.LOG.info("SERVER-LIST", "server list is updated: " + list);
}
serversFromEndpoint = list;
lastSrvRefTime = System.currentTimeMillis();
} catch (Throwable e) {
LogUtils.LOG.warn("failed to update server list", e);
}
}
获取完地址列表后,赋值给serversFromEndpoint,并且记录当前更新时间,在下一次更新时,小于30s,就不更新,避免频繁更新,总的来说,NameProxy的目的就是定时在客户端维护Nacos服务端的最新地址列表。
我们继续往下看,接下来初始化了BeatReactor这个组件,从名字可以猜测,应该是和心跳相关的事情,它初始化的代码如下:
public BeatReactor(NamingProxy serverProxy) {
this.serverProxy = serverProxy;
executorService.scheduleAtFixedRate(new BeatProcessor(), 0, clientBeatInterval, TimeUnit.MILLISECONDS);
}
起了一个定时间隔为10s的任务,去执行BeatProcessor里面的逻辑,BeatProcessor的代码里面,是循环的去取当前客户端注册好的实例,然后向服务端发送一个http的心跳通知请求,告诉客户端,这个服务的健康状态,具体代码如下:
class BeatTask implements Runnable {
BeatInfo beatInfo;
public BeatTask(BeatInfo beatInfo) {
this.beatInfo = beatInfo;
}
@Override
public void run() {
Map<String, String> params = new HashMap<String, String>(2);
params.put("beat", JSON.toJSONString(beatInfo));
params.put("dom", beatInfo.getDom());
try {
String result = serverProxy.callAllServers(UtilAndComs.NACOS_URL_BASE + "/api/clientBeat", params);
JSONObject jsonObject = JSON.parseObject(result);
if (jsonObject != null) {
clientBeatInterval = jsonObject.getLong("clientBeatInterval");
}
} catch (Exception e) {
LogUtils.LOG.error("CLIENT-BEAT", "failed to send beat: " + JSON.toJSONString(beatInfo), e);
}
}
}
这里就是naocs的客户端主动上报服务健康状况的逻辑了,是服务发现功能,比较重要的一个概念,服务健康检查机制,常用的还有服务端主动去探测客户端的接口返回。
最后一步,就是初始化了一个叫HostReactor的实例,我们来看下,它干了些啥:
public HostReactor(EventDispatcher eventDispatcher, NamingProxy serverProxy, String cacheDir) {
this.eventDispatcher = eventDispatcher;
this.serverProxy = serverProxy;
this.cacheDir = cacheDir;
this.serviceInfoMap = new ConcurrentHashMap<>(DiskCache.read(this.cacheDir));
this.failoverReactor = new FailoverReactor(this, cacheDir);
this.pushRecver = new PushRecver(this);
}
第五行,是从缓存文件中加载数据到serviceInfoMap的内存map中,接下来,初始化了一个FailoverReactor的组件,这个是Nacos客户端缓存容灾相关的,它里面的初始化代码如下:
public void init() {
executorService.scheduleWithFixedDelay(new SwitchRefresher(), 0L, 5000L, TimeUnit.MILLISECONDS);
executorService.scheduleWithFixedDelay(new DiskFileWriter(), 30, DAY_PERIOD_MINUTES, TimeUnit.MINUTES);
// backup file on startup if failover directory is empty.
executorService.schedule(new Runnable() {
@Override
public void run() {
try {
File cacheDir = new File(failoverDir);
if (!cacheDir.exists() && !cacheDir.mkdirs()) {
throw new IllegalStateException("failed to create cache dir: " + failoverDir);
}
File[] files = cacheDir.listFiles();
if (files == null || files.length <= 0) {
new DiskFileWriter().run();
}
} catch (Throwable e) {
LogUtils.LOG.error("NA", "failed to backup file on startup.", e);
}
}
}, 10000L, TimeUnit.MILLISECONDS);
}
初始化了3个定时任务,第一个任务的代码如下:
class SwitchRefresher implements Runnable {
long lastModifiedMillis = 0L;
@Override
public void run() {
try {
File switchFile = new File(failoverDir + UtilAndComs.FAILOVER_SWITCH);
if (!switchFile.exists()) {
switchParams.put("failover-mode", "false");
LogUtils.LOG.debug("failover switch is not found, " + switchFile.getName());
return;
}
long modified = switchFile.lastModified();
if (lastModifiedMillis < modified) {
lastModifiedMillis = modified;
String failover = ConcurrentDiskUtil.getFileContent(failoverDir + UtilAndComs.FAILOVER_SWITCH, Charset.defaultCharset().toString());
if (!StringUtils.isEmpty(failover)) {
List<String> lines = Arrays.asList(failover.split(DiskCache.getLineSeperator()));
for (String line : lines) {
String line1 = line.trim();
if ("1".equals(line1)) {
switchParams.put("failover-mode", "true");
LogUtils.LOG.info("failover-mode is on");
new FailoverFileReader().run();
} else if ("0".equals(line1)) {
switchParams.put("failover-mode", "false");
LogUtils.LOG.info("failover-mode is off");
}
}
} else {
switchParams.put("failover-mode", "false");
}
}
} catch (Throwable e) {
LogUtils.LOG.error("NA", "failed to read failover switch.", e);
}
}
}
首先判定下容灾开关是否有,容灾开关是一个磁盘文件的形式存在,通过容灾开关文件名字,判定容灾开关是否打开,1表示打开,0为关闭,读取到容灾开关后,将值更新到内存中,后续解析地址列表时,首先会判定一下容灾开关是否打开,如果打开了,就读缓存的数据,否则从服务端获取最新数据。
第二个定时任务,做的事情如下:
class DiskFileWriter extends TimerTask {
public void run() {
Map<String, ServiceInfo> map = hostReactor.getServiceInfoMap();
for (Map.Entry<String, ServiceInfo> entry : map.entrySet()) {
ServiceInfo serviceInfo = entry.getValue();
if (StringUtils.equals(serviceInfo.getKey(), UtilAndComs.ALL_IPS) || StringUtils.equals(serviceInfo.getName(), UtilAndComs.ENV_LIST_KEY)
|| StringUtils.equals(serviceInfo.getName(), "00-00---000-ENV_CONFIGS-000---00-00")
|| StringUtils.equals(serviceInfo.getName(), "vipclient.properties")
|| StringUtils.equals(serviceInfo.getName(), "00-00---000-ALL_HOSTS-000---00-00")) {
continue;
}
DiskCache.write(serviceInfo, failoverDir);
}
}
}
每隔24小时,把内存中所有的服务数据,写一遍到磁盘中,其中需要过滤掉一些非域名数据的特殊数据,具体可看代码中的描述。最后一个定时任务,是每隔10s,是检查缓存目录是否存在,同时如果缓存里面值没有的话,主动触发一次缓存写磁盘的操作。
以上就是客户端构造一个Nacos实例的初始化全部流程,大部分都是在初始化多个线程池或者定时任务,各司其职,这个也是我们写后端程序的一些基本套路,提高系统的并发能力,同时在对任务的分发和执行,引入一些常用的异步编程模型如队列模型的事件分发,这些都是异步和并发的很好学习素材,这2点也是写高性能程序的基本要求。
客户端初始化总结
我们通过Nacos的NacosFactory构造一个nacos服务实例作为切入点,把客户端的初始化流程给串了一遍,概述下客户端初始化流程做的几件事:
- 初始化事件分发组件,用于处理服务端主动通知下来的变更数据
- 初始化Nacos服务集群地址列表更新组件,用于客户端维护Nacos服务端的最新地址列表
- 初始化服务健康检查模块,主动给服务端上报服务的健康情况
- 初始化客户端的缓存,10s检查一次,如果没有,则创建
- 24小时备份一次客户端的缓存文件
- 5s检查一次容灾开关,更新到内存中,容灾模式情况下,服务地址读的都是缓存
以上就是对Nacos简单介绍,如还有最新的源码将在后续更新。
最后附图
Nacos 架构
逻辑架构及其组件介绍
数据模型
Nacos 数据模型 Key 由三元组唯一确定, Namespace默认是空串,公共命名空间(public),分组默认是 DEFAULT_GROUP。
服务领域模型
配置领域模型
类视图
构建物、部署及启动模式
更多
服务 (Service)
服务是指一个或一组软件功能(例如特定信息的检索或一组操作的执行),其目的是不同的客户端可以为不同的目的重用(例如通过跨进程的网络调用)。Nacos 支持主流的服务生态,如 Kubernetes Service、gRPC|Dubbo RPC Service 或者 Spring Cloud RESTful Service.
服务注册中心 (Service Registry)
服务注册中心,它是服务,其实例及元数据的数据库。服务实例在启动时注册到服务注册表,并在关闭时注销。服务和路由器的客户端查询服务注册表以查找服务的可用实例。服务注册中心可能会调用服务实例的健康检查 API 来验证它是否能够处理请求。
服务元数据 (Service Metadata)
服务元数据是指包括服务端点(endpoints)、服务标签、服务版本号、服务实例权重、路由规则、安全策略等描述服务的数据
服务提供方 (Service Provider)
是指提供可复用和可调用服务的应用方
服务消费方 (Service Consumer)
是指会发起对某个服务调用的应用方
配置 (Configuration)
在系统开发过程中通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成这个步骤。配置变更是调整系统运行时的行为的有效手段之一。
配置管理 (Configuration Management)
在数据中心中,系统中所有配置的编辑、存储、分发、变更管理、历史版本管理、变更审计等所有与配置相关的活动统称为配置管理。
名字服务 (Naming Service)
提供分布式系统中所有对象(Object)、实体(Entity)的“名字”到关联的元数据之间的映射管理服务,例如 ServiceName -> Endpoints Info, Distributed Lock Name -> Lock Owner/Status Info, DNS Domain Name -> IP List, 服务发现和 DNS 就是名字服务的2大场景。
配置服务 (Configuration Service)
在服务或者应用运行过程中,提供动态配置或者元数据以及配置管理的服务提供者。
逻辑架构及其组件介绍:
- 服务管理:实现服务CRUD,域名CRUD,服务健康状态检查,服务权重管理等功能
- 配置管理:实现配置管CRUD,版本管理,灰度管理,监听管理,推送轨迹,聚合数据等功能
- 元数据管理:提供元数据CURD 和打标能力
- 插件机制:实现三个模块可分可合能力,实现扩展点SPI机制
- 事件机制:实现异步化事件通知,sdk数据变化异步通知等逻辑
- 日志模块:管理日志分类,日志级别,日志可移植性(尤其避免冲突),日志格式,异常码+帮助文档
- 回调机制:sdk通知数据,通过统一的模式回调用户处理。接口和数据结构需要具备可扩展性
- 寻址模式:解决ip,域名,nameserver、广播等多种寻址模式,需要可扩展
- 推送通道:解决server与存储、server间、server与sdk间推送性能问题
- 容量管理:管理每个租户,分组下的容量,防止存储被写爆,影响服务可用性
- 流量管理:按照租户,分组等多个维度对请求频率,长链接个数,报文大小,请求流控进行控制
- 缓存机制:容灾目录,本地缓存,server缓存机制。容灾目录使用需要工具
- 启动模式:按照单机模式,配置模式,服务模式,dns模式,或者all模式,启动不同的程序+UI
- 一致性协议:解决不同数据,不同一致性要求情况下,不同一致性机制
- 存储模块:解决数据持久化、非持久化存储,解决数据分片问题
- Nameserver:解决namespace到clusterid的路由问题,解决用户环境与nacos物理环境映射问题
- CMDB:解决元数据存储,与三方cmdb系统对接问题,解决应用,人,资源关系
- Metrics:暴露标准metrics数据,方便与三方监控系统打通
- Trace:暴露标准trace,方便与SLA系统打通,日志白平化,推送轨迹等能力,并且可以和计量计费系统打通
- 接入管理:相当于阿里云开通服务,分配身份、容量、权限过程
- 用户管理:解决用户管理,登录,sso等问题
- 权限管理:解决身份识别,访问控制,角色管理等问题
- 审计系统:扩展接口方便与不同公司审计系统打通
- 通知系统:核心数据变更,或者操作,方便通过SMS系统打通,通知到对应人数据变更
- OpenAPI:暴露标准Rest风格HTTP接口,简单易用,方便多语言集成
- Console:易用控制台,做服务管理、配置管理等操作
- SDK:多语言sdk
- Agent:dns-f类似模式,或者与mesh等方案集成
- CLI:命令行对产品进行轻量化管理,像git一样好用
Thanks for finally talking about >高性能服务发现、配置框架Nacos - RawChen · Blog <Loved it!
Thanks for finally writing about >高性能服务发现、配置框架Nacos -
RawChen · Blog <Liked it!
What's up, I want to subscribe for this web site to get most up-to-date updates, therefore where can i
do it please assist.
https://rawchen.com/feed
谢谢分享,文章不错