【mysql】GitHub 的 MySQL 高可用性實踐分享


原文出處: shlomi-noach    譯文出處:oschina   

GitHub 使用 MySQL 作為所有非 git 倉庫數據的主要存儲, 它的可用性對 GitHub 的訪問操作至關重要。GitHub 站點本身、GitHub 的 API、身份驗證等等都需要進行數據庫訪問。我們運行着多個 MySQL 集群來為不同的服務和任務提供支持。我們的集群使用經典的主從配置, 主集群中的某個節點能夠接受寫入。其余的從集群節點異步同步來自主服務器的更改, 並提供數據的讀取服務。

主節點的可用性尤為重要。沒有主服務器, 集群無法接受寫入:任何需要保留的寫入數據都不能持久化保存,任何傳入的更改(如提交、問題、用戶創建、審閱、新存儲庫等)都將失敗。

為了支持寫操作,我們顯然需要有一個可用的數據寫入節點,一個主集群。但同樣重要的是,我們需要能夠識別或找到該節點。

在一個寫入失敗,提示說主節點崩潰的場景中,我們必須確保能啟用一個新的主節點,並快速表明其身份。檢測故障所需的時間、進行故障轉移並公布新的主節點所花費的時間,構成了總的停機時間。

本文將介紹 GitHub 的 MySQL 高可用性和主服務發現解決方案,它使我們能夠可靠地運行跨數據中心操作,容忍數據中心隔離,並使得出現故障時耗費的停機時間變得更短。

高可用目標

本文描述的解決方案,迭代並改進了之前在 GitHub 實現的高可用(HA)解決方案。隨着規模的擴大,MySQL 的高可用策略必須適應變化。我們希望為 GitHub 中的 MySQL 和其他服務,提供類似的高可用策略。

在考慮高可用和服務發現時,有些問題可以引導你找到合適的解決方案。包含但不限於:

  • 你能容忍多長的中斷時間?

  • 崩潰檢測的可靠性如何?你能容忍錯誤報告(過早的故障轉移)嗎?

  • 故障轉移的可靠性如何?什么情況下可以失敗?

  • 解決方案在跨數據中心的場景下效果如何?在低延遲和高延遲的網絡情況下如何?

  • 解決方案是否允許一個完整的數據中心故障或者出現網絡隔離?

  • 有沒有防止或緩解腦裂(兩台服務器都宣稱是某個集群的主節點,不知情對方的存在,並且都能接受寫操作)的機制。

  • 你能允許數據丟失嗎?在多大程度上?

為了說明上面的一些情況,首先讓我們討論一下之前的高可用方案,並說說我們為什么要修改它。

移除基於 VIP 和 DNS 的服務發現

在之前的迭代版本中,我們:

  • 使用 orchestrator 來做檢測和故障轉移

  • 使用 VIP 和 DNS 做主節點的發現

在這個迭代版本中,客戶端使用名字服務(比如 mysql-writer-1.github.net)來發現寫節點。名字可以解析為一個虛擬 IP(VIP),這個 VIP 指向主節點。

因此,在正常情況下,客戶端只需要解析名稱,連接到解析后的 IP上,然后發現主節點也正在另一邊監聽鏈接(也就是客戶端連上了主節點)。

考慮這個跨越三個不同數據中心的復制拓撲:

當主節點發生故障時,必須在副本集中選出一個服務器,提升為新的主節點。

orchestrator 將會檢測到故障,選舉出一個新的主節點,然后重新分配 name(名稱)和 VIP(虛擬 IP)。客戶端實際上並不知道主節點的真實身份:它們只知道 name(名字),而這個名字現在必須解析給新的主節點。不過,需要考慮:

VIP 是需要協作的:它們由數據庫服務器自己聲明和擁有。為了獲得或釋放 VIP,服務器必須發送 ARP 請求。擁有 VIP 的服務器必須在新提升的主節點獲得 VIP 之前先釋放掉。這還有一些額外的影響:

  • 有秩序的故障轉移操作會首先通知故障主節點並要求它釋放 VIP,然后再通知新提升的主節點並要求它獲取 VIP。如果無法通知到原主節點或者拒絕釋放 VIP 怎么辦?首先要考慮到,該服務器上存在故障場景,它不可能會不及時響應,或根本不響應。

    • 我們最終可能會出現腦裂情況:兩個注解同時聲稱擁有同一個 VIP。根據最短的網絡路徑,不同的客戶端可能會連接到不同的服務器。

    • 事實源於兩個獨立服務器間的協作,並且這個設置是不可靠的。

  • 即使原主節點確實配合,工作流程也浪費了寶貴的時間:當我們通知原主節點時,切換到新主節點的操作一直在等待。

  • 即使 VIP 發生變化,現有的客戶端連接也不能保證與原服務器斷開連接,而且我們可能仍然會經歷腦裂。

VIP 受限於物理位置。它們屬於交換機或者路由器。所以,我們只能將 VIP 重新分配到位於同一位置的服務器上。特別是,當新提升的服務器位於不同的數據中心時,我們無法分配 VIP,只能修改 DNS。

  • 修改 DNS 需要較長的傳播時間。根據配置,客戶端會緩存 DNS 一段時間。跨數據中心(cross-DC)故障轉移則意味着更多的中斷時間:為了讓所有客戶端知曉新主節點的身份,需要花費更長的時間。

僅這些限制,就足以促使我們尋求新的解決方案,但考慮更多的是:

  • 主節點通過 pt-heartbeat 心跳服務進行自行注入,目的是測量延遲和節流。這項服務必須從新提升的主節點開始。如果有可能的話,原主節點的服務將被關閉。

  • 同樣的,Pseudo-GTID 注入也是主節點自己管理的。它將從新的主節點開始,並在原主節點結束。

  • 新的主節點被設為可寫。如果可能的話,原主節點被設為只讀。

這些額外的步驟是導致中斷總時間的一個因素,並且引入了它們自己的故障和摩擦。

該解決方案生效了,GitHub 已經成功完成 MySQL 的故障遷移,但我們希望我們的 HA 在以下方面有所改進:

  • 數據中心不可知

  • 允許數據中心出現故障

  • 刪除不可靠的協作工作流

  • 減少總的中斷時間

  • 盡可能地進行無損故障轉移

GitHub 的高可用解決方案:orchestrator, Consul, GLB

我們的新策略,除了附帶的改進外,還解決或減輕了上面的許多問題。在今天的高可用設置中,我們有:

  • 使用 orchestrator 來做監測和故障轉移。我們使用跨數據中心的 orchestrator/raft 方案,如下圖。

  • 使用 Hashicorp 的 Consul 來做服務發現。

  • 使用 GLB/HAProxy 作為客戶端和寫節點的代理層。

  • 使用選播(anycast)做網絡路由。

新的設置將完全刪除 VIP 和 DNS 的修改。在我們引入更多組件的同時,我們能夠將組件解耦並簡化任務,並且能夠使用可靠、穩定的解決方案。下面逐一分析。

正常流程

正常情況下,應用程序通過 GLB/HAProxy 連接到寫節點。

應用程序永遠不知道主節點的身份。和之前一樣,它們使用名字。例如,cluster1 的主節點命名為 mysql-writer-1.github.net。在我們當前的設置中,名字被解析為一個選播(anycast) IP。

使用選播時,名字在任何地方都被解析為相同的 IP,但流量會根據客戶端位置的不同進行路由。需要指出的是,在我們的每個數據中心,都有 GLB(我們的高可用負載均衡)被部署在不同的容器中。指向 mysql-writer-1.github.net 的流量總是路由到本地數據中心的 GLB 集群。因此,所有客戶端都由本地代理提供服務。

