微服务实战(五) Nacos架构源码分析---配置中心

深渊向深渊呼唤

 

本章节主要内容:Nacos的架构介绍、配置中心源码初步分析

nacos官网 https://nacos.io/

nacos源码 https://github.com/alibaba/Nacos

我们已经在前面章节启动了Nacos,并且作为了 服务注册发现中心 和 配置中心,作为微服务开发者,其实很有必要去了解下微服务这些核心组件的架构、实现,虽然在实际开发中这些组件都是拿来即用的,但是这些组件里面涵盖了很多优秀的分布式架构思想,如果能边用边深入了解,相信随着积累,架构思想会潜移默化地影响你,这也是开发人员差距之所以这么大的原因。

Nacos架构图

微服务实战(五) Nacos架构源码分析---配置中心

微服务实战(五) Nacos架构源码分析---配置中心

Nacos工程结构

首先要做的当然是下载Nacos源码咯,maven构建完成后的工程目录如下:

微服务实战(五) Nacos架构源码分析---配置中心

 

Nacos配置中心

配置中心在微服务架构中主要负责各个微应用自有或者公有的配置信息管理,包括配置信息的添加编辑、读取、变化后的通知等。Nacos还提供了配置信息的版本控制、监听管理等增强功能。下面一起去看一下Nacos配置中心的主要功能吧。

主要功能点

微服务实战(五) Nacos架构源码分析---配置中心

咱们暂且就根据功能图一步步去了解具体功能都是在哪儿怎么实现的吧,一篇文章肯定也不可能逐行把代码分析清楚,这里我就主要流程上的关键节点简单分析一下,其实最重要的是掌握阅读源码的方法,先有个提纲,把流程实现弄明白,具体细节先知道有这么回事就行了,等流程清晰了,每个细节去深入分析也不迟。

【配置CRUD】

配置中心的配置发布以及获取功能我们在之前章节已经演示过了,下面我们打开nacos-config工程,直接进入到 核心功能的控制器中,可以看到该控制器中的接口对应着我们之前用过的几个功能。

配置CRUD控制器的位置:/nacos-config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigController.java

(源码本身有一丢丢中文注释,不愧是国货啊)

微服务实战(五) Nacos架构源码分析---配置中心

我们就抱着试一试的态度简单看一下配置发布这个功能吧,其余的咱也不花太多时间看了,关于看代码这件事嘛,我一直觉得最重要的是要把整体的流程看明白,至于每一步里的一些细节,看个大概(闲得蛋疼的时候可以去看看具体咋实现)。

功能点1:增加或更新配置数据     

方法入口< publishConfig>    /nacos-config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigController.java

重点看这个调用,前面那些花里胡哨的参数校验,参数的处理就先跳过啦!

persistService.insertOrUpdate(srcIp, srcUser, configInfo, time, configAdvanceInfo, false);

数据源的选择

通过多年的临床经验,一眼就能看出这是将提交的配置信息持久化。

看到这儿或许有点懵吧,想起前面咱们的部署过程,并没有去安装啥数据库吧!难道是放到内存中了吗?那还能持久吗?

顺藤摸瓜找到 PersistService--->init()--->dynamicDataSource.getDataSource()

 public DataSourceService getDataSource() {
        DataSourceService dataSourceService = null;

        if (STANDALONE_MODE && !propertyUtil.isStandaloneUseMysql()) {
            dataSourceService = (DataSourceService)applicationContext.getBean("localDataSourceService");
        } else {
            dataSourceService = (DataSourceService)applicationContext.getBean("basicDataSourceService");
        }

        return dataSourceService;
    }

又一眼看出,如果咱们启动模式是 单机模式,就是使用的 localDataSourceService ,而集群模式启动的话,就是用的 basicDataSourceService。  

嗯,单机模式用的是derby数据库,而集群模式的话就需要使用mysql咯(单机模式也可以强制配置使用mysql)。

直接打开 /nacos-config/src/main/java/com/alibaba/nacos/config/server/service/LocalDataSourceServiceImpl.java

private static final String JDBC_DRIVER_NAME = "org.apache.derby.jdbc.EmbeddedDriver";

