最近在项目中受项目客观原因制约,构建了双机双nginx通过心跳包实现主备切换方案,以作记录。
需求效果:用户只有两台服务器,为用户提供唯一访问地址。
大致思路:分别在两台服务器上部署nginx,nginx的配置文件对外的地址及端口均相同,对应下层的服务器的地址为各自项目的地址,通过心跳包实现相互监听,动态控制项目内定时任务,以及两个nginx服务的开启与关闭。
具体代码实现如下:
package com.justner;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import javax.annotation.PostConstruct;
/**
* 心跳实现主备切换
* @author Justner
* @version 2017-12-27
*/
public class HeartbeatService{
private static int port = 2001; // 默认端口
private static int times = 0; // 默认连续累计侦测异常次数
private static int probeTimes = 5; // 默认侦测次数
private static boolean isMaster = true; // 主备标识
private static boolean isFirst = true; // 首次侦听标识
private static InetAddress address; //远程服务器地址
private static DatagramSocket socket; //远程socket
private static InetAddress localAddress; //本地服务器地址
private static DatagramSocket localSocket; //本地socket
@PostConstruct
public static void start() {
// 初始化心跳服务
init();
// 向远程节点发送心跳
startSenderThread();
// 接收远程节点发来的心跳信息
startRecvThread();
}
private static void init() {
/**
* 创建与远程主机的UDP通道
*/
try {
address = InetAddress.getByName("192.168.3.9");
socket = new DatagramSocket(); // 创建套接字
socket.setSoTimeout(1000); //单位ms
} catch (UnknownHostException | SocketException e1) {
e1.printStackTrace();
}
/**
* 本地的心跳监听端口
*/
try {
localAddress = InetAddress.getLocalHost();
localSocket = new DatagramSocket(port, localAddress);
} catch (UnknownHostException | SocketException e) {
e.printStackTrace();
}
}
/**
* 发送数据
*/
private static void startSenderThread() {
// 初始化数据连接
new Thread() {
@Override
public void run() {
// 创建发送方的数据报信息
String context = "...";
DatagramPacket dataGramPacket = new DatagramPacket(context.getBytes(), context.getBytes().length, address, port);
// 持续发送星跳信息
while (true) {
try {
socket.send(dataGramPacket);//通过套接字发送数据
doResultInfo(socket);
// 如果正常收到心跳信息,重置心跳数据标识
times = 0;
// 如果是首次侦听收到心跳回复、则表明当前集群中已有活动主机,则当前主机为从机
if (isFirst) {
setMaster(false);
isFirst = false;
}
} catch (IOException e) {
// 当侦听异常时,则监测侦听失败次数。默认5次,5秒内未收到请求,将当前节点设置为主机
times = times + 1;
if (times >= probeTimes) {
setMaster(true);
// 非首次之后
if (isFirst) {
isFirst = false;
}
}
}
// 休眠进行下一次侦听
try {
Thread.sleep(1000); //单位ms
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印前主机状态
if (isMaster) {
System.out.println("=========心跳标识:主机");
} else {
System.out.println("=========心跳标识:从机");
}
}
}
}.start();
}
private static void doResultInfo(DatagramSocket socket) throws IOException {
byte[] backbuf = new byte[1024];
DatagramPacket backPacket = new DatagramPacket(backbuf, backbuf.length);
socket.receive(backPacket);
// 打印心跳记录
String getMsg = new String(backbuf, 0, backPacket.getLength());
if ("......".equals(getMsg)) {
times = 0;
System.out.println("客户端返回的的数据为:" + getMsg);
} else if ("....".equals(getMsg)) {
setMaster(false);
System.out.println("===========主备切换成功!============");
}
}
/**
* 接收返回消息
*/
private static void startRecvThread() {
new Thread() {
public void run() {
try {
byte[] buf = new byte[1024]; // 定义byte数组
DatagramPacket packet = new DatagramPacket(buf, buf.length); // 创建DatagramPacket对象
while (true) {
localSocket.receive(packet); // 通过套接字接收数据
String getMsg = new String(buf, 0, packet.getLength());
System.out.println("客户端发送的数据为:" + getMsg);
String reContext = "";
if ("...".equals(getMsg)) {
reContext = ".....";
} else if ("..".equals(getMsg)) {
setMaster(true);
reContext = "....";
}
SocketAddress sendAddress = packet.getSocketAddress();
byte[] backbuf = reContext.getBytes();
DatagramPacket sendPacket = new DatagramPacket(backbuf,backbuf.length, sendAddress); // 封装返回给客户端的数据
localSocket.send(sendPacket); // 通过套接字反馈服务器数据
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
/**
* 执行业务更改
* @param stauts
*/
private static void setMaster(boolean stauts) {
// 主机
if ((stauts && !isMaster) || (stauts && isFirst)) {
try {
/*这里处理项目的具体业务,针对定时任务可以设置全局内存变量来判断执行*/
nginxTurn("nginxStart.bat"); //cmd批处理启动nginx命令,脚本文件存于项目中
System.out.println("启动定时任务||设置主机成功");
} catch (Exception e) {
System.out.println("启动定时任务||设置主机失败");
}
// 备机
} else if((!stauts && isMaster) || (!stauts && isFirst)){
try {
/*这里处理项目的具体业务,针对定时任务可以设置全局内存变量来判断执行*/
nginxTurn("nginxQuit.bat"); //cmd批处理退出nginx命令,脚本文件存于项目中
System.out.println("停止定时任务||设置备机成功");
} catch (Exception e) {
System.out.println("停止定时任务||设置备机失败");
}
}
isMaster = stauts;
}
/**
* 手动调用更改主备
* @return
* @throws IOException
*/
public static boolean setOtherMaster() throws IOException {
// 创建发送方的数据报信息
String context = "..";
DatagramPacket dataGramPacket = new DatagramPacket(context.getBytes(),context.getBytes().length, address, port);
// 接收返回数据的数据包
socket.send(dataGramPacket);
doResultInfo(socket);
return !isMaster;
}
/**
* nginx开启关闭
* @param command
* command命令为bat脚本或者sh脚本文件,此处为调用脚本文件开启或者关闭nginx服务
* 脚本命令:启动start nginx,退出nginx -s quit
*/
public static void nginxTurn(String command){
try {
//执行命令
String cmd = "cmd /c start " + command; //Windows命令语句
// String cmd = "chmod 777 /" + command; //linux命令语句
Process process = Runtime.getRuntime().exec(cmd);
InputStream ins = process.getInputStream(); // 获取执行cmd命令后的信息
BufferedReader reader = new BufferedReader(new InputStreamReader(ins));
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line); // 输出
}
int exitValue = process.waitFor();
System.out.println("返回值:" + exitValue);
//关闭流
process.getOutputStream().close();
} catch (Exception e) {
e.printStackTrace();
}
}
}