故障轉移集群 PostgreSQL + Patroni。 實施經驗

在本文中,我將告訴您我們如何解決 PostgreSQL 容錯問題、為什麼它對我們變得如此重要以及最終發生了什麼。

我們擁有高負載的服務:全球有 2,5 萬用戶,每天有超過 50 萬活躍用戶。 服務器位於愛爾蘭一個地區的 Amazone:100 多台不同的服務器持續工作,其中近 50 台帶有數據庫。

整個後端是一個大型的整體式有狀態 Java 應用程序,它與客戶端保持持續的 Websocket 連接。 當多個用戶同時在同一塊板上工作時,他們都會實時看到更改,因為我們將每個更改寫入數據庫。 我們的數據庫每秒約有 10K 個請求。 在 Redis 的峰值負載下,我們每秒寫入 80-100K 請求。
故障轉移集群 PostgreSQL + Patroni。 實施經驗

為什麼我們從 Redis 切換到 PostgreSQL

最初,我們的服務使用 Redis,這是一種將所有數據存儲在服務器 RAM 中的鍵值存儲。

Redis 的優點:

  1. 響應速度高,因為一切都存儲在內存中;
  2. 易於備份和復制。

Redis 對我們來說的缺點:

  1. 沒有真實的交易。 我們嘗試在應用程序級別模擬它們。 不幸的是,這並不總是能很好地工作,並且需要編寫非常複雜的代碼。
  2. 數據量受到內存量的限制。 隨著數據量的增加,內存也會增長,最終我們會遇到所選實例的特徵,這在AWS中需要停止我們的服務來更改實例的類型。
  3. 有必要不斷保持低延遲水平,因為。 我們有大量的請求。 我們的最佳延遲水平是 17-20 毫秒。 在 30-40 毫秒的水平上,我們會收到對應用程序請求的長響應和服務降級。 不幸的是,這種情況在 2018 年 2 月就發生在我們身上,當時 Redis 的一個實例由於某種原因收到的延遲是平時的 XNUMX 倍。 為了解決該問題,我們在中午停止了服務以進行計劃外維護,並更換了有問題的 Redis 實例。
  4. 即使代碼中有很小的錯誤,也很容易出現數據不一致的情況,然後花費大量時間編寫代碼來糾正這些數據。

我們考慮到了缺點,並意識到我們需要轉向更方便的方式,進行正常的交易並減少對延遲的依賴。 進行了研究,分析了許多選項並選擇了 PostgreSQL。

我們已經遷移到新數據庫 1,5 年了,並且只遷移了一小部分數據,所以現在我們同時使用 Redis 和 PostgreSQL。 有關在數據庫之間移動和切換數據的階段的更多信息,請參閱 我同事的文章.

當我們第一次開始遷移時,我們的應用程序直接與數據庫一起工作並訪問主Redis和PostgreSQL。 PostgreSQL 集群由一個主服務器和一個異步複製的副本組成。 數據庫方案如下所示:
故障轉移集群 PostgreSQL + Patroni。 實施經驗

實施 PgBouncer

當我們搬家的時候,產品也在發展:使用 PostgreSQL 的用戶數量和服務器數量增加,我們開始缺乏連接。 PostgreSQL 為每個連接創建一個單獨的進程並消耗資源。 您可以將連接數增加到一定程度,否則有可能獲得次優的數據庫性能。 在這種情況下,理想的選擇是選擇位於底座前面的連接管理器。

我們有兩個連接管理器選項:Pgpool 和 PgBouncer。 但第一個不支持使用數據庫的事務模式,因此我們選擇了 PgBouncer。

我們設置了以下工作方案:我們的應用程序訪問一個 PgBouncer,其後面有 PostgreSQL master,每個 master 後面有一個異步複製的副本。
故障轉移集群 PostgreSQL + Patroni。 實施經驗

同時,我們無法將全部數據存儲在 PostgreSQL 中,並且使用數據庫的速度對我們來說很重要,因此我們開始在應用程序級別對 PostgreSQL 進行分片。 上面描述的方案對此相對方便:當添加新的 PostgreSQL 分片時,更新 PgBouncer 配置就足夠了,應用程序可以立即使用新分片。

PgBouncer 故障轉移

