文章已經收錄在 Github.com/niumoo/JavaNotes ,更有 Java 程序員所須要掌握的核心知識,歡迎Star和指教。
歡迎關注個人 公衆號,文章每週更新。
負載平衡(Load balancing)是一種在多個計算機(網絡、CPU、磁盤)之間均勻分配資源,以提升資源利用的技術。使用負載均衡能夠最大化服務吞吐量,可能最小化響應時間,同時因爲使用負載均衡時,會使用多個服務器節點代單點服務,也提升了服務的可用性。java
負載均衡的實現能夠軟件能夠硬件,硬件如大名鼎鼎的 F5 負載均衡設備,軟件如 NGINX 中的負載均衡實現,又如 Springcloud Ribbon 組件中的負載均衡實現。git
若是看到這裏你還不知道負載均衡是幹嗎的,那麼只能放一張圖了,畢竟沒圖說個啥。程序員
負載均衡要作到在屢次請求下,每臺服務器被請求的次數大體相同。可是實際生產中,可能每臺機器的性能不一樣,咱們會但願性能好的機器承擔的請求更多一些,這也是正常需求。github
若是這樣說下來你看不懂,那我就再舉個例子好了,一排可愛的小熊(服務器)站好。web
這時有人(用戶)要過來打臉(請求訪問)。面試
那麼怎麼樣咱們才能讓這每個可愛的小熊被打的次數大體相同呢?算法
又或者熊 4 比較胖,抗擊打能力是別人的兩倍,咱們怎麼提升熊 4 被打的次數也是別人的兩倍呢?安全
又或者每次出手的力度不一樣,有重有輕,恰巧熊 4 老是承受這種大力度啪啪打臉,熊 4 即將不省熊事,還要繼續打它嗎?服務器
這些都是值的思考的問題。網絡
說了那麼多,口乾舌燥,我雙手已經飢渴難耐了,火燒眉毛的想要擼起代碼了。
上面說了,爲了負載均衡,咱們必須保證屢次出手後,熊 1 到熊 4 被打次數均衡。好比使用隨機訪問法,根據數學上的機率論,隨機出手次數越多,每隻熊被打的次數就會越相近。代碼實現也比較簡單,使用一個隨機數,隨機訪問一個就能夠了。
/** 服務器列表 */ private static List<String> serverList = new ArrayList<>(); static { serverList.add("192.168.1.2"); serverList.add("192.168.1.3"); serverList.add("192.168.1.4"); serverList.add("192.168.1.5"); } /** * 隨機路由算法 */ public static String random() { // 複製遍歷用的集合,防止操做中集合有變動 List<String> tempList = new ArrayList<>(serverList.size()); tempList.addAll(serverList); // 隨機數隨機訪問 int randomInt = new Random().nextInt(tempList.size()); return tempList.get(randomInt); }
由於使用了非線程安全的集合,因此在訪問操做時操做的是集合的拷貝,下面幾種輪訓方式中也是這種思想。
寫一個模擬請求方法,請求10w次,記錄請求結果。
public static void main(String[] args) { HashMap<String, Integer> serverMap = new HashMap<>(); for (int i = 0; i < 20000; i++) { String server = random(); Integer count = serverMap.get(server); if (count == null) { count = 1; } else { count++; } // 記錄 serverMap.put(server, count); } // 路由整體結果 for (Map.Entry<String, Integer> entry : serverMap.entrySet()) { System.out.println("IP:" + entry.getKey() + ",次數:" + entry.getValue()); } }
運行獲得請求結果。
IP:192.168.1.3,次數:24979 IP:192.168.1.2,次數:24896 IP:192.168.1.5,次數:25043 IP:192.168.1.4,次數:25082
每臺服務器被訪問的次數都趨近於 2.5w,有點負載均衡的意思。可是隨機畢竟是隨機,是不能保證訪問次數絕對均勻的。
輪訓訪問就簡單多了,拿上面的熊1到熊4來講,咱們一個接一個的啪啪 - 打臉,熊1打完打熊2,熊2打完打熊3,熊4打完打熊1,最終也是實現了被打均衡。可是保證均勻老是要付出代價的,隨機訪問中須要隨機,輪訓訪問中須要什麼來保證輪訓呢?
/** 服務器列表 */ private static List<String> serverList = new ArrayList<>(); static { serverList.add("192.168.1.2"); serverList.add("192.168.1.3"); serverList.add("192.168.1.4"); serverList.add("192.168.1.5"); } private static Integer index = 0; /** * 隨機路由算法 */ public static String randomOneByOne() { // 複製遍歷用的集合,防止操做中集合有變動 List<String> tempList = new ArrayList<>(serverList.size()); tempList.addAll(serverList); String server = ""; synchronized (index) { index++; if (index == tempList.size()) { index = 0; } server = tempList.get(index);; } return server; }
由代碼裏能夠看出來,爲了保證輪訓,必須記錄上次訪問的位置,爲了讓在併發狀況下不出現問題,還必須在使用位置記錄時進行加鎖,很明顯這種互斥鎖增長了性能開銷。
依舊使用上面的測試代碼測試10w次請求負載狀況。
IP:192.168.1.3,次數:25000 IP:192.168.1.2,次數:25000 IP:192.168.1.5,次數:25000 IP:192.168.1.4,次數:25000
上面演示了輪訓方式,還記的一開始提出的熊4比較胖抗擊打能力強,能夠承受別人2倍的捱打次數嘛?上面兩種方式都沒有體現出來熊 4 的這個特色,熊 4 竊喜,不痛不癢。可是熊 1 到 熊 3 已經在崩潰的邊緣,不行,咱們必需要讓胖着多打,能者多勞,提升總體性能。
/** 服務器列表 */ private static HashMap<String, Integer> serverMap = new HashMap<>(); static { serverMap.put("192.168.1.2", 2); serverMap.put("192.168.1.3", 2); serverMap.put("192.168.1.4", 2); serverMap.put("192.168.1.5", 4); } private static Integer index = 0; /** * 加權路由算法 */ public static String oneByOneWithWeight() { List<String> tempList = new ArrayList(); HashMap<String, Integer> tempMap = new HashMap<>(); tempMap.putAll(serverMap); for (String key : serverMap.keySet()) { for (int i = 0; i < serverMap.get(key); i++) { tempList.add(key); } } synchronized (index) { index++; if (index == tempList.size()) { index = 0; } return tempList.get(index); } }
此次記錄下了每臺服務器的總體性能,給出一個數值,數值越大,性能越好。能夠承受的請求也就越多,能夠看到服務器 192.168.1.5
的性能爲 4,是其餘服務器的兩倍,依舊 10 w 請求測試。
IP:192.168.1.3,次數:20000 IP:192.168.1.2,次數:20000 IP:192.168.1.5,次數:40000 IP:192.168.1.4,次數:20000
192.168.1.5
承擔了 2 倍的請求。
隨機加權的方式和輪訓加權的方式大體相同,只是把使用互斥鎖輪訓的方式換成了隨機訪問,按照機率論來講,訪問量增多時,服務訪問也會達到負載均衡。
/** 服務器列表 */ private static HashMap<String, Integer> serverMap = new HashMap<>(); static { serverMap.put("192.168.1.2", 2); serverMap.put("192.168.1.3", 2); serverMap.put("192.168.1.4", 2); serverMap.put("192.168.1.5", 4); } /** * 加權路由算法 */ public static String randomWithWeight() { List<String> tempList = new ArrayList(); HashMap<String, Integer> tempMap = new HashMap<>(); tempMap.putAll(serverMap); for (String key : serverMap.keySet()) { for (int i = 0; i < serverMap.get(key); i++) { tempList.add(key); } } int randomInt = new Random().nextInt(tempList.size()); return tempList.get(randomInt); }
依舊 10 w 請求測試,192.168.1.5
的權重是其餘服務器的近似兩倍,
IP:192.168.1.3,次數:19934 IP:192.168.1.2,次數:20033 IP:192.168.1.5,次數:39900 IP:192.168.1.4,次數:20133
上面的幾種方式要麼使用隨機數,要麼使用輪訓,最終都達到了請求的負載均衡。可是也有一個很明顯的缺點,就是同一個用戶的屢次請求頗有可能不是同一個服務進行處理的,這時問題來了,若是你的服務依賴於 session ,那麼由於服務不一樣, session 也會丟失,不是咱們想要的,因此出現了一種根據請求端的 ip 進行哈希計算來決定請求到哪一臺服務器的方式。這種方式能夠保證同一個用戶的請求落在同一個服務上。
private static List<String> serverList = new ArrayList<>(); static { serverList.add("192.168.1.2"); serverList.add("192.168.1.3"); serverList.add("192.168.1.4"); serverList.add("192.168.1.5"); } /** * ip hash 路由算法 */ public static String ipHash(String ip) { // 複製遍歷用的集合,防止操做中集合有變動 List<String> tempList = new ArrayList<>(serverList.size()); tempList.addAll(serverList); // 哈希計算請求的服務器 int index = ip.hashCode() % serverList.size(); return tempList.get(Math.abs(index)); }
上面的四種方式看似不錯,那麼這樣操做下來真的體現了一開始說的負載均衡嗎?答案是不必定的。就像上面的最後一個提問。
又或者每次出手的力度不一樣,有重有輕,恰巧熊 4 老是承受這種大力度啪啪打臉,熊 4 即將不省熊事,還要繼續打它嗎?
服務器也是這個道理,每次請求進行的操做對資源的消耗多是不一樣的。好比說某些操做它對 CPU 的使用就是比較高,也很正常。因此負載均衡有時不能簡單的經過請求的負載來做爲負載均衡的惟一依據。還能夠結合服務的當前鏈接數量、最近響應時間等維度進行整體均衡,總而言之,就是爲了達到資源使用的負載均衡。
最後的話
文章已經收錄在 Github.com/niumoo/JavaNotes ,歡迎Star和指教。更有一線大廠面試點,Java程序員須要掌握的核心知識等文章,也整理了不少個人文字,歡迎 Star 和完善,但願咱們一塊兒變得優秀。
文章有幫助能夠點個「贊」或「分享」,都是支持,我都喜歡!
文章每週持續更新,要實時關注我更新的文章以及分享的乾貨,能夠關注「 未讀代碼 」公衆號或者個人博客。