<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
負載均衡在Java領域中有著廣泛深入的應用,不管是大名鼎鼎的nginx,還是微服務治理元件如dubbo,feign等,負載均衡的演演算法在其中都有著實際的使用
負載均衡的核心思想在於其底層的演演算法思想,比如大家熟知的演演算法有 輪詢,隨機,最小連線,加權輪詢等,在現實中不管怎麼設定,都離不開其演演算法的核心原理,下面將結合實際程式碼對常用的負載均衡演演算法做一些全面的總結。
輪詢即排好隊,一個接一個的輪著來。從資料結構上,有一個環狀的節點,節點上面佈滿了伺服器,伺服器之間首尾相連,帶有順序性。當請求過來的時候,從某個節點的伺服器開始響應,那麼下一次請求再來,就依次由後面的伺服器響應,由此繼續
按照這個描述,我們很容易聯想到,可以使用一個雙向(雙端)連結串列的資料結構來模擬實現這個演演算法
1、定義一個server類,用於標識伺服器中的連結串列節點
class Server { Server prev; Server next; String name; public Server(String name) { this.name = name; } }
2、核心程式碼
/** * 輪詢 */ public class RData { private static Logger logger = LoggerFactory.getLogger(RData.class); //標識當前服務節點,每次請求過來時,返回的是current節點 private Server current; public RData(String serverName) { logger.info("init servers : " + serverName); String[] names = serverName.split(","); for (int i = 0; i < names.length; i++) { Server server = new Server(names[i]); //當前為空,說明首次建立 if (current == null) { //current就指向新建立server this.current = server; //同時,server的前後均指向自己 current.prev = current; current.next = current; } else { //說明已經存在機器了,則按照雙向連結串列的功能,進行節點新增 addServer(names[i]); } } } //新增機器節點 private void addServer(String serverName) { logger.info("add new server : " + serverName); Server server = new Server(serverName); Server next = this.current.next; //在當前節點後插入新節點 this.current.next = server; server.prev = this.current; //由於是雙向連結串列,修改下一節點的prev指標 server.next = next; next.prev = server; } //機器節點移除,修改節點的指向即可 private void removeServer() { logger.info("remove current = " + current.name); this.current.prev.next = this.current.next; this.current.next.prev = this.current.prev; this.current = current.next; } //請求。由當前節點處理 private void request() { logger.info("handle server is : " + this.current.name); this.current = current.next; } public static void main(String[] args) throws Exception { //初始化兩臺機器 RData rr = new RData("192.168.10.0,192.168.10.1"); new Thread(new Runnable() { @Override public void run() { while (true) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } rr.request(); } } }).start(); //3s後,3號機器加入清單 Thread.currentThread().sleep(2000); rr.addServer("192.168.10.3"); //3s後,當前服務節點被移除 Thread.currentThread().sleep(3000); rr.removeServer(); } }
結合註釋對程式碼進行理解,這段程式碼解釋開來就是考察對雙端連結串列的底層能力,操作連結串列結構時,最重要的就是要搞清在節點的新增和移除時,理清節點的前後指向,然後再理解這段程式碼時就沒有難度了,下面執行下程式
輪詢演演算法優缺點小結
從可提供的伺服器列表中隨機取一個提供響應。
既然是隨機存取的場景,很容易想到使用陣列可以更高效的通過下標完成隨機讀取,這個演演算法的模擬比較簡單,下面直接上程式碼
/** * 隨機 */ public class RandomMath { private static List<String> ips; public RandomMath(String nodeNames) { System.out.println("init servers : " + nodeNames); String[] nodes = nodeNames.split(","); //初始化伺服器列表,長度取機器數 ips = new ArrayList<>(nodes.length); for (String node : nodes) { ips.add(node); } } //請求處理 public void request() { Random ra = new Random(); int i = ra.nextInt(ips.size()); System.out.println("the handle server is :" + ips.get(i)); } //新增節點,注意,新增節點可能會造成內部陣列擴容 void addnode(String nodeName) { System.out.println("add new node : " + nodeName); ips.add(nodeName); } //移除 void remove(String nodeName) { System.out.println("remove node is: " + nodeName); ips.remove(nodeName); } public static void main(String[] args) throws Exception { RandomMath rd = new RandomMath("192.168.10.1,192.168.10.2,192.168.10.3"); //使用一個執行緒,模擬不間斷的請求 new Thread(new Runnable() { @Override public void run() { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } rd.request(); } } }).start(); //間隔3秒之後,新增一臺新的機器 Thread.currentThread().sleep(3000); rd.addnode("192.168.10.4"); //3s後,當前服務節點被移除 Thread.currentThread().sleep(3000); rd.remove("192.168.10.2"); } }
執行程式碼觀察結果:
隨機演演算法小結
在隨機選擇的基礎上,機器仍然是被隨機被篩選,但是做一組加權值,根據權值不同,機器列表中的各個機器被選中的概率不同,從這個角度理解,可認為隨機是一種等權值的特殊情況
設計思路依然相同,只是每個機器需要根據權值大小,生成不同數量的節點,節點排隊後,隨機獲取。這裡的資料結構主要涉及到隨 機的讀取,所以優選為陣列
/** * 加權隨機 */ public class WeightRandom { ArrayList<String> list; public WeightRandom(String nodes) { String[] ns = nodes.split(","); list = new ArrayList<>(); for (String n : ns) { String[] n1 = n.split(":"); int weight = Integer.valueOf(n1[1]); for (int i = 0; i < weight; i++) { list.add(n1[0]); } } } public void request() { //下標,亂數,注意因子 int i = new Random().nextInt(list.size()); System.out.println("the handle server is : " + list.get(i)); } public static void main(String[] args) throws Exception{ WeightRandom wr = new WeightRandom("192.168.10.1:2,192.168.10.2:1"); for (int i = 0; i < 9; i++) { Thread.sleep(2000); wr.request(); } } }
我們不妨將10.1的權重值再調的大點,比如調為3,再次執行一下,這個效果就更明顯了
加權隨機演演算法小結
在前面的輪詢演演算法中我們看到,輪詢只是機械的旋轉不斷在雙向連結串列中進行移動,而加權輪詢則彌補了所有機器被一視同仁的缺點。在輪詢的基礎上,伺服器初始化 時,各個機器攜帶一個權重值
加權輪詢的演演算法思想不是很好理解,下面我以一個圖進行說明:
加權輪詢演演算法的初衷是希望通過這樣一套演演算法保證整體的請求平滑性,從上圖中也可以發現,經過幾輪的迴圈之後,由可以回到最初的結果,而且在某一個輪詢中,不同機器根據權重值的不同,請求被讀取的概率也會不同
實現思路和輪詢差不多,整體仍然是連結串列結構,不同的是,每個具體的節點需加上權重值屬性
1、節點屬性類
class NodeServer { int weight; int currentWeight; String ip; public NodeServer(String ip, int weight) { this.ip = ip; this.weight = weight; this.currentWeight = 0; } @Override public String toString() { return String.valueOf(currentWeight); } }
2、核心程式碼
/** * 加權輪詢 */ public class WeightRDD { //所有機器節點列表 ArrayList<NodeServer> list; //總權重 int total; //機器節點初始化 , 格式:a#4,b#2,c#1,實際操作時可根據自己業務客製化 public WeightRDD(String nodes) { String[] ns = nodes.split(","); list = new ArrayList<>(ns.length); for (String n : ns) { String[] n1 = n.split("#"); int weight = Integer.valueOf(n1[1]); list.add(new NodeServer(n1[0], weight)); total += weight; } } public NodeServer getCurrent() { for (NodeServer node : list) { node.currentWeight += node.weight; } NodeServer current = list.get(0); int i = 0; //從列表中獲取當前的currentWeight最大的那個作為待響應的節點 for (NodeServer node : list) { if (node.currentWeight > i) { i = node.currentWeight; current = node; } } return current; } //請求,每次得到請求的節點之後,需要對當前的節點的currentWeight值減去 sumWeight public void request() { NodeServer node = this.getCurrent(); System.out.print(list.toString() + "‐‐‐"); System.out.print(node.ip + "‐‐‐"); node.currentWeight -= total; System.out.println(list); } public static void main(String[] args) throws Exception { WeightRDD wrr = new WeightRDD("192.168.10.1#4,192.168.10.2#2,192.168.10.3#1"); //7次執行請求,觀察結果 for (int i = 0; i < 7; i++) { Thread.currentThread().sleep(2000); wrr.request(); } } }
從列印輸出結果來看,也是符合預期效果的,具有更大權重的機器,在輪詢中被請求到的可能性更大
即對當前存取的ip地址做一個hash值,相同的key將會被路由到同一臺機器去。常見於分散式叢集環境下,使用者登入 時的請求路由和對談保持
源地址hash演演算法可以有效解決在跨地域機器部署情況下請求響應的問題,這一特點使得源地址hash演演算法具有某些特殊的應用場景
該演演算法的核心邏輯是需要自定義一個能結合實際業務場景的hash演演算法,從而確保請求能夠儘可能達到源IP機器進行處理
源地址hash演演算法的實現比較簡單,下面直接上程式碼
/** * 源地址請求hash */ public class SourceHash { private static List<String> ips; //節點初始化 public SourceHash(String nodeNames) { System.out.println("init list : " + nodeNames); String[] nodes = nodeNames.split(","); ips = new ArrayList<>(nodes.length); for (String node : nodes) { ips.add(node); } } //新增節點 void addnode(String nodeName) { System.out.println("add new node : " + nodeName); ips.add(nodeName); } //移除節點 void remove(String nodeName) { System.out.println("remove one node : " + nodeName); ips.remove(nodeName); } //ip進行hash private int hash(String ip) { int last = Integer.valueOf(ip.substring(ip.lastIndexOf(".") + 1, ip.length())); return last % ips.size(); } //請求模擬 void request(String ip) { int i = hash(ip); System.out.println("req ip : " + ip + "‐‐>" + ips.get(i)); } public static void main(String[] args) throws Exception { SourceHash hash = new SourceHash("192.168.10.1,192.168.10.2"); for (int i = 1; i < 10; i++) { String ip = "192.168.1." + i; hash.request(ip); } Thread.sleep(3000); System.out.println(); hash.addnode("192.168.10.3"); for (int i = 1; i < 10; i++) { String ip = "192.168.1." + i; hash.request(ip); } Thread.sleep(3000); System.out.println(); hash.remove("192.168.10.1"); for (int i = 1; i < 10; i++) { String ip = "192.168.1." + i; hash.request(ip); } } }
請關注核心的方法 hash(),我們模擬9個隨機請求的IP,下面執行下這段程式,觀察輸出結果
源地址hash演演算法小結
即通過統計當前機器的請求連線數,選擇當前連線數最少的機器去響應新請求。前面的各種演演算法是基於請求的維度,而最小 連線數則是站在機器的連線數量維度
從描述來看,實現這種演演算法需要定義一個連結表記錄機器的節點IP,和機器連線數量的計數器
而為了比較並選擇出最小的連線數的機器,內部採用最小堆做排序處理,請求響應時取堆頂節點即是 最小連線數(可以參考最小頂堆演演算法)
如圖所示,所有機器列表按照類二元樹的結構進行組裝,組裝的依據按照不同節點的存取次數,某次請求過來時,選擇堆頂的元素(待響應的機器)返回,然後堆頂機器的請求數量加1,然後通過演演算法將這個堆頂的元素下沉,把請求數量最小的元素上升為堆頂,以便下次響應最新的請求
1、機器節點
該類記錄了節點的IP以及連線數
class Node { String name; AtomicInteger count = new AtomicInteger(0); public Node(String name) { this.name = name; } public void inc() { count.getAndIncrement(); } public int get() { return count.get(); } @Override public String toString() { return name + "=" + count; } }
2、核心程式碼
/** * 最小連線數演演算法 */ public class LeastRequest { Node[] nodes; //節點初始化 public LeastRequest(String ns) { String[] ns1 = ns.split(","); nodes = new Node[ns1.length + 1]; for (int i = 0; i < ns1.length; i++) { nodes[i + 1] = new Node(ns1[i]); } } ///節點下沉,與左右子節點比對,選裡面最小的交換 //目的是始終保持最小堆的頂點元素值最小【結合圖理解】 //ipNum:要下沉的頂點序號 public void down(int ipNum) { //頂點序號遍歷,只要到1半即可,時間複雜度為O(log2n) while (ipNum << 1 < nodes.length) { int left = ipNum << 1; //右子,左+1即可 int right = left + 1; int flag = ipNum; //標記,指向 本節點,左、右子節點裡最小的,一開始取自己 if (nodes[left].get() < nodes[ipNum].get()) { flag = left; } //判斷右子 if (right < nodes.length && nodes[flag].get() > nodes[right].get()) { flag = right; } //兩者中最小的與本節點不相等,則交換 if (flag != ipNum) { Node temp = nodes[ipNum]; nodes[ipNum] = nodes[flag]; nodes[flag] = temp; ipNum = flag; } else { //否則相等,堆排序完成,退出迴圈即可 break; } } } //請求,直接取最小堆的堆頂元素就是連線數最少的機器 public void request() { System.out.println("‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐"); //取堆頂元素響應請求 Node node = nodes[1]; System.out.println(node.name + " accept"); //連線數加1 node.inc(); //排序前的堆 System.out.println("ip list before:" + Arrays.toString(nodes)); //堆頂下沉,通過演演算法將堆頂下層到合適的位置 down(1); //排序後的堆 System.out.println("ip list after:" + Arrays.toString(nodes)); } public static void main(String[] args) { //假設有7臺機器 LeastRequest lc = new LeastRequest("10.1,10.2,10.3,10.4,10.5,10.6,10.7"); //模擬10個請求連線 for (int i = 0; i < 10; i++) { lc.request(); } } }
請關注 down 方法,該方法是實現每次請求之後,將堆頂元素進行移動的關鍵實現,執行這段程式碼,結合輸出結果進行理解
最小連線數演演算法小結
負載均衡的各種演演算法在Nginx的設定中都有著實際的用途,生產環境中可以結合實際的業務進行設定,瞭解了其底層的演演算法對於我們在後續的設定中更加得心應手,
到此這篇關於Java 負載均衡演演算法作用詳細解析的文章就介紹到這了,更多相關Java 負載均衡內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45