使用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),英文不错的同学可以 ...
随机推荐
- 我的第一个python web开发框架(35)——权限数据库结构设计
接下来要做的是权限系统的数据库结构设计,在上一章我们了解了权限系统是通过什么来管理好权限的,我们选用其中比较常用的权限系统来实现当前项目管理要求. 下面是我们选择的权限系统关系模型: 从以上关系可以看 ...
- 我超级推荐的Navicat Premium 12的下载,破解方法
今天给大家推荐一款炒鸡好用的数据库管理工具,使用它,可以很方便的连接各种主流数据库软件----Navicat Premium 12 但是,它是要钱的,不过我们可以使用破解机来破解它,步骤稍有些复杂,简 ...
- kafka-rest:怎么愉快的build?
愉快的build该项目吧 git clone https://github.com/confluentinc/kafka-restmvn clean install -Dmaven.test.skip ...
- 第一本Docker书读书笔记
日常使用命令 1.停止所有的container,这样才能够删除其中的images: docker stop $(docker ps -a -q) 如果想要删除所有container的话再加一个指令: ...
- fastjson JSON 对象为空保留null
JSONObject jsonObject = JSONObject.parseObject(JSON.toJSONString(Object, SerializerFeature.WriteMapN ...
- Spring Cloud:多环境配置、eureka 安全认证、容器宿主机IP注册
记录一下搭建 Spring Cloud 过程中踩过的一些坑,测试的东西断断续续已经弄了好多了,一直没有时间整理搭建过程,时间啊~时间~ Spring 版本 Spring Boot:2.0.6.RELE ...
- openflow流表分析(草稿)
OVS bridge 有两种模式:“normal” 和 “flow”.“normal” 模式的 bridge 同普通的 Linux 桥,而 “flow” 模式的 bridge 是根据其流表(flow ...
- Navicat for MySQL 安装和破解
1 下载 navicat_trial_11.1.20.0.1449226634.exe .PatchNavicat.exe 2 安装 navicat 3 打开 patchnavicat-选择 安装文件 ...
- Lodop简短问答客户反馈篇 及排查步骤 及注册相关
A.http下打印图片正常,https下打印图片是××.(有的客户端可以,有的不可以)重置ie浏览器试试.客户反馈:(和ie浏览器的设置有关)intenet选项--高级里,我调整为和能打印出图片的电脑 ...
- Lodop、c-lodop注册与角色简短问答
注册与角色:参考http://www.c-lodop.com/demolist/t1.html参考链接里的三种场景,是哪种角色.客户端访问网站后用自己的打印机打印.是客户端本地打印角色.IP和域名注册 ...