這個方案一直有效,直到唯一的 PgBouncer 實例死亡為止。 我們在 AWS 中,所有實例都在定期失效的硬件上運行。 在這種情況下,實例只需轉移到新硬件並再次工作即可。 PgBouncer 也發生過這種情況,但它變得不可用。 今年秋天的結果是我們的服務有 25 分鐘不可用。 AWS建議針對這種情況使用用戶端冗餘,而當時我國還沒有實施這種方式。

之後,我們認真考慮了 PgBouncer 和 PostgreSQL 集群的容錯能力,因為我們的 AWS 賬戶中的任何實例都可能發生類似的情況。

我們構建的PgBouncer容錯方案如下:所有應用服務器都訪問網絡負載均衡器,其後面有兩個PgBouncer。 每個 PgBouncer 都會查看每個分片的同一個 PostgreSQL master。 如果 AWS 實例再次崩潰,所有流量都會通過另一個 PgBouncer 進行重定向。 網絡負載均衡器故障轉移由 AWS 提供。

此方案可以輕鬆添加新的 PgBouncer 服務器。
故障轉移集群 PostgreSQL + Patroni。 實施經驗

創建 PostgreSQL 故障轉移集群

在解決這個問題時,我們考慮了不同的選擇:自寫故障轉移、repmgr、AWS RDS、Patroni。

自寫腳本

他們可以監視主服務器的工作,並在發生故障時將副本提升為主服務器並更新 PgBouncer 配置。

這種方法的優點是最簡單,因為您自己編寫腳本並準確了解它們的工作原理。

缺點:

  • master可能並沒有死掉,而是可能出現了網絡故障。 故障轉移在不知道這一點的情況下,會將副本提升為主服務器,而舊主服務器將繼續工作。 結果,我們將獲得兩台充當主服務器角色的服務器,並且我們不知道其中哪一台擁有最新的數據。 這種情況也稱為裂腦;
  • 我們沒有得到任何回應。 在我們的配置中,master和replica,切換後,replica上移到master,我們就不再有replica了,所以我們必須手動添加一個新的replica;
  • 我們需要對故障轉移操作進行額外的監控,而我們有 12 個 PostgreSQL 分片,這意味著我們必須監控 12 個集群。 隨著分片數量的增加,您還必須記住更新故障轉移。

自寫的故障轉移看起來非常複雜,需要不簡單的支持。 對於單個 PostgreSQL 集群,這將是最簡單的選擇,但它無法擴展,因此不適合我們。

雷普格

Replication Manager for PostgreSQL Cluster,可以管理 PostgreSQL 集群的操作。 同時,它沒有開箱即用的自動故障轉移功能,因此在工作中,您需要在已完成的解決方案之上編寫自己的“包裝器”。 所以一切都可能比自己編寫的腳本更複雜,所以我們甚至沒有嘗試 Repmgr。

AWS RDS

支持我們需要的一切,知道如何進行備份並維護連接池。 它具有自動切換功能:當master死亡時,副本成為新的master,AWS將dns記錄更改為新的master,而副本可以位於不同的AZ。

缺點包括缺乏精細調整。 作為微調的一個例子:我們的實例對 tcp 連接有限制,不幸的是,這在 RDS 中無法完成:

net.ipv4.tcp_keepalive_time=10
net.ipv4.tcp_keepalive_intvl=1
net.ipv4.tcp_keepalive_probes=5
net.ipv4.tcp_retries2=3

此外,AWS RDS的價格幾乎是普通實例價格的兩倍,這也是放棄該解決方案的主要原因。

帕特羅尼

這是一個用於管理 PostgreSQL 的 python 模板,具有良好的文檔、自動故障轉移和 github 上的源代碼。

帕特羅尼的優點:

  • 每個配置參數都有描述,其工作原理一目了然;
  • 自動故障轉移開箱即用;
  • 用python寫的,而且由於我們自己也用python寫了很多,所以我們處理問題會更容易,甚至可能對項目的開發有幫助;
  • 完全管理 PostgreSQL,允許您一次更改集群所有節點上的配置,如果需要重新啟動集群以應用新配置,則可以使用 Patroni 再次完成此操作。

缺點:

  • 文檔中並不清楚如何正確使用 PgBouncer。 雖然很難稱之為減號,因為Patroni的任務是管理PostgreSQL,如何連接到Patroni已經是我們的問題了;
  • 大量實施 Patroni 的例子很少,而從頭開始實施的例子很多。

