第九章 Nacos统一配置管理

1,330 阅读10分钟

配置文件想必大家都不陌生。在Spring Boot项目中,默认会提供一个application.properties或者application.yml文件,我们可以把一些全局性的配置或者需要动态维护的配置写入改文件,不如数据库连接,功能开关,限流阈值,服务地址等。为了解决不同环境下服务连接配置等信息的差异,Spring Boot还提供了基于spring.profiles.active={profile}的机制来实现不同的环境的切换。

随着单体架构向微服务架构的演进,各个应用自己独立维护本地配置文件的方式开始显露出它的不足之处。主要有下面几点。

配置的动态更新:在实际应用会有动态更新位置的需求,比如修改服务连接地址、限流配置等。在传统模式下,需要手动修改配置文件并且重启应用才能生效,这种方式效率太低,重启也会导致服务暂时不可用。

配置集中式管理:在微服务架构中某些核心服务为了保证高性能会部署上百个节点,如果在每个节点中都维护一个配置文件,一旦配置文件中的某个属性需要修改,可想而知,工作量是巨大的。

不同部署环境下配置的管理:前面提到通过profile机制来管理不同环境下的配置,这种方式对于日常维护来说也比较繁琐。

统一配置管理就是弥补上述不足的方法,简单说,最近本的方法是把各个应用系统中的某些配置放在一个第三方中间件上进行统一维护。然后,对于统一配置中心上的数据的变更需要推送到相应的服务节点实现动态跟新,所以微服务架构中,配置中心也是一个核心组件。

9.1 Nacos配置中心简介

\

9.1.1 简介

\

配置中心的开源解决方案很多,比如第6章的Spring Cloud Config,以及ZooKeeper, Disconf、Apollo、QConf、Nacos等。

Nacos是Alibaba开源的中间件,通过上一章的学习Nacos架构中有2个模块,分别是Config Service和Naming Service。其中Config Service就是Nacos用于实现配置中心的核心模块,他实现了对配置的CRUD,版本管理,灰度管理,监听管理,推送轨迹,聚合数据等功能。

\

9.1.2 基本概念

  1. Profile

Java项目一般都会有多个Profile配置,用于区分开发环境,测试环境,准生产环境,生成环境等,每个环境对应一个properties文件(或是yml/yaml文件),然后通过设置 spring.profiles.active 的值来决定使用哪个配置文件。

spring:
  application:
    name: order-service
  profiles:
    active: dev

Nacos Config的作用就把这些文件的内容都移到一个统一的配置中心,即方便维护又支持实时修改后动态刷新应用。

  1. Data ID\

当使用Nacos Config后,Profile的配置就存储到Data ID下,即一个Profile对应一个Data ID

  • Data ID的拼接格式:${prefix}-${spring.profiles.active}.${file-extension}
  • prefix 默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix 来配置
  • spring.profiles.activespring.profiles.active 的值,即为当前环境对应的 profile,当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
  • file-extension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 propertiesyaml 类型。

\

  1. Group
    Group 默认为 DEFAULT_GROUP,可以通过 spring.cloud.nacos.config.group 来配置,当配置项太多或者有重名时,可以通过分组来方便管理

\

9.2 配置中心实战

\

9.2.1 基本配置

\

nacos可以作为配置中心使用,在payment工程中如下步骤,启动nacos配置中心

\

  1. 引入依赖

\

        <!--nacos config-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

\

  1. 配置文件

\

(1)bootstrap.yml

\

注意:Sprnig Boot 2.4.X版本后需要手动添加spring-cloud-starter-bootstrap组件后,才能加载bootstrap.yml配置文件

\

spring:
  application:
    name: payment-service
  profiles:
    active: dev
  cloud:
    nacos:
      config:
        server-addr: localhost:8848
        file-extension: yaml

\

注意:spring.cloud.nacos.config配置必须放到bootstrap.yml配置文件中,保证在优先读取配置文件再启动,否则配置无效

\

(2)application.yml

\

server:
  port: ${port:9001}
spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #配置Nacos地址

\

\

  1. nacos中的配置DataID

\

访问nacos在配置列表中增加如下配置,如图9-1所示。

图9-1 Nacos支付微服务配置

\

当使用Nacos Config后,Profile的配置就存储到Data ID下,即一个Profile对应一个Data ID

  • Data ID的拼接格式:${prefix}-${spring.profiles.active}.${file-extension}
  • prefix 默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix 来配置
  • spring.profiles.activespring.profiles.active 的值,即为当前环境对应的 profile,当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
  • file-extension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 propertiesyaml 类型。

\

\

对应关系,如图9-2所示。

\

9-2 DataID对应关系

\

  1. 业务中读取配置属性

\

@RestController
@RequestMapping("/payment")
@RefreshScope
public class PaymentController {

    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/config/info")
    public String getConfigInfo() {
        return configInfo;
    }
}

\

  1. 测试

\

访问地址http://localhost:9001/payment/config/info,显示config info public default group

\

当修改配置值,会结果已经改变,Nacos自带自动刷新功能。

\

9.2.2 配置隔离

\

通常,企业研发的流程是这样的:先在开发测试环境开发和测试功能,然后灰度,最后发布到生产环境。并且,为了生产环境的稳定,需要将测试环境和生产环境进行隔离,Nacos可以通过三种方式进行配置隔离:Nacos的服务器、namespace命名空间、group分组,在bootstrap.yml文件中可以通过配置Nacos的server-addr、namespace和group来区分不同的配置信息。

  • Nacos的服务器 spring.cloud.nacos.config.server-addr
  • Nacos的命名空间 spring.cloud.nacos.config.namespace,注意,这里使用命名空间的ID不是名称
  • Nacos的分组 spring.cloud.nacos.config.group

