QConf 是一个分布式配置管理工具。 用来替代传统的配置文件,使得配置和业务代码分离,同时配置能够实时同步到客户端保证配置及时生效。
使用场景
单条数据量小
更新频繁(较代码而言)
配置总数可能巨大,但单台机器关心配置数有限
读多写少
特点
- 一处修改,所有机器实时同步更新
- 高效读取配置
- 安装部署方便,使用简单
- 服务器宕机、网络中断、集群迁移等异常情况对用户透明
- 支持c/c++、shell、php、python、lua、java、go、node 等语言
编译安装
QConf采用CMake进行构建(CMake 版本 2.6及以上)
可以使用以下命令完成QConf的编译安装:
mkdir build && cd build
cmake ..
make
make install复制代码
使用
搭建Zookeeper集群
在QConf 配置文件中配置Zookeeper集群地址
vi QCONF_INSTALL_PREFIX/conf/idc.conf复制代码
#all the zookeeper host configuration.
#[zookeeper]
zookeeper.test=127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183 #test机房zookeeper配置复制代码
- 在QConf配置文件中指定本地机房
echo test > QCONF_INSTALL_PREFIX/conf/localidc #指定本地机房为test复制代码
- 启动QConf
cd QCONF_INSTALL_PREFIX/bin && sh agent-cmd.sh start复制代码
使用样例
- shell
qconf get_conf /demo/node1 # get the value of '/demo/node1'复制代码
文档
- Getting Started - QConf 使用说明,包括QConf的安装,运行,API等信息
- 实现介绍
- wiki
- qconf video guide
整体认识
Qconf采用Zookeeper为分布式配置数据集群,利用qconf_agent与zookeeper集群交互获取数据,业务代码通过与qconf进行交互获取配置信息,Qconf服务端与zookeeper集群交互进行配置集群数据。总体来看还是很清晰的,其核心就是利用zookeeper分布式系统及其watcher功能。
架构
进入主题,开始介绍QConf的架构实现,下图展示的是QConf的基本结构,从角色上划分主要包括 QConf客户端 , QConf服务端 , QConf管理端 。
QConf服务端
QConf使用ZooKeeper集群作为服务端提供服务。可以将单条配置内容直接存储在ZooKeeper的一个ZNode上,并利用ZooKeeper的Watch监听功能实现配置变化时对客户端的及时通知。 按照ZooKeeper的设计目标,其只提供最基础的功能,包括顺序一致,原子性,单一系统镜像,可靠性和及时性。
QConf客户端
因为ZooKeeper在接口方面只提供了非常基本的操作,并且其客户端接口原始,所以我们需要在QConf的客户端部分解决如下问题:
降低与ZooKeeper的链接数 原生的ZooKeeper客户端中,所有需要获取配置的进程都需要与ZooKeeper保持长连接,在生产环境中每个客户端机器可能都会有上百个进程需要访问数据,这对ZooKeeper的压力非常大而且也是不必要的。
本地缓存 当然我们不希望客户端进程每次需要数据都走网络获取,所以需要维护一份客户端缓存,仅在配置变化时更新。
容错 当进程死掉,网络终端,机器重启等异常情况发生时,我们希望能尽可能的提供可靠的配置获取服务。
多语言版本接口 目前提供的语言版本包括:c,php,java,python,go,lua,shell。
配置更新及时 可以秒级同步到所有客户端机器。
高效的配置读取 内存级的访问速度。
下面来看下QConf客户端的架构:
可以看到QConf客户端主要有:Agent、各种语言接口、连接他们的消息队列和共享内存。
在QConf中,配置以Key-Value的形式存在,业务进程给出key获得对应Value,这与传统的配置文件方式是一致的。
下面通过两个主要场景的数据流动来说明他们各自的功能和角色:
场景1.业务进程请求数据:
业务进程调用某一种语言的QConf接口,从共享内存中查找需要的配置信息。
如果存在,直接获取,否则会向消息队列中加入该配置key。
Agent从消息队列中感知需要获取的配置key。
Agent向ZooKeeper查询数据并注册监听。
Agent将获得的配置Value序列化后放入共享内存。
业务进程从共享内存中获得最新值。
场景2.配置信息更新:
图6 数据流动-配置更新
ZooKeeper通知Agent某配置项发生变化。
Agent从ZooKeeper查询新值并更新Watcher。
Agent用新值更新共享内存中的该配置项。
通过上面的说明,可以看出QConf的整体结构和流程非常简单。 QConf中各个组件或线程之间仅通过有限的中间数据结构通信,耦合性非常小,各自只负责自己的本职工作和一亩三分地,而不感知整体结构。
下面通过几个点来详细介绍:
无锁
根据上文提到的配置信息的特征,我们认为在QConf客户端进行的是多进程并行读取的过程,对配置数据来说读操作远多于写操作。为了尽可能的提高读效率,整个QConf客户端在操作共享内存时采用的是无锁的操作,同时为了保证数据的正确,采取了如下两个措施:
单点写
将写操作集中到单一线程,其他线程通过中间数据结构与之通信,写操作排队,用这种方法牺牲掉一些写效率。 在QConf客户端,需要对共享内存进行写操作的场景有:
用户进程通过消息队列发送的需获取Key;
ZooKeeper 配置修改删除等触发Watcher通知,需更新;
为了消除Watcher丢失造成的不一致,需要定时对共享内存中的所有配置重新注册Watcher,此时可能会需要更新;
发生Agent重启、网络中断、ZooKeeper会话过期等异常情况之后,需重新拉数据,此时可能需要更新。
读验证
无锁的读写方式,会存在读到未写入完全数据的危险,但考虑到在绝对的读多写少环境中这种情况发生的概率较低,所以我们允许其发生,通过读操作时的验证来发现。共享内存数据在序列化时会带其md5值,业务进程从共享内存中读取时,利用预存的md5值验证是否正确读取。
异常处理
QConf中采取了一些处理来应对不可避免的异常情况:
采用父子进程Keepalive的方式,应对Agent进程异常退出的情况;
维护一份落盘数据,应对断网情况下共享内存又被清空的状况;
网络中断恢复后,对共享内存中所有数据进行检查,并重新注册Watcher;
定时扫描共享内存;
数据序列化
QConf 客户端中有多处需要将数据序列化通信或存储,包括共享内存,消息队列,落盘数据中的内容。 我们采取了如下协议:
图7 数据序列化协议
Agent任务
通过上面的描述,大家应该大致知道了Agent所做的一些事情,下面从Agent内线程分工的角度整理一下,如下图:
图8 Agent内部结构
Send线程:ZooKeeper线程,处理网络数据包,进行协议包的解析与封装,并将Zookeeper的事件加入WaitingEvent队列等待处理。
Event 线程:ZooKeeper线程,依次获取WaitingEvent队列中的事件,并进行相应处理,这里我们关注节点删除、节点值修改、子节点变化、会话过期等事件。对特定的事件会进行相应的操作,以节点值修改为例,Agent会按上边提到的方式序列化该节点Key,并将其加入到WaitingWriting队列,等待Main线程处理。
Msq线程:之前讲数据流动场景的时候有提到,用户进程从共享内存中找不到对应配置后,会向消息队列中加入该配置,Msq线程便是负责从消息队列中获取业务进程的取配置需求,并同样通过WaitingWriting队列发送给Main进程。
Scan线程:扫描共享内存中的所有配置,发现与Zookeeper不一致的情况时,将key值加入WaitingWriting队列。Scan线程会在ZooKeeper重连或轮询期到达时进行上述操作。
Main线程:共享内存的唯一写入线程,从Zookeeper获得数据写入共享内存,维护共享内存中的内容。
Trigger线程:该线程负责一些周边逻辑的调用。