在 GitLab 我們是如何擴充套件資料庫的

Yorick Peterse發表於2017-11-11

在擴充套件 GitLab 資料庫和我們應用的解決方案,去幫助解決我們的資料庫設定中的問題時,我們深入分析了所面臨的挑戰。

很長時間以來 GitLab.com 使用了一個單個的 PostgreSQL 資料庫伺服器和一個用於災難恢復的單個複製。在 GitLab.com 最初的幾年,它工作的還是很好的,但是,隨著時間的推移,我們看到這種設定的很多問題,在這篇文章中,我們將帶你瞭解我們在幫助解決 GitLab.com 和 GitLab 例項所在的主機時都做了些什麼。

例如,資料庫長久處於重壓之下, CPU 使用率幾乎所有時間都處於 70% 左右。並不是因為我們以最好的方式使用了全部的可用資源,而是因為我們使用了太多的(未經最佳化的)查詢去“衝擊”伺服器。我們意識到需要去最佳化設定,這樣我們就可以平衡負載,使 GitLab.com 能夠更靈活地應對可能出現在主資料庫伺服器上的任何問題。

在我們使用 PostgreSQL 去跟蹤這些問題時,使用了以下的四種技術:

  1. 最佳化你的應用程式程式碼,以使查詢更加高效(並且理論上使用了很少的資源)。
  2. 使用一個連線池去減少必需的資料庫連線數量(及相關的資源)。
  3. 跨多個資料庫伺服器去平衡負載。
  4. 分片你的資料庫

在過去的兩年裡,我們一直在積極地最佳化應用程式程式碼,但它不是一個完美的解決方案,甚至,如果你改善了效能,當流量也增加時,你還需要去應用其它的幾種技術。出於本文的目的,我們將跳過最佳化應用程式碼這個特定主題,而專注於其它技術。

連線池

在 PostgreSQL 中,一個連線是透過啟動一個作業系統程式來處理的,這反過來又需要大量的資源,更多的連線(及這些程式)將使用你的資料庫上的更多的資源。 PostgreSQL 也在 max_connections 設定中定義了一個強制的最大連線數量。一旦達到這個限制,PostgreSQL 將拒絕新的連線, 比如,下面的圖表示的設定:

在 GitLab 我們是如何擴充套件資料庫的

這裡我們的客戶端直接連線到 PostgreSQL,這樣每個客戶端請求一個連線。

透過連線池,我們可以有多個客戶端側的連線重複使用一個 PostgreSQL 連線。例如,沒有連線池時,我們需要 100 個 PostgreSQL 連線去處理 100 個客戶端連線;使用連線池後,我們僅需要 10 個,或者依據我們配置的 PostgreSQL 連線。這意味著我們的連線圖表將變成下面看到的那樣:

在 GitLab 我們是如何擴充套件資料庫的

這裡我們展示了一個示例,四個客戶端連線到 pgbouncer,但不是使用了四個 PostgreSQL 連線,而是僅需要兩個。

對於 PostgreSQL 有兩個最常用的連線池:

pgpool 有一點特殊,因為它不僅僅是連線池:它有一個內建的查詢快取機制,可以跨多個資料庫負載均衡、管理複製等等。

另一個 pgbouncer 是很簡單的:它就是一個連線池。

資料庫負載均衡

資料庫級的負載均衡一般是使用 PostgreSQL 的 “熱備機hot-standby” 特性來實現的。 熱備機是允許你去執行只讀 SQL 查詢的 PostgreSQL 副本,與不允許執行任何 SQL 查詢的普通備用機standby相反。要使用負載均衡,你需要設定一個或多個熱備伺服器,並且以某些方式去平衡這些跨主機的只讀查詢,同時將其它操作傳送到主伺服器上。擴充套件這樣的一個設定是很容易的:(如果需要的話)簡單地增加多個熱備機以增加只讀流量。

這種方法的另一個好處是擁有一個更具彈性的資料庫叢集。即使主伺服器出現問題,僅使用次級伺服器也可以繼續處理 Web 請求;當然,如果這些請求最終使用主伺服器,你可能仍然會遇到錯誤。