\

Nacos配置隔离具体步骤如下。

\

  1. 命名空间

创建dev,test,prod,如图9-3所示。

图9-3 Nacos命名空间

\

\

  1. DataID

在不同命名空间下创建如下DataID(克隆即可),如图9-4所示。

\

图9-5 命名空间下的DataID

\

  1. bootstrap.yml

\

配置读取相应的配置,bootstrap.yml代码如下。

\

spring:
  application:
    name: payment-service
  profiles:
    active: dev
  cloud:
    nacos:
      config:
        server-addr: localhost:8848
        file-extension: yaml
        namespace: f521b45b-e0bb-4c4d-b97c-3d0158970001
        group: MY_GROUP

\

注意:如果不配置namespace默认为public,不配置group默认为DEFAULT_GROUP

\

此时读取的配置为namespace=prod,group=MY_GROUP的配置页面显示config info prod public group

\

\

  1. service隔离

同样注册service时可以指定指定namespace隔离注册到哪一个命名空间。application.yml如下。

\

server:
  port: ${port:9001}
spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #配置Nacos地址
        namespace: f521b45b-e0bb-4c4d-b97c-3d0158970001

\

9.2.3 配置拆分

\

  1. 配置拆分策略

\

项目中会有很多的微服务,必然会存在很多具体配置,和重复配置,可以采用如下方案管理配置,如图9-6所示

\

图9-6 配置文件拆分策略

\

\

根据上面分析在nacos中配置,如图9-7所示。

图9-7 配置文件拆分配置

\

\

  1. DataID配置

\

payment-service-dev.yaml,配置如图9-8所示

\

图9-8 payment-service-dev配置

\

common.yaml,配置如图9-9所示。

\

图9-9 common.yaml配置

\

  1. 配置文件

\

bootstrap.yml,配置代码如下所示

\

spring:
  application:
    name: payment-service
  profiles:
    active: dev
  cloud:
    nacos:
      config:
        server-addr: localhost:8848
        file-extension: yaml
        namespace: f521b45b-e0bb-4c4d-b97c-3d0158970001
        group: MY_GROUP
        extension-configs[0]:
          data-id: common.yaml
          refresh: true

\

  • 注:extension-configs配置属性和shared-configs配置属性功能一致,都是读取配置文件,这里我们把重复配置放到common.yaml中,这样在不同的工程中就可以重复使用[n]的值越大,优先级越高。
  • 注:spring.application.namespring.profiles.active配置必须放到bootstrap.yml中,否则影响配置自动刷新功能。

\

\

按照上面方案,整合拆分订单微服务的配置文件,如图9-10所示。

\

图9-10 订单微服务配置

\

order微服务bootstrap.yml

\

spring:
  application:
    name: order-service
  profiles:
    active: dev
  cloud:
    nacos:
      config:
        server-addr: localhost:8848 #nacos 配置中心的地址
        file-extension: yaml
        extension-configs[0]:
          data-id: common.yaml
          refresh: true

\

注意:这里两个工程使用重用的配置文件common.yaml

9.3 Nacos高可用集群

\

如果我们要搭建集群的话,那么肯定是不能用内嵌的数据库,不然数据无法共享。所以,集群搭建的时候我们需要将Nacos对接Mysql进行数据存储。

集群模式跟我们平时进行扩容是一样的,可以通过Nginx转发到多个节点,最前面挂一个域名即可,如图9-11所示。

\

\

图9-11 Nacos高可用集群

\

9.3.1 使用vagrant准备一台虚拟机

\

准备工作先要安装vagrant和virtualBox软件,参考我之前的相关课程

\

执行命令创建vagrantfile配置文件

\

vagrant init centos/7

\

修改配置文件如下:

\

Vagrant.configure("2") do |config|

  # 定义应用服务器
  config.vm.define :s2 do |s2|
    s2.vm.provider "virtualbox" do |v|
          v.customize ["modifyvm", :id, "--name", "s2", "--memory", "4096", "--cpus", "2"]
    end
    s2.vm.box = "centos/7"
    s2.vm.hostname = "s2"
    s2.vm.network :private_network, ip: "192.168.56.111"
  end

end

\

执行命令,安装和启动虚拟机

\

vagrant up

\

安装sz,rz命令

\

yum -y install lrzsz

\

安装vim

\

yum -y install vim*

\

安装net-tool 包

\

yum install net-tools

\

开启远程登陆,修改“/etc/ssh/sshd_config”

\

PermitRootLogin yes 
PasswordAuthentication yes

\

然后重启SSHD

\

systemctl restart sshd

\

使用Xshell或SecureCRT进行远程连接即可

\

9.2.2 安装Mysql

\

  1. 下载Mysql

\

下载地址 downloads.mysql.com/archives/co…,如图9-12所示。

\

图9-12 mysql下载

\

  1. 安装

\

拷贝mysql-5.7.28-linux-glibc2.12-x86_64.tar.gz到/root目录下

\

cd ~
tar -zxvf mysql-5.7.28-linux-glibc2.12-x86_64.tar.gz

\

解压完成后重命名

\

mv mysql-5.7.28-linux-glibc2.12-x86_64 mysql-5.7.28
mv mysql-5.7.28 /usr/local/

