现在有一个分布在多台主机上,每个主机上都有多个进程的http服务,这些所有的应用都是独立、平等的,唯一联系是同时都连接着同一个数据库集群。
现在有一个需求,需要从另外一个数据源( http 接口)定时同步数据到我的数据库中。
最简单的办法是每个进程都设置一个定时同步任务,但是这样的话,不论是对对方接口的请求,还是对数据库的更新都会有很多的无效操作,如果没有其它更好的方案的话,才会考虑使用这个方案。
之后想到了一个方案,虽然应用是多进程的,但是每个 http 请求都只有一个特定应用来处理,这样的话,我只要开发一个接口,触发接口之后才开始同步数据,这样部署完应用的时候我只去触发一次接口,就可以保证只有一个进程在定时同步数据。这种方案的问题有1.无法立即确认确实有一个进程在同步了,这个可以通过在同步时加入日志,通过日志来确认 2.这种模式下,每次部署完之后都要先手动调用一次这个接口,不够自动化 3.如果执行同步任务的进程因为什么原因挂掉了,除非经常检查日志,否则很难及时发现同步任务已经挂掉 4.难以确认是哪台机器的哪个进程在做同步任务,这个同样可以通过查看日志来确认
然后又想到了一个通过数据库来解决问题的方案。在进程启动的时候首先每个进程都向数据库写入一个记录,然后等待一段时间后,比如半小时或者一小时之后,检查数据库上的记录中中的最后一个,把其它的记录都删掉,然后各个服务器上的各个进程开始和数据库中的记录对比,剩余的一条是哪个进程写入的那个记录,就使用那个进程来同步数据。但是这样仍然不能解决执行同步任务的进程挂掉时候,不能及时发现并解决问题的缺点。
最后,在数据库集群治理模式上找到了灵感。我完全可以模仿mongodb复制集, 把这些相互独立的进程看作一个集群,每次同步时从中选择一个进程作为同步进程即可,这样只要有至少一个进程存活即可。而如果所有进程都没有在存活了,那么服务整体都已经不可用了,也会很容易就发现问题。
最终解决方案的具体实现方法是:
首先定义一个心跳时间,例如每10s。
每隔一个心跳时间,每个进程都向数据库更新自己的最后存活时间;
然后设定如果每个进程超过3个心跳时间没有与数据库做交互,就判定为该进程已挂掉;
每2个心跳时间,每个进程都去验证自己是不是独特进程,如果是独特进程,就继续执行预定的定时任务,如果已经不是(有可能由于后来突然又新增了进程导致当前独特进程不再是独特进程),就停止掉当前进程的定时任务。
下面是具体的步骤和示例代码:
1. 注册应用:初始化应用启动时间、最后存活时间
id = db.model.create(createTime, time);
2. 所有应用间隔每个心跳时间定时更新自己的最后存活时间
setInterval( update(id, time), heartbeatTime );
3. 所有应用获取按应用id排序倒数10个应用id(也就是最后10个启动的应用),用最后有效存活进程作为可用进程,在所有有效存活应用中,选择按id排序而非时间排序的最后一个进程作为独特进程(也可以按创建时间来选择,只要不是按照最后存活时间就行),这个步骤也是在每个心跳时间定时执行
setInterval(func() {
list = db.getList().sort({ id: -1 }).limit(10);
list = list.filter( now - time < heartbeatTime * 3 );
porcessIsUnique = ( id == list[0].id );
}, heartbeatTime)
4. 定时检查当前进程是否是独特进程
4.1.当前进程是独特进程
4.1.1.定时任务还没有开始执行,开始执行定时任务
4.1.2.定时任务已经开始执行,继续执行定时任务,不做其他操作
4.2. 当前进程不是独特进程
4.2.1. 如果定时任务已经在执行,停止定时任务
4.2.2. 如果没有定时任务,不做其它操作
setInterval( func() {
interval = null;
if(processIsUnique) {
if(!interval) {
// 这里执行需要执行的定时任务
interval = setInterval( syncJob, syncTime );
}
}
else {
if(interval) {
clearInterval(interval);
interval = null;
}
}
}, heartbeatTime * 2)
这个模式有两大优点: 1. 完全自治,部署完即可自动运行,不再需要手动控制 2.无缝热更新扩展,无论是扩大还是缩小集群规模,均可自动适配,不需要重新部署