Categories
程式開發

GitHub負載均衡器深度解析


本文是一篇演講整理。 Joe Williams在演講中介紹了GitHub負載均衡器(GLB)的架構。

GitHub在HAProxy之上構建了一個彈性的自定義解決方案,以智能地路由來自各種客戶端(包括Git、SSH和MySQL)的請求。 GLB分為兩大部分:GLB導向器和GLB代理,後者是基於HAProxy構建的。 HAProxy有許多優點,包括負載平衡、高級運行狀況檢查和可觀察性等。有了與Consul的緊密集成,可以進行實時配置更改。部署使用一個GitHub流,並包含一個擴展的CI流程,並通過Slack上的ChatOps管理所有金絲雀部署。

GitHub負載均衡器深度解析 1

演講內容

我是Joe,在GitHub工作,今天我要講的是GLB。我們在2015年前後開始構建GLB,並於2016年投入生產。它是由我本人和GitHub的另一位工程師Theo Julienne共同構建的。我們構建GLB是為了替代一組不可擴展的HAProxy主機,後者是單體架構,難以測試,非常令人困惑而且有些可怕。這套基礎設施是在我加入GitHub之前很久就建立的,但我的工作就是要替換掉它。

在那個時候,我們在構建GLB時要遵循一些設計原則,以緩解之前系統存在的許多問題。其中一條原則是:我們想要的系統應該運行在貨架硬件上。之前的討論中我們談到了F5的故事,我們不想重蹈F5的複轍。

我們想要的是能夠水平擴展,支持高可用性,並且不會在任何時候中斷TCP會話的系統。我們想要支持連接清空(connection draining),可以在生產環境中輕鬆加入或撤出主機,而又不會破壞TCP連接的系統。我們想要的系統應該是以服務為單位的。 GitHub上有很多不一樣的服務。 GitHub.com是一項重要的服務,但我們還有很多內部服務,希望將它們隔離到各自的HAProxy實例中。

我們需要的系統應該可以像在GitHub上隨處可見的代碼一樣迭代,並且存儲在Git倉庫中。我們需要在每一層上都可​​以做測試,這樣就能確保每個組件都在正常工作。我們已經擴展到了全球範圍內的多個數據中心上,所以希望構建出為多個PoP和數據中心設計的產品。最後,我們想要一種能夠抵禦DoS攻擊的系統,因為不幸的是這種攻擊對於GitHub來說是非常普遍的。

GitHub負載均衡器深度解析 2

首先,我要深入談一談GitHub的請求路徑,並介紹一些我們用來管理它的開源工具;然後,我將深入探討GLB的兩大組成部分,分別是基於DPDK的導向器(Director)和基於HAProxy的代理。

GitHub負載均衡器深度解析 3

上圖是GitHub上請求路徑的高級概覽。由於我們對內部和外部請求都使用GLB,因此這套流程基本上適用於進出GitHub的所有請求和服務(包括MySQL之類的服務)。接下來介紹兩大組件,也就是GLB導向器和GLB代理。

GitHub負載均衡器深度解析 4

先從客戶端開始。 GitHub上有很多不同種類的客戶端和協議。如果你看一下自己的Git倉庫配置,就會發現它是GitHub.com。所以這意味著我必須弄清楚哪些客戶端基於HTTP,哪些客戶端基於Git,哪些是SSH,之類的各種事情。如果我能回到過去並與GitHub的創建者交流,讓他們把所有Git流量都放在git.github.com上,我肯定會這麼幹的;但我必須弄清楚端口443上都在跑什麼,諸如此類。因此,我們必須在HAProxy配置中做很多瘋狂的事情來解決所有問題。

我們有諸如Git、SSH和MySQL之類的客戶端,還有GLB的內部和外部版本,用來路由所有流量。另外Git有一點很特別,那就是它不支持重定向或重試之類的功能。如果你在拉取Git的過程中曾經按過Control+C,你肯定知道它會從頭重新開始;因此GLB最重要的任務之一,就是我們要避免任何種類的連接重置或類似的事情,以免幹掉Git的拉取和推送。