\

检查mysql组和用户是否存在,如果没有则创建

\

cat /etc/group|grep mysql
groupadd mysql
useradd -r -g mysql mysql  #useradd -r参数表示mysql用户是系统用户,不可用于登录系统

\

安装数据库

\

创建data目录
cd /usr/local/mysql-5.7.28
mkdir data

\

将/usr/local/mysql-5.7.28的所有者及所属组改为mysql

\

chown -R mysql.mysql /usr/local/mysql-5.7.28

\

在/usr/local/mysql-5.7.28/support-files目录下创建my_default.cnf

\

cd /usr/local/mysql-5.7.28/support-files
vim my_default.cnf

\

具体配置内容如下:

\

[mysqld]

#设置mysql的安装目录
basedir =/usr/local/mysql-5.7.28
#设置mysql数据库的数据存放目录
datadir = /usr/local/mysql-5.7.28/data
#设置端口
port = 3306

socket = /tmp/mysql.sock
#设置字符集
character-set-server=utf8
#日志存放目录
log-error = /usr/local/mysql-5.7.28/data/mysqld.log
pid-file = /usr/local/mysql-5.7.28/data/mysqld.pid
#允许时间类型的数据为零(去掉NO_ZERO_IN_DATE,NO_ZERO_DATE)
sql_mode=ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
#ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION

\

拷贝配置文件,是否覆盖,是

\

cp my_default.cnf /etc/my.cnf

\

初始化Mysql

\

./bin/mysqld --initialize --user=mysql --basedir=/usr/local/mysql-5.7.28/ --datadir=/usr/local/mysql-5.7.28/data/

\

如果报错:./bin/mysqld: error while loading shared libraries: libaio.so.1: cannot open shared object file: No such file or 就安装libaio,如果没有则跳过

\

yum install libaio

\

初始化完成之后查看日志

\

cat /usr/local/mysql-5.7.28/data/mysqld.log

\

蓝框里的是临时密码,如图9-13所示。

\

图9-13 mysql默认密码

\

把启动脚本放到开机初始化目录

\

cp support-files/mysql.server /etc/init.d/mysql

\

启动mysql

\

service mysql start

\

进入mysql并更改密码

\

cd /usr/local/mysql-5.7.28
./bin/mysql -u root -p 上面生成的初始化密码
mysql> set password=password('123456');
mysql> grant all privileges on *.* to root@'%' identified by '123456';
mysql> flush privileges;

\

添加远程访问权限

\

mysql> use mysql;
mysql> update user set host='%' where user = 'root';
mysql> flush privileges;

\

如果更改时报错:ERROR 1062 (23000): Duplicate entry '%-root' for key 'PRIMARY',就先查询一下是否已更改,最后执行刷新。

\

重启mysql生效

\

service mysql restart

\

9.2.3 安装JDK

\

yum安装很方便,实在是懒得一直拷贝安装包了。

\

安装方法

\

yum install java-1.8.0-openjdk.x86_64
yum install -y  java-1.8.0-openjdk-devel

\

查找jdk安装的位置

\

[root@localhost]# which java
/bin/java
[root@localhost]# ls -l /bin/java
lrwxrwxrwx. 1 root root 22 Mar 22 01:01 /bin/java -> /etc/alternatives/java
[root@localhost]# ls -l /etc/alternatives/java
lrwxrwxrwx. 1 root root 73 Mar 22 01:01 /etc/alternatives/java -> /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-0.el7_7.x86_64/jre/bin/java

\

配置JAVA_HOME

\

vi /etc/profile

\

最后一行加入

\

export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-0.el7_7.x86_64/jre
export PATH=$PATH:$JAVA_HOME/bin

\

配置生效

\

source /etc/profile

\

9.2.4 安装Nginx

\

  1. 准备工作

Nginx 是 C语言 开发,先安装相关依赖包

\

(1)gcc 安装
安装 nginx 需要先将官网下载的源码进行编译,编译依赖 gcc 环境,如果没有 gcc 环境,则需要安装:

\

yum install gcc-c++

\

(2)PCRE pcre-devel 安装
PCRE(Perl Compatible Regular Expressions) 是一个Perl库,包括 perl 兼容的正则表达式库。nginx 的 http 模块使用 pcre 来解析正则表达式,所以需要在 linux 上安装 pcre 库,pcre-devel 是使用 pcre 开发的一个二次开发库。nginx也需要此库。命令:

\

yum install -y pcre pcre-devel

\

(3)zlib 安装
zlib 库提供了很多种压缩和解压缩的方式, nginx 使用 zlib 对 http 包的内容进行 gzip ,所以需要在 Centos 上安装 zlib 库。

\

yum install -y zlib zlib-devel

\

(4)OpenSSL 安装
OpenSSL 是一个强大的安全套接字层密码库,囊括主要的密码算法、常用的密钥和证书封装管理功能及 SSL 协议,并提供丰富的应用程序供测试或其它目的使用。
nginx 不仅支持 http 协议,还支持 https(即在ssl协议上传输http),所以需要在 Centos 安装 OpenSSL 库。

\

yum install -y openssl openssl-devel

\

(5)官网下载

\

直接下载.tar.gz安装包,地址:nginx.org/en/download…,如图9-14所示。

\

图9-14 Nginx下载

\

  1. 安装Nginx

\

(1)解压

\

依然是直接命令:

\

tar -zxvf nginx-1.18.0.tar.gz
cd nginx-1.18.0

\

(2)配置