我們在 HAProxy 上運行 GLB。我們的 HAProxy 維護了一個寫連接池:每個 MySQL 集群一個連接池,其中每個連接池只有一個后端服務器:集群的主節點。DC 中的所有 GLB/HAProxy 容器都具有相同的連接池,並且它們都指向相同的后端服務器。這樣,如果一個應用程序想要寫入 mysql-writer-1.github.net,它連接到哪個 GLB 服務器並不重要。它總會被路由到實際的 cluster1 主節點上。

對於應用程序而言,服務發現結束於 GLB,並且不再需要重新發現。就這樣,通過 GLB 將流量路由到正確地址。

GLB 如何知道哪些服務器可以作為后端服務器,以及如何將更改傳播到 GBL 呢?

Consul 的服務發現

Consul 是著名的服務發現解決方案,它也提供 DNS 服務。然而,在我們的解決方案中,我們用它作為高效的鍵值存儲系統。

在 Consul 的鍵值存儲中,我們寫入了集群主控的標識。對於每一個集群,都有一個鍵值對記錄標識集群的主 FQDN,端口,IPV4,IPV6。

每一個 GLB/HAProxy 節點都運行 consul 模板:每一個服務都在監聽 consul 數據的變更(這里主要是對集群主控的數據變更)。consul 模板會生成一個有效的配置文件並且當配置變更時,能夠自動重載 HAProxy。

因此,Consul 中主控標識的變更會被每一個 GLB/HAProxy 觀察到,然后它們立即重新配置它們自己,在集群后端池中設置新的主控作為單一對象,並且進行重載以反映這些變更。

在 GitHub 中,每一個數據中心都有一個 Consul 設置,並且每一個設置都具有高可用性。然而,這些設置又互相獨立,它們之間不進行互相復制或數據共享。

那么 Consul 是如何獲得變更通知,在交叉數據中心中,信息又是如何分布的呢?

orchestrator/raft

運行一個 orchestrator/raft 設置:orchestrator 節點之間通過 raft 一致性算法進行通信。每一個數據中心有 1~2 個 orchestrator 節點。

orchestrator 負責失敗檢測,MySQL 故障轉移,以及 Consul 主控的變更通知。故障轉移通過單個 orchestrator/raft 主導節點進行操作,但是對於主控變更,產生新主控的消息會通過 raft 機制被傳播到所有 orchestrator 節點。

一旦 orchestrator 節點接收到主控變更的消息,它們會與自己對應的本地 Consul 設置通信:它們都執行 KV 寫操作。具有多個 orchestrator 節點的數據中心會有多個完全相同的 Consul 寫操作。

整體流程

在主節點故障的場景中:

  • orchestrator 節點檢測到故障。

  • orchestrator/raft 主導節點開始恢復。一個新的主節點被設置為 promoted 狀態。

  • orchestrator/raft 向所有 raft 集群節點通知主節點變更。

  • 所有 orchestrator/raft 成員接收到主節點變更的通知。每個成員都向本地 Consul 寫入包含新主節點身份的 KV 記錄。

  • 每個 GLB/HAProxy 都運行一個 consul 模版,用於監視 Consul KV 存儲的變化,並重新配置和重新加載 HAProxy。

  • 客戶端流量被重定向到新的主節點上。

每個組件都有明確的責任歸屬,而且整個設計簡單並且解耦。orchestrator 不需要知道負載均衡。Consul 不需要知道這些信息是從哪里來的。代理只關心 Consul,客戶端只關心代理。

而且:

  • 沒有 DNS 的變更需要傳播。
  • 沒有 TTL。
  • 整個流程不需要原故障主節點的配合,它在很大程度上已被忽略。

其他細節

