介绍:[http://gitlab.alibaba-inc.com/middleware/diamond/wikis/Diamond-Intro]
客户端:
一、 client获取server地址
diamond-client在使用时没有指定server地址的代码,地址获取对用户是透明的。
server地址存储在一台具有域名的机器上的HTTP server中,我们称它为地址服务器,diamond-client使用前需要在本地进行正确的域名绑定,启动时它会根据域名绑定,去对应环境的地址服务器上获取diamond-server地址列表。
获取的地址列表,会保存在client本地,当出现网络异常,无法从网络获取地址列表时,client会使用本地保存的地址列表。
client启动后会启动一个定时任务,定时从HTTP server上获取地址列表并保存在本地,以保证地址是最新的。
- 1、指定地址:http://jmenv.tbsite.net:8080/diamond-server/diamond
- 2、读取详细过程
- (1)定时轮询获取地址服务器上的地址信息: 在DiamondEnvRepo类定义静态变量DiamondEnv defaultEnv 时,初始化ServerList信息,实际初始化工作,交由类ServerListManager执行,ServerListManager类的功能介绍为:“启动时和运行时定期获取地址列表。启动时拿不到地址列表,进程退出”。在ServerListManager.start()方法中,首先启动一个线程task,循环地去获取地址服务器上的地址信息,直到成功。然后,在启动一个调度器ScheduledExecutorService.scheduleWithFixedDelay(Runnable task, 0L, 30L, TimeUnit.SECONDS),每隔30s,重新去获取地址服务器地址信息。
- (2)获取到地址服务器数据之后: 得到地址服务器的serverUrlList之后,校验serverUrlList是否有更新,若更新,则把新的serverList保存到本地。同时,若有次事件的监听者的话,则需要通知监听者,系统默认没有监听者。
二、client主动获取数据
client调用getAvailableConfigInfomation(), 即可获取一份最新的可用的配置数据,获取过程实际上是拼接http url,使用http-client调用http method的过程。
为了避免短时间内大量的获取数据请求发向server,client端实现了一个带有过期时间的缓存,client将本次获取到的数据保存在缓存中,在过期时间内的所有请求,都返回缓存内的数据,不向server发出请求。???
剖析:
获取数据顺序:按照本地容灾 -> server -> 本地缓存的优先级获取配置。
第一步,从服务器本地容灾目录获取,若本地没有放此文件,则跳过,本地目录:System.getProperty("JM.LOG.PATH", System.getProperty("user.home")) + File.separator + "diamond"+env.serverMgr.name + "_diamond"。
第二步,前提:(1)不成功。通过http get请求,从服务器上获取,若没有,则跳过;
第三步,前提:(1)(2)不成功。则获取本地缓存文件内容,快照目录:localFileRootPath+envName + "_diamond"。
三、客户端轮询获取更新的配置信息:
1、如何实现“基于推模型的客户端长轮询的方案”?
推模式轮询:
推模式详解图
长轮询连接使用场景剖析:
(1)在客户端探测配置文件(所有)是否有变更时,将在ClientWork中做一次post请求。
(2)服务端接受到此请求后,并不是直接同步地在服务器进行处理,而是使用了servlet3.0特性AsyncContext进行处理。服务器端使用单独线程ClientLongPulling,进行判断是否有数据更新。在run()方法中,首先会检测一遍此groupKey配置是否有更新,若有更新则读取配置并发送响应,若没有更新,则不了了之。接着,将把此长轮询连接ClientLongPulling加入到长连接池子allSubs(加入到池子是有用的,在后面将讲到,服务器端数据更新时,服务端能把更新的数据,根据groupKey找到对应的ClientLongPulling,并把此数据设置到http response中,发送响应。),同时,启动一个定时调度,用于在这个长连接超时的时间点,把这个长连接关掉。
(3)当客户端接收到响应或者等待超时时,此轮的轮询结束。同时ClientWork中的调度器ScheduledExecutorService.scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)将延迟指定的时间,再次发起轮询。这个调度器方法解析为:“创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。”
上面的机制跟Comet非常类似,Comet是这样解释的: Comet 是一个 Web 应用模型,在该模型中,请求被发送到服务器端并保持一个很长的存活期,直到超时或是有服务器端事件发生。在该请求完成后,另一个长生存期的 Ajax 请求就被送去等待另一个服务器端事件。
上面讲述的代码示例:
a. 使用单独线程执行asyncContext:
final AsyncContext asyncContext = req.startAsync();
// AsyncContext.setTimeout()的超时时间不准,所以只能自己控制
asyncContext.setTimeout(0L);
scheduler.execute(new ClientLongPulling(asyncContext, clientMd5Map, probeRequestSize));
b.asyncContext发送响应的动作
asyncContext.complete();
AsyncContext是个异步上下文,包含了request所有信息, final AsyncContext asyncContext = req.startAsync(); 这个语句,将释放掉当前HTTP请求线程,同时使响应延迟,只有调用asyncContext.complete()方法或dispatch()为止 ,才会发送响应。
2、在ClientWork轮询时,详细步骤如下:
(1)checkLocalConfigInfo();//检查本地容灾文件,若是否使用本地容灾的策略有改变的话,需要触发Listener
(2)checkServerConfigInfo();//
在ClientWork.checkLocalConfigInfo()方法中,若使用本地容灾策略,检测到内容有更新,即可触发Listener进行处理checkListenerMd5(env),本地策略变化,需要要触发Listener,本地文件在什么情况下被修改?运维人员手动拷贝文件到diamond文件容灾目录?是这样的。
触发Listener的方式,使用的是EDA(事件驱动模式),new 一个新线程,来执行Listener业务逻辑。
3、探测服务器配置文件是否有更新 checkServerConfigInfo(),在ClientWork.getProbeUpdateString(DiamondEnv env)方法,怎样判断是否有更新的?//为啥跟userLocalConfig有关?
4、探测服务器配置文件是否更新逻辑:先通过http接口(post)获取更新的dataIds,在逐一获取(get)每个dataId的配置。
5)使用HTTP Get读取数据时,分别从serverList中,依次请求server,只有其中一个server请求成功了,就终止请求,返回结果。
服务端:
一、具有可靠。主要依赖以下几点特性。
1、无中心化。 即分布式系统体现,数据同步机制。
Q:服务端每个节点都以本地磁盘文件的形式保存了全量的配置数据作为cache,如何做到的?
A: 配置新增或更新后,先把配置保存到数据库,然后根据serverList,通知所有的服务器,让服务器从数据库读取数据进行同步。详细流程见下面《server集群数据同步》分析。
2、mysql 一主两备
3、使用地址服务器组件
上面已经有说明。
4、容灾机制:
详细讲解,可以参考淘宝中间件博客:http://jm-blog.aliapp.com/?p=1617。
二、server集群数据同步
diamond-server将数据存储在mysql和本地文件中,mysql是一个中心,diamond认为存储在mysql中的数据绝对正确,除此之外,server会将数据存储在本地文件中。
同步数据有两种方式:
1、server写数据时,先将数据写入mysql,然后写入本地文件,写入完成后发送一个HTTP请求给集群中的其他server,其他server收到请求,从mysql中dump刚刚写入的数据至本地文件。
剖析:
(1)发送http请求通知其他server部分:
这里使用事件驱动模式,EventDispatcher是事件分发器,把数据更新封装成一个ConfigDataChangeEvent,然后通过fireEvent(),把此Event分发给对应的监听者Listener(一般一种Event类型对应一个Listener),这里的Listener实现类是:NotifyService,NotifyService 类提供onEvent()方法,在onEvent()方法中进行分发操作。
分发过程也很精彩,先讲这里使用到了工具类-任务管理类TaskManager。这个类作用:“用于处理一定要执行成功的任务 单线程的方式处理任务,保证任务一定被成功处理”。在这个类中,使用一个taskList来保存任务,然后有一个专门的线程,不断地反复执行taskList中的任务,执行频率为100ms。任务类为Task实现类,同时需要定义一个任务处理器TaskProcessor,任务处理器对任务进行处理,一般通过TaskProcessor.process(task)方法。
回来看看分发是怎样进行的。数据更新分发类NotifyService,单独使用了一个的TaskManager(就是自己new了一个啦),用于执行分发任务。同时,实现了Task接口,实现类NotifyTask;实现了TaskProcessor接口,实现类NotifyTaskProcessor。重点就在这个NotifyTaskProcessor了,NotifyTaskProcessor类中process()方法中,遍历集群中的所有服务器,逐一通知各个服务器,数据有更新了,要各自在本地执行一次dump。
通知各个服务器使用的是http请求方式,调用的url为:http://{0}:8080/diamond-server/notify.do?method=notifyConfigInfo&dataId={1}&group={2}。这里只是告知各个服务器数据更新了,dump数据是从数据库进行dump的,并不是在HTTP通知请求中附带过去,这样做的好处是,确保数据的一致性。
好,下面来看一看各个服务器收到数据更新的请求后,是如何进行处理的。
(2)集群所有服务器收到数据更新请求,dump数据到本地文件部分:
从HTTP请求可以找到处理类:NotifyController,处理方法是:NotifyController.notifyConfigInfo()。
各个服务器dump数据到本地文件的过程,也是通过工具类-任务管理类TaskManager完成,即每个更新通知,都使用单独的一个Task,把这个Task交给TaskManager进行处理。
dump服务类DumpService,也单独使用了个TaskManager(new 了一个),同时实现了TaskProcessor接口,实现类DumpProcessor。
在DumpProcessor.process()中把数据更新到本地磁盘。
自此,服务端更新数据的分发完成。
紧接着上面继续往下看,服务端发现更新的数据跟本地保存的不一致时,服务端同样使用事件驱动模式,触发相应的监听器,把更新的数据进行处理。
在ConfigService中执行dump操作,数据保存到本地磁盘之后,做checkMD5操作,发现数据有更新,将生成一个本地更新事件LocalDataChangeEvent,把这个事件交由相应的监听器LongPullingService进行处理。
在LongPullingService的onEvent()处理方法中,新建各一个任务DataChangeTask,这个任务由一个线程调度器执行。其实DataChangeTask就是一个实现了Runnable接口的线程类。
在这个线程类DataChangeTask的run()方法中,做了一件事情:先从长轮询连接List<ClientLongPulling>中,找到与此更新数据groupKey相同的长轮询连接ClientLongPulling A;然后把更新数据设置到这个ClientLongPulling A的响应中,结束此长轮询连接。有关长轮询连接,这是个很有意思的机制,是servlet3.0提供的,servlet可以支持异步响应。请参考文章《AsyncContext 简介》。
总结,过程如下:服务器端还接受客户端长轮询,客户端探测配置是否更新,在客户端ClientWork中checkUpdateDataIds()进行探测,探测超时时间为30s。在服务端ConfigServlet中doPost()接受请求,使用长轮询类:LongPullingService进行处理,其中ClientLongPulling中的run()方法进行定时调度,用于终止连接。
2、server启动后会启动一个定时任务,定时从mysql中dump所有数据至本地文件。
在DumpService构造函数中,服务器启动时先全量dump一次所有数据,同时启动定时器,间隔6小时dump全量数据至本地文件。