\

其实在 nginx-1.18.0 版本中你就不需要去配置相关东西,默认就可以了。当然,如果你要自己配置目录也是可以的。
1.使用默认配置

\

./configure

\

自定义配置(不推荐)

\

./configure \
--prefix=/usr/local/nginx \
--conf-path=/usr/local/nginx/conf/nginx.conf \
--pid-path=/usr/local/nginx/conf/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-http_gzip_static_module \
--http-client-body-temp-path=/var/temp/nginx/client \
--http-proxy-temp-path=/var/temp/nginx/proxy \
--http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
--http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
--http-scgi-temp-path=/var/temp/nginx/scgi

\

注:将临时文件目录指定为/var/temp/nginx,需要在/var下创建temp及nginx目录

\

(3)编译安装

\

make
make install

\

查找安装路径:

\

whereis nginx

\

(4)启动、停止nginx

\

cd /usr/local/nginx/sbin/
./nginx 
./nginx -s stop
./nginx -s quit
./nginx -s reload

\

9.2.5 Nacos持久化配置

\

拷贝nacos-server-1.4.1.tar.gz,解压到/user/local/nacos下

\

tar -zxvf nacos-server-1.4.1.tar.gz -C /usr/local

\

创建nacos数据库,导入nacos\conf\nacos-mysql.sql脚本

\

修改conf\application.properties配置文件,如下:

\

#*************** Config Module Related Configurations ***************#
### If use MySQL as datasource:
spring.datasource.platform=mysql

### Count of DB:
db.num=1

### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123456

### Connection pool configuration: hikariCP
db.pool.config.connectionTimeout=30000
db.pool.config.validationTimeout=10000
db.pool.config.maximumPoolSize=20
db.pool.config.minimumIdle=2

\

让3个Nacos节点都使用同一个数据源

\

修改nacos\conf\cluster文件:

\

10.0.2.15:8848
10.0.2.15:8849
10.0.2.15:8850

\

拷贝2个nacos,修改端口号,分别为8848、8849、8850

\

server.port=8849

\

\

启动3个Nacos节点

\

从之前Nacos导入配置,修改如下,如图9-15所示。

\

图9-15 Nacos集群配置

\

9.2.6 Nginx反向代理配置

\

修改/usr/local/nginx/conf配置如下:

\

    #gzip  on;
    upstream nacos_cluster {
       server 127.0.0.1:8848;
       server 127.0.0.1:8849;
       server 127.0.0.1:8850;
    }
    server {
        listen       1111;
        server_name  localhost;
        location / {
                proxy_pass http://nacos_cluster;
                proxy_set_header Host $host:$server_port;
        }
    }

\

bootstrap.yml配置文件指定nginx地址代码如下。

\

spring:
  application:
    name: order-service
  profiles:
    active: dev
  cloud:
    nacos:
      config:
        server-addr: 192.168.56.110:1111
        file-extension: yaml
        extension-configs[0]:
          data-id: common.yaml
          refresh: true

\

DataID,common.yaml配置如图9-16所示。

\

图9-16 common.yaml集群配置

\

启动测试,关闭一个Nacos节点,测试高可用,Nacos其他两个节点依然可用

9.4 NacosConfig原理解析

Nacos Config针对配置管理提供了4中操作。针对针对这4中操作,Nacos提供了SDK和Open API的方式进行访问。

  • 获取配置
API(GET):/nacos/v1/cs/configs
SDK:public String getConfig(String dataId, String group, long timeoutMs) throws NacosException
  • 监听配置
API(POST):/nacos/v1/cs/configs/listener
SDK:public void addListener(String dataId, String group, Listener listener) 
  • 发布配置
API(GET):/nacos/v1/cs/configs
SDK:public boolean publishConfig(String dataId, String group, String content, String type) throws NacosException;
  • 删除配置
API(DELETE):/nacos/v1/cs/configs
SDK:public boolean removeConfig(String dataId, String group) throws NacosException

这4个操作可以归纳为2中类型,分别是配置的CRUD和配置的动态监听。

9.4.1 配置的CRUD

\

对于Nacos Config来说,其实就是提供了配置的集中式管理功能,然后对外提供CRUD的访问接口使的应用系统可以完成配置的基本操作,实际上这种场景比不复杂,对于服务端来说,付伟就是配置如何存储,以及是否需要持久化,对于客户端来说,就是通过接口从服务端查询到相应的数据,然后返回即可,如图9-17所示。

图9-17 Nacos配置中心

需要注意的是,Nacos服务端的数据存贮默认采用的Derby数据库,除此之外,支持MySql数据库。

9.4.2 动态监听值Pull Or Push

\

当Nacos Config Server上的配置发生变化时,需要让相关的应用程序感知配置的变化,这就需要客户端针对感兴趣的配置实现监听。那么Nacos客户端是如何实现配置变更实时跟新的呢?

一般来说,客户端和服务端之间的数据交互无非就2种方式:Pull和Push。

  • Pull表示客户端从服务端主动拉取数据。
  • Push表示服务端主动把数据推送到客户端。

这两种方式没有什么优劣之分,只是看那种方式更适合当前的场景。

对于Push模式来说,服务端需要维持与客户端的长链接,如果客户端的数量比较多,那么服务端需要消耗大量的内存资源来保存每个链接,并且为了检测链接的有效性,还需要心跳机制来维持每个连接的状态。