為了進一步確保流程的安全,我們還提供了以下內容:

  • 將 HAProxy 的配置項 hard-stop-after 設置為一個非常短的時間。當在寫連接池中使用新的后端服務器重新加載時,它會自動終止所有到原主節點的連接。

    • 通過使用 hard-stop-after 配置項,我們甚至不需要客戶端的配合,這也就緩解了腦裂的情況。值得注意的是,這並不是絕對的,我們還是需要一些時間來消滅舊連接。但是,在某個時間點之后,我們就會感到舒服,因為不會出現令人厭惡的意外。

  • 我們並不嚴格要求 Consul 隨時可用。實際上,我們只需要它在故障轉移期間可用。如果 Consul 恰好這時不可用,GLB 將繼續使用已知的信息運作,不采用任何極端的行動。
  • GLB 被用於驗證新提升的主節點的身份。類似於我們的 context-aware MySQL pools(上下文感知的 MySQL 線程池),在后端服務器上進行檢查,以確保它確實是一個可寫的節點。如果我們恰好在 Consul 中刪除了主節點的身份,沒有問題;空的條目會被忽略。如果我們在 Consul 中錯誤的寫入了一個非主節點的名稱,沒有問題;GLB 將拒絕更新它並以最后已知的狀態繼續運行。

我們會在以下章節進一步完成備受關注和期望的高可用目標。

orchestrator/raft 失敗檢測

orchestrator使用全面方法來檢測失敗,因此這種方法非常可靠。我們不會觀察到誤報 —— 因為我們沒有進行過早的故障轉移,所以也不會產生不必要的中斷時間。

通過完全的 DC 網絡隔離(又稱 DC 柵欄),orchestrator/raft 進一步處理這個問題。一個 DC 網絡隔離會引起一些混淆:這個 DC 中的服務器是可以互相通信的。他們是與其他 DC 網絡隔離,還是其他 DC 被網絡隔離?

在一個 orchestrator/raft 設置中,raft 的 leader 節點就是運行故障轉移的那個節點。leader 是取得了大多數節點支持的節點(特定數量)。我們的 orchestrator 節點部署就是這樣,沒有單一數據中心可以占大多數,任何 n-1 的 DC 也是如此。

在一個完全 DC 網絡隔離的事件中,這個 DC 的 orchestrator 節點與其它 DC 中的對應節點失去連接。最終,隔離 DC 中的 orchestrator 節點不能作為 raft 集群的 leader 節點。如果任何這種節點碰巧成為了 leader 節點,它就會退出。一個新的 leader 節點可以從任何一個其他 DC 分配。leader 節點會得到其他所有 DC 的支持,這些 DC 彼此之間可以進行通信。

因此,調用 shots 的 orchestrator 節點將位於網絡隔離數據中心之外。一個隔離 DC 應該有一個主服務器,orchestrator 會使用可用 DC 中的其中一個服務器將它替換來初始化故障轉移。我們委托非隔離 DC 中的那些節點來做這個決定,以此來緩解 DC 隔離。

更快的公告

通過發出公告說主分支即將修改,可以進一步減少運行停機的總時間。如何實現這個想法?

當 orchestrator 開始進行故障遷移的時候,它會觀察可用於升級的服務器隊列。在了解自我復制的規則,以及接受提示和限制的情況下,在最好的行動方針中,它能做出基於一定訓練的決策。

它可能意識到一個可以升級的服務器也是一個理想的候選策略,例如:

  • 沒有什么可以阻止服務器的升級(潛在用戶已經暗示服務器優先升級),而且

  • 認為服務器可以將它所有的版本視為復本。

在這個例子中 orchestrator 首先將服務器設置為可寫,然后立即公告服務器的升級(我們的例子中是寫到了 Consul KV),即使異步開始修復復制樹,這種操作通暢會花費更多幾秒的運算。

有可能當我們的 GLB 服務器完全重載時,復制樹已經完好無損,但是這不是嚴格要求的。服務器可以接收到寫操作!

半同步復制

在 MySQL 的半同步復制中,在獲知更改已發送到一個或多個副本之前,主服務器不會確認事務已提交。它提供了一種實現無損故障轉移的方法:應用於主服務器的任何更改都將應用於或等待應用於其中一個副本。

一致性帶來的成本是:可用性風險。如果沒有副本確認收到更改,主服務器將被阻塞並且寫入操作將停止。幸運的是,這里有一個超時設置,在這之后主服務器可以恢復到異步復制模式,使寫入操作再次可用。

