package com.cnn.service.HA;
import com.cnn.service.ScheduledExecutorServiceDelay.ScheduledExecutorServiceDelayTask;
import com.google.common.collect.ConcurrentHashMultiset;
import javassist.bytecode.analysis.Executor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import javax.net.ssl.*;
import java.io.IOException;
import java.net.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Iterator;
import java.util.concurrent.*;
import java.util.logging.Handler;
/**
* HACutover
*
* @author Administrator
* @version 1.0
* @project delay_queue
* @title
* @date 2021/1/10
*/
@Slf4j
public class HACutover {
private static String LOCAL = "local"; //本地
private static String FIRST = "first"; //第一个机房
private static String SECOND = "second"; //第二个机房
private static boolean status = false; //状态
private static int timeout = 3 * 1000; //超时时间
private static int queueSize = 24; //队列长度
private static int exception = 3; //异常数量
private static double exceptionPercent = 0.5; //异常占比
private static String SERVER_ROOM = "local"; //获取机房配置(这里可以用动态获取配置实现,根据机房不同获取不同的配置)
private static ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
private static ConcurrentHashMap<String, String> avaiUrlMap = new ConcurrentHashMap<>();
private static ConcurrentHashMap<String, LinkedBlockingDeque<UrlInfo>> queueMap = new ConcurrentHashMap<>();
/**
* 初始化配置
*/
public HACutover() {
}
/**
* 设置可用地址
*
* @param key
* @param avaiUrl
* @return
*/
private static String setAvaiUrl(String key, String avaiUrl) {
String check1 = avaiUrlMap.get(key);
if (StringUtils.isEmpty(check1) || (StringUtils.isNotEmpty(check1) && !avaiUrl.equals(check1))) {
synchronized (Handler.class) {
String check2 = avaiUrlMap.get(key);
if (StringUtils.isEmpty(check2) || (StringUtils.isNotEmpty(check1) && !avaiUrl.equals(check2))) {
avaiUrlMap.put(key, avaiUrl);
}
}
}
return avaiUrl;
}
/**
* 获取可用地址
* @param firstConfig 第一个机房
* @param secondConfig 第二个机房
* @return
*/
public static String getAvaiUrl(String firstConfig, String secondConfig) {
keepAliaved();
String key = firstConfig + "%-%" + secondConfig;
String avaiUrl = avaiUrlMap.get(key);
if (StringUtils.isEmpty(avaiUrl)) {
if (StringUtils.isNotEmpty(firstConfig) && StringUtils.isNotEmpty(secondConfig)) {
if (FIRST.equals(SERVER_ROOM)) {
avaiUrl = setAvaiUrl(key, ConfigManager.get(firstConfig));
}
if (SECOND.equals(SERVER_ROOM)) {
avaiUrl = setAvaiUrl(key, ConfigManager.get(secondConfig));
}
//如果没有获取到机房配置,则默认取第一个机房的配置
if (FIRST.equals(SERVER_ROOM)) {
avaiUrl = setAvaiUrl(key, ConfigManager.get(firstConfig));
}
return avaiUrl;
} else {
if (StringUtils.isNotEmpty(firstConfig)) {
return avaiUrl = ConfigManager.get(firstConfig);
}
if (StringUtils.isNotEmpty(secondConfig)) {
return avaiUrl = ConfigManager.get(secondConfig);
}
}
}
log.info("地址map信息:{}", avaiUrlMap);
return avaiUrl;
}
/**
* 保活
*/
private static void keepAliaved() {
if (!status) {
executorService.scheduleAtFixedRate(() -> detect(), 0, 5, TimeUnit.SECONDS);
status = true;
} else {
log.info("已经初始化过了");
}
}
/**
* 关闭探测
*/
public static void closeDetect() {
executorService.shutdownNow();
status = false;
}
private static void detect() {
log.info("开始探测!");
try {
avaiUrlMap.forEach((k, url) -> {
LinkedBlockingDeque<UrlInfo> urlInfoQueue = queueMap.get(k);
if (null != urlInfoQueue) {
boolean telnet = connectingAddress(url);
if (urlInfoQueue.size() >= queueSize) {
UrlInfo poll = urlInfoQueue.poll();//移除队列头部元素
}
UrlInfo urlInfo = new UrlInfo();
urlInfo.setStatus(telnet);
urlInfo.setTime(new Date());
urlInfoQueue.offer(urlInfo);
calculate(k, url, urlInfoQueue);
} else {
LinkedBlockingDeque<UrlInfo> queue = new LinkedBlockingDeque<>(queueSize);
UrlInfo urlInfo = new UrlInfo();
urlInfo.setStatus(true);
urlInfo.setTime(new Date());
try {
queue.offer(urlInfo);
queueMap.put(k, queue);
} catch (Exception e) {
log.error("", e);
}
}
});
} catch (Exception e) {
log.error("定时任务执行出现异常:{}", e);
}
}
/**
* 计算异常比例,并切换域名,切换逻辑采用(5-3)
*
* @param k
* @param url
* @param urlinfoQueue
*/
private static void calculate(String k, String url, LinkedBlockingDeque<UrlInfo> urlinfoQueue) {
int exceptNum = 0; //异常个数
int contiExceptNum = 0;//连接异常个数
if (null != urlinfoQueue && urlinfoQueue.size() >= exception) {
Iterator iter = urlinfoQueue.iterator();
while (iter.hasNext()) {
UrlInfo urlInfo = (UrlInfo) iter.next();
if (!urlInfo.status) {
exceptNum += 1;
contiExceptNum += 1;
} else {
contiExceptNum = 0;
}
double percent = exceptNum / urlinfoQueue.size();
if (contiExceptNum >= exception || percent >= exceptionPercent) {
cutover(k, url);
urlinfoQueue.clear(); //清空队列,避免重复触发切换条件
}
}
}
}
/**
* 切换域名
*
* @param k
* @param url
*/
private static void cutover(String k, String url) {
String[] urlArray = k.split("%-%");
for (int i = 0; i < urlArray.length; i++) {
String currentConfig = urlArray[i];
if (!(StringUtils.isNotEmpty(url) && url.equals(currentConfig))) {
String currentUrl = ConfigManager.get(currentConfig);
//先探测将要切换的地址是否正确,如果也是异常的则不作切换
boolean connect = connectingAddress(currentUrl);
log.info("自动切换地址检测:{}", connect);
if (connect) {
log.info("自动切换连接地址:{}", currentUrl);
setAvaiUrl(k, currentUrl);
}
}
}
}
/**
* 探测地址连通性
*
* @param remoteInetAddr
* @return
*/
private static boolean connectingAddress(String remoteInetAddr) {
String tempUrl = remoteInetAddr.substring(0, 5); //取出地址前5位
if (tempUrl.contains("http")) { //判断传过来的地址中是否有http
if (tempUrl.equals("https")) { //判断服务器是否是https协议
try {
trustAllHttpsCertificates(); //当协议是https时
} catch (Exception e) {
log.error("证书设置异常:{}", e);
}
}
boolean telnet = false;
if (remoteInetAddr.replace("://", "").contains(":")) {
telnet = telnet(remoteInetAddr);
} else {
telnet = isConnServerByHttp(remoteInetAddr);
}
return telnet;
} else {
return isReachable(remoteInetAddr);
}
}
static HostnameVerifier hv = new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
};
/**
* IP地址是否可达,相当于Ping命令
*
* @param remoteInteAddr
* @return
*/
private static boolean isReachable(String remoteInteAddr) {
boolean reachable = false;
try {
String host = getAddress(remoteInteAddr);
InetAddress address = InetAddress.getByName(host);
reachable = address.isReachable(timeout);
} catch (Exception e) {
e.printStackTrace();
}
return reachable;
}
/**
* 服务联通性
*
* @param serverUrl
* @return
*/
private static boolean isConnServerByHttp(String serverUrl) {
boolean connFlag = false;
URL url;
HttpURLConnection conn = null;
try {
url = new URL(serverUrl);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(timeout);
if (conn.getResponseCode() == 200) { //如果连接成功则设置为true
connFlag = true;
}
} catch (Exception e) {
log.error("探测连接出现异常:{},异常信息:{}", serverUrl, e);
} finally {
conn.disconnect();
}
return connFlag;
}
/**
* telnet查看地址是否可用
*
* @param serverUrl
* @return
*/
public static boolean telnet(String serverUrl) {
Socket socket = new Socket();
boolean isConnected = false;
try {
socket.connect(new InetSocketAddress(getAddress(serverUrl), getPort(serverUrl)), timeout); //简历连接
isConnected = socket.isConnected(); //通过现有方法查看连通状态
} catch (IOException e) {
isConnected = false;
} finally {
try {
socket.close(); //关闭连接
} catch (IOException e) {
isConnected = false;
}
}
return isConnected;
}
/**
* 以下是Https适用
*
* @throws Exception
*/
private static void trustAllHttpsCertificates() throws Exception {
TrustManager[] trustAllCerts = new TrustManager[1];
TrustManager tm = new miTM();
trustAllCerts[0] = tm;
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, null);
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}
static class miTM implements TrustManager, X509TrustManager {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public boolean isServerTrusted(X509Certificate[] certs) {
return true;
}
public boolean isClientTrusted(X509Certificate[] certs) {
return true;
}
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
return;
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
return;
}
}
/**
* 获取地址
*
* @param remoteInetAddr
* @return
*/
public static String getAddress(String remoteInetAddr) {
if (remoteInetAddr.contains("://")) {
remoteInetAddr = remoteInetAddr.substring(remoteInetAddr.indexOf("://")).replace("://", "");
}
if (remoteInetAddr.contains(":")) {
remoteInetAddr = remoteInetAddr.substring(0, remoteInetAddr.indexOf(":"));
}
return remoteInetAddr;
}
/**
* 获取端口
*
* @param remoteInetAddr
* @return
*/
public static int getPort(String remoteInetAddr) {
remoteInetAddr = remoteInetAddr.replace("://", "");
if (remoteInetAddr.contains(":")) {
remoteInetAddr = remoteInetAddr.substring(remoteInetAddr.indexOf(":")).replace(":", "");
}
if (remoteInetAddr.contains("/")) {
remoteInetAddr = remoteInetAddr.substring(0, remoteInetAddr.indexOf("/"));
}
if (StringUtils.isNotEmpty(remoteInetAddr)) {
try {
return Integer.valueOf(remoteInetAddr);
} catch (NumberFormatException e) {
log.error("获取端口异常:{}", e);
}
}
return 80;
}
@Data
static class UrlInfo {
private boolean status; //状态
private Date time; //入队列的时间
}
/**
* 动态获取配置
*/
static class ConfigManager {
public static String get(String config) {
return "";
}
}
}