对Pull模式下,客户单需要定时从服务端拉取一次数据,由于定时任务会存在一定的时间间隔,所以不能保证数据的实时性。并且在服务端配置长时间不更新的情况下,客户端的定时任务会做一些无效的Pull。

Nacos采用的是Pull模式,但并不是简单的Pull,而是一种长轮询机制,他结合Push和Pull两者的有时。客户端采用长轮询的方式定时发起Pull请求,去检查服务端配置信心是否发生了变更,如果发生了变更,则客户端会根据变更的数据获得最新的配置。所谓长轮询,是客户端发起轮询请求之后,服务端如果有配置变更,就直接返回,如图9-18所示。

\

图9-18 Nacos Client发起Pull请求

如果客户端发起Pull请求后,发现服务端的配置和客户端的配置是保持一致的,那么服务端会先“Hold”住这个请求,也就是服务端拿到这个链接之后在指定的时间段内一致不返回结果,知道这段时间内配置发生变化,服务端会把原来“Hold”住的请求进行返回,如图9-19所示,Nacos服务端收到请求之后,先检查配置是否发生了变更,如果没有,则设置一个定时任务,延期29.5s执行并且把当前的客户端长轮询连接加入allSubs队列。这时候有两种方式触发该连接结果的返回。

  • 第一种是在等待29.5s后触发自动检查机制,这时候不管配置有没有发生变化,都会把结果返回客户端。而29.5s就是这个长连接保持的时间。
  • 第二种是在29.5s内任意一个时刻,通过Nacos Dashbord或者API的方式对配置进行了修改,这会触发一个时事件机制,监听到该事件的任务会遍历allSubs队列,找到发生变更的配置项对应的ClientLongPolling任务,将变更的数据通过该任务中的连接进行返回,就完成了一次“推送”操作。

图9-19 Nacos长轮询机制

9.5 Nacos源码解析

\

Spring Cloud Alibaba Nacos Config中能够通过environment().getProperty("config.info")获得Nacos Config服务器上的数据,最重要的实现类是NacosPropertySourceLocator,其中包含了一个locate方法,他的重要作用是。

  • 初始化ConfigService对象,这是Nacos客户端提供的用于访问实现配置中心基本操作的类。
  • 按照顺序分别加载共享配置、扩展配置、应用名称对应的配置。
	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;
	}

按照loadApplicationConfiguration方法继续跟进,会看到如下代码。

	private List<PropertySource<?>> loadNacosData(String dataId, String group,
			String fileExtension) {
		String data = null;
		try {
			data = configService.getConfig(dataId, group, timeout);
			if (StringUtils.isEmpty(data)) {
				log.warn(
						"Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]",
						dataId, group);
				return Collections.emptyList();
			}
			if (log.isDebugEnabled()) {
				log.debug(String.format(
						"Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId,
						group, data));
			}
			return NacosDataParserHandler.getInstance().parseNacosData(dataId, data,
					fileExtension);
		}
		catch (NacosException e) {
			log.error("get data from Nacos error,dataId:{} ", dataId, e);
		}
		catch (Exception e) {
			log.error("parse data from Nacos error,dataId:{},data:{}", dataId, data, e);
		}
		return Collections.emptyList();
	}

上述代码的路径为:

loadApplicationConfiguration-->loadNacosDataIfPresent-->loadNacosPropertySource-->build-->loadNacosData

不难发现,最终是基于configService.getConfig从Nacos配置中心上的加载配置进行的填充的,那么事件订阅机制在哪里实现的呢?,看一下NacosContextRefresher类。它里面实现了一个ApplicationReadyEvent事件监听,也就是上下文已经准备完毕的时候会触发这个时间。

public class NacosContextRefresher
		implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
	@Override
	public void onApplicationEvent(ApplicationReadyEvent event) {
		// many Spring context
		if (this.ready.compareAndSet(false, true)) {
			this.registerNacosListenersForApplications();
		}
	}
}

当监听到事件之后,会调用registerNacosListenersForApplications方法来实现Nacos事件监听的注册,代码如下。

	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);
		}
	}

上面这段代码的主要功能是,当收到配置变更的回调时,会通过applicationContext.publishEvent发布一个RefreshEvent事件,而这个事件的监听实现在RefreshEventListener类中

public class RefreshEventListener implements SmartApplicationListener {
    ...
	public void handle(RefreshEvent event) {
		if (this.ready.get()) { // don't handle events before app is ready
			log.debug("Event received " + event.getEventDesc());
			Set<String> keys = this.refresh.refresh();
			log.info("Refresh keys changed: " + keys);
		}
	}

}

最终,在handler方法中会调用this.refresh.refresh();方法完成配置的更新,下面我们重点关注客户端和服务端之间的长轮询机制,以及服务端是如何实现推送配置更新消息给客户端的。

9.5.1 NacosFactory.createConfigService

\

客户端的长轮询定时任务是在NacosFactory.createConfigService构建ConfigService对象实例的时候启动的,最终的调用代码如下。

  • 通过Class.forName来加载NacosConfigService类。
  • 通过反射完成NacosConfigService类的实例化。
    public static ConfigService createConfigService(Properties properties) throws NacosException {
        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(NacosException.CLIENT_INVALID_PARAM, e);
        }
    }

9.5.2 NacosConfigService构造函数

\