我們已經把我們的超時設置在一個合理的低值:500ms。將更改從主服務器發送到本地 DC 副本,通常也發送到遠程 DC,這個閾值是綽綽有余的。設置這個超時時間之后,我們可以觀察到完美的半同步行為(無需回退到異步復制),並且在確認失敗的情況下,可以在非常短的阻塞周期內獲得讓人滿意的表現。

我們在本地 DC 副本上啟用半同步,並且在主服務器宕機的情況下,我們期望(盡管不嚴格地執行)無損故障轉移。對完整的 DC 故障進行無損故障轉移的代價很高昂,我們並不期待這么做。

在試驗半同步超時的同時,我們還觀察到一種對我們有利的行為:主服務器在發生故障時,我們能夠影響最佳候選人的標識。通過在指定的服務器上啟用半同步,並將它們標記為候選服務器,我們可以通過影響故障的結果來減少總的停機時間。在我們的試驗中,我們觀察到,我們通常最終會得到最佳候選服務器,並因此發布快速公告。

心跳注入

我們沒有在已提升/已降級的主設備上管理 pt-heartbeat 服務的啟動/關閉,作為替代,我們選擇隨時隨地運行它。這需要進行打一些補丁,以便使 pt-heartbeat 可以支持服務器端來回更改它們的 read_only 狀態或其完全崩潰。

在我們目前的設置中,在主服務器及其副本上運行 pt-heartbeat 服務。在主服務器上,他們產生心跳事件。在副本服務器上,他們識別到服務器是只讀的,並定期重新檢查其狀態。只要服務器升級為主服務器,該服務器上的 pt-heartbeat 會將服務器標識為可寫,並開始注入心跳事件。

orchestrator 所有權委托

我們進一步委托到 orchestrator:

  • 偽-GTID注入

  • 設置被提升的主控作為可寫的,清除它的復寫狀態

  • 如果可能,設置老的主控為只讀狀態

對於新主控,以上所有這些操作減少了沖突的可能性。一個剛剛被提升的主控應該是在線的並且可接入,否則我們就不應該提升它。然后讓 orchestrator 直接應用變更到被晉升的主控上也應該是合理的。

限制和缺點

代理層使得應用程序不知道主服務器的身份,但是對於主服務器它也掩蓋了應用程序的身份。所有主服務器看到的連接都來自代理層,我們丟失了關於連接實際來源的信息。

隨着分布式系統的發展,我們仍然遺留了未處理的場景。

值得注意的是,在數據中心隔離場景中,假設主服務器位於 DC 中,DC 中的應用程序仍然能寫入主服務器。一旦網絡恢復正常,可能會導致狀態不一致。我們正努力在非常獨立的 DC 中,通過實現一個可靠的 STONITH 來緩解這種腦裂。和以前一樣,在將主服務器之前需要花費一些時間,可能出現短暫的腦裂。而避免腦裂的操作成本非常高。

更多的場景存在:故障轉移時 Consul 的終端;部分 DC 隔離;其他的。我們知道,使用這種性質的分布式系統不可能消除所有的漏洞,因此,我們將焦點放在最重要的案例上。

結果

orchestrator/GLB/Consul 設置給我們提供以下功能:

  • 可靠的故障檢測

  • 數據中心不可知的故障遷移

  • 典型的無損故障遷移

  • 對數據中心網絡隔離的支持

  • 緩解腦裂的問題(仍在實現中)

  • 無合作相關的依賴

  • 多數場景下大約 10~13 秒的斷電恢復能力。(我們觀察到一些場景下最長 20 秒的斷電恢復和極端場景下最長 25 秒的情況)

結語

編排/代理/服務發現范例在解耦架構中使用眾所周知的可信組件,這使得部署、運維和觀察變得更加容易,並且每個組件可以獨立擴展或縮減。我們將不斷測試我們的設置,以繼續尋求改進。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM