花了两天的时间研究了下Diamond,因为写得比较急,而且并没有使用过,只是单纯的做逆向建模,所以难免会有细节缺失,后面会时不时过来看看,然后做些补充。
背景知识
比较早的时候,应用一般都是单体的,配置修改后,只要通过预留的管理界面刷新进行reload即可。
慢慢的,应用都主动或被动被拆分,从单一系统拆分成多个子系统,每个子系统还会对应多个运行实例。此时就面临多个问题:
1. 配置分散在多个业务子系统里,对同一配置的翻译在多个子系统里经常不一致。比如订单和购物车都有货币类型的配置,如果购物车上了一种新的货币类型而订单却没有相应同步增加配置项就会造成程序错误。
2. 将配置收敛成一个公有服务后,以上情况开始好转,但是又带来其他问题。在复杂应用里,修改一个配置项后,无法确切的知道需要刷新哪些相关子系统。最终只能做全量刷新,甚至是停机发布。这对于一些停机敏感的行业例如电商几乎是无法接受的。
3. 配置收敛后,成为了应用中的单点,配置如果挂了,相应的应用也会跟着产生异常甚至也挂掉,配置服务器需要保证高可用。
Diamond就是为了解决这些问题而产生的。
Diamond的配置类型
配置是Diamond的核心域,也是Diamond致力于去解决的问题。首先看下Diamond的两个主要配置类型– single和aggr。二者结构如下:
ConfigInfo ConfigInfoAggr
dataId dataId
group group
content content
md5 datumId
appName appName
Aggr没有md5多一个datumId。md5是用于校验数据是否一致对配置数据进行md5编码生成的字符串。Aggr显然不是一个完整的配置项,因为没有md5就无法校验配置是否改变。Aggr会通过后台的定时任务转成single,最后一节会做具体介绍。
整体架构设计
下图是Diamond的组件视图。Diamond主要有ops, sdk, client和server 4个组件。Ops是运维用的配置工具,主要用于下发以及查询配置等;server则是Diamond的后台,处理配置的一些逻辑;sdk则是提供给ops或者其他第三方应用的开发工具包;client则是编程api,它和sdk乍看有点像,其实差别很大,sdk是用于构建前台运维配置程序的,本质是对数据的维护,而client则是这些数据的消费者,事实上准确的说是diamond的消费者们(各子系统)都是通过client组件对server访问。
配置是保存在mysql数据的,为了避免数据库被峰值访问压垮,Diamond server通过local file做本地缓存,来自各子系统访问压力都被分摊到了server集群的各个节点。
Diamond server是无中心节点的逻辑集群,这样就能避免单点故障。Diamond的同质节点之间会相互通信以保证数据的一致性,每个节点都有其它节点的地址信息,其中一个节点发生数据变更后会响应的通知其他节点,保证数据的一致性。
为了保证高可用,client还会在app端缓存一个本地文件,这样即使server不可用也能保证app可用。Client不断长轮询server,获取最新的配置推送,尽量保证本地数据的时效性。
长轮询
Client默认启动周期任务对server进行长轮询感知server的配置变化,server感知到配置变化就发送变更的数据编号,客户端通过数据编号再去拉取最新配置数据;否则超时结束请求(默认10秒)。拉取到新配置后,client会通知监听者(MessageListener)做相应处理,用户可以通过Diamond#addListener监听。
服务端通过AsyncContext对请求做非阻塞处理,通过定时任务感知配置变化。
详细描述下上图,
1. server收到请求后启动AsyncContext,并基于它构造ClientLongPulling。ClientLongPulling除了AsyncContext还有一个超时回复任务对长轮询请求做超时处理
2. 之后ClientLongPulling被加入等待列表。LongPullingService关联一个感知数据变化的定时任务,当有数据变化时,就会循环等待列表里的ClientLongPulling,推送数据变化回客户端。
4. Dump是抽象出来的一块儿概念,server的数据变化都会触发响应的dump task,并会发送相应的事件,由server感知,DataChangeTask也是一个事件监听者,能感知local file的数据变化。
服务端架构设计
再来看一下server的架构设计。第二节介绍过Diamond集群是去中心化的,并且会通过通知机制保证集群各节点数据一致。
1. Diamond集群内每个节点都有其他节点的地址信息,当一台节点对配置做了修改后会触发ConfigDataChangeEvent,从而触发通知所有节点数据变更的任务。这里需要注意的一点是通知所有节点也包括自己,下发配置的请求只会更新数据库,并不会更新本地文件缓存。
2. 通知发送到所有节点后,通过dump更新local file,更新完毕后发送LocalDataChangeEvent,之后就是上节提到的过程。
为了保证Diamond的数据一致性,server还会run一些其他任务,如下图:
1. DumpAllTask会6小时run一次做全量dump,删除老的缓存数据,全量生成新缓存。
2. DumpChangeTask做增量dump。通过和数据库的配置做md5对比,删除被移除配置的文件缓存,更新有md5不一致的文件缓存等等。
3. 清除历史数据是用于清除1周前的数据库his表数据。
4. merge把dataId相同的aggr合并成单条single,插入到数据库,并触发相应的配置变更事件,见上节。
5. 心跳任务用于记录心跳时间,节点服务重启时会判断距离上次心跳时间是否已经超过一小时,超过一小时则做全量dump,否则做增量。
剩下的都很好理解,不再一一介绍,需要注意的是,这些任务里并不是所有的都是定时做,有些事事件触发的,例如DumpTask;还有些是Diamond服务启动时触发的,如merge,DumpChangeTask。