導讀:GitHub GLB director 是 GitHub 最近開源的負載均衡器,定位為更好的資料中心負載均衡器,本文詳細介紹了 GLB 特性。
在GitHub,我們在網路邊緣的metal cloud上每秒處理數萬個請求。 我們之前文章已經介紹過GLB,這是我們針對裸機資料中心的可擴充套件負載均衡解決方案,它支援大多數GitHub的對外服務,並且還為我們最關鍵的內部系統提供負載均衡服務,例如高可用MySQL叢集。 今天,我們很高興能夠分享有關負載均衡器設計的更多細節,並將GLB Director開源。
GLB Director是4層負載均衡器,可在大量物理機器上擴充套件單個IP地址,同時嘗試在修改期間最大限度地減少連線中斷。 GLB Director不會替換像haproxy和nginx這樣的服務,而是部署在這些服務(或任何TCP服務)之前,允許它們跨多個物理機器擴充套件,而不需要每臺機器都有唯一的IP地址。
使用ECMP擴充套件IP
4層負載均衡器的基本屬性是能夠使用單個IP地址在多個伺服器之間實現均衡連線。 為了擴充套件單個IP以處理更多的流量,我們不僅需要在後端伺服器之間進行流量拆分,還需要能夠擴充套件負載均衡器本身。 這實際上是另一層負載均衡。
通常,我們將IP地址視為單個物理機器,將路由器視為將資料包移動到下一個最近路由器的機器。 在最簡單的情況下,總是有一個最佳的下一跳,路由器選擇該跳並轉發所有資料包直到達到目的地。
實際上,大多數網路都要複雜得多。 兩臺計算機之間通常有多條路徑可用,例如,使用多個ISP或者兩臺路由器透過多條物理電纜連線在一起以增加容量並提供冗餘。 這是等價多路徑(ECMP)路由發揮作用的地方 – 而不是由路由器選擇單個最佳下一跳,ECMP中很多路徑具有相同成本(通常定義為到目的地的AS的數量), 路由器分散流量以便在所有可用的相同成本路徑之間均衡連線。
ECMP透過對每個資料包進行hash以確定其中一個可用路徑。此處使用的hash函式因裝置而異,但通常是基於源和標的IP地址以及TCP流量的源和標的埠的一致性hash。這意味著同一個TCP連線的多個資料包通常會遍歷相同的路徑,這意味著即使路徑具有不同的延遲,資料包也會以相同的順序到達。值得註意的是,在這種情況下,路徑可以在不中斷連線的情況下進行更改,因為它們總是最終位於同一個標的伺服器上,此時它所採用的路徑大多無關緊要。
ECMP的另一種用法是當我們想要跨多個伺服器而不是跨多個路徑上的同一伺服器時。每個伺服器都可以使用BGP或其他類似的網路協議使用相同的IP地址,從而使連線在這些伺服器之間進行分片,路由器不知道連線是在不同的地方處理的,而非傳統做法那樣所有的連線都同一臺機器上處理。
雖然ECMP會像對流量進行分片,但它有一個巨大的缺點:當相同IP的伺服器更改(或沿途的任何路徑或路由器發生變化)時,連線必須重新均衡,才能保證每個伺服器上的連線比較均衡。 路由器通常是無狀態裝置,只是為每個資料包做出最佳決策而不考慮它所屬的連線,這意味著在這種情況下某些連線會中斷。
在上面的例子中,我們可以想象每種顏色代表一個活動的連線。 新增新的代理伺服器使用相同的IP。 路由器保證一致性雜湊,將1/3連線移動到新伺服器,同時保持2/3連線在老伺服器上。 不幸的是,對於進行中的1/3連線,資料包現在到達了無連線狀態的伺服器,因此連線會失敗。
將director/proxy分離
以前僅使用ECMP的解決方案的問題在於它不知道給定資料包的完整背景關係,也不能為每個資料包/連線儲存資料。事實證明,通常使用Linux Virtual Server(LVS)等工具。我們建立了一個新的“director”伺服器層,它透過ECMP從路由器獲取資料包,但不是依靠路由器的ECMP hash來選擇後端代理伺服器,而是對所有連結控制hash和儲存狀態(選擇後端)。當我們更改代理層伺服器時,director層有望不變,我們的連線也不會斷掉。
雖然這在許多情況下效果很好,但它確實有一些缺點。在上面的示例中,我們同時添加了LVS director和後端代理伺服器。新的director接收到一些資料包,但是還沒有任何狀態(或者具有延遲狀態),因此將其作為新連線進行hash處理並可能使其出錯(並導致連線失敗)。 LVS的典型解決方法是使用多播連線同步來保持所有LVS director伺服器之間共享的連線狀態。這仍然需要傳播連線狀態,並且仍然需要重覆狀態 – 不僅每個代理都需要Linux核心網路堆疊中每個連線的狀態,而且每個LVS director還需要儲存連線到後端代理伺服器的對映。
將所有狀態從director層移除
當我們設計GLB時,我們決定要改善這種情況而不是重覆狀態。 透過使用已儲存在代理伺服器中的流狀態作為維護來自客戶端的已建立Linux TCP連線的一部分,GLB採用與上述方法不同的方法。
對於每個進入的連線,我們選擇可以處理該連線的主伺服器和輔助伺服器。 當資料包到達主伺服器且無效時,會將資料包轉發到輔助伺服器。 選擇主/輔助伺服器的雜湊是預先完成一次,並儲存在查詢表中,因此不需要在每個流或每個資料包的基礎上重新計算。 新增新的代理伺服器時,對於1/N連線,它將成為新的主伺服器,舊的主伺服器將成為輔助伺服器。 這允許現有流程完成,因為代理伺服器可以使用其本地狀態(單一事實來源)做出決策。 從本質上講,這使得資料包在到達保持其狀態的預期伺服器時具有“第二次機會”。
即使director仍然會將連線傳送到錯誤的伺服器,該伺服器也會知道如何將資料包轉發到正確的伺服器。 就TCP流而言,GLB director層是完全無狀態的:director伺服器可以隨時進出,並且總是選擇相同的主/輔伺服器,只要它們的轉發表匹配(但它們很少改變)。 在變更代理時有些細節需要註意,我們將在下麵介紹。
維護Hash集合不變
GLB Director設計的核心歸結為始終如一地選擇主伺服器和輔助伺服器,並允許代理層伺服器根據需要排空和填充。 我們認為每個代理伺服器都有一個狀態,當有伺服器加入或者退出時調整狀態。
我們建立一個靜態二進位制轉發表,它以相同方式在每個控制器伺服器上生成,以將進入的連線對映到給定的主伺服器和輔助伺服器。 我們並沒有採用在資料包處理時從所有可用伺服器中選擇伺服器的這種複雜邏輯,而是透過建立表(65k行)這種間接的方式,每行包含主伺服器和輔助伺服器IP地址。 該表以二維陣列的方式將資料儲存在記憶體中,每個表大約512kb。 當資料包到達時,我們始終將其(僅基於資料包資料)hash到該表中的同一行(使用hash作為陣列的索引),這提供了一致的主伺服器和輔助伺服器對。
我們希望每個伺服器在主要和輔助欄位中大致相同,並且永遠不會出現在同一行中。 當我們新增新伺服器時,我們希望某些行使其主伺服器成為輔助伺服器,並且新伺服器將成為主伺服器。 同樣,我們希望新伺服器在某些行中成為輔助伺服器。 當我們刪除伺服器時,在它是主伺服器的任何行中,我們希望輔助伺服器成為主伺服器,而另一個伺服器則成為輔助伺服器。
這聽起來很複雜,但可以用幾個不變數簡潔地概括:
-
當我們更改伺服器集時,應保持現有伺服器的相對順序。
-
伺服器的順序應該是可計算的,除了伺服器串列之外沒有任何其他狀態(可能還有一些預定義的種子)。
-
每個伺服器在每行中最多應出現一次。
-
每個伺服器在每列中的出現次數應大致相同。
針對上述的一些問題,集合hash是一個理想的選擇,因為它可以很好地滿足這些不變數。 每個伺服器(在我們的例子中,IP)都與行號一起進行hash,伺服器按該hash(只是一個數字)進行排序,並且我們獲得該給定行的伺服器的唯一順序。 我們分別將前兩個作為主要和次要。
將保持相對順序,因為無論包含哪些其他伺服器,每個伺服器的hash都是相同的。 生成表所需的唯一資訊是伺服器的IP。由於我們只是對一組伺服器進行排序,因此伺服器只出現一次。 最後,如果我們使用偽隨機的良好hash函式,那麼排序將是偽隨機的,因此分佈將如我們所期望的那樣均勻。
代理(Proxy)相關操作
新增或刪除代理伺服器,我們需要一些特別的處理方式。這是因為轉發表條目僅定義主要/輔助代理,因此排空/故障轉移僅適用單個代理主機。 我們為代理伺服器定義以下有效狀態和狀態轉換:
當代理伺服器處於活動狀態,耗盡或填充時,它將包含在轉發表條目中。 在穩定狀態下,所有代理伺服器都是活動的,並且上面描述的集合點雜湊將在主列和輔助列中具有大致均勻且隨機的每個代理伺服器分佈。
當代理伺服器轉換為耗盡時,我們透過交換我們原本包含的主要和次要條目來調整轉發表中的條目:
這具有將資料包傳送到先前次要的伺服器的效果。 由於它首先接收資料包,它將接受SYN資料包,因此接受任何新連線。 對於任何不理解為與本地流有關的資料包,它將其轉發到其他伺服器(先前的主伺服器),這允許完成現有連線。
這樣可以優雅地耗盡所需的連線伺服器,之後可以完全刪除它,並且代理可以隨機填充到第二個空槽:
填充中的節點看起來就像活動一樣,因為該表本身允許第二次機會:
此實現要求一次只有一個代理伺服器處於活動狀態以外的任何狀態,這實際上在GitHub上執行良好。對代理伺服器的狀態更改可以與需要維護的最長連線持續時間一樣快。我們正致力於設計的擴充套件,不僅支援主要和次要,而且一些元件(如下麵列出的標題)已經包含對任意伺服器串列的初始支援。
資料中心內封裝
現在有了一個演演算法來一致地選擇後端代理伺服器,但是如何在資料包內把輔助伺服器(secondary server )的資訊也封裝進去呢?這樣主伺服器可以在不理解資料包的情況下轉發資料包。
LVS 的傳統方式是使用IP over IP(IPIP)隧道。客戶端 IP 資料包封裝在內部IP資料包內,並轉發到代理伺服器,代理伺服器對其進行解封裝。但很難在 IPIP 資料包中編碼其他伺服器的元資料,因為唯一可用的空間是 IP 選項,資料中心路由器傳遞未知 IP 的資料包到處理軟體(稱之為“第2層慢速路徑”),速度從每秒數百萬到數千個資料包。
為了避免這種情況,需要將資料隱藏在路由器不同資料包格式中,避免它試圖去理解。我們最初採用原始 Foo-over-UDP(FOU)和自定義 GRE載荷(payload),基本上封裝了 UDP 資料包中的所有內容。我們最近轉換到通用 UDP 封裝(GUE),它提供了封裝內部 IP 協議的標準 UDP 資料包。我們將輔助伺服器的 IP 放在GUE標頭的私有資料中。從路由器的角度來看,這些資料包都是兩個普通伺服器之間的內部資料中心 UDP 資料包。
使用 UDP 的另一個好處是源埠可以使用每個連線的雜湊填充,以便它們透過不同的路徑(在資料中心內使用ECMP)在資料中心內流動,並可在代理伺服器的 NIC 的不同 RX 佇列上接收訊息(類似使用 TCP/IP 頭欄位的雜湊)。這對 IPIP 是不可能的,因為大多數資料中心的 NIC 只能理解普通 IP,TCP/IP 和 UDP/IP。值得註意的是,NIC 無法檢視 IP/IP 資料包。
當代理伺服器想要將資料包發送回客戶端時,它不需要封裝或透過我們的導向器層(director tier)傳回,它可以直接傳送資料到客戶端(通常稱為“Direct Server Return”)。這是典型的負載均衡器設計,對於內容提供商尤其有用,因為大多數情況都是出站流量遠大於入站流量。
資料包流如下圖所示:
引入DPDK
自從首次公開討論了我們的初始設計以來,我們已經完全使用 DPDK重寫了 glb-director 。DPDK 是一個開源的透過繞過Linux核心,允許從使用者空間進行非常快速的資料包處理的專案。這樣就能夠在普通 NIC 上透過 CPU 上實現 NIC 線路速率處理,並可輕鬆擴充套件導向器層,以處理與公共連線所需的入站流量一樣多的流量。這在防 DDoS 攻擊中尤為重要,我們不希望負載均衡器成為瓶頸。
GLB 最初的標的之一是可以在通用資料中心的硬體上執行,而無需任何特殊的硬體配置。 GLB 的 Director 和代理伺服器都可像資料中心的普通伺服器一樣供應。每個伺服器都有一對系結的網路介面,這些介面在 GLB Director 伺服器上的 DPDK 和 Linux 系統之間共享。
現代 NIC 支援SR-IOV,這種技術可以使單個 NIC 從作業系統的角度看起來像多個 NIC。這通常由虛擬機器管理程式使用,以要求真實 NIC(“Physical Function”)為每個 VM 建立多個虛擬 NIC(“Virtual Functions”)。為了使 DPDK 和 Linux 內核能夠共享 NIC,我們使用 flow bifurcation,它將特定流量(標的是 GLB IP 地址)傳送給我們DPDK 在 Virtual Function 上處理,同時將剩餘的資料包與 Linux 內核的網路堆疊保留在 Physical Function 上。
我們發現 Virtual Function 上 DPDK 的資料包處理速率可以滿足要求。 GLB Director 使用 DPDK Packet Distributor樣式來分發封裝資料包的任務到機器上的 CPU,支援任意數量的 CPU 核心,因為它是無狀態的,可以高度並行化。
GLB Director 支援匹配和轉發包含 TCP 有效負載的入站 IPv4 和 IPv6 資料包,以及作為 Path MTU Discovery的一部分的入站 ICMP Fragmentation Required 訊息。
使用Scapy為DPDK加入測試用例
一個典型的問題是,在建立(或使用)那些使用了低階原語(例如直接與NIC通訊)但是高速執行的技術時,它們變得非常難以測試。作為建立GLB Director的一部分,我們也建立了一個測試環境,支援對我們的DPDK應用進行簡單的端對端包流測試,透過影響DPDK的方式支援一個環境抽象層(EAL),允許物理NIC和基於libpcap的本地介面,在應用檢視中是相同的。
這允許我們在Scapy中寫測試,使用簡單的Python的lib包檢視,操作和寫資料包。透過建立一個Linux的虛擬網絡卡驅動,一邊用Scapy,另一邊用DPDK,我們能傳輸定製的包並且驗證我們軟體在另一邊支援的功能,這是一個完整GUE封裝的後端代理服務期望的資料包。
該方法允許我們測試更多的複雜行為,例如為了正確路由,遍歷傳輸層的ICMPv4/ICMPv6頭獲取源IP和TCP埠,以便正確轉發來自外部路由器的ICMP訊息。
健康檢查
GLB的設計包含了優雅地處理伺服器故障的部分。目前設計包含主/備,對於給定的轉發表/客戶端,意味著我們可以透過健康檢查透過觀察每個Director來解決單伺服器故障。我們執行一個名為glb-healthcheck的服務,它不斷驗證每個後端伺服器的GUE隧道和任意HTTP埠。
當伺服器出現故障時,我們將切換主/備,將備換成主。這是伺服器的“軟切換”,支援故障轉移的好辦法。如果健康檢查失敗是誤報,則連線不會中斷,它們只會換一條不同的路徑遍歷。
proxy使用iptables提供第二次機會
構成GLB的最後一個元件是Netfilter模組和iptables的標的,它在每個代理伺服器上執行,並提供“第二次機會”進行設計。
此模組提供了一個簡單的任務,根據Linux核心TCP堆疊,確定每個GUE資料包的內部TCP / IP資料包是否在本地有效,如果不是,則將其轉發到下一個代理伺服器(備伺服器),而不是在當前伺服器解封裝。
在資料包是SYN(新連線)或在本地對已建立的連線有效的情況下,當前伺服器會接收它。然後,我們接收GUE包,使用包含fou模組的Linux 核心4.x GUE在本地處理它。
已經開源
當我們準備開始寫一個更好的資料中心負載均衡器時,我們決定將其開源,以便其他人可以從我們的工作中受益。我們很高興能在github/glb-director上開源所有這些元件。我們允許其他人能使用它,並且將它作為負載均衡的通用解決方案,在物理資料中心環境中的商用硬體上執行。
開源專案地址:
https://github.com/github/glb-director
英文原文:
https://githubengineering.com/glb-director-open-source-load-balancer/
相關閱讀:
微服務閘道器哪家強?一文看懂Zuul, Nginx, Spring Cloud, Linkerd效能差異
微服務閘道器終結者?Spring Cloud推出新成員Spring Cloud Gateway
本文作者 Theo Julienne,由方圓、王淵命、林統傳、鄧啟明翻譯。轉載本文請註明出處,歡迎更多小夥伴加入翻譯及投稿文章的行列,詳情請戳公眾號選單「聯絡我們」。
高可用架構
改變網際網路的構建方式
長按二維碼 關註「高可用架構」公眾號