QConf 架构 - Qihoo360/QConf GitHub Wiki
QConf是分布式配置管理系统,它取代传统的配置文件,实现自动化的配置管理,并实时更新到所有的客户端机器。
1. QConf 的整体架构
QConf的整个系统由若干个角色组成,这些角色分为两类:一类是部署在客户端的机器上的,主要包括agent和各种语言的SDK;另一类是服务端,主要包括ZooKeeper服务器以及用户搭建的反馈服务器等,整体架构图如下:
1.1 ZooKeeper
ZooKeeper服务器用来存储所有的配置信息。它以节点树的形式来组织所有数据,类似于文件系统的目录结构,一个节点有名称和值两个属性,节点下又可以建立若干个子节点,如/a、/b/c、/d/e/f等都是合法的节点名。我们使用节点名代表配置项的名称,如username、timeout等,使用节点值代表配置项的值,如jack、5等。另外它以至少3台服务器组成集群,以提供高可用,它的数据是强一致性。
那么为什么不使用MySQL、Redis或MongoDB作为存储呢?因为ZooKeeper不单单是一个存储,更是一个服务发现系统。对于传统的存储系统如MySQL,只能被动接受客户端的增删改查等操作,而ZooKeeper允许客户端向服务端注册对感兴趣的事件的监视(称为watcher),这些事件包括“节点被创建(ZOO_CREATED_EVENT)”、“节点被删除(ZOO_DELETED_EVENT)”、“节点值改变(ZOO_CHANGED_EVENT)”、“子节点变化(ZOO_CHILD_EVENT)”、“客户端断开或重新连接(ZOO_SESSION_EVENT)”等,一旦这些事件发生,ZooKeeper服务端就会通知客户端,使得客户端事先注册的回调函数被执行,在回调函数内对该事件进行相应的处理。
ZooKeeper的这种回调通知的机制非常适合我们实时感知配置变化的应用场景。设想有个配置值名为timeout,其值现在为5(代表5秒),每台客户端都从ZooKeeper读取了该值,同时也都向服务端注册了对ZOO_CHANGED_EVENT的watcher。现在我们决定把timeout由5秒改为3秒,于是我们将ZooKeeper服务端上的该节点的值修改为3,此时所有客户端都会收到服务端的通知,于是客户端重新把最新的节点值从服务端读取回来。这也是QConf能实现配置值实时更新到客户端的基础。
1.2 agent
上小节介绍了ZooKeeper的回调通知机制,这个特性虽然很好,但有一个前提,就是客户端需要与服务端保持长连接。而agent就被作为ZooKeeper的客户端来使用,它是一个守护进程,一经启动就连接上服务端,并时刻监视各类事件有无发生,并在发生时执行相应的逻辑。
agent共有4+2n个线程,这里2n是由于qconf能够从多个集群获取数据,而每打开一个zookeeper集群就会创建2个线程,该线程负责和zookeeper通信,对应的线程模型如下:
master thread启动后,会从一个队列中获取用户指定的节点,然后从服务器上获得对应数据并更新到共享内存中,如果更新共享内存成功,那么就会将数据发送到另外一个队列中用于一些额外的操作。
msg_thread负责从消息队列里取节点名。因为系统的消息队列大小有限制,若队列内的数据未及时取走,则可能因为SDK在短时间内大量写入节点名而将队列填满,之后的节点名将无法再写入,造成错误。而agent获得节点名后,需要通过网络去ZooKeeper上取得节点值,相对较慢(10ms左右),因此很可能发生队列的积压填满。子线程1的作用就是迅速把节点名从队列里读到本进程的内存里,及时清除旧数据,防止填满队列。主线程从内存里获得需要读取的节点名,再去ZooKeeper上取值。
assist thread负责周期性扫描共享内存。因为ZooKeeper的watcher机制是不能设置为持久监视,而是一次性的,也就是说,一个事件被触发一次后,该watcher就被自动销毁,如果要继续监视下次事件,只能重新注册watcher。所以我们采取的策略是事件发生后,第一时间先重新注册watcher,再执行具体的处理逻辑,尽量减少上个watcher销毁与下个watcher注册的时间间隔。尽管如此,这二者仍然不是事务性操作,因此仍有极小概率会丢失事件。为了防止这种情况发生,我们会每隔30-60分钟扫描共享内存里的所有节点,并与ZooKeeper上的最新值比较,若有差异则更新到最新值,并为可能遗漏监视的节点重新注册watcher。
trigger thread实际上是一个多功能线程,主要是数据在共享内存更新完成后需要执行的额外操作,当前主要操作有dump,执行用户脚本以及信息反馈。
1.2.1 dump
为防止在网络中断的同时机器重启(此时共享内存内容为空,也无法通过网络去ZooKeeper上取值;同时会打印相关错误日志信息),agent会定期把共享内存里的所有内容持久化到磁盘上的一个dump文件里,保证在上述情况下可以取到上次备份的配置信息。
1.2.2 执行脚本
有些时候配置更新,在客户端上用户希望立即感知并进行一些操作,比如在配置更新时把一些本地服务给重启了,那么这个时候用户就可以创建自己的脚本,然后在数据更新的时候执行这些脚本。这样可以省去定时去共享内存中获取最新数据的操作。
1.2.3 信息反馈
用户希望查看每台客户端机器读取哪些配置项,也希望能确认客户端是否已将配置项更新到最新值(虽然除了网络中断,还未发现有未同步的情况发生)。当前该功能默认是关闭的,如果用户搭建了反馈服务器,那么就可以使用该功能来了解每台客户端数据更新情况。
比如节点/a的值由1变成2,A机器上的agent感知到了这个事件,并且把最新值2从ZooKeeper上读出,然后把这个更新的记录通过HTTP方式汇报给反馈服务器。用户可以到ZooKeepe服务器上获得当前值并去反馈服务器上的值进行比较,即可得知客户机器是否更新;
1.3 共享内存
从架构图上可以看出,agent与SDK并没有直接的通讯,二者的交互有两个渠道,其中一个是共享内存,用来存储SDK读取过的配置项。
共享内存的作用在于,SDK可能会多次读取相同的配置项(其值可能并不会每次都改变),如果没有在本机存储这些配置项,那么就必然需要每次通过网络去ZooKeeper服务端读取(相同的值),一是存在网络延迟,二是ZooKeeper本身性能并不高,不适合大量的频繁读操作。
之所以选用共享内存这种存储方式,是看中了其性能优势,节点信息在共享内存里以哈希表的形式存储,因此查询时间是常数级,实际测试一次读取耗时在16us左右,完全可以满足用户对低延迟的要求。
因为涉及到多个进程(agent和客户端进程)对共享内存的同时读写,所以需要避免冲突。最初的方案是使用进程锁,后来改为无锁的方案,共享内存里同时存储节点值与其MD5值,SDK会把二者一次全读出,然后校验MD5值是否正确,若不正确则会重新读取,这就避免了agent尚未写入完整的节点值被SDK读走,也消除了锁争用,进一步提升性能。
1.4 消息队列
agent与SDK交互的另一个渠道是消息队列。从架构图上看,SDK与ZooKeeper没有通讯,而用户只通过调用SDK的接口来读取配置值,用户与agent没有通信。那么agent如何得知需要去ZooKeeper读取哪些配置项(节点)、并监视哪些节点的变化呢?
答案就是这个消息队列。SDK收到用户读取某节点的请求后,先去共享内存里检索,若找到则直接返回,若未找到,则将该节点的名称写入消息队列。而agent时刻监视消息队列中是否有数据,一旦有数据被写入,agent即读出数据(节点名),再根据节点名去ZooKeeper把节点值读取回来,并写入共享内存。SDK会等待直到共享内存里出现所要读取的节点,然后读出节点值并返回。
1.5 SDK
SDK封装了操作共享内存的逻辑,为用户提供读取配置值的接口。
QConf现在提供多种语言的SDK,包括C/C++、Java、PHP、Python、Lua、Go 等等。