在内部会执行初始化脚本,建立好数据表,derby的初始化脚本: META-INF/schema.sql。

  @Override
    public void reload() {
        DataSource ds = jt.getDataSource();
        if (ds == null) {
            throw new RuntimeException("datasource is null");
        }
        try {
            execute(ds.getConnection(), "META-INF/schema.sql");
        } catch (Exception e) {
            if (logger.isErrorEnabled()) {
                logger.error(e.getMessage(), e);
            }
            throw new RuntimeException("load schema.sql error." + e);
        }
    }

 

而打开 /nacos-config/src/main/java/com/alibaba/nacos/config/server/service/BasicDataSourceServiceImpl.java

 private static final String DEFAULT_MYSQL_DRIVER = "com.mysql.jdbc.Driver";

mysql的初始化脚本:/nacos-config/src/main/resources/META-INF/nacos-db.sql

 

最终的持久化

位置:/nacos-config/src/main/java/com/alibaba/nacos/config/server/service/PersistService.java

public void addConfigInfo(final String srcIp, final String srcUser, final ConfigInfo configInfo,
                              final Timestamp time, final Map<String, Object> configAdvanceInfo, final boolean notify) {
        tjt.execute(new TransactionCallback<Boolean>() {
            @Override
            public Boolean doInTransaction(TransactionStatus status) {
                try {
                    long configId = addConfigInfoAtomic(srcIp, srcUser, configInfo, time, configAdvanceInfo);
                    String configTags = configAdvanceInfo == null ? null : (String) configAdvanceInfo.get("config_tags");
                    addConfiTagsRelationAtomic(configId, configTags, configInfo.getDataId(), configInfo.getGroup(),
                        configInfo.getTenant());
                    insertConfigHistoryAtomic(0, configInfo, srcIp, srcUser, time, "I");
                    if (notify) {
                        EventDispatcher.fireEvent(
                            new ConfigDataChangeEvent(false, configInfo.getDataId(), configInfo.getGroup(),
                                configInfo.getTenant(), time.getTime()));
                    }
                } catch (CannotGetJdbcConnectionException e) {
                    fatalLog.error("[db-error] " + e.toString(), e);
                    throw e;
                }
                return Boolean.TRUE;
            }
        });
    }

方法里面是使用了编程式事务(TransactionTemplate),首先调用 addConfigInfoAtomic 向数据库添加了一条配置信息,然后 addConfiTagsRelationAtomic 添加关系表数据(config和tag之间的关系表),insertConfigHistoryAtomic 添加配置历史的记录 。然后判断如果是需要通知,再向 EventDispatcher 添加一条 配置变更的 事件。

 

 

功能点2:获取配置 

方法入口  <doGetConfig>  /nacos-config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigServletInner.java

该读取方法是同步的,调用前会先加锁(加锁是在groupKey内部,即在使用同一个group的配置,在读取时会加锁)

int lockResult = tryConfigReadLock(groupKey);

在顺利进入到同步区域之后,会读取数据库(derby/mysql)获取对应的配置数据。

configInfoBase = persistService.findConfigInfo(dataId, group, tenant);

读取的实现如下:

public ConfigInfo findConfigInfo(final String dataId, final String group, final String tenant) {
        final String tenantTmp = StringUtils.isBlank(tenant) ? StringUtils.EMPTY : tenant;
        try {
            return this.jt.queryForObject(
                "SELECT ID,data_id,group_id,tenant_id,app_name,content,md5,type FROM config_info WHERE data_,
                new Object[] {dataId, group, tenantTmp}, CONFIG_INFO_ROW_MAPPER);
        } catch (EmptyResultDataAccessException e) { // 表明数据不存在, 返回null
            return null;
        } catch (CannotGetJdbcConnectionException e) {
            fatalLog.error("[db-error] " + e.toString(), e);
            throw e;
        }
    }

【版本管理】

版本管理模块主要是将配置信息的变更过程全部以历史表的形式记录下来。

/nacos-config/src/main/java/com/alibaba/nacos/config/server/controller/HistoryController.java



/**
 * 管理控制器。
 *
 * @author Nacos
 */
