当前位置: 首页 > 科技观察

做一个简单的配置中心,顺便集成到SpringCloud中

时间:2023-03-14 15:05:27 科技观察

大家好,我是三友~~最近突然突发奇想(正好闲着),想做一个简单的配置中心,顺便说一下,我也是照葫芦画瓢集成到SpringCloud上的。配置中心概述随着历史车轮的不断前行和技术的不断进步,单体架构的系统正在逐渐转向微服务架构。虽然微服务架构有很多优势,但是随着服务实例越来越多,配置也越来越多,传统的配置文件方式已经不能继续适用于业务开发,因此迫切需要一个可以管理配置的应用文件以统一的方式。在这个配置中心下诞生了。因此,配置中心是一个用来统一管理各种服务配置的组件,本质上是一个Web应用。配置中心的核心功能一个配置中心的核心功能其实包括两个主要功能:配置访问,配置变更通知,配置访问是配置中心不可或缺的功能,配置中心需要能够保存配置,而且磁盘文件也存在嘛,是不是数据库无所谓。总之,需要持久化,配置中心也要提供配置查询功能。配置变更通知也是一个很重要的功能。一旦配置中心的配置发生变化,使用该配置的客户端需要知道配置发生了变化,才能进行相应的更改。手摇一个简单的配置中心上面分析了一个配置中心的核心功能,接下来将实现这两??个核心功能。一、文件工程整体分析文件工程整体分为客户端和服务端。Server:单独部署的web应用,端口为8888,提供http接口,用于增删改查配置。客户端(SDK):业务系统需要引用相应的依赖包封装与服务端交互的代码。二、服务端实现详解1、配置文件ConfigFile的数据存储模型在配置中心存储配置时,需要指定如下信息publicclassConfigFile{privateStringfileId;私有字符串名称;私有字符串扩展;私有字符串内容;privateLonglastUpdateTimestamp;}fileId:文件的唯一id,由配置中心服务器在新增配置文件时自动生成。extension:文件扩展名,指的是要配置的文件类型,如properties、yml等。content:配置文件的内容,不同的扩展名有不同的格式要求。lastUpdateTimestamp:最后一次文件更新的时间戳。当文件存储或更新时,需要更新时间戳。该字段用于确定文件是否已更改。2.文件存储层ConfigFileStorage对于文件存储层,我提供了一个ConfigFileStorage接口,publicinterfaceConfigFileStorage{voidsave(ConfigFileconfigFile);无效更新(ConfigFileconfigFile);voiddelete(StringfileId);配置文件selectByFileId(StringfileId);ListselectAll();}这个接口提供了配置存储的crud操作,目前我已经实现了基于内存存储磁盘文件的代码可以在项目启动的时候在配置文件中指定,是否基于磁盘文件存储或内存存储,默认基于磁盘文件存储。当然,如果要将配置信息存储到数据库中,只需要添加一个storingtodata的实现即可。3.ConfigControllerConfigController为配置文件提供crudhttp接口。ConfigController通过调用ConfigManager完成配置文件的crud。ConfigManager其实是一个服务层,就是简单的参数封装。最后调用ConfigFileStorage存储层的实现完成配置。存储功能。这样就实现了配置中心的配置接入功能。所以服务器端比较简单。其实和平时写的业务系统的crud没什么区别,就是把数据库存储换成了磁盘文件的存储。至于上面提到的配置文件变化通知功能,我是基于客户端实现的。三、客户端实现客户端工程代码如下1、ConfigFileChangedListenerConfigFileChangedListener是配置变化的监听器。当客户端监听某个配置时,如果配置的内容发生变化,客户端会回调监听器,并传入最新的配置2、ConfigService封装了客户端的核心功能,可以在一个文件中添加监听器和获取文件的配置内容。使用示例://创建一个ConfigService,传入配置中心服务器的地址ConfigServiceconfigService=newConfigService("localhost:8888");//从服务器获取配置文件的内容,以及配置中心的id添加配置文件时自动添加文件GenerateConfigFileconfig=configService.getConfig("69af6110-31e4-4cb4-8c03-8687cf012b77");//监听一个配置文件configService.addListener("69af6110-31e4-4cb4-8c03-8687cf012b77",newConfigFileChangedListener(){@OverridepublicvoidonFileChanged(ConfigFileconfigFile){System.out.printf("fileId=%s配置文件已更改,最新内容为:%s%n",configFile.getFileId(),configFile.getContent());}});下面介绍配置变更通知的实现原理。首先,对于客户端来说,如果想知道哪个配置文件发生了变化,有两种方式。第一种是通过推送。当配置文件发生变化时,服务端主动将变化后的配置文件推送给客户端。这种方法实现起来比较麻烦。一方面,服务端要保存客户端的服务信息,因为服务端需要知道推送到哪个服务器;另一方面,客户端需要提供一个接口来接收来自服务器的推送。request,所以这个方法整体实现起来比较麻烦。但是,这种推送方式更加实时。一旦配置文件发生变化,客户端可以第一时间知道配置的变化。第二种方式是基于pull模式来实现。客户端每隔一段时间主动从服务端拉取配置文件,判断文件内容是否有变化,一旦有变化,就会回调监听器。这种实现方式比push简单很多,因为服务端不需要关心客户端的信息,所有的操作都由客户端来完成。但是,这个时间间隔不容易控制。如果太长,可能会导致实时性差。如果太短,可能会导致无效请求过多,因为配置可能根本不会改变。但是这里我选择了第二种方式,因为实现起来简单。.到这里就实现了变更通知代码,一个简单的配置中心服务端和客户端就完成了。这里用一张图来概括一下配置中心的核心原理。接下来将这个简单的配置中心集成到SpringCloud中。SpringCloud配置中心原理1.项目如何启动从配置中心加载数据?在SpringCloud环境下,项目启动时,会先创建一个容器,然后再创建SpringBoot应用容器。这个容器非常重要。该容器用于与配置中心交互,拉取配置。这个容器在启动的时候会做两件事:加载bootstrap配置文件,这也是为什么需要在bootstrap配置文件中写入配置中心的配置信息。加载所有spring.factories文件的关键是org.springframework.cloud。bootstrap.BootstrapConfiguration对应的配置类,将这些配置类注入到这个容器中,注意这里不会加载@EnbaleAutoConfiguration自动组装的类,当这两件事做完后,所有的PropertySourceLocators都会从这个容器中获取this的实现类对象接口依次调用locate方法。PropertySourceLocator类非常重要。首先,让我们看一下为环境定位(可能是远程)属性源的注释策略。实现不应该失败,除非它们打算阻止应用程序启动。丢进有道翻译如下:Locatingfortheenvironment(probablyisthestrategyfortheremote)propertysource。实现不应该失败,除非它们打算阻止应用程序启动。简单来说就是用来定位(也就是获取)项目启动所需的属性信息。同时需要注意的是,括号中可能远程告诉了我们一个非常重要的信息,即获取的配置信息不仅可以存在于本地,还可以存在于远程。远程?笔者这里就不直接说了,可以从配置中心获取。.所以从这个注释中我们可以发现,原来PropertySourceLocator是在SpringCloud环境中从配置中心获取配置的。PropertySourceLocator是一个接口,所以只要不同的配置中心都实现了这个接口,那么就可以将不同的配置中心集成到SpringCloud中,从而可以将配置属性从配置中心加载到Spring环境中。2、注入Bean的属性如何实现动态刷新?上面介绍了SpringCloud在项目启动时如何从配置中心加载数据,主要是新建一个容器,加载bootstrap配置文件和一些配置类,最后调用PropertySourceLocator从配置中心获取配置信息。那么在SpringCloud环境下,如何实现注入Bean的属性的动态刷新呢?比如在UserService类中添加@RefreshScope注解后,当配置中心sanyou.username属性发生变化时,此时注入的用户名也会随之变化。这种变化是如何发生的?SpringCloud规定一旦配置中心客户端感知到服务端某个配置发生变化,需要发布一个RefreshEvent事件告诉SpringCloud配置发生了变化。在SpringCloud中,RefreshEventListener类会监听这个事件。一旦侦听此事件,它将执行两个步骤来刷新注入到对象中的属性。RefreshEventListener再次从配置中心拉取属性值,本次拉取的代码逻辑与项目启动时拉取属性值的核心逻辑几乎一致。它还新建了一个spring容器,加载配置文件和配置类,最后通过PropertySourceLocator获取属性,复用了这部分的核心代码逻辑。获得最新属性后,开始刷新对象的属性。刷新逻辑实现的非常巧妙。并不是像你想的那样简单的给对象重新注入新的属性,而是通过动态代理来实现的。对于类上带有@RefreshScope注解的Bean,Spring会在生成Bean时进行动态代理。这里我们就分析上面的UserService例子。生成UserService分两步生成一个UserService对象,将从配置中心拉取的配置sanyou.username注入到UserService对象中。由于添加了@RefreshScope,所以会在上一步生成。UserService对象作为代理生成一个代理对象,实际上是暴露给我们使用的。如图所示,由于暴露的代理对象是一个代理对象,所以在调用getUsername方法的时候,实际上是在调用UserService,代理对象的getUsername方法会找到UserService,调用UserService的getUsername获取属性用户名的价值。当配置中心的配置有changerefresh属性时,Spring会销毁UserService对象(非代理对象),重新创建一个UserService对象,注入最新的属性值。当再次通过UserService代理对象获取username属性时,会找到新创建的UserService对象,此时可以获取到最新的属性值。每次刷新配置,UserService对象都会被销毁,然后重新创建,但是暴露出来的UserService代理对象永远不会改变。这样,对于用户来说,看似UserService对象的属性自动刷新了,但实际上UserService代理对象最终找到的UserService对象发生了变化。至此,你应该知道为什么加了@RefreshScope的对象可以实现配置的自动刷新了。其实是靠动态代理来完成的。3、源码执行流程图由于上面没有涉及整体执行过程的源码分析,所以我特地结合源码画了两张源码执行流程图。有兴趣的朋友可以照着图翻看具体的源码。3.1启动时加载配置过程最终从配置中心获取的属性会放在项目启动时创建的Environment对象中。3.2ConfigurationRefreshSourceCodeProcess本图对添加@ConfigurationProperties数据绑定的对象原理进行分析。集成SpringCloud并测试1.集成SpringCloud1的配置信息和ConfigCenterProperties配置中心。这里需要配置配置中心服务器的地址和使用的配置文件的id。当然,这部分信息是需要写在bootstrap配置文件中的,具体原因前面已经讲过了。2、上面对ConfigCenterPropertySourceLocator的分析可以看出,在项目启动刷新时,SpringCloud是通过PropertySourceLocator的实现从配置中心加载配置信息的,所以这里的核心逻辑是根据配置的id从配置中心拉取文件配置信息,然后解析配置。3.ConfigContextRefresher这个用来注册文件变化的监听器,用来刷新文件信息。上面提到,当配置发生变化时,需要发布一个RefreshEvent事件来触发刷新配置的功能。核心逻辑是为项目启动时使用的配置文件注册一个监听器。侦听器的实现是在发生配置更改时发布RefreshEvent事件。4、两个配置类4.1ConfigCenterBootstrapConfiguration配置ConfigCenterPropertySourceLocator、ConfigCenterProperties、ConfigService4.2ConfigCenterAutoConfiguration配置ConfigContextRefresher、ConfigCenterProperties、ConfigService。最后需要在spring.factories中配置这两个配置类。这里需要注意一件事。前面提到,SpringCloud会新建一个容器来加载配置,而这个容器只会加载spring.factories文件中key为@BootstrapConfiguration注解的配置类,所以需要将ConfigCenterBootstrapConfiguration和BootstrapConfiguration配对,因为ConfigCenterBootstrapConfigurationConfigCenterPropertySourceLocator是配置。好了,SpringCloud的集成到这里就真正完成了。二、测试1、添加新的配置文件启动配置中心的server端,然后打开ApiPost,添加新的配置文件,添加新的文件类型作为properties,内容为sanyou.username=sanyoukey-valuepair,当然你可以写很多Key-valuepair,我这里写了一个。添加成功后,返回文件的id:79765c73-c1ef-4ea2-ba77-5d27a64c46852。对于测试客户端,这里为了方便,我把测试代码写在和客户端同一个文件里。一个服务,正常的情况肯定是和SpringCloud代码做一个依赖,引入到项目中。在bootstrap.yml文件中配置配置中心的相关信息。配置中心服务器地址为:localhost:8888刚刚创建的使用的配置文件id:79765c73-c1ef-4ea2-ba77-5d27a64c4685测试controller提供接口并注入提到的UserService启动项目,调用接口从这里的break可以看出,实际注入的是一个UserService代理对象,最后这个时候调用了UserService对象com.sanyou.configcenter.test.UserService@3a1e4fd3返回值为:sanyou接下来测试一下函数自动刷新属性。现在修改配置中心的sanyou.username为sanyou666,静等5秒。.此时控制台打印Refreshkeyschanged:[sanyou.username],即sanyou.username属性发生变化。这时候再获取username,可以看到UserService代理对象没有变化,只是UserService对象变成了com.sanyou。configcenter.test.UserService@4237b3cd此时获取到的用户名变成了sanyou666这样,到这里我们就成功的将我们写的简易版配置中心集成到了SpringCloud中了。不足与改进我们这里的配置中心虽然具备了配置中心的基本功能,但是还有很多不足和需要改进的地方。1、配置变更推送问题前面说到,在判断配置是否发生变更时,每5s从服务器获取一次,可能需要5s才能感知到配置变更,无法达到实时的效果,而且由于这里是client判断的,会导致无效请求过多,因为配置可能根本没有变化,但是还是每5s获取一次配置信息,浪费资源解决这个问题可以换成上面说的也可以在push方式中进行,或者将轮询方式改为长轮询方式。如果对push、polling、longpolling不了解,可以看看RocketMQ的push消费方式。太聪明了。本文。2.高可用性问题。这里只有一个服务器实例。如果不支持集群方式,就会出现单点故障问题,不支持高可用。在实际项目中,需要支持集群模式,保证即使某个服务实例挂掉,整个集群仍然可以继续对外提供服务。比如nacos支持集群模式,你可以自由选择使用AP模式还是CP模式。3、通信协议和序列化协议对于通信协议,这里为了方便,我选择了基于http协议的客户端和服务端的通信方式。当然你也可以自定义协议,或者使用其他协议,比如gRPC协议。事实上,在nacos2.x版本中,nacos开始全面拥抱gRPC协议。至于序列化协议,这里选择了json协议,因为它简单、通用、应用广泛、跨语言。当然你也可以选择其他的,比如hessian序列化协议等等。4、多租户隔离一个合格的配置中心需要能够支持不同应用的隔离,以及同一个应用的不同环境的隔离。这里,好省事,直接用一个文件id来表示,虽然也可以做到隔离(不同的系统使用不同的文件id),但是这种方式比较low。比如nacos会根据配置的名称和后缀名自动生成一个文件id(dataId),也有分组的概念,其实就是为了达到隔离的效果。5.AuthenticationAuthentication是一个系统中比较常见的东西,这里就不赘述了。6.控制页面的所有CRUD配置都是基于ApiPost的,但在实际操作中,必须通过页面来操作。至于这里,要不我自己写一个页面,给大家自己看看吧~~最后,本文代码地址:https://github.com/sanyou3/sanyou-config-center