本期聊一聊咱们360基础架构的另一款工具 "QConf",最近刚在 "中华架构师大会" 上分享过,本文会对QConf的架构和实现原理进一步具体介绍。
-------------------------------
QConf一个集中式的配置管理系统,它可以取代传统的配置文件,实现可视化与自动化的配置管理,并实时更新到所有的客户端机器。
QConf的整个系统由若干个角色组成,这些角色分为两类:一类是部署在客户端的机器上的,主要包括agent和各种语言的SDK;另一类是服务端,主要包括ZooKeeper、admin服务器、feedback服务器和hulk平台。整体架构图如下:
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能实现配置值实时更新到客户端的基础。
agent
上小节介绍了ZooKeeper的回调通知机制,这个特性虽然很好,
但有一个前提,就是客户端需要与服务端保持长连接。而agent就被作为ZooKeeper的客户端来使用,它是一个守护进程,一经启动就连接上服务端,并时刻监视各类事件有无发生,并在发生时执行相应的逻辑。
agent共有5个线程,主线程启动后创建其他4个线程,并负责从ZooKeeper服务端取节点值,再写入共享内存。
子线程1负责周期性扫描共享内存。因为ZooKeeper的watcher机制有个缺陷,即不能设置为持久监视,而是一次性的,也就是说,一个事件被触发一次后,该watcher就被自动销毁,如果要继续监视下次事件,只能重新注册watcher。所以我们采取的策略是事件发生后,第一时间先重新注册watcher,再执行具体的处理逻辑,尽量减少上个watcher销毁与下个watcher注册的时间间隔。尽管如此,这二者仍然不是事务性操作,因此仍有极小概率会丢失事件。为了防止这种情况发生,我们会每隔30分钟扫描共享内存里的所有节点,并与ZooKeeper上的最新值比较,若有差异则更新到最新值,并为可能遗漏监视的节点重新注册watcher。另外子线程1还负责把所有节点值持久化到dump文件中。
子线程2负责从消息队列里取节点名。因为系统的消息队列大小有限制,若队列内的数据未及时取走,则可能因为SDK在短时间内大量写入节点名而将队列填满,之后的节点名将无法再写入,造成错误。而agent获得节点名后,需要通过网络去ZooKeeper上取得节点值,相对较慢(10ms左右),因此很可能发生队列的积压填满。子线程2的作用就是迅速把节点名从队列里读到本进程的内存里,及时清除旧数据,防止填满队列。主线程从内存里获得需要读取的节点名,再去ZooKeeper上取值。
子线程3负责将节点的更新记录以HTTP方式汇报给反馈服务器,反馈服务器存储的更新记录供管理服务器查询。
子线程4由ZooKeeper的C库自动创建,等待ZooKeeper服务端的事件通知,并执行事先注册的回调函数。
共享内存
从架构图上可以看出,agent与SDK并没有直接的通讯,二者的交
互有两个渠道,其中一个是共享内存,用来存储SDK读取过的配置项。
共享内存的作用在于,SDK可能会多次读取相同的配置项(其值可能并不会每次都改变),如果没有在本机存储这些配置项,那么就必然需要每次通过网络去ZooKeeper服务端读取(相同的值),一是存在网络延迟,二是ZooKeeper本身性能并不高,不适合大量的频繁读操作。
之所以选用共享内存这种存储方式,是看中了其性能优势,节点信息在共享内存里以哈希表的形式存储,因此查询时间是常数级,实际测试一次读取耗时在10us左右,完全可以满足用户对低延迟的要求。
因为涉及到多个进程(agent和客户端进程)对共享内存的同时读写,所以需要避免冲突。最初的方案是使用锁,后来改为无锁的方案,共享内存里同时存储节点值与其MD5值,SDK会把二者一次全读出,然后校验MD5值是否正确,若不正确则会重新读取,这就避免了agent尚未写入完整的节点值被SDK读走,也消除了锁争用,进一步提升性能。
消息队列
agent与SDK交互的另一个渠道是消息队列。从架构图上看,SDK
与ZooKeeper没有通讯,而用户只通过调用SDK的接口来读取配置值,用户与agent没有通讯。那么agent如何得知需要去ZooKeeper读取哪些配置项(节点)、并监视哪些节点的变化呢?
答案就是这个消息队列。SDK收到用户读取某节点的请求后,先去共享内存里检索,若找到则直接返回,若未找到,则将该节点的名称写入消息队列。而agent时刻监视消息队列中是否有数据,一旦有数据被写入,agent即读出数据(节点名),再根据节点名去ZooKeeper把节点值读取回来,并写入共享内存。SDK会等待直到共享内存里出现所要读取的节点,然后读出节点值并返回。
SDK
SDK封装了操作共享内存的逻辑,为用户提供读取配置值的接口。
QConf现在提供6种语言的SDK,包括C/C++、Java、PHP、Python、Lua、Go,能满足大部分业务的使用要求。
dump文件
为防止在网络中断的同时机器重启(此时共享内存内容为空,也无
法通过网络去ZooKeeper上取值),agent会定期把共享内存里的所有内容持久化到磁盘上的一个dump文件里,保证在上述情况下可以取到上次备份的配置信息。
反馈服务器
用户希望查看每台客户端机器读取哪些配置项,也希望能确认客
户端是否已将配置项更新到最新值(虽然除了网络中断,还未发现有未同步的情况发生)。反馈服务器由两台Web Server和LVS组成,拥有公网IP,它接收agent发来的数据更新报告,并记录下来,供管理服务器查询。
比如节点/a的值由1变成2,A机器上的agent感知到了这个事件,并且把最新值2从ZooKeeper上读出,然后把这个更新的记录通过HTTP方式汇报给反馈服务器。管理服务器在查询时,会去ZooKeeper上查询/a的最新值,并与反馈服务器上记录的值对比,若一致则说明A机上的节点/a的值已同步到最新,否则说明未能同步,需要排查问题。
hulk
hulk是一个用于内部机器管理的Web系统,QConf的管理功能也
已经集成进hulk,用户通过Web界面可以实现节点的创建、删除、修改、查看等操作,也可以很方便地将节点从一个机房复制到另一个机房,另外也可以把配置值从传统的配置文件导入QConf、查看每个节点有哪些机器读取、把节点值回滚到旧的状态等。
管理服务器
管理服务器QConf@hulk的后台系统,它接受hulk发来的用户的请求,对ZooKeeper服务器上的节点进行相应的增删改查操作,并负责用户权限控制、记录操作日志等。QConf@hulk与管理服务器组成的管理系统是QConf非常重要的组成角色,对用户的使用体验影响极大,如果没有这部分,用户将只能手动修改ZooKeeper上存储的信息,导入、回滚等附加功能也无法使用。
除去hulk外,为了实现自动化,管理服务器也对用户开放了HTTP接口,用户的程序或脚本可以直接以HTTP方式完成各种管理操作,在某些场合会更加方便。
转自:http://chuansong.me/n/923964