背景说明: 有一套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)的更多相关文章

  1. 使用ZooKeeper协调多台Web Server的定时任务处理(方案2)

    承接上个博文, 这次是方案2的实现, 本方案的特点:1. 该方案能很好地从几台服务器中选出一个Master机器, 不仅仅可以用于定时任务场景, 还可以用在其他场景下. 2. 该方案能实现Master节 ...

  2. [SDK2.2]Windows Azure Virtual Network (4) 创建Web Server 001并添加至Virtual Network

    <Windows Azure Platform 系列文章目录> 在上一章内容中,笔者已经介绍了以下两个内容: 1.创建Virtual Network,并且设置了IP range 2.创建A ...

  3. Nginx负载均衡:分布式/热备Web Server的搭建

    Nginx是一款轻量级的Web server/反向代理server及电子邮件(IMAP/POP3)代理server.并在一个BSD-like 协议下发行.由俄罗斯的程序设计师Igor Sysoev所开 ...

  4. Web Server 分布式服务: Nginx负载均衡

    Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器.由俄罗斯的程序设计师Igor Sysoev所开发,供俄国大型的入口网站及搜索引擎Rambler使用.其 ...

  5. Azkaban2.5安装部署(系统时区设置 + 安装和配置mysql + Azkaban Web Server 安装 + Azkaban Executor Server安装 + Azkaban web server插件安装 + Azkaban Executor Server 插件安装)(博主推荐)(五)

    Azkaban是什么?(一) Azkaban的功能特点(二) Azkaban的架构(三) Hadoop工作流引擎之Azkaban与Oozie对比(四) 不多说,直接上干货! http://www.cn ...

  6. 【转】推荐介绍几款小巧的Web Server程序

    原博地址:http://blog.csdn.net/heiyeshuwu/article/details/1753900 偶然看到几个小巧有趣的Web Server程序,觉得有必要拿来分享一下,让大家 ...

  7. Jexus-5.6.3使用详解、Jexus Web Server配置

    一.Jexus Web Server配置   在 jexus 的工作文件夹中(一般是“/usr/jexus”)有一个基本的配置文件,文件名是“jws.conf”. jws.conf 中至少有 Site ...

  8. 多台web如何共享session进行存储(转载)

    session的存储了解以前是怎么做的,搞清楚了来龙去脉,才会明白进行共享背后的思想和出发点.我喜欢按照这样的方式来问(或者去搞清楚):为什么要session要进行共享,不共享会什么问题呢? php中 ...

  9. 如何写一个简单的Web Server(一)

      在本篇博文中我将介绍如何写一个Web Server.博文中大部分资料我是参考的这篇文章(http://www.linuxhowtos.org/C_C++/socket.htm),英文不错的同学可以 ...

随机推荐

  1. Python基础——6面向对象编程

    类和实例 类是抽象的模版,例如汽车:而实例则是拥有相同方法的类的实现,例如汽车里面有大众.宝马.奔驰等等,这些车都能在地面上跑,但是它们的具体数据可以不一样. calss Student(object ...

  2. MySQL 数据查询

    SELECT子句:用来指定查询返回字段,星号(*)表示返回所有字段 SELECT  [DISTINCT]*|字段列表        #DISTINCT 用来过滤重复数据 FROM子句:用来指定数据来源 ...

  3. 《PHP扩展及核心》

    本文地址:http://www.cnblogs.com/aiweixiao/p/8202365.html 原文地址: 欢迎关注微信公众号  程序员的文娱情怀 一.主要内容: 1️⃣php扩展的概念和底 ...

  4. oc中的委托模式

    通过一个例子来理解委托模式 首先定义个协议 协议(protocol) :它可以声明一些必须实现的方法和选择实现的方法  (在java中称为接口) // // StudentDelegate.h // ...

  5. vue 应用生产环境的 webpack 打包配置优化

    转:https://blog.csdn.net/robin_star_/article/details/83856363 前言:很好的打包优化的帖子,还没来的急去实测验证 1. 去掉 console ...

  6. CVS简单介绍

    版权声明:本文为博主原创文章,转载请注明出处. https://blog.csdn.net/Jerome_s/article/details/27990707 CVS - Concurrent Ver ...

  7. np.mgrid的用法

    功能:返回多维结构,常见的如2D图形,3D图形 np.mgrid[ 第1维,第2维 ,第3维 , …] 第n维的书写形式为: a:b:c c表示步长,为实数表示间隔:该为长度为[a,b),左开右闭 或 ...

  8. Oracle 执行计划(一)-------基本介绍

    本文参照自:https://www.cnblogs.com/Dreamer-1/p/6076440.html 打开SQL执行计划: 1.选中一句正在执行的SQL 2.F5快捷键,就会出现下图,这就是执 ...

  9. P5239 回忆京都(洛谷3月月赛T2)

    题目描述 射命丸文在取材中发现了一个好玩的东西,叫做组合数. 组合数的定义如下:从n个不同元素中,任取m(m≤n)个元素并成一组,叫做从n个不同元素中取出m个元素的一个组合.所有组合的数量,就是组合数 ...

  10. .net core 的图片处理及二维码的生成及解析

    写代码这事,掐指算来已经十有余年. 从html到css到javascript到vbscript到c#,从兴趣到职业,生活总是失落与惊喜并存. 绝大部分时候,出发并不是因为知道该到哪里去,只是知道不能再 ...