转载自:http://www.tuicool.com/articles/BJfiMbr
360 如何用 QConf 搞定 2W+ 服务器的配置管理
时间 2015-06-29 09:27:47 佚名
此文根据【QCON高可用架构群】分享内容,由群内【编辑组】志愿整理,转发请注明出处。
王康,奇虎360基础架构组资深工程师 目前负责分布式配置管理服务QConf的研发和维护,并推动其在奇虎360的应用。专注于服务端底层通用工具、框架和系统的研发,为公司的Web服务端及服务端提供易用、可靠的基础服务支持。
QConf是奇虎360广泛使用的配置管理服务,现已开源,欢迎大家关注使用。
https://github.com/Qihoo360/QConf
本文从设计初衷,架构实现,使用情况及相关产品比较四个方面进行介绍。
设计初衷
在分布式环境中,出于负载、容错等种种需要,几乎所有的服务都会在不同的机器节点上部署多个实例。而业务项目中又总少不了各种类型的配置文件。因此,我们常常会遇到这样的问题,仅仅是一个配置内容的修改,便需要重新进行代码提交SVN/Git、打包、分发上线的全部流程。当部署的机器有很多时,分发上线本身就是一个很繁杂的工作。何况,配置文件的修改频率又远远大于代码本身。
追本溯源,我们认为麻烦的根源是日常管理和发布过程中不加区分配置和代码造成的。配置本身源于代码,是我们为了提高代码的灵活性而提取出来的一些经常变化的或需要定制的内容,而正是配置的这种天生的变化特征给我们带了巨大的麻烦。
因此,我们开发了分布式配置管理系统QConf,并依托QConf在360内部提供了一整套配置管理服务,QConf致力于将配置内容从代码中完全分离出来,及时可靠高效地提供配置访问和更新服务。
整体认识
为了让大家对之后的内容有个直观的认识,先来介绍一下如果你要在自己的项目中使用QConf应该怎么做:
首先,需要在自己的机器上部署QConf,QConf采用cmake构建,非常容易。
mkdir build && cd build
cmake ..
make
make install
其次,通过Zookeeper客户端或QConf管理界面在Zookeeper上建立自己的节点结构,节点完整路径便是QConf的key值,以360公司内部QConf管理界面为例:
图1 QConf管理界面
最后,选择自己项目的语言版本的QConf库,并在需要需要获得配置内容的位置,直接调用类似于get_conf(key)这样的接口,并放心每次取得的都是最新鲜出炉的配置。以shell命令为例:
图2 QConf shell接口使用
需要说明的是,使用QConf后已经没有所谓的配置文件的概念,你要做的就是在你需要的地方获取正确的内容。我们认为,这才是你真正想要的。
架构介绍
了解了QConf的设计初衷和对其有了整体认识之后,相信大家已经有一个对QConf实现的猜想。在介绍架构之前,还需要向大家申明一下我们对配置信息的定位,因为这个定位直接决定了我们的结构设计和组件选择。
- 单条数据量小
- 更新频繁(较代码而言)
- 配置总数可能巨大,但单台机器关心配置数有限
- 读多写少
进入主题,开始介绍QConf的架构实现,下图展示的是QConf的基本结构,从角色上划分主要包括 QConf客户端 , QConf服务端 , QConf管理端 。
图3 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客户端的架构:
图4 QConf客户端架构
可以看到QConf客户端主要有:Agent、各种语言接口、连接他们的消息队列和共享内存。
在QConf中,配置以Key-Value的形式存在,业务进程给出key获得对应Value,这与传统的配置文件方式是一致的。
下面通过两个主要场景的数据流动来说明他们各自的功能和角色:
场景1.业务进程请求数据:
图5 数据流动-业务进程请求数据
- 业务进程调用某一种语言的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线程:该线程负责一些周边逻辑的调用。
关于周边操作的补充说明:
- Dump操作:将共享内存的内容同步一份到本地,QConf采用的gdbm。
- Feedback操作:QConf支持更新反馈的功能,可向用户指定Web服务以一定的格式发送反馈。
- Script操作:在某些情况下,业务希望当配置变化时,做一些自定义的操作,QConf支持配置变化时调用用户脚本,Agent按一种固定的约定在配置发生变化时调用对应的脚本。
QConf管理端
管理端是业务修改配置的页面入口,利用数据库提供一些如批量导入,权限管理,版本控制等上层功能。 由于公司内的一些业务耦合和需求定制,当前开源的QConf管理端这边提供了一个简易的页面,和一套下层的c++接口,之后计划进一步完善以及跟社区合作提供更友好的界面。如下图:
图9 QConf简易管理界面
QConf的结构及实现大概就介绍到这,接下来...
One More Thing
QConf 除了存储配置的基本功能外,还在公司内提供了一套简单的服务发现功能,该功能允许业务在QConf上配置一组服务,QConf会监控其服务的存活。当业务进程调用获取服务的接口时,会根据用户需求,返回全部可用服务,或某一可用服务。
不同于普通配置:
- 结构上多一个Monitor的角色,来监控所有服务的存活。
- 提供对应的客户端接口,
get_host
获取某一可用服务,get_allhost
获取所有可用服务。 - 管理端页面对应的展示方式及操作,尤其是对指定服务的添加删除,上线下线。
如下图:
图10 提供服务发现的QConf结构
需要明确的是,目前Monitor事实上仅仅是通过查看服务端口的存活来判断的,在实际生产环境中,该功能多与实际服务提供者的监控结合,由服务提供者的监控调用QConf的相应接口实现服务的上下线。
使用方式及使用场景
目前360内部已经广泛的使用QConf,覆盖云盘、大流程、系统部、DBA、图搜、影视、地图、硬件、手机卫士、好搜等大部分业务。部署国内外共51几个机房,客户端机器超两万台,稳定运行两年。
使用的方式主要包括:
简单配置
公司内使用最广泛的用法,QConf非常适合经常需要变动的配置使用,如开关信息、版本信息、推荐信息、超时时间等。
服务方式
这种方式多被服务提供者采用,如DBA,系统部等,采用上述的服务配置的方式,通过QConf向公司的所有业务提供存储,计算及WEB服务。
配置管理类产品比较
通过上面的介绍,大家应该感觉到QConf与puppet,confd这类产品还是有很大不同的。 国内与之类似的产品包括淘宝的diamond,微博的vintage,百度的disconf。 其中diamond和vintage比较类似,他们将配置数据存在像mysql或redis这样的存储中,并需要通过客户端的轮训来感知配置变化,这样会有很多的无效通讯,因此采取比较md5值的方式来避免整个配置值的传输;
disconf与QConf类似,同样采用ZooKeeper的通知机制,不同的是,其将真实的配置数据存在mysql中,并且整个与java耦合很重,且配置复杂。
QConf因为其对配置信息的定位,使得整个结构非常简单,容易部署和使用。
Q&A
Q1:QConf客户端主要有:Agent、各种语言接口、以及连接他们的消息队列和共享内存。这样会不会客户端太重了,CPU和内存负载如何呢?
从上面的描述上可以看到,当配置不更新的时候,Agent其实是没有什么事需要做的,而数据的量一般不会太大,所以对CPU和内存,压力都不大。数据主要在共享内存,默认开128M,对单台服务器的配置量来说很充足了。而共享内存和消息队列都是操作系统维护的基础IPC。这里客户端太重可能主要体现在部署成本上,不过我们采用cmake的方式,可以使部署很方便。
Q2:如果ZooKeeper死掉一个,再添加新的ZooKeeper的时候,怎么让其他客户端知道这个新添加的?
如果IP没有变化的话没关系,新增ZooKeeper现在确实是需要重新配置的,而且ZooKeeper集群需要重启。
Q3:请问,客户端与 Agent 之间是同步还是异步?
1.是一个异步的过程,客户进程把不存在的Key放入消息队列中后,需要等Agent取,现在的做法是分100次重试,每次5ms,如果取到就返回,生产环境一般在10到15ms通常就可以取到。
2.由于一些业务对等待时间较敏感,我们也提供了不等待的方式,加入消息队列后直接返回,重试时间有上层决定,可以由特定的参数制定。
Q4:如果Agent挂了,是否无法感知?
1.Agent采用父子进程keepalive的方式,可以一定程度上避免。
2.生产环境会部署Agent的监控从外部感知这件事,并及时解决,解决的过程中,还可以访问到原始数据。
3.我们有反馈接口,可以在管理端接受这个反馈,在每次修改后确保自己的服务全都正常更新。
Q5: 配置项变更时,怎样解决客户端拉取时间差的问题?
这就是我们采取Zookeeper的一个好处,不需要客户端轮训的查询变化情况;变化后,服务端会通过Watcher通知客户端,这个时间很快,生产环境能保证是秒级,而且没有无效通讯。
Q6: 有没有办法修改QConf的目录?我看代码 貌似在链接二进制文件的时候把目录指定为prefix了,我向编译成二进制文件后,通过部署系统发布,目录可能变化。
可以的,cmake安装的时候可以指定
Q7:为啥不考虑持久化配置文件,再reload进程,可以彻底避免ipc带来的成本?
1.现在有持久化配置,采用的是gdbm,不过只有在机器重启且网络中断的情况下使用。
2.直接使用配置文件会有访问效率的问题。现在这种使用方式,业务需要每次都从QConf读数据,当读取次数比较多的时候会很影响业务进程。
3.如果我们同时维护一份内存数据的话,同样有两个版本不同步的问题,有一个是优先使用的,就像我们现在的状况,共享内存>网络>持久化配置。
Q8:服务方式可以展开下吗?提供计算服务资源什么意思?服务地址?
服务方式对QConf来说是一种特殊的配置,在Zookeeper上有一些特殊的结构来存储,服务就是ip:port,不论是dba的存储服务还是系统部的计算服务,在QConf看来是一样的,就是监控这些ip:port存活并在用户调用相关接口如get_host时返回可用的服务地址。
Q9:能否展开讲一下『管理端是业务修改配置的页面入口,配合数据库提供一些如批量导入,权限管理,版本控制等上层功能。』?
管理端是用来方便用户管理修改配置的,在内部提供了的一些如批量导入,权限管理,版本控制的功能,这些上层功能有时会需要数据库配合存一些数据,这部分目前的开源版本还没有这么完整,会在后续完善。
Q10:同样的配置,在不同的机器上有个别配置项值不同,QConf是怎么处理?
内部这种情况多发生在不同机房的情况下,我们是在每个机房都配置一套Zookeeper集群,这样就可以不同机房不同的配置取到的值不同,不知道这种可否满足。
Q11:因为相同的服务的不同实例可能有少量配置不同,为了区分这些不同的实例可能需要给每个实例分配一个id以便在管理端区别对待。QConf 中有没有遇到这种问题?是怎么处理的呢?id 是由管理端分配还是服务实例自动生成?
现在使用是不同的配置需要不同的配置项,把相同的配在一起。目前管理端没有做这种自动生成id,不过Zookeeper有类似功能,可以具体讨论下,如果比较常见的话,我们可以考虑支持的。
Q12:dump数据是怎么做的,是获取到某个时刻所有数据的镜像去dump还是边读配置边写文件?
1.dump是采用gdbm做的,所以很方便修改添加单条数据。
2.现在是每次更新或删除共享内存中的配置,就对应dump一份该条配置。
Q13:360现在Zookeeper集群的客户数达到了多大规模?之前有听说Zookeeper集群规模是有限的,到三四千个客户数就很难上去了。是这样吗?
我们总的客户端大概两三万机器,但因为每个机房都有部署Zookeeper集群,一共有51个机房,所以单个上面的压力还好,Zookeeper主要是Watcher的消耗,现在主力机房有Watcher过两万的,这些会用配置较好的实体机,一般机房虚机就够了。
Q14:Zookeeper的Znode多了会影响性能吗?配置文件是按组来存在Znode的Value中,还是说每个配置项就是一个Znode,那样会很多吧?
单个配置就是个Znode,Zookeeper在几十万的Path级别内没有性能问题,现在主力机房有Znode达到2万,没问题,不过如果Wather比较多的话会很吃内存,另外快照文件生成较多,最好定时清理。
Q15:Zookeeper丢失通知可以感知到吗?
1.Watcher触发过程中的变化,客户端感知不到,所以Agent每次收到通知都会去取数据并重新注册Watcher。
2.如果是端或者网络的问题会导致Zookeeper会话失效,重连的时候Aagent会把需要的节点都重新拉一遍。
3.其他丢失情况较小,Agent的Scan操作就是以防这种情况的,大概半个小时到一个小时一次。
Q16:监控到一个Zookeeper挂掉了,它会不会自动切换到另一个?还是要重新手动部署?
不用,zk集群只要有一半以上的机器存活就能正常提供服务。
感谢四正的记录与整理,其他多位编辑组志愿者对本文亦有贡献。更多关于架构方面的内容,请长按下面图片,关注“高可用架构”公众号,获取通往架构师的宝贵经验。