有一樣功能我們是沒法支持的,那就是TCP Anycast。我們的設計完全是圍繞在GeoDNS之上構建事物,並使用DNS而非Anycast來管理我們的流量。我們跨多個提供商和機構來管理DNS,並使用開源項目OctoDNS來完成所有這些工作。這是我與GitHub的另一位工程師Ross McFarland一起開發的項目。

GitHub負載均衡器深度解析 5

在網絡的邊緣,我們使用ECMP和客戶端發出請求,而ECMP則將這些客戶端分片到我們的GLB導向器層上。 GLB導向器是一個基於DPDK的L4代理。它使用直接代理返回(應用通用UDP封裝),對客戶端和L7代理完全透明。

GitHub負載均衡器深度解析 6

從導向器這裡,請求被轉發到HAProxy上,所有這些操作都主要使用Consul和Consul-Template以及稱為Kube Service Exporter的工具來管理,後者是我​​們在Kubernetes和HAProxy之間的一種橋樑。我們完全不使用任何入口控制器。我們直接與kube節點上的Nodeports對話,而Kube Service Exporter簡化了整個過程,而且它也是完全開源的。

GitHub負載均衡器深度解析 7

下面我們來深入研究GLB本身,首先關注導向器,然後是代理的具體內容。

GitHub負載均衡器深度解析 8

首先,我們使用等價多路徑(Equal-Cost Multi-Path,ECMP)對跨多個服務器的單個IP進行負載均衡,就像前面提到的一樣,它基本上就是為跨多個機器的特定IP哈希一個客戶端。我們將其稱為跨多個機器“拉伸IP”。

之前我們將GLB的設計分為L4代理/L7代理。這種設計的一個不錯的特性是,一般來說,L4代理不會經常更改,而L7代理則每小時或每天都會更改一次;因此我們在更新這些配置時可以減輕一些風險。

GitHub負載均衡器深度解析 9

我們在導向器中採取了很多謹慎措施,以盡可能減少狀態。 GLB導向器完全沒有像IPVS那樣的狀態共享。我們不會在節點之間進行任何類型的TCP連接多播共享或類似操作。我們要做的基本上就是建立一個轉發表,該轉發表在每台主機上都是一樣的,並且和ECMP有些相似;我們將客戶端哈希到這個轉發表上,然後將它們的請求路由到後端中的特定代理。

每個客戶端都使用zip哈希對它們的源IP和源端口進行哈希處理,然後把這些哈希基於稱為集合哈希(rendezvous hash)的工具分配給L4代理。集合哈希有一個很好的屬性,它可以在更改轉發表時僅重置1/n個連接。它們被分配給特定的主機。這樣,為了盡可能不斷開TCP連接,我們只斷開了與發生故障的主機連接的客戶端的連接。作出這些更改的另一種方式是,在每個導向器主機上都有一個運行狀況健康檢查程序。它們不斷檢查代理主機,進行更改並更新這個轉發表。

GitHub負載均衡器深度解析 10

這個哈希表也是協調代理主機維護的一種方式。我們在導向器內部有一個狀態機,我們可以轉發它,也可以通過它處理每個代理主機,圖上就是一個示例。這讓我們可以將代理主機拉入和退出生產環境,還能清空主機在將它們填充回去,而不會破壞這些連接。這種設計的唯一缺點是,我們必須使狀態表在所有導向器之間保持同步,這只在我們要更新狀態表以做維護等情況下才會成為問題。這種情況下代理主機就會失敗,因為我們處於一種怪異的狀態,其中流量以一種奇怪的方式被路由。所幸這種事情很少見,我們沒見過那麼多故障。

GitHub負載均衡器深度解析 11

為了在發生故障時減少連接重置的狀況,我們引入了一種稱為“故障轉移第二次機會”的操作,我認為這也是GLB導向器的一種簡潔且新穎的設計:為了允許最近失敗的主機完成TCP流,我們有一個名為glb-redirect的IP表模塊;在代理主機上,該模塊檢查我們在每個數據包中發送給代理主機的額外元數據,從而將這些數據包從活動主機重定向到最近發生故障的主機上。這樣,如果發生故障的主機還能處理請求,它將繼續處理下去。這種工作機制基本上是這樣的,如果主服務器因為數據包不是SYN包,或者因為包與已經建立的流對應而無法理解這個包,則glb-redirect將接收該包,並將其轉發到轉發表中現有的“失敗主機”上。這裡的元數據由GLB導向器注入,接下來會介紹細節。