NacosConfigService构造函数的代码如下。

  • 初始化一个HttpAgent,这里用到了装饰模式,实际工作的是ServerHttpAgent,MetricsHttpAgent内部也调用了ServerHttpAgent的方法,增加了监控统计的信息。
  • ClientWorker是客户端的一个工作类,agent作为参数传入ClientWorker,可以基本猜测到,里面会用agent做一些与远程相关的工作。
    public NacosConfigService(Properties properties) throws NacosException {
        ValidatorUtils.checkInitParam(properties);
        String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
        if (StringUtils.isBlank(encodeTmp)) {
            this.encode = Constants.ENCODE;
        } else {
            this.encode = encodeTmp.trim();
        }
        initNamespace(properties);
        
        this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        this.agent.start();
        this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
    }

9.5.3 ClientWorker

\

ClientWorker构造方法如下,主要的功能是构建两个定时调度的线程池,并且启动一个定时任务。

  • 第一个线程池executor只拥有一个核心线程,每个10s就会执行一次checkConfigInfo()方法,从方法名上可以知道每10ms检查一次配置信息
  • 第二个线程池executorService只完成了初始化,后续用用到,主要用于实现客户端的定时长轮询功能。

\

    public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,
            final Properties properties) {
        this.agent = agent;
        this.configFilterChainManager = configFilterChainManager;
        
        // Initialize the timeout parameter
        
        init(properties);
        
        this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
                t.setDaemon(true);
                return t;
            }
        });
        
        this.executorService = Executors
                .newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread t = new Thread(r);
                        t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
                        t.setDaemon(true);
                        return t;
                    }
                });
        
        this.executor.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                try {
                    checkConfigInfo();
                } catch (Throwable e) {
                    LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
                }
            }
        }, 1L, 10L, TimeUnit.MILLISECONDS);
    }

9.5.4 ClientWorker.checkConfigInfo

在ClientWorker构造方法中,通过executor.scheduleWithFixedDelay启动了一个每个10s执行一次的定时任务,其中调用的方法是checkConfigInfo,这个方法主要用来检查配置是否发生了变化,用到了executorService这个定时调度的线程池。

    public void checkConfigInfo() {
        //分任务
        int listenerSize = cacheMap.size();
        //向上取整为批数
        int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
        if (longingTaskCount > currentLongingTaskCount) {
            for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
                // The task list is no order.So it maybe has issues when changing.
                executorService.execute(new LongPollingRunnable(i));
            }
            currentLongingTaskCount = longingTaskCount;
        }
    }

上述代码逻辑比较有意思,简单解释一下

  • cacheMap:ConcurrentHashMap<String, CacheData> cacheMap用来存储监听变更的缓存集合,key是根据dataId/group/tenant(租户)拼接的值。Value是的应用的存储在Nacos服务器上的配置文件的内容。
  • 长轮询任务查分:默认情况下,每个长轮询LongPollingRunnable任务处理3000个监听配置集。如果超过3000个,则需要启动多个LongPollingRunnable去执行。

9.5.5 LongPollingRunnable.run

\

LongPollingRunnable实际是一个线程,所以我们可以直接找到LongPollingRunnable的run方法。

  • 通过checkLocalConfig方法检查本地配置。
  • 执行checkUpdateDataIds方法和在服务端建立长轮序机制,从服务端获取发生变更的数据。
  • 遍历变更数据集合changedGroupKeys,通过调用getServerConfig方法,根据Data ID,Group,Tanant去服务端读取对应的配置信息并保存到本地文件中。
        public void run() {
            
            List<CacheData> cacheDatas = new ArrayList<CacheData>();
            List<String> inInitializingCacheList = new ArrayList<String>();
            try {
                // 遍历CacheData,检查本地配置
                for (CacheData cacheData : cacheMap.values()) {
                    if (cacheData.getTaskId() == taskId) {
                        cacheDatas.add(cacheData);
                        try {
                            checkLocalConfig(cacheData);
                            if (cacheData.isUseLocalConfigInfo()) {
                                cacheData.checkListenerMd5();
                            }
                        } catch (Exception e) {
                            LOGGER.error("get local config info error", e);
                        }
                    }
                }
                
                // 通过长轮询请求检查服务端对应的配置是否发生了变更
                List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
                if (!CollectionUtils.isEmpty(changedGroupKeys)) {
                    LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
                }
                //遍历存在变更的groupKey,重新加载更新数据
                for (String groupKey : changedGroupKeys) {
                    String[] key = GroupKey.parseKey(groupKey);
                    String dataId = key[0];
                    String group = key[1];
                    String tenant = null;
                    if (key.length == 3) {
                        tenant = key[2];
                    }
                    try {
                        String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                        CacheData cache = cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
                        cache.setContent(ct[0]);
                        if (null != ct[1]) {
                            cache.setType(ct[1]);
                        }
                        LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}",
                                agent.getName(), dataId, group, tenant, cache.getMd5(),
                                ContentUtils.truncateContent(ct[0]), ct[1]);
                    } catch (NacosException ioe) {
                        String message = String
                                .format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
                                        agent.getName(), dataId, group, tenant);
                        LOGGER.error(message, ioe);
                    }
                }
                //触发事件通知
                for (CacheData cacheData : cacheDatas) {
                    if (!cacheData.isInitializing() || inInitializingCacheList
                            .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                        cacheData.checkListenerMd5();
                        cacheData.setInitializing(false);
                    }
                }
                inInitializingCacheList.clear();
                
                //继续执行当前线程
                executorService.execute(this);
                
            } catch (Throwable e) {
                
                // If the rotation training task is abnormal, the next execution time of the task will be punished
                LOGGER.error("longPolling error : ", e);
                executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
            }
        }
    }