@RestController
@RequestMapping(Constants.HISTORY_CONTROLLER_PATH)
public class HistoryController {

    @Autowired
    protected PersistService persistService;

    @GetMapping(params = "search=accurate")
    public Page<ConfigHistoryInfo> listConfigHistory(@RequestParam("dataId") String dataId, //
                                                     @RequestParam("group") String group, //
                                                     @RequestParam(value = "tenant", required = false,
                                                         defaultValue = StringUtils.EMPTY) String tenant,
                                                     @RequestParam(value = "appName", required = false) String appName,
                                                     @RequestParam(value = "pageNo", required = false) Integer pageNo,
                                                     //
                                                     @RequestParam(value = "pageSize", required = false)
                                                         Integer pageSize, //
                                                     ModelMap modelMap) {
        pageNo = null == pageNo ? 1 : pageNo;
        pageSize = null == pageSize ? 100 : pageSize;
        pageSize = Math.min(500,pageSize);
        // configInfoBase没有appName字段
        return persistService.findConfigHistory(dataId, group, tenant, pageNo, pageSize);
    }

    /**
     * 查看配置历史信息详情
     */
    @GetMapping
    public ConfigHistoryInfo getConfigHistoryInfo(HttpServletRequest request, HttpServletResponse response,
                                                  @RequestParam("nid") Long nid, ModelMap modelMap) {
        return persistService.detailConfigHistory(nid);
    }

}

可以用接口或者界面的形式查看和对比配置信息经过了哪些变化。

而历史表的数据插入是在调用 PersistService.addConfigInfo 或者 PersistService.updateConfigInfo  去更新配置信息时内部执行的。

 private void insertConfigHistoryAtomic(long id, ConfigInfo configInfo, String srcIp, String srcUser,
                                           final Timestamp time, String ops) {
        String appNameTmp = StringUtils.isBlank(configInfo.getAppName()) ? StringUtils.EMPTY : configInfo.getAppName();
        String tenantTmp = StringUtils.isBlank(configInfo.getTenant()) ? StringUtils.EMPTY : configInfo.getTenant();
        final String md5Tmp = MD5.getInstance().getMD5String(configInfo.getContent());
        try {
            jt.update(
                "INSERT INTO his_config_info (id,data_id,group_id,tenant_id,app_name,content,md5,src_ip,src_user,gmt_modified,op_type) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
                id, configInfo.getDataId(), configInfo.getGroup(), tenantTmp, appNameTmp, configInfo.getContent(),
                md5Tmp, srcIp, srcUser, time, ops);
        } catch (DataAccessException e) {
            fatalLog.error("[db-error] " + e.toString(), e);
            throw e;
        }
    }

【灰度发布】

灰度发布主要是在配置信息发布或者更新时,考虑到对系统的影响,所以先只针对一部分用户进行生效,等待实际测试确认后,再正式发布。

Nacos里面主要是通过IP名单来实现的。

ConfigController.publishConfig

String betaIps = request.getHeader("betaIps");
        ConfigInfo configInfo = new ConfigInfo(dataId, group, tenant, appName, content);
        if (StringUtils.isBlank(betaIps)) {
            if (StringUtils.isBlank(tag)) {
                persistService.insertOrUpdate(srcIp, srcUser, configInfo, time, configAdvanceInfo, false);
                EventDispatcher.fireEvent(new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));
            } else {
                persistService.insertOrUpdateTag(configInfo, tag, srcIp, srcUser, time, false);
                EventDispatcher.fireEvent(new ConfigDataChangeEvent(false, dataId, group, tenant, tag, time.getTime()));
            }
        } else { // beta publish
            persistService.insertOrUpdateBeta(configInfo, betaIps, srcIp, srcUser, time, false);
            EventDispatcher.fireEvent(new ConfigDataChangeEvent(true, dataId, group, tenant, time.getTime()));
        }

可以看到,如果发布时,提交了 betaIps 的话,会进入到 下面这个代码分支,而在  persistService.insertOrUpdateBeta 方法中,数据会被插入到 config_info_beta 这个表中;如果非灰度模式,数据被插入到  config_info 表中。