然而,這種方法很難實現。例如,一旦它們包含寫操作,事務顯然需要在主伺服器上執行。此外,在寫操作完成之後,我們希望繼續使用主伺服器一會兒,因為在使用非同步複製的時候,熱備機伺服器上可能還沒有這些更改。

分片

分片是水平分割你的資料的行為。這意味著資料儲存在特定的伺服器上並且使用一個分片鍵檢索。例如,你可以按專案分片資料並且使用專案 ID 做為分片鍵。當你的寫負載很高時,分片資料庫是很有用的(除了一個多主設定外,均衡寫操作沒有其它的簡單方法),或者當你有大量的資料並且你不再使用傳統方式儲存它也是有用的(比如,你不能把它簡單地全部放進一個單個磁碟中)。

不幸的是,設定分片資料庫是一個任務量很大的過程,甚至,在我們使用諸如 Citus 的軟體時也是這樣。你不僅需要設定基礎設施 (不同的複雜程式取決於是你執行在你自己的資料中心還是託管主機的解決方案),你還得需要調整你的應用程式中很大的一部分去支援分片。

反對分片的案例

在 GitLab.com 上一般情況下寫負載是非常低的,同時大多數的查詢都是隻讀查詢。在極端情況下,尖峰值達到每秒 1500 元組寫入,但是,在大多數情況下不超過每秒 200 元組寫入。另一方面,我們可以在任何給定的次級伺服器上輕鬆達到每秒 1000 萬元組讀取。

儲存方面,我們也不使用太多的資料:大約 800 GB。這些資料中的很大一部分是在後臺遷移的,這些資料一經遷移後,我們的資料庫收縮的相當多。

接下來的工作量就是調整應用程式,以便於所有查詢都可以正確地使用分片鍵。 我們的一些查詢包含了一個專案 ID,它是我們使用的分片鍵,也有許多查詢沒有包含這個分片鍵。分片也會影響提交到 GitLab 的改變內容的過程,每個提交者現在必須確保在他們的查詢中包含分片鍵。

最後,是完成這些工作所需要的基礎設施。伺服器已經完成設定,監視也新增了、工程師們必須培訓,以便於他們熟悉上面列出的這些新的設定。雖然託管解決方案可能不需要你自己管理伺服器,但它不能解決所有問題。工程師們仍然需要去培訓(很可能非常昂貴)並需要為此支付賬單。在 GitLab 上,我們也非常樂意提供我們用過的這些工具,這樣社群就可以使用它們。這意味著如果我們去分片資料庫, 我們將在我們的 Omnibus 包中提供它(或至少是其中的一部分)。確保你提供的服務的唯一方法就是你自己去管理它,這意味著我們不能使用主機託管的解決方案。

最終,我們決定不使用資料庫分片,因為它是昂貴的、費時的、複雜的解決方案。

GitLab 的連線池

對於連線池我們有兩個主要的訴求:

  1. 它必須工作的很好(很顯然這是必需的)。
  2. 它必須易於在我們的 Omnibus 包中運用,以便於我們的使用者也可以從連線池中得到好處。

用下面兩步去評估這兩個解決方案(pgpool 和 pgbouncer):

  1. 執行各種技術測試(是否有效,配置是否容易,等等)。
  2. 找出使用這個解決方案的其它使用者的經驗,他們遇到了什麼問題?怎麼去解決的?等等。

pgpool 是我們考察的第一個解決方案,主要是因為它提供的很多特性看起來很有吸引力。我們其中的一些測試資料可以在 這裡 找到。

最終,基於多個因素,我們決定不使用 pgpool 。例如, pgpool 不支援粘連線sticky connection。 當執行一個寫入並(嘗試)立即顯示結果時,它會出現問題。想像一下,建立一個工單issue並立即重定向到這個頁面, 沒有想到會出現 HTTP 404,這是因為任何用於只讀查詢的伺服器還沒有收到資料。針對這種情況的一種解決辦法是使用同步複製,但這會給錶帶來更多的其它問題,而我們希望避免這些問題。

另一個問題是, pgpool 的負載均衡邏輯與你的應用程式是不相干的,是透過解析 SQL 查詢並將它們傳送到正確的伺服器。因為這發生在你的應用程式之外,你幾乎無法控制查詢執行在哪裡。這實際上對某些人也可能是有好處的, 因為你不需要額外的應用程式邏輯。但是,它也妨礙了你在需要的情況下調整路由邏輯。

由於配置選項非常多,配置 pgpool 也是很困難的。或許促使我們最終決定不使用它的原因是我們從過去使用過它的那些人中得到的反饋。即使是在大多數的案例都不是很詳細的情況下,我們收到的反饋對 pgpool 通常都持有負面的觀點。雖然出現的報怨大多數都與早期版本的 pgpool 有關,但仍然讓我們懷疑使用它是否是個正確的選擇。

結合上面描述的問題和反饋,最終我們決定不使用 pgpool 而是使用 pgbouncer 。我們用 pgbouncer 執行了一套類似的測試,並且對它的結果是非常滿意的。它非常容易配置(而且一開始不需要很多的配置),運用相對容易,僅專注於連線池(而且它真的很好),而且沒有明顯的負載開銷(如果有的話)。也許我唯一的報怨是,pgbouncer 的網站有點難以導航。

使用 pgbouncer 後,透過使用事務池transaction pooling我們可以將活動的 PostgreSQL 連線數從幾百個降到僅 10 - 20 個。我們選擇事務池是因為 Rails 資料庫連線是持久的。這個設定中,使用會話池session pooling不能讓我們降低 PostgreSQL 連線數,從而受益(如果有的話)。透過使用事務池,我們可以調低 PostgreSQL 的 max_connections 的設定值,從 3000 (這個特定值的原因我們也不清楚) 到 300 。這樣配置的 pgbouncer ,即使在尖峰時,我們也僅需要 200 個連線,這為我們提供了一些額外連線的空間,如 psql 控制檯和維護任務。

對於使用事務池的負面影響方面,你不能使用預處理語句,因為 PREPARE 和 EXECUTE 命令也許最終在不同的連線中執行,從而產生錯誤的結果。 幸運的是,當我們禁用了預處理語句時,並沒有測量到任何響應時間的增加,但是我們 確定 測量到在我們的資料庫伺服器上記憶體使用減少了大約 20 GB。

為確保我們的 web 請求和後臺作業都有可用連線,我們設定了兩個獨立的池: 一個有 150 個連線的後臺程式連線池,和一個有 50 個連線的 web 請求連線池。對於 web 連線需要的請求,我們很少超過 20 個,但是,對於後臺程式,由於在 GitLab.com 上後臺執行著大量的程式,我們的尖峰值可以很容易達到 100 個連線。

今天,我們提供 pgbouncer 作為 GitLab EE 高可用包的一部分。對於更多的資訊,你可以參考 “Omnibus GitLab PostgreSQL High Availability”。

GitLab 上的資料庫負載均衡

使用 pgpool 和它的負載均衡特性,我們需要一些其它的東西去分發負載到多個熱備伺服器上。

對於(但不限於) Rails 應用程式,它有一個叫 Makara 的庫,它實現了負載均衡的邏輯幷包含了一個 ActiveRecord 的預設實現。然而,Makara 也有一些我們認為是有些遺憾的問題。例如,它支援的粘連線是非常有限的:當你使用一個 cookie 和一個固定的 TTL 去執行一個寫操作時,連線將粘到主伺服器。這意味著,如果複製極大地滯後於 TTL,最終你可能會發現,你的查詢執行在一個沒有你需要的資料的主機上。

Makara 也需要你做很多配置,如所有的資料庫主機和它們的角色,沒有服務發現機制(我們當前的解決方案也不支援它們,即使它是將來計劃的)。 Makara 也 似乎不是執行緒安全的,這是有問題的,因為 Sidekiq (我們使用的後臺程式)是多執行緒的。 最終,我們希望儘可能地去控制負載均衡的邏輯。