因此,我們選擇 Patroni 來創建故障轉移集群。

帕特羅尼實施流程

在 Patroni 之前,我們有 12 個 PostgreSQL 分片,採用異步複製的一主一副本配置。 應用程序服務器通過網絡負載均衡器訪問數據庫,網絡負載均衡器後面是兩個帶 PgBouncer 的實例,後面都是 PostgreSQL 服務器。
故障轉移集群 PostgreSQL + Patroni。 實施經驗

為了實現 Patroni,我們需要選擇分佈式存儲集群配置。 Patroni 與分佈式配置存儲系統配合使用,例如 etcd、Zookeeper、Consul。 我們市場上只有一個成熟的 Consul 集群,它與 Vault 結合使用,但我們不再使用它。 這是開始使用 Consul 來實現其預期目的的一個重要原因。

Patroni 如何與 Consul 合作

我們有一個Consul集群,它由三個節點組成,還有一個Patroni集群,它由一個領導者和一個副本組成(在Patroni中,主節點稱為集群領導者,從節點稱為副本)。 Patroni 集群的每個實例不斷向 Consul 發送有關集群狀態的信息。 因此,從 Consul 中你總是可以了解 Patroni 集群當前的配置以及當前誰是領導者。

故障轉移集群 PostgreSQL + Patroni。 實施經驗

要將Patroni連接到Consul,研究一下官方文檔就足夠了,它說你需要指定一個http或https格式的主機,具體取決於我們如何與Consul合作,以及連接方案,可選:

host: the host:port for the Consul endpoint, in format: http(s)://host:port
scheme: (optional) http or https, defaults to http

看起來很簡單,但陷阱就從這裡開始了。 使用 Consul,我們通過 https 建立安全連接,我們的連接配置將如下所示:

consul:
  host: https://server.production.consul:8080 
  verify: true
  cacert: {{ consul_cacert }}
  cert: {{ consul_cert }}
  key: {{ consul_key }}

但這是行不通的。 啟動時,Patroni 無法連接到 Consul,因為它無論如何都會嘗試通過 http。

Patroni 的源代碼有助於解決這個問題。 好東西是用 python 寫的。 原來是沒有對host參數進行任何解析,必須在scheme中指定協議。 這就是我們使用 Consul 的工作配置塊的樣子:

consul:
  host: server.production.consul:8080
  scheme: https
  verify: true
  cacert: {{ consul_cacert }}
  cert: {{ consul_cert }}
  key: {{ consul_key }}

領事模板

因此,我們選擇了配置的存儲。 現在我們需要了解當 Patroni 集群中的 Leader 發生變化時,PgBouncer 將如何切換其配置。 文檔中沒有這個問題的答案,因為。 原則上,這裡沒有描述與 PgBouncer 的工作。

在尋找解決方案時,我們找到了一篇文章(不幸的是我不記得標題了),其中寫道 Сonsul-template 在配對 PgBouncer 和 Patroni 方面幫助很大。 這促使我們研究 Consul-template 是如何工作的。

原來,Consul-template 不斷監控 Consul 中 PostgreSQL 集群的配置。 當領導者發生變化時,它會更新 PgBouncer 配置並發送命令來重新加載它。

故障轉移集群 PostgreSQL + Patroni。 實施經驗

模板的一大優點是它存儲為代碼,因此當添加新分片時,只需進行新的提交並自動更新模板即可,支持基礎設施即代碼原則。

Patroni 的新架構

結果,我們得到了以下工作方案:
故障轉移集群 PostgreSQL + Patroni。 實施經驗

所有應用服務器都訪問平衡器 → 後面有兩個 PgBouncer 實例 → 在每個實例上,啟動 Consul-template,它監視每個 Patroni 集群的狀態並監視 PgBouncer 配置的相關性,該配置向當前領導者發送請求每個簇的。

手動測試

我們在小型測試環境上啟動該方案之前運行了該方案並檢查了自動切換的操作。 他們打開了木板,移動了貼紙,就在那一刻,他們“殺死”了集群的領導者。 在 AWS 中,這就像通過控制台關閉實例一樣簡單。

故障轉移集群 PostgreSQL + Patroni。 實施經驗

