正文開始
前面幾篇的文章中,我們知道如何儘可能的在單機上,可以以最少的資源做最多的事,但是單機一定有它的限制,因此接下來我們要開始正式進入所謂的『 分散式系統 』。
分散式系統不是簡單的增加機器就可以增加效能那麼簡單,它不是簡單的 1 + 1 = 2 的這種概念,有時後 1 + 1 還會小於 2 或小於 1。
最要的原因在於要達成一致性的難度更高,並且維護與管理更複雜,除非你單機真的已經到了極限,不然如果是為了『 性能 』而加機器,那也只是會浪費你更多的時間,不過在實務上有時是為了可用性而加機器那這到還可接受。
本篇文章將分為以下幾個章節 :
- 應用層擴展基本架構
- 負載均衡架構優化
- 擴展後第一個問題 - Session
應用層擴展基本架構
應用層擴展基本上 90 % 都會是長的如下圖 1 架構。
圖 1 : 基本擴展型
基本上會將應用服務變成多台,然後前面加一個負載均衡 ( Load balancing ),每當用戶有請求進來時,會先通過負載均衡服務,然後它會選一台應用服務來將請求送過去。
目前在 web 領域比較常用的負載均衡服務應該是『 nginx 』,它的基本架構就如同咱們前面章節所說的 reactor 架構,所以基本上它可以處理非常多的連線。
30-07 之應用層的 I/O 優化 - 非阻塞 I/O 模型 Reactor
然後 nginx 它有提供以下幾種的分配演算法 :
- 輪詢 : 也就是所謂的輪流分配,每個能基本上都可以平均的收到。
- 加權 : 根據應用服務的能力來決定分配,例如機器性能較好的就給他權限較高,差的則給權限較低,這個地方在 nginx 還有很多變化。
- ip_hash : 就是同 ip 的都會打到同一台,這個在 socketio 擴展時很重要,如果沒設置,建立 socketio 連線時會打錯台。
- url_hash : 打同一個 url 會到同一台。
- fair : 簡單的說就是智能的演算法,它會根據頁面大小、加載時間來智慧的選擇應用服務。
負載均衡優化方向
接下來這個章節,咱們要來看看負載均衡的一些優化方向
1. 基本型 - 第七層負載均衡
首先咱們先看最基本型的詳細運行流程,如下圖 2 所示。
- 至 dns 請求 https://marklin.com 的 ip。
- dns 解析後回傳 120.1.1.10 位置回來給 client。
- 用戶往 120.1.1.10 發送請求至負載均衡服務。
- 負載均衡根據分配算法,將請求發送至第一台。
圖 2 : 基本型的傳輸流程
2. 進化型 - 負載均衡高可用性
上面的基本型有什麼問題呢 ?
負載均衡服務單機固障
嗯對很明顯,如果負載均衡服務固障了,那就整個系統就要說掰掰了,所以通常會準備多台的服務然後使用 keepalived 機制來確保某個正在服務的服務固障時,可以快速的轉換成備用的。
keepalived 是一個使用 vrrp 協議的高可用方案,有興趣的有友人可以自已去 google 研究一下。
接下來咱們來看一下加入以後整個流程是如何跑。
- 用戶先至 dns 尋問 https://marklin.com 這個位置的 ip。
- dns 回傳對應 ip 120.1.1.10 給用戶。
- 用戶使用 120.1.1.10 發送 http 請求。
- nat 會將外網 ip 120.1.1.10 轉換成內網的虛擬 ip 10.1.1.120。
- master 的負載均衡服務會負責接客,並選用其中一台應用服務來做 http 處理。
其中這裡要注意,keepalived 是會裝在兩台 nginx 機器中,然後對外都統一用 keepalived 這配置的虛擬 ip ( 10.1.1.120 ),當 master 有問題時 ( slave 偵測到 ),會自動的將 slave 升為 master。還有對外都是使用 keepalived 所配置的 ip,所以也不需要修改 nat 的轉換表。
圖 3 : nginx 加入高可用性
對了為啥要用虛擬 ip 呢 ? 首先第一個原因靈為快與省對外封包,你可以想成虛擬 ip 都是包含在同一個內網世界,每當某台內網服務要找另一台服務時,只要擴播到內網就夠了,而如果是在外網 ip 上,你就要去每個中端機器問一下這是你家的東西嗎 ? 然後找到這個 ip 是某家族的以後,在內裡面才會在擴播一次,這裡只是很簡單的說一下,詳細的內容自已去找找。
第二個原因,安全,通常躲在內網裡,正常的情況下,前面都要放一層防火牆,有些情況下 nat 本身也有防火牆的功能,這樣可以防止你內部的服務不被打。
第三個原因,外網 ip 很貴,你覺得有可能每一台備機都要有個外網 ip 嗎 ?
~ 小知識 ~ 這上面多了一個叫做 nat 的服務,它專門用來將公網 ip 轉換成內網 ip,詳細 nat 的知識可以參考之前寫的這一篇文章。
30-28之 WebRTC 連線前傳 - 為什麼 P2P 連線很麻煩 ? ( NAT )
3. 性能增加進化型 - 第四層負載均衡
到了這裡要來介紹另一個負載均衡服務『 LVS ( Linux Virtual Server ) 』。
上面說提到的 nginx 負載均衡服務它被稱為『 第七層負載均衡 』,而這裡所提的第七層就是指網路協議的『 應用層 』,也就是說它是專門用來負載均衡一些 http 這種應用層協議的服務。
而 lvs 被稱為『 第四層負載均衡 』,它就是專門用來處理第四層『 傳輸層 』協議的,也就是說它是用來專門處理 tcp 或 udp 傳輸層的負載均衡。
事實上在台灣大部份的系統大約上面兩個階段就很足夠了,下面這種結構除非你真的衝過頭,不然基本上應該是用不太到的,不過這裡還是可以來理解一下。
這種架構如下圖 4 所示。運行流程如下。
- 用戶先至 dns 尋問 https://marklin.com 這個位置的 ip。
- dns 回傳對應 ip 120.1.1.10 給用戶。
- 用戶使用 120.1.1.10 發送 http 請求。
- nat 會將外網 ip 120.1.1.10 轉換成內網的虛擬 ip 10.1.1.12。
- lvs 會在 tcp 傳輸層級將它轉發到後面的 nginx 應用層。
- nginx 應用層負載均衡服務收到後,在將它丟到後面某個應用層服務來處理。
圖 4 : 加入第二層負載均衡。
~ 小備註 ~ lvs 它事實上也有所所謂的 nat 功能,所以事實上可以將前面的 nat 拿掉。
擴展後第一個問題 - Session
上面基本上就是咱們比較常見的應用層擴展架構,那接下來咱們要來談談,當應用層擴展以下,你通常會碰到的第一個問題 :
那就是 session 如何處理呢 ?
這裡先簡單說一下 session 的概念。
session 簡單的說代表這『 狀態 』,咱們都知道 http 是屬於無狀態的協議,但通常咱們還是會需要有狀態的情況,例如登入以後,接下來的請求就代表這某個人,所以通常運行會是如下圖 6 所示。
注意一下通常咱們會將 sessionId 設定在 cookie 上,如圖 6 的第 2 個步驟,然後接下來的所有請求,都會自動代這個 sessionId 來進行請求,就如同第 3 個步驟,也就代表接下來的請求都是某個用戶所發的。
圖 6 : session 狀態
接下來來在應用層擴後,問題就出在 :
session 是存在某一台 server 上
所以如果有多台 server 就會發生如下圖 7 這種事情。
- mark 登入
- 在 server a 建立一個 session 對應表,abc sessionId 將對應到 mark 這個人
- 回傳時 header 會炎上這個 sessionId 回去。
- client 在帶上這個 sessionId abc 至 server b 請求。
- server b 不認識這個 sessionId。
圖 7 : server session 混亂圖
然後在現在咱們看看以下幾個常見的解法。
解法 1 : 外部儲存服務 ( Ex. Redis )
解法就是將 session Id 所對應到的用戶資訊儲放在外部儲放服務,例如 redis,它的運行流程如下圖 8 所示。
- 用戶 mark 發送登入請求。
- server 收到以後產生出個 session Id abc ,然後再將 session Id abc 與 mark 使用者資訊的對應關係儲放在 redis 中。
- server 將 session Id 設置到 cookie 中。
- 用戶 mark 帶這 session Id abc 發送一個請求。
- server 收到以後使用 session Id abc 去 redis 取得對應用用戶資料 mark。
- 回應 mark 訊息。
圖 8 : 使用外部儲存服務解決 session 問題
解法 2 : 負載均衡 ip hash
簡單的說就是同一個使用者只會打到同一台 server,這樣就不會發生找不到用戶 session 的問題。這個方法在某些情況下不錯用,例如需要多次請求建立連線的 socket.io,則下一篇會詳說。
解法 3 : Token
然後接下來這個解法嚴格來說就是不要用 session 來代表 user,而改用 token 來代表 user,這個 token 的產生方法有不少,不過比較常看到的是所謂的『 JWT Token 』,這裡就不說它是如何產生,反正它就是個 token,只是它是不好破的東東。
然後這裡有個重點那就是 :
token 中包含使用者資訊
也就是說 server 解完這個 token 後,它就知道是誰了,也就不需要在去其它地方拿用戶資料。
接下來咱們看一下這個方法的流程,如下圖 9 所示,由於 server 又變回所謂的『 無狀態 』模式後,又沒有擴展所帶來的問題囉。
- 用戶 Mark 發送登入請求。
- server 收到以後,使用 Mark 用戶資訊加密成一個 token xxxx ,然後回傳給用戶。
- 接下來用戶在表頭的 authorization 帶這個 token xxxx 發送一個請求。
- server 收到以後解密 token xxxx 後得到 Mark 這個用戶資訊,然後回應請求。
圖 9 : token 的解決流程
結論與心得
本篇文章中,咱們簡單的學習了應用層的基本擴展,最基本的用法就是前面加一台負載均衡服務,然後後面在放幾台應用層服務。
但是這感覺沒啥毛病啊 ? 好像也沒有發生什麼一致性的問題啊 ?
嗯對在大部份的情況下,應用層如果用上述這種基本擴展,是沒啥毛病的,因為每一台基本上都是獨立無相關,唯一相關的項多就是咱們上面提到的 session 問題。
但如果是有相關的話,就不是很好處理了……