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流程耦合,配置可以独立进行更新。
配置中心实现了这两个概念,通过一个独立的基础服务为微服务提供集中式的配置管理。
配置中心与Python
对于JAVA应用来说,配置中心与开发框架的集成已经很成熟,比如Spring或者Spring Boot集成了Netflix开源的Archaius与Spring cloud,另外比如携程开源的Apollo配置中心也可以集成进Spring框架。
但是对于Python服务开发者而言,配置中心与开发框架的集成还没有一个成熟的开源方案,为了优雅的使用配置中心实现动态配置,我们为Python开发框架集成了Consul客户端。
Consul配置中心
先简单介绍下Consul本身。Consul是一个开源的分布式K/V系统,可以用来实现服务注册发现与配置中心。下面是Consul的架构图。
和etcd、zookeeper等一样,Consul运行几个server节点来维护系统一致性,配置项即存储在server节点的内存中并且对外提供读写服务,另外在每台服务器上可以运行一个agent节点来转发请求到server集群,这样服务可以不用知道Consul集群的具体位置。我们就是使用了agent节点提供的restful api来读取最新的配置数据。这些api可以将我们的服务接入到Consul集群。
实践的过程中只有api接入是远远不够的,为此我们从性能、可靠性、运维三个方面出发,还设计实现了实时推送、定时同步与本地备份、部署客户端三大功能。
实时推送
传统的配置文件可以加载配置项到内存中,服务直接读取内存中的配置项最简单最快速,但是传统配置文件修改后需要重新启动服务,配置中心克服了这个缺点但不能以性能损失为代价,我们仍然希望读取配置的过程简单快速。出于性能的考虑,服务不应该直接请求Consul集群来获取最新的数据,那有没有什么办法可以让我们将数据存放到Consul集群的同时也能达到从内存中读取最新配置的速度呢?
我们在每个服务启动之前启动一个进程来专门维护这个服务对应的配置数据到内存中,并将这块内存通过IPC的方式共享给本机的服务使用。这个进程称为watch进程。
watch进程会向Consul集群发送请求读取最新的配置项并记下这份数据的index,在下一次发送请求时会带上这个index,Consul集群收到请求后会阻塞请求30秒,在这30秒内如果数据有变化就立即返回,如果在30秒后没有变化就返回timeout断开连接,watch进程如果收到返回就更新内存中的数据,如果收到timeout就发起下一次请求。这样通过watch进程就可以实时的将最新数据维护在内存当中了,因为服务进程是通过IPC的方式从本机内存中读取配置项,所以读取速度可以与从进程内存中读取相当。并且我们将这个过程封装起来,对外暴露一个代理对象,将这个对象注入到服务进程中,所以对于服务的开发者而言,读取最新的配置项就是读取这个代理对象的属性,非常简单透明。
watch进程本身实际上就是一个事件循环,所以我们采用tornado提供的IoLoop来实现watch进程,并通过tornado的coroutine实现了异步的request,我们保证watch进程足够简单进而足够可靠。
定时同步与本地备份
如何保证在watch进程挂掉后服务还能从内存中获取新的数据?
我们在服务启动之前会再启动一个心跳进程,心跳进程会每隔15分钟获取一次全量数据,并将数据更新到内存中,这样我们的服务就有了双保险,watch与心跳机制任何一个失效都不会影响到微服务。
但是还有一种情况会影响到服务的运行,那就是当Consul集群不可用时,虽然这发生的概率很低。为此心跳进程在更新完内存之后会将数据再更新到本地的文件中,当整个Consul集群都不可用时,如果服务不重启依然可以从内存中获取最后一份数据,即使服务重启也可以从本地文件中读取备份数据到内存中。第三重机制保障了服务的可用性不会受到Consul集群可用性的影响。
客户端的部署
如何部署这个客户端呢?最直接的方式是在每台服务器上都部署一份客户端Agent。但我们并没有这样选择,原因有两点:首先这会增加运维成本,这是我们不愿意看到的,其次客户端是为本机的服务提供代理服务的所以没有必要设计成常驻的进程。
所以我们将客户端的启动嵌入到服务的启动中,一旦服务代码中声明使用了Consul配置中心,客户端就会在服务启动之前启动,并读取一份最新的配置到内存中,紧接着我们的服务就可以启动了,同样的在服务关闭之后客户端进程也会跟着关闭,这样做的原因是我们的服务器并非固定发布一种服务,所以我们自然不希望在服务发布后有其他服务的Consul客户端还在继续运行。通过这种方式我们的客户端在不增加任何运维成本的前提下提供了Consul集群的代理服务。