GitHub負載均衡器深度解析 12

為了提供這種元數據,我們使用通用UDP封裝(Generic UDP Encapsulation)。我們在GLB設計階段的早期就決定使用直接服務器或直接代理返回。這種設計有很多影響,這裡講一些重點的部分。首先,它簡化了導向器的設計,因為我們只處理一個方向的數據包流。這也讓導向器對客戶和L7代理都是透明的。因此我們不必為X-Forwarded-For之類的事情煩惱。我們還可以向代理層的每個數據包添加額外的元數據,以實現”second-chance flow”。

我們使用稱為通用UDP封裝的相對較新的內核功能來執行這種操作。我們將元數據添加到這個GLB私有數據域中。在引入通用UDP封裝之前,我們使用了內核中稱為Foo-over-UDP的某種功能,我認為這兩種事物都是來自谷歌的。基本上,結果就是我們將所有目的地為L7代理的TCP會話都包裝在一個特殊的UDP包中,然後在代理端解包,就好像HAProxy直接從客戶端看到它一樣。那麼導向器的內容就講的差不多了,下面開始談代理的事情。

GitHub負載均衡器深度解析 13

我們很好地構建了我們的代理,或者一般來說是GLB,但更具體地說,我們的代理層基本上是在服務集群中。上面的樹形圖提供了高層次的配置概覽。

這種做法導致的結果是,我們通常會在其作業或服務上拆分HA配置,但是我們有許多配置恰好是多租戶的。 “服務”一詞有很多定義,但是我們在GLB中給服務下的定義是,服務是單個HAProxy配置文件,它可以偵聽任意數量的端口或IP之類的內容。像我之前提到的那樣,其中一些是特定於協議的。我們有Git配置;我們有SSH配置;我們有HTTP、SMTP和MySQL。

然後,在我們看來“集群”基本上就是這些HAProxy服務或配置的集合,它們被分配給特定區域或數據中心,然後我們為它們命名。在樹圖中可以看到,我們在一個集群中有一個區域和一個數據中心,而該集群由一堆服務組成。

GitHub負載均衡器深度解析 14

這種設計落地到現實世界,結果就是我們最終在CI中構建了所有HAProxy配置。這些是預構建的配置文件打包工件。部署的所有內容最後都成為了一種甚至還沒見到服務器時就已經預先構建好的配置。

我們曾經使用Puppet來部署這些配置,然後我們決定搞一些打包的配置工件出來。這些配置包與HAProxy本身分開部署,而HAProxy實際上是由Puppet管理的。如你在GitHub的屏幕截圖中所見,我們為GLB本身提供了48個配置包,你看過其中某個的話就會知道,它幾乎就是用Jenkins構建的矩陣。也就是說,這裡的48個構建代表了我們擁有的每個數據中心、集群和站點組合配置。

GitHub負載均衡器深度解析 15

接下來,就像在做CI作業的時候那樣,我們會運行大量測試。我在GitHub的前六個月工作,就是為GitHub的上一代負載均衡器編寫500個集成測試。因為我們對它的工作機制不了解,也不了解配置文件的生成方式,所以為了遷移到新系統,我們必須採用某種方式編寫測試,然後確保新配置能夠繼續工作。在CI作業期間,我們有一個測試套件,其在我們的測試環境中配置一個具有真實IP和實時後端的完整HAProxy GLB堆棧。然後我們使用數百個針對性的集成測試,這些測試用上了curl、Git、OpenSSL和我們可以想到的所有客戶端,以對所有這些HAProxy實例運行測試,並讓被正確終止和路由的請求失效。

通過這種開發流程,我們就可以有效地使用HAProxy配置進行測試驅動的開發工作,我認為這是非常簡潔的。在截圖中你可以看到一些測試示例;這些測試完成度達到了100%,過去的五年一直表現出色。

