大家好,我是三友~~
最近突然心血来潮(就是闲的)就想着撸一个简单的配置中心,顺便也照葫芦画瓢给整合到SpringCloud。
本文大纲
配置中心的概述
随着历史的车轮不断的前进,技术不断的进步,单体架构的系统都逐渐转向微服务架构。虽然微服务架构有诸多优点,但是随着越来越多的服务实例的数量,配置的不断增多,传统的配置文件方式不能再继续适用业务的发展,所以急需一种可以统一管理配置文件应用,在此之下配置中心就诞生了。
所以配置中心就是用来统一管理各种服务配置的一个组件,本质上就是一个web应用。
配置中心的核心功能
一个配置中心的核心功能其实主要包括两个:
- 配置的存取
- 配置变更的通知
配置的存取是配置中心不可缺失的功能,配置中心需要能够将配置进行保存,存在磁盘文件也好,又或是数据库也罢,总之需要持久化,同时配置中心也得提供配置查询的功能。
配置变化的通知也是一个很重要的功能,一旦配置中心的配置有变动的话,那么使用到这个配置的客户端就需要知道这个配置有变动,从而可以做到相应的变动的操作。
手撸一个简易的配置中心
上文分析了一个配置中心的核心功能,接下来就实现这两个核心的功能。
文件工程整体分为客户端与服务端
- 服务端:单独部署的一个web应用,端口是8888,提供了对于配置增删改查的http接口
- 客户端(SDK):业务系统需要引用对应的依赖,封装了跟服务端交互的代码
在配置中心存储配置的时候,需要指明以下信息
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ConfigFile</span> </span>{<br><br>    <span class="hljs-keyword">private</span> String fileId;<br><br>    <span class="hljs-keyword">private</span> String name;<br><br>    <span class="hljs-keyword">private</span> String extension;<br><br>    <span class="hljs-keyword">private</span> String content;<br><br>    <span class="hljs-keyword">private</span> Long lastUpdateTimestamp;<br><br>}
- fileId: 文件的唯一id,由配置中心服务端在新增配置文件存储的时候自动生成,全局唯一
- name: 就是文件的名字,没有什么要求,见名知意就行
- extension: 文件后缀名,指的是该配置是什么类型的文件,比如是properties、yml等
- content: 就是配置文件的内容,不同的后缀名有不同的格式要求
- lastUpdateTimestamp: 上一次文件更新的时间戳。当文件存储或者更新的时候,需要更新时间戳,这个字段是用来判断文件是否有改动
对于文件存储层,我提供了一个ConfigFileStorage接口,
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ConfigFileStorage</span> </span>{<br><br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">save</span><span class="hljs-params">(ConfigFile configFile)</span></span>;<br><br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">update</span><span class="hljs-params">(ConfigFile configFile)</span></span>;<br><br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">delete</span><span class="hljs-params">(String fileId)</span></span>;<br><br>    <span class="hljs-function">ConfigFile <span class="hljs-title">selectByFileId</span><span class="hljs-params">(String fileId)</span></span>;<br><br>    <span class="hljs-function">List<configfile> <span class="hljs-title">selectAll</span><span class="hljs-params">()</span></configfile></span>;<br><br>}
这个接口提供了对于配置存储的crud操作,目前我已经实现了基于内存和磁盘文件的存储的代码
可以在项目启动的时候,在配置文件指定是基于磁盘文件存储还是基于内存存储,默认是基于磁盘文件存储。
当然,如果想把配置信息存储到数据库,只要新增一个存储到数据的实现就行。
ConfigController提供了对于配置文件的crud的http接口
ConfigController是通过调用ConfigManager来完成配置文件的crud
其实就是一个service层,就是简单的参数封装,最终是调用ConfigFileStorage存储层的实现来完成配置的存储功能。
这样配置中心的配的存取的功能就实现了。
所以,服务端还是比较简单的。其实就是跟平时写的业务系统的crud没什么区别,就是将数据库存储替换成了磁盘文件的存储。
至于前面说的配置文件变更通知的功能,我是基于客户端来实现的。
客户端工程代码如下
配置变动的监听器,当客户端对某个配置监听的时候,如果这个配置的内容有变化的话,客户端就会回调这个监听器,传入最新的配置
封装了客户端的核心功能,可以添加对某个文件的监听器和获取某个文件的配置内容。
使用示例:
<br>ConfigService configService = <span class="hljs-keyword">new</span> ConfigService(<span class="hljs-string">"localhost:8888"</span>);<br><br><br>ConfigFile config = configService.getConfig(<span class="hljs-string">"69af6110-31e4-4cb4-8c03-8687cf012b77"</span>);<br><br><br>configService.addListener(<span class="hljs-string">"69af6110-31e4-4cb4-8c03-8687cf012b77"</span>, <span class="hljs-keyword">new</span> ConfigFileChangedListener() {<br>    <br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onFileChanged</span><span class="hljs-params">(ConfigFile configFile)</span> </span>{<br>        System.out.printf(<span class="hljs-string">"fileId=%s配置文件有变动,最新内容为:%s%n"</span>, configFile.getFileId(), configFile.getContent());<br>    }<br>});
这里说一下配置变更通知的实现原理。
首先对于客户端来说,要想知道哪个配置文件进行了改动,有两种方式
第一种是通过push的方式来实现。当配置文件发生变动的时候,服务端主动将变动的配置文件push给客户端。这种方式实现起来比较麻烦,一方面是服务端还得存储客户端的服务的信息,因为服务端得知道push到哪台服务器上;另一方面,客户端需要提供一个接口来接收服务端push的请求,所以这种方式整体实现起来比较麻烦。但是这种push方式时实性比较好,一旦配置文件有变动,第一时间客户端就能够知道配置有变动。
第二种方式就是基于pull模式来实现。客户端定时主动去服务端拉取配置文件,判断文件内容是否有变动,一旦有变动就进行监听器的回调。这种实现相比push来说简单不少,因为服务端不需要关心客户端的信息,所有的操作都由客户端来完成。但是这个定时的时间间隔不好控制,太长可能会导致时实性差,太短会导致可能无效请求过多,因为配置压根可能没有变化。
但是这里我选择了第二种方式,因为实现起来简单。。
到这,一个简单的配置中心的服务端的和客户端就完成了,这里画张图来总结一下配置中心的核心原理。
接下来就把这个简易的配置中心整合到SpringCloud中。
SpringCloud配置中心的原理
在SpringCloud环境下,当项目启动的时候,在SpringBoot应用容器创建之前,会先创建一个容器,这个容器非常重要,这个容器是用来跟配置中心交互,拉取配置的。
这个容器在启动的时候会干两件事:
- 加载bootstrap配置文件,这就是为什么配置中心的配置信息需要写在bootstrap配置文件的重要原因
- 加载所有spring.factories文件中的键为org.springframework.cloud.bootstrap.BootstrapConfiguration对应的配置类,将这些配置类注入到这个容器中,注意这里是不会加载@EnbaleAutoConfiguration自动装配的类
当这两件事都做好之后,会从这个容器中获取到所有的PropertySourceLocator这个接口的实现类对象,依次调用locate方法。
这个类很重要,先来看看注释
Strategy for locating (possibly remote) property sources for the Environment. Implementations should not fail unless they intend to prevent the application from starting.
扔到有道翻译如下:
为环境定位(可能是远程)属性源的策略。实现不应该失败,除非它们打算阻止应用程序启动。
说的简单点就是用来定位到(也就是获取的意思)项目启动所需要的属性信息。同时要注意到括号内的 可能是远程 告诉我们一个很重要的信息,那就是获取的配置信息不仅仅可以存在本地,而且还可以存在远程。
远程?作者这里就差直接告诉你可以从配置中心获取了。。
所以从这个注释就可以发现,原来PropertySourceLocator就是起到在SpringCloud环境下从配置中心获取配置的作用。
PropertySourceLocator是一个接口,所以只要不同的配置中心实现这个接口,那么不同的配置中心就可以整合到了SpringCloud,从而实现从配置中心加载配置属性到Spring环境中了。
上面讲了在项目启动的时候SpringCloud是如何从配置中心加载数据的,主要是通过新建一个容器,加载bootstrap配置文件和一些配置类,最后会调用PropertySourceLocator来从配置中心获取到配置信息。
那么在SpringCloud环境下,是如何实现注入到Bean中的属性动态刷新的呢?
举个例子
当在类上加一个@RefreshScope注解之后,那么当配置中心sanyou.username的属性有变化的时候,那么此时注入的username也会跟着变化。
这种变化是如何实现的呢?
SpringCloud中规定,当配置中心客户端一旦感知到服务端的某个配置有变化的时候,需要发布一个RefreshEvent事件来告诉SpringCloud配置有变动。
在SpringCloud中RefreshEventListener类会去监听这个事件,一旦监听到这个事件,就会进行两步操作来刷新注入到对象的属性。
- 从配置中心再次拉取属性值,而这个拉取的代码逻辑跟项目启动时拉取的属性值核心逻辑几乎是一样的,也是创建一个新的spring容器,加载配置文件和配置类,最后通过PropertySourceLocator获取属性,这一部分核心的代码逻辑是复用的。
- 有了最新的属性之后,就开始刷新对象的属性。
刷新的逻辑实现的非常的巧妙,可不是你以为的简单地将新的属性重新注入对象中,而是通过动态代理的方式来实现的。
对于在类上加了@RefreshScope注解的Bean,Spring在生成这个Bean的时候,会进行动态代理。
这里我们就上面举个UserService例子来分析,在生成UserService有两步操作
- 生成一个UserService对象,将从配置中心拉到的配置sanyou.username注入给UserService对象
- 由于加了@RefreshScope,会给上一步骤生成的UserService对象进行代理,生成一个代理对象
最后真正暴露出去供我们使用的其实是就是这个代理对象,如图所示
由于暴露出去的是一个代理对象,所以当调用getUsername方法的时候,其实是调用UserService的代理对象的getUsername方法,从而就会找到UserService,调用UserService的getUsername获取到username的属性值。
当配置中心的配置有变动刷新属性的时候,Spring会把UserService这个对象(非代理对象)给销毁,重新创建一个UserService对象,注入最新的属性值。
当再次通过UserService代理对象获取username属性的时候,就会找最新创建的那个UserService对象,此时就能获取到最新的属性值。
配置每刷新一次,UserService对象就会先销毁再重新创建,但是暴露出去的UserService代理对象一直不会变。
到这应该就知道为什么加了@RefreshScope的对象能够实现配置的自动刷新了,其实依靠的是动态代理完成的。
由于上面并没有涉及整体执行流程的源码分析,所以我特地结合源码画了两张源码的执行流程图,有兴趣的小伙伴可以对照着图翻一翻具体的源码。
最终从配置中心获取到的属性会放在项目启动时创建的 Environment 对象里面。
这个图新增了对于加了@ConfigurationProperties数据绑定的对象原理的分析。
整合SpringCloud和测试
配置中心的配置信息,这里需要配置配置中心服务端的地址和使用的配置文件的id。当然这部分信息需要写在bootstrap配置文件中,前面也说过具体的原因。
上面分析知道,项目启动和刷新的时候,SpringCloud是通过PropertySourceLocator的实现从配置中心加载配置信息,所以这里就得实现一下
核心的逻辑就是根据所配置的文件的id,从配置中心拉取配置信息,然后解析配置。
这个是用来注册文件变动的监听器,来刷新文件的信息的。
因为上面提到,当配置发生变化的时候,需要发布一个RefreshEvent事件来触发刷新配置的功能。
核心的逻辑就是当项目启动的时候,对所使用的配置文件进行注册一个监听器,监听器的实现就是当发生配置改动的时候,就发布一个RefreshEvent事件。
配置了ConfigCenterPropertySourceLocator、ConfigCenterProperties、ConfigService
配置了ConfigContextRefresher、ConfigCenterProperties、ConfigService
最后需要将两个配置类在spring.factories配置一下。
这里有个需要注意,前面说过,SpringCloud会创建新的容器来加载配置,而这个容器只会加载spring.factories文件中键为@BootstrapConfiguration注解的配置类,所以需要将ConfigCenterBootstrapConfiguration跟BootstrapConfiguration配对,因为ConfigCenterBootstrapConfiguration配置了ConfigCenterPropertySourceLocator。
好了,到这里真的就完成了对SpringCloud整合了。
启动配置中心的server端,然后打开ApiPost,新增一个配置文件
新增文件类型为properties一个配置,内容为sanyou.username=sanyou键值对,当然可以写很多键值对,我这里就写了一个,新增成功之后,返回了文件的id:79765c73-c1ef-4ea2-ba77-5d27a64c4685
这里我为了方便,就把测试代码跟客户端写在同一个服务了,正常情况肯定是把跟SpringCloud代码打成一个依赖引到项目中。
在bootstrap.yml文件中配置配置中心的相关信息
- 配置中心服务端的地址是:localhost:8888
- 使用的配置文件的id是刚才创建的:79765c73-c1ef-4ea2-ba77-5d27a64c4685
测试Controller
提供一个接口,注入上面提到的UserService
启动项目,调用接口
从断这里可以看出,实际注入的是一个UserService代理对象,并且最终找的是com.sanyou.configcenter.test.UserService@3a1e4fd3这个UserService对象
此时这次调用的返回值就是:sanyou
接下来测试一下自动刷新属性的功能
现在修改一下配置中心的sanyou.username为sanyou666
静静等待5秒钟。。
此时控制台打印出 Refresh keys changed: [sanyou.username] ,也就是sanyou.username属性变了
此时再次获取username
可以看出,UserService代理对象没变,但是UserService对象已经变成了com.sanyou.configcenter.test.UserService@4237b3cd
此时获取到的username就已经变成了sanyou666
所以,到这里就成功将我们自己写的那个简易版的配置中心整合到了SpringCloud中了。
不足和改进
虽然我们这里的配置中心有了配置中心基本的功能,但是其实还有很多的不足和可以改进的地方。
问题前面也说过,在判断配置是否变更的时候,这里是每隔5s从服务端获取一次,这里就会可能5s之后才能感知到配置有变化,达不到真正时实的效果,并且由于这里是由客户端根据来判断,会导致无效的请求过多,因为可能配置压根没有变化,但是还是每隔5s获取一次配置信息,白白浪费资源
解决这个问题可以换成上面提到的push方式来做,或者将轮询方式改成长轮询的方式实现也是可以的,如果不清楚push、轮询、长轮询的,可以翻一下 RocketMQ的push消费方式实现的太聪明了这篇文章。
这里服务端的实例只有一个,不支持集群的方式,就会有单点故障的问题,不支持高可用。在实际项目中,肯定要支持集群的方式,保证即使有服务实例挂了,整个集群仍然可以继续对外提供服务,比如nacos就支持集群的方式,并且可以自由选择是使用AP模式还是CP模式。
对于通信协议,这里为了方便,我选择了客户端和服务端的通信方式是基于http协议的,当然也可以自定义协议,或者使用其它的协议,比如gRPC协议。其实在nacos2.x的版本中,nacos开始全面拥抱gRPC协议了。
至于序列化协议,这里选择了json协议,因为很简单、常见、使用范围广、跨语言,当然也可以选择其它的,比如hessian序列化协议等等。
一个合格的配置中心需要能支持不同应用的隔离,还有同一个应用不同环境的隔离,这里就图省事,直接就是有一个文件id来表示,虽然也可以做到隔离(不同系统用不同的文件id),但是这种方式比较low。像nacos会自动根据配置的名称和后缀名之类的,生成文件id(dataId),同时还有分组的概念,其实就是为了做到隔离的效果。
鉴权是一个系统比较常见的东西,这里就不做过多赘述
上面所有对于配置的crud都是基于ApiPost来的,但是实际怎么也得通过一个页面来操作吧,至于这里我为啥不自己写个页面,给你个眼神自己体会~~
最后,本文代码地址:
https://github.com/sanyou3/sanyou-config-center
扫码或者搜索关注公众号 三友的java日记 ,及时干货不错过,公众号致力于通过画图加上通俗易懂的语言讲解技术,让技术更加容易学习,回复 面试 即可获得一套面试真题。
Original: https://www.cnblogs.com/zzyang/p/16791296.html
Author: 三友的java日记
Title: 撸了一个简易的配置中心,顺带整合到了SpringCloud
相关阅读
Title: 如何在 JupyterLab 中把 ipykernel 切换到不同的 conda 虚拟环境?
内容简介:学习虚拟环境配置时,您可能见过在 JupyterLab (或 Jupyter Notebook) 中切换不同 kernel 的做法,看着就很酷哈哈。那么,这样做有什么作用呢?能够更加方便地使用虚拟环境吗?要怎么做才能实现这个功能呢?本文将为您详细介绍。
本文的基础是会使用 conda 配置虚拟环境,欢迎阅读我的文章: 使用 conda 配置虚拟环境与管理安装包。
学了 Anaconda 虚拟环境配置之后,我看到有运行环境配置文件 (environment.yml) 的教程,就想安装虚拟环境试试。本来觉得自己掌握了 conda 配置虚拟环境的各种操作,没想到很快就遇到了新问题。 在”Hands-On Machine Learning with Scikit-Learn, Keras & TensorFLow, 2nd Edition, by Aurelien Geron(O’Reilly)” 这本书的配套代码中 (代码的GitHub地址),作者老师写了详细的安装指南,开始的步骤和 使用 conda 配置虚拟环境与管理安装包 一文中介绍的方法没什么不同,也是用 conda env create
把 .yml 的环境配置安装上,建立一个名为 “tf2” 的虚拟环境。然而,接下来要在 tf2 环境中,输入:
(tf2)python3 -m ipykernel install --user --name=python3
据说可以设定 Jupyter Notebook 的 kernel,打开 Jupyter 就是 tf2 的环境配置。这让我十分困惑,之前我在虚拟环境(比如这里的 “tf2″)中运行 JupyterLab,import packages 的版本号都对啊。为什么还要再安装 ipykernel 呢?而且,我在使用中突然发现,import packages 引用的库及其版本号还是基于 base 环境,这是怎么回事?
stackoverflow 的这篇问答 解答了我的问题。原来,有三种方法可以使用不同虚拟环境配置的 JupyterLab (或 Jupyter Notebook) 。下面结合 tf2 这个例子来说明。
方法一
就是上文提到、我以前用的方法。有三步:1.进入虚拟环境 2.安装 jupyterlab 3.在该虚拟环境中,运行 jupyterlab。
安装:
(base)conda activate tf2
(tf2)conda install jupyterlab
使用:
(tf2)jupyter lab
这种方法简单易行,但有两个缺点:
[En]
This method is simple and easy, but has two disadvantages:
1.每次都要先进入这个虚拟环境,再运行代码。
2.每个虚拟环境都要安装 jupyterlab (或 jupyter notebook),其实不同虚拟环境的 jupyter 之间只是运行 python 代码的 kernel 不同(kernel 可以理解为 jupyter 中运行代码所使用的 python 解释器),编辑器等框架部分都相同。
既然这样,可不可以都用 base 环境的框架 (jupyterlab),把里面的 kernel 换一下就好了,这就是方法二的思路。
方法二
该方法需要先在虚拟环境中安装 ipykernel 及关联到 Jupyter 中,之后在 base 环境打开 Jupyter ,也可以使用虚拟环境配置了。
安装:
(base)conda activate tf2
(tf2)conda install ipykernel
(tf2)ipython kernel install --user --name=tf2
注意:kernel 命名不一定要和虚拟环境名完全相同,比如这里也可以是: --name=tf2-kernel
,但最好还是和虚拟环境名相似或相关,要不然以后可能会想不起来哪个 kernel 对应的是哪个环境了。
使用:
可以在 base 环境打开 JupyterLab ,如下所示,
(base)jupyter lab
通过切换 kernel 即可在 JupyterLab 中使用 tf2 环境配置,而不必进入 tf2 环境中。具体操作为:打开 JupyterLab ,点击 kernel 项,如下图所示,在下拉框中可以看到不同的 kernels,选择当前环境对应的 kernel,比如这里的 “tf2″,即可切换到 tf2 环境。

这其实就挺方便了,在 base 环境中打开 JupyterLab 就可以切换多个环境的 kernel。
能不能进一步简化 kernel 配置?只要虚拟环境装了 ipykernel(可以在新建虚拟环境时安装),就自动配置 kernel,都不用进入虚拟环境中写 ipython kernel install --user --name=KernelName
。这就是方法三的思路。
方法三
安装:
在 base 环境中,输入:
(base) conda install nb_conda_kernels
就安装了一个自动化的工具,实现 ipython kernel install
的功能。以后,新建的虚拟环境中只要安装了 ipykernel,该环境的 Jupyter kernel 就会自动新建。
例如,新建一个名为 “test” 的虚拟环境,新建环境时安装 ipykernel,
(base) conda create -n test ipykernel
然后就不用管了,test 环境的 kernel 会自动生成,是不是很省事呢哈哈。
使用:
和方法二相同,在 base 环境打开 JupyterLab 即可,会看到 kernel 选项新增了 “Python [conda env: test-kernel]”,选择该 kernel ,Jupyter 就使用相应的虚拟环境配置。
如果使用 JupyterLab,在初始页面还会看到新生成的不同 kernels,如下图所示。如果要新建一个 Jupyter Notebook,点击某个 kernel,即可生成一个该虚拟环境配置的空白 .ipynb 文件。

看了上面这三种方法的介绍,你可能已经知道了问题一的答案。为什么要装 ipykernel?这就是方法一和方法二的区别。装了 ipykernel,可以在 base 环境打开 JupyterLab 就使用该虚拟环境配置,而无需每次进入该虚拟环境再打开 JupyterLab。两种方法的效果是相同的。
学习了 ipykernel 之后,可能又会有新的疑问:为什么使用方法一只有默认的 Python3 kernel,没有其它 kernel 可选,那我的环境配置会不会是 base 环境?
不会,我测试了一下,因为方法一是在该虚拟环境中打开 Jupyter,就是用的这个环境配置,你可以用:
import package-name
print(package-name.__version__)
查询一下安装 package 的版本,是该虚拟环境而不是 base 环境的版本。
回到问题二,import packages 的版本号还是用的 base 环境,这是怎么回事?
我分析,问题可能出在 python3 -m ipykernel install --user --name=python3
中的命名 --name=python3
。安装指南中提到,这样可以把 tf2 的 kernel 设置为默认 kernel,每次打开不用切换 kernel。但是让 tf2 的 kernel 和 base 环境的 kernel 命名相同,结果 kernel 还是 base 环境配置。在 Anaconda Prompt 输入 jupyter kernelspec list
查看 Jupyter 的 kernels,显示只有一个 kernel: python3。用不同的命名,比如 python3 -m ipykernel install --user --name=tf2
,就有了 tf2 环境配置的 kernel 。为什么 tf2 的 ipykernel 用 python3 命名,不会改变默认 Python3 kernel 的环境配置,这个问题我也还没弄懂,欢迎交流和指教,谢谢!
本文对您有帮助的话,请点赞支持一下吧,谢谢!
关注我宁萌Julie ,互相学习,多多交流呀!
参考文章:
配置 kernel 的三种方法:
1.https://stackoverflow.com/questions/58068818/how-to-use-jupyter-notebooks-in-a-conda-environment/58068850
2.https://blog.csdn.net/u014264373/article/details/119390267
JupyterLab 安装 ipykernel :
3.https://zhuanlan.zhihu.com/p/478628221
Original: https://blog.csdn.net/applebear1123/article/details/125490022
Author: 宁萌Julie
Title: 如何在 JupyterLab 中把 ipykernel 切换到不同的 conda 虚拟环境?
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/363517/
转载文章受原作者版权保护。转载请注明原作者出处!