在上一篇博客我們介紹了 Nginx 一個很重要的功能——代理,包括正向代理和反向代理。這兩個代理的核心區別是:正向代理代理的是客戶端,而反向代理代理的是服務器。其中我們又重點介紹了反向代理,以及如何通過 Nginx 來實現反向代理。那麼瞭解了Nginx的反向代理之後,我們要通過Nginx的反向代理實現另一個重要功能——負載均衡。
1、負載均衡的由來
早期的系統架構,基本上都是如下形式的:
客戶端發送多個請求到服務器,服務器處理請求,有一些可能要與數據庫進行交互,服務器處理完畢後,再將結果返回給客戶端。
這種架構模式對於早期的系統相對單一,併發請求相對較少的情況下是比較適合的,成本也低。但是隨着信息數量的不斷增長,訪問量和數據量的飛速增長,以及系統業務的複雜度增加,這種架構會造成服務器相應客戶端的請求日益緩慢,併發量特別大的時候,還容易造成服務器直接崩潰。很明顯這是由於服務器性能的瓶頸造成的問題,那麼如何解決這種情況呢?
我們首先想到的可能是升級服務器的配置,比如提高CPU執行頻率,加大內存等提高機器的物理性能來解決此問題,但是我們知道摩爾定律的日益失效,硬件的性能提升已經不能滿足日益提升的需求了。最明顯的一個例子,天貓雙十一當天,某個熱銷商品的瞬時訪問量是極其龐大的,那麼類似上面的系統架構,將機器都增加到現有的頂級物理配置,都是不能夠滿足需求的。那麼怎麼辦呢?
上面的分析我們去掉了增加服務器物理配置來解決問題的辦法,也就是說縱向解決問題的辦法行不通了,那麼橫向增加服務器的數量呢?這時候集羣的概念產生了,單個服務器解決不了,我們增加服務器的數量,然後將請求分發到各個服務器上,將原先請求集中到單個服務器上的情況改爲將請求分發到多個服務器上,將負載分發到不同的服務器,也就是我們所說的負載均衡。
負載均衡完美的解決了單個服務器硬件性能瓶頸的問題,但是隨着而來的如何實現負載均衡呢?客戶端怎麼知道要將請求發送到那個服務器去處理呢?
2、Nginx實現負載均衡
Nginx 服務器是介於客戶端和服務器之間的中介,通過上一篇博客講解的反向代理的功能,客戶端發送的請求先經過 Nginx ,然後通過 Nginx 將請求根據相應的規則分發到相應的服務器。
主要配置指令爲上一講的 pass_proxy 指令以及 upstream 指令。負載均衡主要通過專門的硬件設備或者軟件算法實現。通過硬件設備實現的負載均衡效果好、效率高、性能穩定,但是成本較高。而通過軟件實現的負載均衡主要依賴於均衡算法的選擇和程序的健壯性。均衡算法又主要分爲兩大類:
靜態負載均衡算法:主要包括輪詢算法、基於比率的加權輪詢算法或者基於優先級的加權輪詢算法。
動態負載均衡算法:主要包括基於任務量的最少連接優化算法、基於性能的最快響應優先算法、預測算法及動態性能分配算法等。
靜態負載均衡算法在一般網絡環境下也能表現的比較好,動態負載均衡算法更加適用於複雜的網絡環境。
例子:
①、普通輪詢算法
這是Nginx 默認的輪詢算法。
例子:兩臺相同的Tomcat服務器,通過 localhost:8080 訪問Tomcat1,通過 localhost:8081訪問Tomcat2,現在我們要輸入 localhost 這個地址,可以在這兩個Tomcat服務器之間進行交替訪問。
一、分別修改兩個Tomcat服務器的端口爲8080和8081。然後再修改Tomcat的首頁,使得訪問這兩個頁面時能夠區分。如下:
修改端口號文件爲 server.xml :
修改首頁的路徑爲:webapps/ROOT/index.jsp
修改完成之後,分別啓動這兩個Tomcat服務器,然後分別輸入相應的地址端口號:
輸入地址:localhost:8081
輸入地址:localhost:8080
二、修改 nginx 的配置文件 nginx.conf
upstream OrdinaryPolling {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://OrdinaryPolling;//反向代理鏈接
index index.html index.htm index.jsp;
}
}
三、啓動 nginx。然後在瀏覽器輸入localhost 地址,觀看頁面變化:
②、基於比例加權輪詢
上述兩臺Tomcat服務器基本上是交替進行訪問的。但是這裏我們有個需求:
由於Tomcat1服務器的配置更高點,我們希望該服務器接受更多的請求,而 Tomcat2 服務器配置低,希望其處理相對較少的請求。
那麼這時候就用到了加權輪詢機制了。
nginx.conf 配置文件如下:
upstream OrdinaryPolling {
server 127.0.0.1:8080 weight=5;
server 127.0.0.1:8081 weight=2;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://OrdinaryPolling;
index index.html index.htm index.jsp;
}
}
其實對比上面不加權的輪詢方式,這裏在 upstream 指令中多了一個 weight 指令。該指令用於配置前面請求處理的權重,默認值爲 1。
也就是說:第一種不加權的普通輪詢,其實其加權值 weight 都爲 1。
下面我們看頁面相應結果:
明顯 8080 端口號出現的次數更多,試驗的次數越多越接近我們配置的比例。
③、基於IP路由負載
我們知道一個請求在經過一個服務器處理時,服務器會保存相關的會話信息,比如session,但是該請求如果第一個服務器沒處理完,通過nginx輪詢到第二個服務器上,那麼這個服務器是沒有會話信息的。
最典型的一個例子:用戶第一次進入一個系統是需要進行登錄身份驗證的,首先將請求跳轉到Tomcat1服務器進行處理,登錄信息是保存在Tomcat1 上的,這時候需要進行別的操作,那麼可能會將請求輪詢到第二個Tomcat2上,那麼由於Tomcat2 沒有保存會話信息,會以爲該用戶沒有登錄,然後繼續登錄一次,如果有多個服務器,每次第一次訪問都要進行登錄,這顯然是很影響用戶體驗的。
這裏產生的一個問題也就是集羣環境下的 session 共享,如何解決這個問題?
通常由兩種方法:
1、第一種方法是選擇一箇中間件,將登錄信息保存在一箇中間件上,這個中間件可以爲 Redis 這樣的數據庫。那麼第一次登錄,我們將session 信息保存在 Redis 中,跳轉到第二個服務器時,我們可以先去Redis上查詢是否有登錄信息,如果有,就能直接進行登錄之後的操作了,而不用進行重複登錄。
2、第二種方法是根據客戶端的IP地址劃分,每次都將同一個 IP 地址發送的請求都分發到同一個 Tomcat 服務器,那麼也不會存在 session 共享的問題。
而 nginx 的基於 IP 路由負載的機制就是上訴第二種形式。大概配置如下:
upstream OrdinaryPolling {
ip_hash;
server 127.0.0.1:8080 weight=5;
server 127.0.0.1:8081 weight=2;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://OrdinaryPolling;
index index.html index.htm index.jsp;
}
}
注意:我們在 upstream 指令塊中增加了 ip_hash 指令。該指令就是告訴 nginx 服務器,同一個 IP 地址客戶端發送的請求都將分發到同一個 Tomcat 服務器進行處理。
④、基於服務器響應時間負載分配
根據服務器處理請求的時間來進行負載,處理請求越快,也就是響應時間越短的優先分配。
upstream OrdinaryPolling {
server 127.0.0.1:8080 weight=5;
server 127.0.0.1:8081 weight=2;
fair;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://OrdinaryPolling;
index index.html index.htm index.jsp;
}
}
⑤、對不同域名實現負載均衡
通過配合location 指令塊我們還可以實現對不同域名實現負載均衡。
upstream wordbackend {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
upstream pptbackend {
server 127.0.0.1:8082;
server 127.0.0.1:8083;
}
server {
listen 80;
server_name localhost;
location /word/ {
proxy_pass http://wordbackend;
index index.html index.htm index.jsp;
}
location /ppt/ {
proxy_pass http://pptbackend;
index index.html index.htm index.jsp;
}
}