上述代码并不是特别复杂,无非就是根据taskId对cacheMap进行数据分割,在比较本地配置文件的数据是否存在变更,如果有变更则直接触发通知。这里需要注意的是,在${user}\nacos\config\目录下回缓存一份服务端的配置信息,checkLocalConfig回和本地此案盘中的文件内容进行比较,如果内存中的数据和磁盘中的数据不一致说明数据发生了变更,需要触发事件通知。

接着调用checkUpdateDataIds方法,基于长连接方法来监听服务端配置的变化,最后根据变化数据的key去服务端获取最新数据。checkUpdateDataIds最终会调用checkUpdateConfigStr方法,所以我们重点关注该方法。

    List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws Exception {
        
        Map<String, String> params = new HashMap<String, String>(2);
        params.put(Constants.PROBE_MODIFY_REQUEST, probeUpdateString);
        Map<String, String> headers = new HashMap<String, String>(2);
        headers.put("Long-Pulling-Timeout", "" + timeout);
        
        // told server do not hang me up if new initializing cacheData added in
        if (isInitializingCacheList) {
            headers.put("Long-Pulling-Timeout-No-Hangup", "true");
        }
        
        if (StringUtils.isBlank(probeUpdateString)) {
            return Collections.emptyList();
        }
        
        try {
            // In order to prevent the server from handling the delay of the client's long task,
            // increase the client's read timeout to avoid this problem.
            
            long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
            HttpRestResult<String> result = agent
                    .httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params, agent.getEncode(),
                            readTimeoutMs);
            
            if (result.ok()) {
                setHealthServer(true);
                return parseUpdateDataIdResponse(result.getData());
            } else {
                setHealthServer(false);
                LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", agent.getName(),
                        result.getCode());
            }
        } catch (Exception e) {
            setHealthServer(false);
            LOGGER.error("[" + agent.getName() + "] [check-update] get changed dataId exception", e);
            throw e;
        }
        return Collections.emptyList();
    }

checkUpdateConfigStr方法实际上是通过agent.httpPost调用/listener接口实现长轮询请求。长轮询请求在实际层面只是设置了一个比较长的超时时间,默认是30s。如果服务端的数据发生了变更,客户端或收到一个HttpResult,服务端返回的是存在数据变更的Data ID、Group、Tenant。获得浙西信息之后,在LongPollingRunnable.run方法中调用getServerConfig去Nacos服务器上读取具体的配置内容。

    public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout)
            throws NacosException {
        String[] ct = new String[2];
        if (StringUtils.isBlank(group)) {
            group = Constants.DEFAULT_GROUP;
        }
        
        HttpRestResult<String> result = null;
        try {
            Map<String, String> params = new HashMap<String, String>(3);
            if (StringUtils.isBlank(tenant)) {
                params.put("dataId", dataId);
                params.put("group", group);
            } else {
                params.put("dataId", dataId);
                params.put("group", group);
                params.put("tenant", tenant);
            }
            result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, 
                                   null, params, agent.getEncode(), readTimeout);
		....			
    }

9.5.6 服务端长轮询处理机制

\

前面分析了客户端是如何监听服务端的数据的,那么服务端是如何实现的呢?,找到Nacos源码中nacos-config模块,在controller包中专门提供了一个ConfigController类来实现配置的基本操作,其中有一个/listener接口,它是客户端发起数据监听的接口。

  • 获取客户端需要监听的可能发生变化的配置,并计算MD5值。
  • inner.doPollingConig开始执行长轮询请求。
    @PostMapping("/listener")
    @Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
    public void listener(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
        String probeModify = request.getParameter("Listening-Configs");
        if (StringUtils.isBlank(probeModify)) {
            throw new IllegalArgumentException("invalid probeModify");
        }
        
        probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);
        
        Map<String, String> clientMd5Map;
        try {
            clientMd5Map = MD5Util.getClientMd5Map(probeModify);
        } catch (Throwable e) {
            throw new IllegalArgumentException("invalid probeModify");
        }
        
        // do long-polling
        inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
    }

doPollingConfig是一个长轮询的处理接口,部分代码如下。

    public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
            Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {
        
        // 长轮询
        if (LongPollingService.isSupportLongPolling(request)) {
            longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
            return HttpServletResponse.SC_OK + "";
        }
        
        // else兼容短轮询逻辑
        List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);
        
        // 兼容短轮询结果
        String oldResult = MD5Util.compareMd5OldResult(changedGroups);
        String newResult = MD5Util.compareMd5ResultString(changedGroups);
        
        ...
    }

上述代码中,首先会判断当前请求是否为长轮询,如果是,则调用addLongPollingClient。

  • 获取客户端请求的超时时间,减去500ms后赋值给timeout变量。
  • 判断isFixedPolling,如果为true,定时任务将会在30s后开始执行,否则,在29.5s后开始执行。
  • 和服务端的数据进行MD5对比,如果发生过变化,则直接返回。
  • ConfigExecutor.executeLongPolling执行ClientLongPolling线程。
    public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
            int probeRequestSize) {
        //获取客户端请求超时时间
        String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
        String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
        String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
        String tag = req.getHeader("Vipserver-Tag");
        int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
        
        //提前500ms返回响应,以避免客户端超时 
        long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
        if (isFixedPolling()) {
            timeout = Math.max(10000, getFixedPollingInterval());
            // Do nothing but set fix polling timeout.
        } else {
            long start = System.currentTimeMillis();
            List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
            if (changedGroups.size() > 0) {
                generateResponse(req, rsp, changedGroups);
                LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "instant",
                        RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                        changedGroups.size());
                return;
            } else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
                LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
                        RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                        changedGroups.size());
                return;
            }
        }
        String ip = RequestUtil.getRemoteIp(req);
        
        // Must be called by http thread, or send response.
        final AsyncContext asyncContext = req.startAsync();
        
        // AsyncContext.setTimeout() is incorrect, Control by oneself
        asyncContext.setTimeout(0L);
        
        ConfigExecutor.executeLongPolling(
                new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
    }

