背景说明: 有一套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. 用 PLSQL 创建新用户及导入 dmp

    一.创建表空间 在导入 dmp 文件之前,你要在数据库里面给它分配一片存储它的地方(表空间). 如果我们知道需要导入的数据库的表空间直接创建就可以,如果不不知道,也没有关系,我们可以用 txt 打开 ...

  2. linux环境快速编译安装python3.6

    一.下载python3源码包 cd /tmp/wget https://www.python.org/ftp/python/3.6.2/Python-3.6.2.tgz 二.下载python3编译的依 ...

  3. 【Python 25】52周存钱挑战5.0(datetime库和import)

    1.案例描述 按照52周存钱法,存钱人必须在一年52周内,每周递存10元.例如,第一周存10元,第二周存20元,第三周存30元,直到第52周存520元. 记录52周后能存多少钱?即10+20+30+. ...

  4. gpio led学习

    2.弄清楚寄存器,gpio等之间的关系,to thi tha 比如: https://www.ggdoc.com/bGludXggZ3Bpb_aTjeS9nA2/NmIzNDIyZGZmMTExZjE ...

  5. HBase源码实战:CreateRandomStoreFile

    /* * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agre ...

  6. 数据库连接池性能比对(hikari druid c3p0 dbcp jdbc)

    https://blog.csdn.net/qq_31125793/article/details/51241943 背景 对现有的数据库连接池做调研对比,综合性能,可靠性,稳定性,扩展性等因素选出推 ...

  7. Vue父组件向子组件传递一个动态的值,子组件如何保持实时更新实时更新?

    原文:https://blog.csdn.net/zhouweixue_vivi/article/details/78550738 2017年11月16日 14:22:50 zhouweixue_vi ...

  8. git-将dev代码合并到test

    1. 在dev分支上刚开发完项目,执行以下命令: git add git commit -m 'dev' git push -u origin dev 2.切换到test分支上 如果是多人开发,先把远 ...

  9. Go语言中定时器cron的基本使用

    安装:go get github.com/robfig/cron  如果出不去就用gopm 例子: package main import ( "fmt" "github ...

  10. cnblogs 支持 iframe 标签 !

    bilibili 视频嵌入支持 网易云音乐支持 关注窝(求求你 ฅฅ) 这是我制作的第一个鬼畜(好傻的,视频直接录制的,进度条都录制上了,不过没关系的,反正以后也不做了(* /ω\*)) 说明 原来是 ...