A litmus test for whether an app has all config correctly factored out of the code is whether the codebase could be made open source at any moment, without compromising any credentials
问题的来源
配置是软件开发中一个古老而有用的概念, 我们需要通过配置来控制代码运行的方式,比如缓存时间,数据库地址等等。长久以来我们使用配置文件来记录配置项,软件在启动时读取配置文件并将配置项加载到内存中, 在软件运行过程中就可以从内存中读取配置项。如果需要修改配置项,只需要修改配置文件并且重新发布服务就可以了,这个方式被沿用了几十年直到分布式系统伴随着互联网时代的到来。因为对于一个分布式应用重新发布服务意味着重新启动分布在几十台甚至几千台服务器上的服务,但是重新发布系统耗时耗力而且可能会引起整个系统的波动,这显然不是一个优雅的方式。为此需要一种动态调整配置而不需要重启应用的方式。
动态配置
为了解决上面提到的问题,动态配置出现了,动态配置包含两个概念:
- 配置与代码区分开来对待,配置不再是代码的一部分,配置可以进行独立更新,同样的代码在不同的配置下可以表现出不同的功能。
- 配置更新与代码更新严格区分,配置不再与DI/DC
在微服务实践当中,服务注册发现与配置中心是必要的两个基础服务。JAVA因为有Netflix公司开源的一套微服务生态加上与Spring架构的无缝衔接,可以很方便的使用这两个组件,在Python当中还没有一套完整的生态可以利用。
所以我们选择基于开源项目自己来开发一些对Python友好的组件,今天介绍下为Consul开发的Python客户端。
Consul是一个开源的分布式K/V系统,可以用来实现服务注册发现与配置中心。下面是consul的架构图,和etcd,zookeeper等一样,consul运行几个server节点来维护系统一致性,并且对外提供服务,我们使用了restful api。另外在每台服务器上可以运行一个agent节点来转发请求到server集群,这样服务可以不用知道consul集群的具体位置了。
刚才提到了我们使用了consul的restful api,这些api可以将我们的服务接入到conusl集群。但是在实践的过程中我们发现只有api接入是不够的,为此我们自己开发了一个客户端实现如下的功能:
实时推送数据到微服务内存
出于性能的考虑,微服务不应该直接请求consul集群来获取最新的数据,因为传统的配置文件可以加载数据到内存中,服务直接读取内存中的数据是最快的选择,但是传统配置文件修改后需要重新启动服务,而consul的优势在于不需要重新启动服务就可以获取最新的配置,那有没有什么办法可以让我们将数据存放到consul集群的同时也能达到从内存中读取最新配置的速度呢?为此我们在每个微服务启动之前启动一个进程来专门维护这个服务对应的配置数据到内存中,并将这块内存共享给本机的微服务使用。这个进程称为watch进程,watch进程会向consul集群发送请求最新数据并记下这份数据的index,在下一次发送获取最新数据请求时会带上这个index,consul集群收到请求后会阻塞请求30秒,在这30秒内如果数据有变化就立即返回,如果在30秒后没有变化就返回timeout断开连接,watch进程如果收到返回就更新内存中的数据,如果收到timeout就发起下一次请求。这样通过watch进程就可以实时的将最新数据保存到内存当中了,本机的微服务进程就可以从内存中读取最新的数据,并且对于微服务而言这一切都是无感知而且高效的。
定时同步与本地备份
上面说了watch进程,虽然watch运行良好,但我们认为不应该只依靠这一个机制,我们需要保证在watch进程挂掉后微服务还能从内存中获取新的数据。为此我们在服务启动之前会再启动一个心跳进程,心跳进程会每隔15分钟获取一次全量数据,并将数据更新到内存中,这样我们的服务就有了双保险,watch与心跳机制任何一个失效都不会影响到微服务。
但是还有一种情况会影响到服务的运行,那就是当consul集群不可用时,虽然这发生的概率很低,但我们依然要将这种情况考虑进去,为此心跳进程在更新内存之后会将数据更新到本地的文件中,当整个consul集群都不可用时,如果微服务不重启依然可以从内存中获取最后一份数据,即使微服务重启也可以从本地文件中读取备份数据到内存中。第三重机制保障了微服务不会受到consul集群可用性的影响。
客户端的部署
具有这三重机制的客户端如何部署呢,是不是要在每台服务器上都部署一份呢?我们并没有这样选择,原因有两点,1.这会增加运维成本,这是我们不愿意看到的 2. 客户端是为本机的服务提供代理服务的所以没有必要设计成常驻的进程
所以我们将客户端的启动嵌入到微服务的启动中,一旦微服务代码中使用了consul服务,客户端就会在微服务之前启动,并读取一份最新的配置到内存中,紧接着我们的微服务就可以启动了,同样的在微服务关闭之后客户端进程也会跟着关闭,这样做的原因是我们的服务器并非固定发布一种服务,所以我们自然不希望在服务发布后有其他微服务的consul客户端还在继续运行。通过这种方式我们的客户端在不增加任何运维成本的前提下提供了consul集群的代理服务。