从addLongPollingClient方法中可以看到,他的主要作用是把客户端的长轮询请求封装成ClientLongPolling交给scheduler执行。

9.5.7 ClientLongPolling

\

ClientLongPolling是一个线程,其run方法代码如下。

  • 通过ConfigExecutor.scheduleLongPolling启动一个定时任务并且延时时间为29.5s。
  • 将ClientLongPolling实例本身添加到allSubs队列中,它主要维护一个长轮询的订阅关系。
  • 定时任务执行后,先把ClientLongPolling实例本身从allSubs队列中移除。
  • 通过MD5比较客户端请求的groupKeys是否发生了变更,并将变更的结果通过response返回给客户端。
    class ClientLongPolling implements Runnable {
        
        @Override
        public void run() {
            //启动定时任务
            asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {
                @Override
                public void run() {
                    try {
                        getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
                        
                        //删除订阅关系
                        allSubs.remove(ClientLongPolling.this);
                        
                        if (isFixedPolling()) { //比较数据的MD5值判断是否发生了变更
                            LogUtil.CLIENT_LOG
                                    .info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "fix",
                                            RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
                                            "polling", clientMd5Map.size(), probeRequestSize);
                            List<String> changedGroups = MD5Util
                                    .compareMd5((HttpServletRequest) asyncContext.getRequest(),
                                            (HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
                            if (changedGroups.size() > 0) {
                                sendResponse(changedGroups); //返回结果
                            } else {
                                sendResponse(null);
                            }
                        } else {
                            LogUtil.CLIENT_LOG
                                    .info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "timeout",
                                            RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
                                            "polling", clientMd5Map.size(), probeRequestSize);
                            sendResponse(null);
                        }
                    } catch (Throwable t) {
                        LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());
                    }
                    
                }
                
            }, timeoutTime, TimeUnit.MILLISECONDS);
            
            allSubs.add(this);
        }
    }

从上述这段代码的实现来看,所谓的长轮询就是服务端收到请求之后,不立即返回,而是延迟(30-0.5)s才把请求结果返回给客户端,这就使得客户端和服务端之间在30s之内数据没有发生变化的情况下一直处于连接状态。

最后一个问题,当我们通过控制台或者API的方式修改了配置之后,如何实时通知呢?目前看来,定时任务是延后29.5s执行的,并没有达到实时的目的

在LongPollingService构造函数中,注册了一个Subscriber,可以看到一个LocalDataChangeEvent事件

    @SuppressWarnings("PMD.ThreadPoolCreationRule")
    public LongPollingService() {
        allSubs = new ConcurrentLinkedQueue<ClientLongPolling>();
        
        ConfigExecutor.scheduleLongPolling(new StatTask(), 0L, 10L, TimeUnit.SECONDS);
        
        // Register LocalDataChangeEvent to NotifyCenter.
        NotifyCenter.registerToPublisher(LocalDataChangeEvent.class, NotifyCenter.ringBufferSize);
        
        // Register A Subscriber to subscribe LocalDataChangeEvent.
        NotifyCenter.registerSubscriber(new Subscriber() {
            
            @Override
            public void onEvent(Event event) {
                if (isFixedPolling()) {
                    // Ignore.
                } else {
                    if (event instanceof LocalDataChangeEvent) {
                        LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
                        ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
                    }
                }
            }
            
            @Override
            public Class<? extends Event> subscribeType() {
                return LocalDataChangeEvent.class;
            }
        });
        
    }

这个事件就是在服务端的配置数据发生变化是发布的一个事件,我们暂且不关注它在哪里发布的,先来一下收到事件后的处理行为。

当onEvent监听到事件后,通过线程池来执行一个DataChangeTask任务,具体执行逻辑如下。

  • 遍历allSubs中的客户端长轮询请求。
  • 比较每一个客户端长轮询请求携带的groupKey,如果服务端变更的配置和客户端请求关注的配置一致,则直接返回。

\

    class DataChangeTask implements Runnable {
        
        @Override
        public void run() {
            try {
                ConfigCacheService.getContentBetaMd5(groupKey);
                for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
                    ClientLongPolling clientSub = iter.next();
                    if (clientSub.clientMd5Map.containsKey(groupKey)) {
                        // If published tag is not in the beta list, then it skipped.
                        if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {
                            continue;
                        }
                        
                        // If published tag is not in the tag list, then it skipped.
                        if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
                            continue;
                        }
                        
                        getRetainIps().put(clientSub.ip, System.currentTimeMillis());
                        iter.remove(); // Delete subscribers' relationships.
                        LogUtil.CLIENT_LOG
                                .info("{}|{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - changeTime), "in-advance",
                                        RequestUtil
                                                .getRemoteIp((HttpServletRequest) clientSub.asyncContext.getRequest()),
                                        "polling", clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
                        clientSub.sendResponse(Arrays.asList(groupKey));
                    }
                }
            } catch (Throwable t) {
                LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));
            }
        }