貼紙在 10-20 秒內返回,然後再次開始正常移動。 這意味著 Patroni 集群工作正常:它更改了領導者,將信息發送到 Сonsul,然後 Сonsul-template 立即獲取此信息,替換 PgBouncer 配置並發送重新加載命令。

如何在高負載下生存並最大限度地減少停機時間?

一切都很完美! 但有新的問題:它在高負載下如何工作? 如何快速、安全地推出生產中的一切?

我們進行負載測試的測試環境幫助我們回答了第一個問題。 它在架構方面與生產完全相同,並且生成的測試數據量與生產數據量大致相等。 我們決定在測試期間“殺死”一位 PostgreSQL master,看看會發生什麼。 但在此之前,檢查自動滾動非常重要,因為在此環境中我們有多個 PostgreSQL 分片,因此我們將在生產之前對配置腳本進行出色的測試。

這兩項任務看起來都雄心勃勃,但我們有 PostgreSQL 9.6。 可以立即升級到11.2嗎?

我們決定分兩步進行:首先升級到 2,然後啟動 Patroni。

PostgreSQL 更新

要快速更新 PostgreSQL 版本,請使用該選項 -k,其中在磁盤上創建硬鏈接,無需複制數據。 在300-400GB的基礎上,更新需要1秒。

我們有很多分片,所以更新需要自動完成。 為此,我們編寫了一個 Ansible 劇本來為我們處理整個更新過程:

/usr/lib/postgresql/11/bin/pg_upgrade 
<b>--link </b>
--old-datadir='' --new-datadir='' 
 --old-bindir=''  --new-bindir='' 
 --old-options=' -c config_file=' 
 --new-options=' -c config_file='

這裡需要注意的是,在開始升級之前,必須使用參數來執行 - 查看以確保您可以升級。 我們的腳本還在升級期間替換配置。 我們的腳本在 30 秒內完成,這是一個非常好的結果。

啟動帕特羅尼

解決第二個問題,只要看Patroni的配置即可。 官方存儲庫有一個 initdb 的示例配置,它負責在您第一次啟動 Patroni 時初始化一個新數據庫。 但由於我們已經有一個現成的數據庫,我們只是從配置中刪除了這一部分。

當我們開始在現有的 PostgreSQL 集群上安裝 Patroni 並運行它時,我們遇到了一個新問題:兩台服務器都作為領導者啟動。 Patroni 對集群的早期狀態一無所知,並嘗試將兩台服務器作為兩個具有相同名稱的獨立集群啟動。 要解決這個問題,需要刪除slave上有數據的目錄:

rm -rf /var/lib/postgresql/

這只需要在從站上完成!

當連接一個乾淨的副本時,Patroni 會創建一個 BaseBackup Leader 並將其恢復到副本中,然後根據 wal 日誌趕上當前狀態。

我們遇到的另一個困難是所有 PostgreSQL 集群默認都命名為 main。 當每個集群對另一個集群一無所知時,這是正常的。 但是當你想使用Patroni時,那麼所有集群都必須有一個唯一的名稱。 解決方案是更改 PostgreSQL 配置中的集群名稱。

負載測試

我們啟動了一項模擬用戶在板上體驗的測試。 當負載達到我們的平均每日值時,我們重複完全相同的測試,我們關閉了 PostgreSQL 領導者的一個實例。 自動故障轉移按我們的預期進行:Patroni 更改了領導者,Consul-template 更新了 PgBouncer 配置並發送了重新加載的命令。 根據我們在 Grafana 中的圖表,很明顯,與數據庫連接相關的服務器存在 20-30 秒的延遲和少量錯誤。 這是正常情況,這樣的值對於我們的故障轉移來說是可以接受的,並且絕對比服務停機要好。

將 Patroni 投入生產

因此,我們提出了以下計劃:

  • 將Consul模板部署到PgBouncer服務器並啟動;
  • PostgreSQL更新至11.2版本;
  • 更改集群名稱;
  • 啟動 Patroni 集群。

同時,我們的方案允許我們幾乎在任何時候都可以提出第一點,我們可以依次將每個 PgBouncer 從工作中刪除,並在其上部署並運行 consul-template。 所以我們做到了。

