从配置文件到分布式配置管理QConf
QConf是奇虎360广泛使用的配置管理服务,现已开源: QConf Source Code。欢迎大家关注使用。
本文从设计初衷,架构实现,使用情况及相关产品比较四个方面进行介绍。
设计初衷
在分布式环境中,出于负载、容错等种种原因,几乎所有的服务都需要在不同的机器节点上部署多个实例。同时,业务项目中总少不了各种类型的配置文件。这种情况下,有时仅仅是一个配置内容的修改,便需要重新进行代码提交svn/git,打包,分发上线的流程。当部署的机器有很多时,分发上线本身也是一个很繁杂的工作。而配置文件的修改频率又远远大于代码本身。 追本溯源,我们认为所有的这些麻烦是由于我们对配置和代码在管理和发布过程中不加区分造成的。配置本身源于代码,是为了提高代码的灵活性而提取出来的一些经常变化的或需要定制的内容,而正是配置的这种天生的变化特征给我们带了很大的麻烦。 因此,我们开发了分布式配置管理系统QConf,并依托QConf在360内部提供了一整套配置管理服务,QConf致力于将配置内容从代码中完全分离出来,及时可靠高效地提供配置访问和更新服务。
整体认识
为了让大家对之后的内容有个直观的认识,先来介绍一下如果需要在自己的项目中使用QConf应该怎么做:
- 首先,部署QConf,QConf采用cmake构建,依次执行如下命令。
cmake .. make make install
- 之后,通过Zookeeper客户端或QConf管理界面在Zookeeper上建立自己的节点结构,节点完整路径作为QConf的key值,以360公司内部QConf管理界面为例:
- 最后,选择所需语言版本的QConf库,并在需要获得配置内容的代码位置,直接调用Qconf客户端接口,并放心每次取得的都是最新鲜出炉的配置内容。以shell命令为例:
需要说明的是,使用QConf后已经没有所谓的配置文件的概念,你要做的就是在需要的地方获取正确的内容,QConf认为,这才是你真正想要的。
架构介绍
了解了QConf的设计初衷和使用方式,相信大家已经对QConf有一个整体的认识并且对其实现有了大概的猜想。在介绍架构之前,还需要申明一下QConf对配置信息的定位,因为这个定位直接决定了其结构设计和组件选择。
- 单条数据量小
- 更新频繁(较代码而言)
- 配置总数可能巨大,但单台机器关心配置数有限
- 读多写少
进入主题,开始介绍QConf的架构实现:
上图展示的是QConf的基本结构,从角色上划分主要包括QConf客户端,QConf服务端和QConf管理端。
QConf服务端。
QConf使用ZooKeeper集群作为服务端提供服务。众所周知,ZooKeeper是一套分布式应用程序协调服务,根据上面提到的对配置内容的定位,我们认为可以将单条配置内容直接存储在ZooKeeper的一个ZNode上,并利用ZooKeeper的Watch监听功能实现配置变化时对客户端的及时通知。 按照ZooKeeper的设计目标,其只提供最基础的功能,包括顺序一致,原子性,单一系统镜像,可靠性和及时性。另外Zookeeper还有如下特点:
- 类文件系统的节点组织
- 稳定,无单点问题
- 订阅通知机制
关于Zookeeper,更多见:https://zookeeper.apache.org/
QConf客户端
但在接口方面,ZooKeeper本身只提供了非常基本的操作,并且其客户端接口原始,所以我们需要在QConf的客户端部分解决如下问题:
- 降低与ZooKeeper的链接数 原生的ZooKeeper客户端中,所有需要获取配置的进程都需要与ZooKeeper保持长连接,在生产环境中每个客户端机器可能都会有上百个进程需要访问数据,这对ZooKeeper的压力非常大而且也是不必要的。
- 本地缓存 当然我们不希望客户端进程每次需要数据都走网络获取,所以需要维护一份客户端缓存,仅在配置变化时更新。
- 容错 当进程退出、网络中断、机器重启等异常情况发生时,我们希望能尽可能的提供可靠的配置获取服务
- 多语言版本接口 目前提供的语言版本包括:c,php,java,python,go,lua,shell
- 配置更新及时,可以秒级同步到所有客户端机器
- 高效的配置读取,内存级的访问速度
下面来看下QConf客户端的架构:
102 103 通过上面的说明,可以看出QConf的整体结构和流程非常简单。 104 QConf中各个组件或线程之间仅通过有限的中间数据结构通信,耦合性非常小,各自只负责自己的本职工作和一亩三分地,而不感知整体结构。下面通过几个点来详细介绍: 105 106 - 无锁 107 根据上文提到的配置信息的特征,我们认为在QConf客户端进行的是多进程并行读取的过程,对配置数据来说读操作远多于写操作。为了尽可能的提高读效率,整个QConf客户端在操作共享内存时采用 的是无锁的操作,同时为了保证数据的正确,采取了如下两个措施: 108 - 单点写,将写操作集中到单一线程,其他线程通过中间数据结构与之通信,写操作排队,用这种方法牺牲掉一些写效率。在QConf客户端,需要对共享内存进行写操作的场景有: 109 1. 用户进程通过消息队列发送的需获取key; 110 2. ZooKeeper 配置修改删除等触发Watcher通知,需更新; 111 3. 为了消除watcher丢失造成的不一致,需要定时对共享内存中的所有配置重新注册watcher,此时可能会需要更新; 112 4. 发生agent重启、网络中断、ZooKeeper会话过期等异常情况之后,需重新拉数据,此时可能需要更新。 113 - 读验证,无锁的读写方式,会存在读到未写入完全数据的危险,但考虑到在绝对的读多写少环境中这种情况发生的概率较低,所以我们允许其发生,通过读操作时的验证来发现。共享内存数据在> 序列化时会带其md5值,业务进程从共享内存中读取时,利用预存的md5值验证是否正确读取。 可以看到QConf客户端主要有:agent、各种语言接口、连接他们的消息队列和共享内存。 在QConf中,配置以key-value的形式存在,业务进程给出key获得对应value,这与传统的配置文件方式是一致的。
下面通过两个主要场景的数据流动来说明他们各自的功能和角色:
- 业务进程请求数据
- 业务进程调用某一种语言的QConf接口,从共享内存中查找需要的配置信息;
- 如果存在,直接获取,否则会向消息队列中加入该配置key;
- agent从消息队列中感知需要获取的配置key;
- agent向ZooKeeper查询数据并注册监听;
- agent将获得的配置value序列化后放入共享内存;
- 业务进程从共享内存中获得最新值。
- 配置信息更新
- 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 客户端中有多处需要将数据序列化通信或存储,包括共享内存,消息队列,落盘数据中的内容。采取如下协议:
- agent任务 通过上面的描述,大家应该大概了解了agent所做的一些事情,下面从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 线程:该线程负责一些周边逻辑的调用,包括:
- dump操作:将共享内存的内容同步一份到本地,QConf采用的gdbm;
- feedback操作:QConf支持更新反馈的功能,可向用户指定web服务以一定的格式发送反馈;
- script操作:在某些情况下,业务希望当配置变化时,做一些自定义的操作,QConf支持配置变化时调用用户脚本,agent按一种固定的约定在配置发生变化时调用对应的脚本。
QConf管理端
管理端是业务修改配置的页面入口,利用数据库提供一些如批量导入,权限管理,版本控制等上层功能。 由于公司内的一些业务耦合和需求定制,当前开源的QConf管理端这边提供了一个简易的页面,和一套下层的c++接口,如下图:
之后计划进一步完善以及跟社区合作提供更友好的界面。
QConf的结构及实现大概就介绍到这,接下来…
One More Thing
QConf 除了存储配置的基本功能外,还在公司内提供了一套简单的服务发现功能,该功能允许业务在QConf上配置一组服务,QConf会监控其服务的存活。当业务进程调用获取服务的接口时,会根据用户需求,返回全部可用服务,或某一可用服务。 不同于普通配置:
- 结构上多一个Monitor的角色,来监控所有服务的存活, 如下图:
- 提供对应的客户端接口,get_host获取某一可用服务,get_allhost获取所有可用服务
- 管理端页面对应的展示方式及操作,尤其是对指定服务的添加删除,上线下线
需要明确的是,目前Monitor事实上仅仅是通过查看服务端口的存活来判断的,在实际生产环境中,该功能多与实际服务提供者的监控结合,由服务提供者的监控调用QConf的相应接口实现服务的上下线。
使用方式及使用场景
目前360内部已经广泛的使用QConf。覆盖云盘、大流程、系统部、dba、图搜、影视、地图、硬件、手机卫士、广告、好搜等大部分业务。 部署国内外共51几个机房,客户端机器超两万台,稳定运行两年。
使用的方式主要包括:
- 简单配置 公司内使用最广泛的用法,QConf非常适合经常需要变动的配置使用,如开关信息、版本信息、推荐信息、超时时间等。
- 服务方式 这种方式多被服务提供者采用,如dba,系统部等,采用上述的服务配置的方式,通过QConf向公司的所有业务提供存储,计算及web服务。
QConf因为其对配置信息的定位,使得整个结构非常简单,容易部署和使用。在Github上可以找到完整代码:QConf Source Code 欢迎关注。