而在ConfigServletInner.doGetConfig 中会根据 ip 判断,如果是在beta灰度列表中的,则会变成灰度读取模式。

CacheItem cacheItem = ConfigService.getContentCache(groupKey);
                if (cacheItem != null) {
                    if (cacheItem.isBeta()) {
                        if (cacheItem.getIps4Beta().contains(clientIp)) {
                            isBeta = true;
                        }
                    }
                    String configType = cacheItem.getType();
                    response.setHeader("Config-Type", (null != configType) ? configType : "text");
                }

具体读取的时候,如果是beta灰度模式,则会调用 persistService.findConfigInfo4Beta 从 config_info_beta 表中读取数据。

 if (isBeta) {
                    md5 = cacheItem.getMd54Beta();
                    lastModified = cacheItem.getLastModifiedTs4Beta();
                    if (STANDALONE_MODE && !PropertyUtil.isStandaloneUseMysql()) {
                        configInfoBase = persistService.findConfigInfo4Beta(dataId, group, tenant);
                    } else {
                        file = DiskUtil.targetBetaFile(dataId, group, tenant);
                    }
                    response.setHeader("isBeta", "true");
                }

【监听管理】

配置中心的监听任务主要是在 /nacos-config/src/main/java/com/alibaba/nacos/config/server/monitor 包里面实现的。

在当前版本中,主要是开启了这三个任务。

1、内存信息监听任务

2、请求响应时间监听任务

3、待推送队列的监听任务

public MemoryMonitor(AsyncNotifyService notifySingleService) {

        TimerTaskService.scheduleWithFixedDelay(new PrintMemoryTask(), DELAY_SECONDS,
            DELAY_SECONDS, TimeUnit.SECONDS);

        TimerTaskService.scheduleWithFixedDelay(new PrintGetConfigResponeTask(), DELAY_SECONDS,
            DELAY_SECONDS, TimeUnit.SECONDS);

        TimerTaskService.scheduleWithFixedDelay(new NotifyTaskQueueMonitorTask(notifySingleService), DELAY_SECONDS,
            DELAY_SECONDS, TimeUnit.SECONDS);

    }

【推送轨迹】

主要是以切面的方式,记录了所有配置信息的推送轨迹。

/nacos-config/src/main/java/com/alibaba/nacos/config/server/aspect/RequestLogAspect.java

 

 

 

本地运行nacos-config

了解了nacos-config的两个关键功能大概实现之后,我们本地把这个工程运行起来吧,之前是直接用的nacos的部署包部署的,这次我们在开发环境单独运行nacos-config配置中心

这里面还是有比较多的坑的,

本地运行的话,改成这样的模式, 单机+mysql。

首先需要在nacos-config中加上SpringBoot的配置文件,(对了,mysql的建表脚本需要自己在数据库里初始化一下哦)

微服务实战(五) Nacos架构源码分析---配置中心

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
db.user=nacos
db.password=123123

 然后呢,Config启动类需要修改下 @SpringBootApplication 注解 ,改成跟我一样吧,去掉spring security 的验证,因为验证功能是在集成了控制台才会需要的。这个比较坑,一旦引入了security的jar包,默认就开启了验证,这样启动后,所有的接口都是401 无法访问。(别问我,我一下午翻遍了整个代码来解决这个问题,后来百度了一下发现是这个原因)

@EnableScheduling
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class, 
        ManagementWebSecurityAutoConfiguration.class})
public class Config {

    public static void main(String[] args) {
    	
    	System.setProperty(STANDALONE_MODE_PROPERTY_NAME, "true");
    	
    	
        SpringApplication.run(Config.class, args);
    }
}

启动了之后就验证一把呗。

先调用这个接口来添加一个配置

POST "http://127.0.0.1:8848/nacos/v1/cs/configs?data

微服务实战(五) Nacos架构源码分析---配置中心

 

然后再调用获取接口,把刚刚添加的配置获取出来

GET "http://127.0.0.1:8848/nacos/v1/cs/configs?data

微服务实战(五) Nacos架构源码分析---配置中心

 

ok了,点到为止吧!  关于服务注册中心的源码我后面再补充吧。

栏目