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 ""; } } }