除了 Makara 之外 ,還有一個 Octopus ,它也是內建的負載均衡機制。但是 Octopus 是面向分片資料庫的,而不僅是均衡只讀查詢的。因此,最終我們不考慮使用 Octopus。

最終,我們直接在 GitLab EE構建了自己的解決方案。 新增初始實現的合併請求merge request可以在 這裡找到,儘管一些更改、提升和修復是以後增加的。

我們的解決方案本質上是透過用一個處理查詢的路由的代理物件替換 ActiveRecord::Base.connection 。這可以讓我們均衡負載儘可能多的查詢,甚至,包括不是直接來自我們的程式碼中的查詢。這個代理物件基於呼叫方式去決定將查詢轉發到哪個主機, 消除了解析 SQL 查詢的需要。

粘連線

粘連線是透過在執行寫入時,將當前 PostgreSQL WAL 位置儲存到一個指標中實現支援的。在請求即將結束時,指標短期儲存在 Redis 中。每個使用者提供他自己的 key,因此,一個使用者的動作不會導致其他的使用者受到影響。下次請求時,我們取得指標,並且與所有的次級伺服器進行比較。如果所有的次級伺服器都有一個超過我們的指標的 WAL 指標,那麼我們知道它們是同步的,我們可以為我們的只讀查詢安全地使用次級伺服器。如果一個或多個次級伺服器沒有同步,我們將繼續使用主伺服器直到它們同步。如果 30 秒內沒有寫入操作,並且所有的次級伺服器還沒有同步,我們將恢復使用次級伺服器,這是為了防止有些人的查詢永遠執行在主伺服器上。

檢查一個次級伺服器是否就緒十分簡單,它在如下的 Gitlab::Database::LoadBalancing::Host#caught_up? 中實現:

def caught_up?(location)
  string = connection.quote(location)

  query = "SELECT NOT pg_is_in_recovery() OR " \
    "pg_xlog_location_diff(pg_last_xlog_replay_location(), #{string}) >= 0 AS result"

  row = connection.select_all(query).first

  row && row['result'] == 't'
ensure
  release_connection
end

這裡的大部分程式碼是執行原生查詢(raw queries)和獲取結果的標準的 Rails 程式碼,查詢的最有趣的部分如下:

SELECT NOT pg_is_in_recovery()
OR pg_xlog_location_diff(pg_last_xlog_replay_location(), WAL-POINTER) >= 0 AS result"

這裡 WAL-POINTER 是 WAL 指標,透過 PostgreSQL 函式 pg_current_xlog_insert_location() 返回的,它是在主伺服器上執行的。在上面的程式碼片斷中,該指標作為一個引數傳遞,然後它被引用或轉義,並傳遞給查詢。

使用函式 pg_last_xlog_replay_location() 我們可以取得次級伺服器的 WAL 指標,然後,我們可以透過函式 pg_xlog_location_diff() 與我們的主伺服器上的指標進行比較。如果結果大於 0 ,我們就可以知道次級伺服器是同步的。

當一個次級伺服器被提升為主伺服器,並且我們的 GitLab 程式還不知道這一點的時候,新增檢查 NOT pg_is_in_recovery() 以確保查詢不會失敗。在這個案例中,主伺服器總是與它自己是同步的,所以它簡單返回一個 true

後臺程式

我們的後臺程式程式碼  總是  使用主伺服器,因為在後臺執行的大部分工作都是寫入。此外,我們不能可靠地使用一個熱備機,因為我們無法知道作業是否在主伺服器執行,也因為許多作業並沒有直接繫結到使用者上。

連線錯誤

要處理連線錯誤,比如負載均衡器不會使用一個視作離線的次級伺服器,會增加主機上(包括主伺服器)的連線錯誤,將會導致負載均衡器多次重試。這是確保,在遇到偶發的小問題或資料庫失敗事件時,不會立即顯示一個錯誤頁面。當我們在負載均衡器級別上處理 熱備機衝突 的問題時,我們最終在次級伺服器上啟用了 hot_standby_feedback ,這樣就解決了熱備機衝突的所有問題,而不會對錶膨脹造成任何負面影響。

