使用ZooKeeper协调多台Web Server的定时任务处理(方案1)
背景说明: 有一套Web服务程序, 为了保证HA, 需要在多台服务器上部署, 该服务程序有一些定时任务要执行, 现在要保证的是, 同一定时任务不会在多台机器上被同时执行.
方案1 --- 任务级的主备方案:
每个定时任务启动后, 都发起任务级的主节点的竞争, 胜出者执行具体任务.
方案2 --- 服务器级的主备方案, 需要两个组件:
组件一: 一个后台线程用来竞争Leader
具体细节为: Web服务程序中开一个后台线程来竞争作为服务级别的Leader, 优胜者将自己的 ${服务器名}+${端口} 记录到zk 中.
组件二:每个定时作业, 在开始的时候, 先对比本机是否是服务主节点, 如果是主节点即执行具体任务, 否则跳过.
方案1的说明:
1. 每个定时任务都需要竞争Leader, 任务的执行效率较差.
2. 如果两个服务器的时间不同步, 定时任务耗时又很短, 在这种情况下容易double run, 需要故意延长任务执行时间以避免double run.
3. 有的定时任务在其中一台上执行, 另一些在另一台上执行, 查日志不是很方便.
4. 因为是在每个定时任务启动的时候竞争leader, 不必关心任务执行过程中, 由于zk客户端长连接断开需要进行leader切换的问题.
5. 本方案采用了Curator 的 LeaderLatch 选举机制.
方案2的说明:
1. 该方案能很好地从几台服务器中选出一个Master机器, 不仅仅可以用于定时任务场景, 还可以用在其他场景下.
2. 该方案能实现Master节点的自动 failover, 经我测试 failover 过程稍长, 接近1分钟.
5. 本方案采用了Curator 的 LeaderSelector 选举机制.
==============================
LeaderLatch 和 LeaderSelector 两种选举实现
==============================
LeaderLatch 的方式:
是以一种抢占的方式来决定选主. 比较简单粗暴, 逻辑相对简单. 类似非公平锁的抢占, 所以, 多节点是一个随机产生主节点的过程, 谁抢到就算谁的.
LeaderSelector 方式:
内部通过一个分布式锁来实现选主, 并且选主结果是公平的, zk会按照各节点请求的次序成为主节点.
LeaderLatch 和 LeaderSelector 本身也提供 Master 节点的自动failover, 经我测试 failover 过程都稍长, 有时会接近1分钟.
下文先讲解 LeaderLatch 相关知识, 以及用 LeaderLatch 实现方案1 的过程.
==============================
Curator 中 LeaderLatch 相关函数
==============================
最简单的构造子
public LeaderLatch(CuratorFramework client, String latchPath)
leaderLatch.start()
start()让zk 客户端立即参与选举, zk server最终会确定某个客户端成为leader.
leaderLatch.hasLeadership()
检查是否是Leader, 返回值为boolean, 该函数调用会立即返回.
leaderLatch.await()
阻塞调用, 直到本客户端成为Leader才返回.
leaderLatch.await(long timeout, TimeUnit)
阻塞调用, 并设定一个等待时间.
leaderLatch.close()
对于参与者是Leader, 只有调用该方法, 当前参与者才能失去Leader资格, 其他参与者才能获取Leader资格.
对于其他参与者, 调用该方法将主动退出选举.
leaderLatch.addListener()
增加一个Listener监听器, 当参与者成为Leader或失去Leader资格后, 自动触发该监听器.
client.getConnectionStateListenable().addListener()
为zk 客户端增加一个监听器, 用来监听连接状态, 共有三种状态: RECONNECT/LOST/SUSPEND
当连接状态为 ConnectionState.LOST 时, 写代码强制客户端重连, 以便该客户端能继续参与Leader选举.
当连接状态为 ConnectionState.SUSPEND 时, 我们一般不用处理, 输出log即可.
=============================
环境准备
=============================
在 VM (192.168.1.11) 上启动一个 zookeeper 容器
docker run -d --name myzookeeper --net host zookeeper:latest
在Windows开发机上, 使用 zkCli.cmd 应该能连上虚机中的 zk server.
zkCli.cmd -server 192.168.1.11:2181
=============================
SpringBoot 服务程序
=============================
增加下面三个依赖项, 引入 actuator 仅仅是为了不写任何代码就能展现一个web UI.
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<!--org.apache.curator 依赖 slf4j -->
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
完整Java 代码
package com.example.demo; import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.framework.recipes.leader.LeaderLatch;
import org.apache.curator.framework.recipes.leader.LeaderLatch.State;
import org.apache.curator.framework.recipes.leader.LeaderLatchListener;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.curator.framework.state.ConnectionStateListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.utils.CloseableUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; /*
* 主程序类
* */
@EnableScheduling
@SpringBootApplication
public class ZkServiceApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(ZkServiceApplication.class, args);
}
} /*
* 常量工具类
*/
class ZkTaskConst {
public static final String SERVICE_NAME = "ServiceA";
public static final String SERVICE_SERVER = "Server1:8080";
public static final String ZK_URL = "localhost:2181"; // 为了确保能选出一个leader, 需要等待一会儿,
public static final int WAIT_SECONDS_ENSURE_BE_LEADER = 20; // Task运行完毕后, 再Hold leader一会儿, 以防止多个服务器时间不准导致作业double run
public static final int SLEEP_SECONDS_AFTER_TASK = 30; public static String getZkLatchPath(String taskName) {
return String.format("/%s/%s", SERVICE_NAME, taskName);
}
} /*
* Leader 选举Listener
*/
class ZkTaskLeaderLatchListener implements LeaderLatchListener {
private static final Logger log = LoggerFactory.getLogger(ZkTaskLeaderLatchListener.class); @Override
public void isLeader() {
log.info(String.format("The server (%s) become the leader", ZkTaskConst.SERVICE_SERVER));
} @Override
public void notLeader() {
log.debug(String.format("The server (%s) has not been the leader", ZkTaskConst.SERVICE_SERVER));
}
} /*
* Zk Connection 监听器
* 如果 zk client 长连接断开后, 需要重连以保证该客户端仍能参与 Leader 选举.
* 对于定时任务级的Leader选举, 这个监听器并不重要.
* 对于服务器级别的Leader选举, 这个监听器很重要.
*/
class ZkConnectionStateListener implements ConnectionStateListener {
private static final Logger log = LoggerFactory.getLogger(ZkConnectionStateListener.class); @Override
public void stateChanged(CuratorFramework client, ConnectionState newState) {
log.debug("Zk connection change to " + newState);
if (ConnectionState.CONNECTED != newState) {
while (true) {
try {
log.error("Disconnected to the Zk server. Try to reconnect Zk server");
client.getZookeeperClient().blockUntilConnectedOrTimedOut();
log.info("Succeed to reconnect Zk server");
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
}
}
}
}
} /*
* 定时任务控制器类
*/
class ZkTaskController {
private static final Logger log = LoggerFactory.getLogger(ZkTaskController.class);
private CuratorFramework client;
private LeaderLatch leaderLatch;
private String taskName;
public boolean isLeader = false; public ZkTaskController(String taskName) {
this.taskName = taskName;
} private void start() throws Exception {
client = getClient(false);
client.getConnectionStateListenable().addListener(new ZkConnectionStateListener());
leaderLatch = new LeaderLatch(client, ZkTaskConst.getZkLatchPath(taskName));
leaderLatch.addListener(new ZkTaskLeaderLatchListener());
client.start();
if (leaderLatch.getState() != State.STARTED) {
leaderLatch.start();
}
} private void awaitForLeader(long timeout, TimeUnit unit) {
try {
this.leaderLatch.await(timeout, unit);
isLeader = leaderLatch.hasLeadership();
} catch (InterruptedException e) {
// log.error(e.getMessage(), e);
}
} private void stop(boolean closeLeaderLatch) {
if (closeLeaderLatch) {
CloseableUtils.closeQuietly(leaderLatch);
}
client.close();
} private static CuratorFramework getClient(boolean autoStart) {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient(ZkTaskConst.ZK_URL, retryPolicy);
if (client.getState() != CuratorFrameworkState.STARTED && autoStart) {
client.start();
}
return client;
} public <T> void runTask(Runnable action) {
ZkTaskController zkTaskController = new ZkTaskController(taskName);
try {
zkTaskController.start();
zkTaskController.awaitForLeader(ZkTaskConst.WAIT_SECONDS_ENSURE_BE_LEADER, TimeUnit.SECONDS);
if (zkTaskController.isLeader) {
log.info(String.format("The task %s will run on this task's leader server", taskName));
action.run();
} else {
log.info(String.format("The task %s will not this task's non-leader server", taskName));
}
// 再Hold leader一会儿, 以防止多个服务器时间不准导致作业double run
Thread.sleep(1000 * ZkTaskConst.SLEEP_SECONDS_AFTER_TASK);
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
zkTaskController.stop(true);
}
}
} /*
* 定时任务类
*/
@Component
class MyTasks { /**
* 一个定时任务 reportCurrentTimeTask 方法 (每分钟运行)
*/
@Scheduled(cron = "0 * * * * *")
public void reportCurrentTimeTask() {
ZkTaskController zkTaskController = new ZkTaskController("reportCurrentTime");
zkTaskController.runTask(new ReportCurrentTimeTaskInternal());
} /**
* 定时任务 reportCurrentTimeTask 真正执行的内容
*/
class ReportCurrentTimeTaskInternal implements Runnable {
private final Logger log = LoggerFactory.getLogger(ReportCurrentTimeTaskInternal.class);
private final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); @Override
public void run() {
log.info(String.format("The server (%s) is now %s", ZkTaskConst.SERVICE_SERVER,
dateFormat.format(new Date())));
}
} }
======================
参考
======================
https://www.cnblogs.com/leesf456/p/6032716.html
https://www.jianshu.com/p/70151fc0ef5d
https://www.codelast.com/%e5%8e%9f%e5%88%9b-zookeeper%e6%b3%a8%e5%86%8c%e8%8a%82%e7%82%b9%e7%9a%84%e6%8e%89%e7%ba%bf%e8%87%aa%e5%8a%a8%e9%87%8d%e6%96%b0%e6%b3%a8%e5%86%8c%e5%8f%8a%e6%b5%8b%e8%af%95%e6%96%b9%e6%b3%95/
http://www.cnblogs.com/francisYoung/p/5464789.html
https://www.cnblogs.com/LiZhiW/p/4930486.html
使用ZooKeeper协调多台Web Server的定时任务处理(方案1)的更多相关文章
- 使用ZooKeeper协调多台Web Server的定时任务处理(方案2)
承接上个博文, 这次是方案2的实现, 本方案的特点:1. 该方案能很好地从几台服务器中选出一个Master机器, 不仅仅可以用于定时任务场景, 还可以用在其他场景下. 2. 该方案能实现Master节 ...
- [SDK2.2]Windows Azure Virtual Network (4) 创建Web Server 001并添加至Virtual Network
<Windows Azure Platform 系列文章目录> 在上一章内容中,笔者已经介绍了以下两个内容: 1.创建Virtual Network,并且设置了IP range 2.创建A ...
- Nginx负载均衡:分布式/热备Web Server的搭建
Nginx是一款轻量级的Web server/反向代理server及电子邮件(IMAP/POP3)代理server.并在一个BSD-like 协议下发行.由俄罗斯的程序设计师Igor Sysoev所开 ...
- Web Server 分布式服务: Nginx负载均衡
Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器.由俄罗斯的程序设计师Igor Sysoev所开发,供俄国大型的入口网站及搜索引擎Rambler使用.其 ...
- Azkaban2.5安装部署(系统时区设置 + 安装和配置mysql + Azkaban Web Server 安装 + Azkaban Executor Server安装 + Azkaban web server插件安装 + Azkaban Executor Server 插件安装)(博主推荐)(五)
Azkaban是什么?(一) Azkaban的功能特点(二) Azkaban的架构(三) Hadoop工作流引擎之Azkaban与Oozie对比(四) 不多说,直接上干货! http://www.cn ...
- 【转】推荐介绍几款小巧的Web Server程序
原博地址:http://blog.csdn.net/heiyeshuwu/article/details/1753900 偶然看到几个小巧有趣的Web Server程序,觉得有必要拿来分享一下,让大家 ...
- Jexus-5.6.3使用详解、Jexus Web Server配置
一.Jexus Web Server配置 在 jexus 的工作文件夹中(一般是“/usr/jexus”)有一个基本的配置文件,文件名是“jws.conf”. jws.conf 中至少有 Site ...
- 多台web如何共享session进行存储(转载)
session的存储了解以前是怎么做的,搞清楚了来龙去脉,才会明白进行共享背后的思想和出发点.我喜欢按照这样的方式来问(或者去搞清楚):为什么要session要进行共享,不共享会什么问题呢? php中 ...
- 如何写一个简单的Web Server(一)
在本篇博文中我将介绍如何写一个Web Server.博文中大部分资料我是参考的这篇文章(http://www.linuxhowtos.org/C_C++/socket.htm),英文不错的同学可以 ...
随机推荐
- Mockito单元测试
Mockito简介 Mockito是一个单元测试框架,需要Junit的支持.在我们的项目中,都存在相当多的依赖关系,当我们在测试某一个业务相关的接口或则方法时,绝大多数时候是没有办法或则很难去添加所有 ...
- JS中的闭包(closure)
JS中的闭包(closure) 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现.下面就是我的学习笔记,对于Javascript初学者应该是很有用 ...
- PSQLException: FATAL: no pg_hba.conf entry for host "127.0.0.1", user "ambari", database "ambari", SSL off
On your Postgres server, you will need to update your pg_hba.conf file to allow access for the ambar ...
- 无post按钮提交表单
<form id="form1" name="form" action="url" method="GET"> ...
- UVA - 11374 - Airport Express(堆优化Dijkstra)
Problem UVA - 11374 - Airport Express Time Limit: 1000 mSec Problem Description In a small city c ...
- php常用数组array函数实例总结【赋值,拆分,合并,计算,添加,删除,查询,判断,排序】
本文实例总结了php常用数组array函数.分享给大家供大家参考,具体如下: array_combine 功能:用一个数组的值作为新数组的键名,另一个数组的值作为新数组的值 案例: <?php ...
- TK2 USB修复
https://www.jianshu.com/p/bb4587014349 开发板刷机过程全程联网 除了Jetson TX2之外,您还需要另一台带有Intel或AMD x86处理器的台式机或笔记本电 ...
- spring boot拦截器中获取request post请求中的参数(转)
文章转自 https://www.jianshu.com/p/69c6fba08c92
- printf 函数原型
typedef char *va_list; #define _AUPBND (sizeof (acpi_native_int) - 1) #define _ADNBND (sizeof (acpi_ ...
- Effective STL 读书笔记
Effective STL 读书笔记 标签(空格分隔): 未分类 慎重选择容器类型 标准STL序列容器: vector.string.deque和list(双向列表). 标准STL管理容器: set. ...