高性能服务发现、配置框架Nacos
   软件工程   4 评论   7133 浏览

高性能服务发现、配置框架Nacos

   软件工程   4 评论   7133 浏览

Nacos是什么

https://nacos.io/zh-cn/
https://github.com/alibaba/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的功能。

  1. 从以上 GitHub 链接中的 Release 中下载 nacos-server-2.0.1.zip
  2. 在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 架构

逻辑架构及其组件介绍

数据模型

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)

在服务或者应用运行过程中,提供动态配置或者元数据以及配置管理的服务提供者。

逻辑架构及其组件介绍:

本文由 RawChen 发表, 最后编辑时间为:2021-08-28 11:23
如果你觉得我的文章不错,不妨鼓励我继续写作。

发表评论
选择表情
  1. Thanks for finally writing about >高性能服务发现、配置框架Nacos -
    RawChen · Blog <Liked it!

       Windows 8   搜狗浏览器 回复
  2. 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.

       Windows 7   Chrome 67 回复
    1. RawChen 博主
         Windows 10   Chrome 91 回复
  3. 谢谢分享,文章不错

       Windows 7   Firefox 45 回复
Top