為了快速部署,我們使用了 Ansible,因為我們已經在測試環境中測試了所有 playbook,並且每個分片的完整腳本的執行時間為 1,5 到 2 分鐘。 我們可以在不停止服務的情況下將所有內容依次部署到每個分片,但我們必須關閉每個 PostgreSQL 幾分鐘。 在這種情況下,數據在這個分片上的用戶此時無法完全工作,這對我們來說是不可接受的。

解決這種情況的方法是每 3 個月進行一次計劃維護。 這是計劃工作的窗口,當我們完全關閉服務併升級數據庫實例時。 距離下一個窗口期還有一周時間,我們決定等待並進一步準備。 在等待期間,我們還保護了自己:對於每個PostgreSQL 分片,我們都會創建一個備用副本,以防無法保留最新數據,並為每個分片添加一個新實例,該實例應該成為Patroni 集群中的新副本,以免執行刪除數據的命令。 所有這些都有助於最大限度地減少錯誤風險。
故障轉移集群 PostgreSQL + Patroni。 實施經驗

我們重新啟動了服務,一切正常,用戶繼續工作,但在圖表上我們注意到 Consul 服務器上的負載異常高。
故障轉移集群 PostgreSQL + Patroni。 實施經驗

為什麼我們在測試環境中沒有看到這個? 這個問題很好地說明了有必要遵循基礎設施即代碼原則,完善從測試環境到生產的整個基礎設施。 否則,很容易出現我們遇到的問題。 發生了什麼? Consul首先出現在生產環境中,然後出現在測試環境中,因此,在測試環境中,Consul的版本高於生產環境。 就在其中一個版本中,使用 consul-template 時出現的 CPU 洩漏問題得到了解決。 因此,我們只需更新Consul,就解決了問題。

重啟Patroni集群

然而,我們遇到了一個我們甚至沒有想到的新問題。 更新 Consul 時,我們只需使用 consul left 命令從集群中刪除 Consul 節點 → Patroni 連接到另一個 Consul 服務器 → 一切正常。 但是,當我們到達 Consul 集群的最後一個實例並向其發送 consul left 命令時,所有 Patroni 集群都會重新啟動,並且在日誌中我們看到以下錯誤:

ERROR: get_cluster
Traceback (most recent call last):
...
RetryFailedError: 'Exceeded retry deadline'
ERROR: Error communicating with DCS
<b>LOG: database system is shut down</b>

Patroni 集群無法檢索有關其集群的信息並重新啟動。

為了找到解決方案,我們通過 github 上的問題聯繫了 Patroni 作者。 他們建議改進我們的配置文件:

consul:
 consul.checks: []
bootstrap:
 dcs:
   retry_timeout: 8

我們能夠在測試環境中復制該問題並在那裡測試了這些選項,但不幸的是它們不起作用。

問題仍然沒有解決。 我們計劃嘗試以下解決方案:

  • 在每個Patroni集群實例上使用Consul-agent;
  • 修復代碼中的問題。

我們明白錯誤發生在哪裡:問題可能是使用默認超時,該超時沒有通過配置文件覆蓋。 當最後一個Consul服務器從集群中移除時,整個Consul集群掛起超過一秒,正因如此,Patroni無法獲取集群的狀態並徹底重啟整個集群。

幸運的是,我們沒有再遇到任何錯誤。

使用 Patroni 的結果

成功啟動 Patroni 後,我們在每個集群中添加了一個額外的副本。 現在,每個集群中都有一個仲裁的外觀:一個領導者和兩個副本,用於在切換時出現裂腦時提供安全網。
故障轉移集群 PostgreSQL + Patroni。 實施經驗

Patroni 已經投入製作三個多月了。 在這段時間裡,他已經設法幫助我們了。 最近,其中一個集群的領導者在 AWS 中去世,自動故障轉移工作正常,用戶繼續工作。 帕特羅尼完成了它的主要任務。

Patroni的使用小總結:

  • 易於配置更改。 在一個實例上更改配置就足夠了,它將被拉動到整個集群。 如果需要重新啟動才能應用新配置,Patroni 會通知您。 Patroni 只需一條命令就可以重啟整個集群,也非常方便。
  • 自動故障轉移有效並且已經成功幫助我們擺脫困境。
  • PostgreSQL 更新無需應用程序停機。 您必須首先將副本更新到新版本,然後更改 Patroni 集群中的領導者並更新舊的領導者。 在這種情況下,將進行必要的自動故障轉移測試。

來源: www.habr.com

添加評論