我們使用的過程很簡單:對於次級伺服器,我們在它們之間用無延遲試了幾次。對於主伺服器,我們透過使用越來越快的回退嘗試幾次。

更多資訊你可以檢視 GitLab EE 上的原始碼:

資料庫負載均衡首次引入是在 GitLab 9.0 中,並且   支援 PostgreSQL。更多資訊可以在 9.0 release post 和 documentation 中找到。

Crunchy Data

我們與 Crunchy Data 一起協同工作來部署連線池和負載均衡。不久之前我還是唯一的 資料庫專家,它意味著我有很多工作要做。此外,我對 PostgreSQL 的內部細節的和它大量的設定所知有限 (或者至少現在是),這意味著我能做的也有限。因為這些原因,我們僱用了 Crunchy 去幫我們找出問題、研究慢查詢、建議模式最佳化、最佳化 PostgreSQL 設定等等。

在合作期間,大部分工作都是在相互信任的基礎上完成的,因此,我們共享私人資料,比如日誌。在合作結束時,我們從一些資料和公開的內容中刪除了敏感資料,主要的資料在 gitlab-com/infrastructure#1448,這又反過來導致產生和解決了許多分立的問題。

這次合作的好處是巨大的,它幫助我們發現並解決了許多的問題,如果必須我們自己來做的話,我們可能需要花上幾個月的時間來識別和解決它。

幸運的是,最近我們成功地僱傭了我們的 第二個資料庫專家 並且我們希望以後我們的團隊能夠發展壯大。

整合連線池和資料庫負載均衡

整合連線池和資料庫負載均衡可以讓我們去大幅減少執行資料庫叢集所需要的資源和在分發到熱備機上的負載。例如,以前我們的主伺服器 CPU 使用率一直徘徊在 70%,現在它一般在 10% 到 20% 之間,而我們的兩臺熱備機伺服器則大部分時間在 20% 左右:

CPU Percentage

在這裡, db3.cluster.gitlab.com 是我們的主伺服器,而其它的兩臺是我們的次級伺服器。

其它的負載相關的因素,如平均負載、磁碟使用、記憶體使用也大為改善。例如,主伺服器現在的平均負載幾乎不會超過 10,而不像以前它一直徘徊在 20 左右:

CPU Percentage

在業務繁忙期間,我們的次級伺服器每秒事務數在 12000 左右(大約為每分鐘 740000),而主伺服器每秒事務數在 6000 左右(大約每分鐘 340000):

Transactions Per Second

可惜的是,在部署 pgbouncer 和我們的資料庫負載均衡器之前,我們沒有關於事務速率的任何資料。

我們的 PostgreSQL 的最新統計資料的摘要可以在我們的 public Grafana dashboard 上找到。

我們的其中一些 pgbouncer 的設定如下:

設定
default_pool_size 100
reserve_pool_size 5
reserve_pool_timeout 3
max_client_conn 2048
pool_mode transaction
server_idle_timeout 30

除了前面所說的這些外,還有一些工作要作,比如: 部署服務發現(#2042), 持續改善如何檢查次級伺服器是否可用(#2866),和忽略落後於主伺服器太多的次級伺服器 (#2197)。

值得一提的是,到目前為止,我們還沒有任何計劃將我們的負載均衡解決方案,獨立打包成一個你可以在 GitLab 之外使用的庫,相反,我們的重點是為 GitLab EE 提供一個可靠的負載均衡解決方案。

如果你對它感興趣,並喜歡使用資料庫、改善應用程式效能、給 GitLab上增加資料庫相關的特性(比如: 服務發現),你一定要去檢視一下我們的 招聘職位 和  資料庫專家手冊 去獲取更多資訊。


via: https://about.gitlab.com/2017/10/02/scaling-the-gitlab-database/

作者:Yorick Peterse 譯者:qhwdw 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

相關文章