GitHub負載均衡器深度解析 16

現在,我們希望在CI中部署我們的配置。 CI通過後,我們可以將其部署到GLB集群中HAProxy…的一台或多台主機中。部署都是特定的Git分支。就像開源項目一樣,我們使用了GitHub Flow、PR等等功能。我們通常從“無操作”部署開始,這樣就能預覽將要做出的更改,並對比當前配置和新配置之間的區別。如果結果看起來是正確的,則我們會繼續進行金絲雀部署。金絲雀部署通常是在單個節點上進行的,然後我們可以觀察這個節點的客戶流量,並確保一切都能順利工作。接下來,我們可以部署到整個集群,並將更改合併到主集群中;這些工作都是通過Slack完成的。

GitHub負載均衡器深度解析 17

一旦有一台代理主機部署了配置,基本上實時更新就開始了。而這一切都是由Consul-Template、Consul和Kube Service Exporter編排的。對於Kubernetes,我們使用自己的開源項目Kube Service Exporter來管理服務和Consul。基本上,Kube Exporter是在Kubernetes中運行並導出kube服務數據的服務。 Kube服務不同於GLB服務,前者與Kubernetes API對話,拉回Kubernetes服務信息元數據並將其轉儲到Consul中。

然後,我們從Consul中獲取這份數據,並使用Consul-Template根據該數據構建HAProxy配置。就像我提過的那樣,我們絕不使用Kubernetes入口控制器。我們直接與每個kube節點上的Nodeport對話。在最初部署Kubernetes的過程中我們發現,至少對於我們來說,Kubernetes入口控制器是一種實際上並不需要的間接訪問機制,因為我們已經擁有瞭如此強大的負載平衡基礎架構,並且已經解決了DNS之類的問題。

GitHub負載均衡器深度解析 18

一旦部署了一個配置,我們可能就需要進行維護工作了。上圖是服務器後端線路的示例。為了進行維護工作,我們在每台GLB代理主機上都使用了一個小型服務,稱為Agent Checker。 Agent Checker知道如何對話,知道如何處理HAProxy中的agent-check和agent-send。 Agent Checker將有關後端及其當前狀態的元數據存儲在本地配置文件中,然後由Consul和Consul-Template管理。因此,我們可以在整個集群中作出更改。下圖是我們使用ChatOps拉入服務器,和拉出服務器以備維護的具體方式。

GitHub負載均衡器深度解析 19

現在客戶流量上來了,我們顯然會想要監控這些流量。我們幾乎記錄了能想到的所有內容,並將它們全部轉儲到Splunk中,以便對數據切片和切塊。我們還做了一些事情,例如使用HAProxy映射將IP地址映射到國家和自治系統編號上,以便我們追踪性能並提交報告。

我們還為每個請求設置了唯一的請求ID,然後確保GLB後面的服務在整個流程中都包含該標頭,以便我們跟踪整個堆棧中的請求。這裡有一些Splunk的屏幕截圖。最上面的是按國家/地區劃分的客戶端連接時間,最下面的是按自治系統劃分的客戶端連接時間。我必須加進去自Splunk的服務器指標,因為它確實很大。同樣,我們在導向器和HAProxy裡也會跟踪許多指標。

GitHub負載均衡器深度解析 20

目前我們將所有這些數據都轉儲到DataDog中。我們可以將這些數據切片和切塊,我們還有一個自定義的DataDog插件,該插件為我們要切片和切塊的元數據添加了一堆標籤。因此在這裡,我們可以按數據中心、群集、服務主機之類的標準來對GLB群集分類。 GitHub上的所有團隊都使用這些儀表板來監視GLB,及其背後服務的運行狀況和性能,並對任何類型的問題發出警報。

GitHub負載均衡器深度解析 7

GLB就講到這裡吧。正如我所提到的,它自2016年以來一直處於生產環境,並且幾乎處理了GitHub內部和外部每個服務的所有請求。

原文鏈接:

https://www.haproxy.com/user-spotlight-series/inside-the-github-load-balancer/