在他的演講中,Andrey Borodin 將告訴您他們在設計連接池時如何考慮 PgBouncer 縮放的經驗
視頻:
大家好! 我的名字是安德魯。
在 Yandex,我正在開發開源數據庫。 今天我們有一個關於連接池連接的話題。
如果您知道如何用俄語調用連接池,請告訴我。 我真的很想找到一個很好的技術術語,應該在技術文獻中建立起來。
這個話題相當複雜,因為在許多數據庫中,連接池是內置的,你甚至不需要知道它。 當然,有些設置無處不在,但在 Postgres 中這是行不通的。 同時(在 HighLoad++ 2019 上)Nikolai Samokhvalov 發表了一份關於在 Postgres 中設置查詢的報告。 我知道來到這裡的人已經完美地配置了請求,這些人面臨著與網絡、資源利用相關的罕見系統問題。 在某些地方,從問題不明顯的意義上來說,這可能是相當困難的。
Yandex 有 Postgres。 許多 Yandex 服務都在 Yandex.Cloud 中。 我們有數 PB 的數據,在 Postgres 中每秒至少產生一百萬個請求。
我們為所有服務提供了一個相當典型的集群——這是節點的主要主節點,通常有兩個副本(同步和異步),備份,副本上讀取請求的縮放。
每個集群節點都是Postgres,上面除了Postgres和監控系統,還安裝了一個connection pooler。 連接池用於防護及其主要目的。
連接池的主要目的是什麼?
Postgres 採用進程模型來處理數據庫。 這意味著一個連接就是一個進程,一個 Postgres 後端。 而且這個後端有很多不同的緩存,針對不同的連接做出不同的緩存是相當昂貴的。
此外,在 Postgres 代碼中有一個名為 procArray 的數組。 它包含有關網絡連接的基本數據。 而且幾乎所有的procArray處理算法都具有線性複雜度,它們貫穿整個數組的網絡連接。 這是一個相當快的周期,但是隨著更多的傳入網絡連接,事情變得有點昂貴。 當事情變得更昂貴時,您最終會為大量網絡連接付出非常高的代價。
有 3 種可能的方法:
- 在應用端。
- 在數據庫端。
- 和之間,也就是所有可能的組合。
不幸的是,內置池目前正在開發中。 PostgreSQL Professional 的朋友大多這樣做。 何時出現很難預測。 事實上,對於架構師的選擇,我們有兩種解決方案。 這些是應用程序端池和代理池。
應用程序端池是最簡單的方法。 幾乎所有客戶端驅動程序都為您提供了一種方法:將代碼中的數百萬個連接表示為數十個數據庫連接。
有一個問題是,在某個時候你想擴展後端,你想將它部署到許多虛擬機上。
然後你仍然意識到你有幾個可用區,幾個數據中心。 客戶端池化方法會產生大量數據。 大的大約有 10 個連接。 這是一個可以正常工作的優勢。
如果我們談論代理池,那麼有兩個池可以做很多事情。 他們不僅是台球手。 它們是 pooler + 更酷的功能。 這
但是,不幸的是,並不是每個人都需要這個附加功能。 這導致池化程序僅支持會話池,即一個傳入客戶端,一個傳出客戶端到數據庫。
這不太適合我們的任務,所以我們使用 PgBouncer,它實現了事務池,即服務器連接僅在事務期間映射到客戶端連接。
在我們的負擔上-這是真的。 但是有幾個問題.
當您要診斷會話時,問題就開始了,因為所有傳入連接都是本地的。 每個人都帶有環回,不知何故很難追踪會話。
當然你可以使用application_name_add_host。 這是將 IP 地址添加到 application_name 的 Bouncer 端方法。 但是 application_name 是由附加連接設置的。
在此圖表中,黃線是實際請求,藍線是飛入數據庫的請求。 而這個區別恰恰是application_name的設置,只是tracing需要的,但一點都不閒。
此外,Bouncer 不能限制一個池,即每個用戶、每個數據庫的數據庫連接數。
這會導致什麼? 你有一個用 C ++ 編寫的加載服務和一個節點上附近某個地方的小服務,該服務對基礎沒有任何問題,但它的驅動程序變得瘋狂。 它打開 20 個連接,其他一切都將等待。 即使你的代碼是正確的。
當然,我們為 Bouncer 寫了一個小補丁,添加了這個設置,即限制客戶端進入池。
可以在 Postgres 端執行此操作,即將數據庫中的角色限制為連接數。
但是你會失去理解為什麼你沒有連接到服務器的能力。 PgBouncer 不會拋出連接錯誤,它總是返回相同的信息。 而且您無法理解:也許您的密碼已更改,也許數據庫剛剛崩潰,也許出了什麼問題。 但是沒有診斷。 如果會話不能建立,你就不知道為什麼不能建立。
在某個時刻,您查看應用程序的圖形,發現該應用程序不工作。
查看頂部可以看到 Bouncer 是單線程的。 這是服役生涯的轉折點。 您了解您準備在一年半內擴展數據庫,並且需要擴展 pooler。
我們得出的結論是我們需要更多的 PgBouncer。
保鏢已略有修補。
他們做到了,因此可以通過重用 TCP 端口來提高多個 Bouncer。 並且操作系統已經通過循環自動在它們之間傳輸傳入的 TCP 連接。
這對客戶端是透明的,即看起來你有一個 Bouncer,但你在運行的 Bouncer 之間有空閒連接碎片。
在某些時候,您可能會注意到這 3 個 Bouncer 各自吃掉了 100% 的核心。 你需要相當多的保鏢。 為什麼?
因為你有 TLS。 您有一個加密連接。 如果您對使用和不使用 TLS 的 Postgres 進行基準測試,您會發現在啟用加密的情況下,已建立的連接數幾乎下降了兩個數量級,因為 TLS 握手會消耗 CPU 資源。
在頂部,您可以看到在一波傳入連接期間執行的相當多的加密函數。 由於我們的主節點可以在可用性區域之間切換,因此一波傳入連接是一種相當典型的情況。 也就是說,由於某種原因,舊的主節點不可用,整個負載被發送到另一個數據中心。 他們都會同時過來向 TLS 打招呼。
而大量的TLS握手可能已經不是問候Bouncer了,而是擠住了他的喉嚨。 由於超時,一波傳入連接可能會變得暢通無阻。 如果您在沒有指數退避的情況下重試基地,他們將不會以相干波一遍又一遍地回來。
下面是 16 個 PgBouncer 的示例,它們以 16% 的速度加載 100 個內核。
我們已經到達級聯 PgBouncer。 這是我們可以在 Bouncer 負載上實現的最佳配置。 我們的外部 Bouncer 用於 TCP 握手,而內部 Bouncer 用於真正的池化,以免嚴重分散外部連接。
在此配置中,可以進行軟重啟。 您可以將這 18 個保鏢一一重啟。 但是維護這樣的配置是相當困難的。 系統管理員、DevOps 和真正負責這個服務器的人不會對這個方案很滿意。
看起來我們所有的改進都可以在開源中推廣,但是Bouncer支持得不是很好。 例如,在同一個端口上運行多個 PgBouncer 的能力是一個月前提交的。 具有此功能的拉取請求是幾年前的。
或者再舉一個例子。 在 Postgres 中,您可以通過將秘密發送到另一個連接而無需額外身份驗證來取消正在運行的請求。 但是有些客戶端只是簡單地發送一個 TCP 重置,即它們會斷開網絡連接。 Bouncer 會用這個做什麼? 他什麼也不會做。 它將繼續執行請求。 如果你已經收到了大量的連接,這些連接已經為小請求奠定了基礎,那麼簡單地斷開與 Bouncer 的連接是不夠的,你還需要完成那些正在數據庫中運行的請求。
這已被修補,問題仍未合併到 Bouncer 的上游。
因此我們得出結論,我們需要自己的連接池,它會被開發、修補,可以快速解決問題,當然,它必須是多線程的。
我們將多線程設置為主要任務。 我們需要能夠很好地處理傳入的 TLS 連接浪潮。
為此,我們必須開發一個名為 Machinarium 的獨立庫,該庫旨在將網絡連接的機器狀態描述為串行代碼。 如果您查看 libpq 源代碼,您會看到相當複雜的調用,這些調用可以向您返回結果並說,“稍後給我打電話。 現在我暫時有 IO,但是當 IO 通過時,處理器就會有負載。 這是一個多層次的方案。 網絡交互通常由狀態機來描述。 很多規則,比如“如果我之前收到了一個大小為 N 的數據包頭,那麼現在我正在等待 N 個字節”,“如果我發送了一個 SYNC 數據包,那麼現在我正在等待一個帶有結果元數據的數據包。” 事實證明這是一個相當困難的反直覺代碼,就好像迷宮被轉換成線掃描一樣。 我們這樣做是為了讓程序員以普通命令式代碼的形式描述主要的交互路徑,而不是狀態機。 就在這個命令式代碼中,您需要插入執行序列需要通過等待來自網絡的數據來中斷的地方,將執行上下文傳遞給另一個協程(綠色線程)。 這種做法類似於我們將迷宮中最期待的路徑連續寫下來,然後在其中添加分支。
結果,我們有一個線程使 TCP 接受並循環將 TPC 連接傳遞給許多工作人員。
在這種情況下,每個客戶端連接始終在一個處理器上運行。 這使您可以使其緩存友好。
此外,我們稍微改進了將小數據包收集為一個大數據包的功能,以減輕系統 TCP 堆棧的負擔。
此外,我們改進了事務池,因為 Odyssey 在配置後可以在網絡連接失敗的情況下發送 CANCEL 和 ROLLBACK,即如果沒有人在等待請求,Odyssey 將告訴數據庫不要嘗試完成會浪費寶貴資源的請求。
只要有可能,我們就會保持與同一個客戶端的連接。 這避免了必須重新安裝 application_name_add_host。 如果可能,那麼我們不會對診斷所需的參數進行額外的重置。
我們為 Yandex.Cloud 的利益而工作。 如果您使用的是託管 PostgreSQL 並且安裝了連接池,則可以向外創建邏輯複製,即如果需要,請使用邏輯複製。 邏輯複製外流的Bouncer是不會給的。
這是設置邏輯複製的示例。
此外,我們還支持向外進行物理複製。 在雲中,當然,這是不可能的,因為集群會給你太多關於它自己的信息。 但是在您的安裝中,如果您需要通過 Odyssey 中的連接池進行物理複製,這是可能的。
Odyssey 與 PgBouncer 的監控完全兼容。 我們有執行幾乎所有相同命令的相同控制台。 如果缺少某些內容,請發送拉取請求,或者至少在 GitHub 上提出問題,我們將完成必要的命令。 但是我們已經有了 PgBouncer 控制台的主要功能。
當然,我們有錯誤轉發。 我們將返回基地報告的錯誤。 你會得到你為什麼不在基地的信息,而不僅僅是你不在基地。
如果您需要與 PgBouncer 100% 兼容,則禁用此功能。 我們可以表現得像保鏢,以防萬一。
設計
關於奧德賽源代碼的幾句話。
例如,有“暫停/恢復”命令。 它們通常用於更新數據庫。 如果你需要升級 Postgres,你可以在連接池中暫停它,執行 pg_upgrade,然後恢復。 從客戶端來看,數據庫似乎只是在變慢。 此功能是由社區人員提供給我們的。 她還沒有死,但很快一切都會死去。 (已經死了)
另外,PgBouncer 中的一個新特性是 SCRAM Authentication support,這也是一個不在 Yandex.Cloud 工作的人給我們帶來的。 兩者都是複雜的功能並且很重要。
因此,我想告訴您 Odyssey 是由什麼構成的,以防您現在也想編寫一些代碼。
您擁有原始的 Odyssey 基地,它依賴於兩個主要圖書館。 Kiwi 庫是 Postgres 消息協議的一個實現。 也就是說,Postgres 的原生 proto 3 是前端和後端可以交換的標準消息。 它們在 Kiwi 庫中實現。
Machinarium 庫是一個線程實現庫。 這個 Machinarium 的一小部分是用彙編語言編寫的。 不過不用擔心,只有 15 行。
奧德賽建築。 有一台運行協程的主機。 這台機器實現了接受傳入的 TCP 連接並在 worker 之間分配。
在一個 worker 中,可以處理多個 clients 的 handler。 在主線程中,控制台和 crone 任務的處理也在旋轉,以刪除池中不再需要的連接。
Odyssey 使用標準的 Postgres 測試套件進行測試。 我們只是通過 Bouncer 和 Odyssey 運行安裝檢查,我們得到一個空的 div。 有幾個與日期格式相關的測試在 Bouncer 和 Odyssey 中完全相同。
此外,還有許多驅動程序有自己的測試。 我們用他們的測試來測試奧德賽。
此外,由於我們的級聯配置,我們必須測試各種捆綁包:Postgres + Odyssey、PgBouncer + Odyssey、Odyssey + Odyssey,以確保如果 Odyssey 在級聯中的任何部分中,它仍能按預期工作.
耙
我們在生產中使用 Odyssey。 如果我說一切正常,那是不公平的。 不,即是,但並非總是如此。 例如,在生產中一切正常,然後我們來自 PostgreSQL Professional 的朋友來了,說我們有內存洩漏。 他們真的是,我們修復了它們。 但這很簡單。
然後我們發現連接池有傳入的 TLS 連接和傳出的 TLS 連接。 連接需要客戶端證書和服務器證書。
Bouncer和Odyssey服務端證書由pcache重新讀取,但是客戶端證書不需要從pcache重新讀取,因為我們可擴展的Odyssey最終還是依賴於讀取這個證書的系統性能。 這讓我們感到意外,因為他沒有立即休息。 起初,它是線性擴展的,在 20 個傳入的並發連接之後,這個問題就顯現出來了。
可插入身份驗證方法是使用內置 lunux 工具進行身份驗證的能力。 在 PgBouncer 中,它的實現方式是有一個單獨的線程等待來自 PAM 的響應,並且有一個主 PgBouncer 線程為當前連接提供服務,並可以要求它們駐留在 PAM 線程中。
出於一個簡單的原因,我們沒有實現這一點。 我們有很多流。 我們為什麼需要它?
結果,這會產生問題,因為如果您有 PAM 身份驗證和非 PAM 身份驗證,那麼一大波 PAM 身份驗證會顯著延遲非 PAM 身份驗證。 這是我們尚未解決的問題之一。 但如果你想修復它,你可以這樣做。
另一個問題是我們有一個線程接受所有傳入連接。 然後它們被轉移到工作池,TLS 握手將在那裡進行。
結果,如果你有 20 個網絡連接的連貫波,它們都會被接受。 在客戶端,libpq 將開始報告超時。 默認情況下,它就像 000 秒。
如果他們不能同時全部進入基地,那麼他們就不能進入基地,因為這一切都可以通過非指數重試來覆蓋。
我們最終在這裡複製了 PgBouncer 方案,這樣我們就可以限制我們接受的 TCP 連接數。
如果我們看到我們正在接受連接,但他們最後沒有時間握手,我們就把他們放在一個隊列中,這樣他們就不會消耗 CPU 資源。 這導致可能不會對所有已到達的連接執行同時握手。 但至少有人會進入數據庫,即使負載足夠大。
路線圖
你希望未來在奧德賽看到什麼? 我們準備好自己發展什麼,我們對社區有什麼期望?
2019 年 XNUMX 月。
這就是八月份的奧德賽路線圖:
- 我們想要 SCRAM 和 PAM 身份驗證。
- 我們想將讀取請求轉發到備用。
- 我想在線重啟。
- 以及在服務器上暫停的能力。
該路線圖的一半已經完成,而不是由我們完成。 這很好。 因此,讓我們討論剩下的內容並添加更多內容。
原則上,在 Postgres 中,從 10 開始,可以在連接時指定 session_attrs。 您可以列出連接中的所有數據庫主機,並說明為什麼要訪問數據庫:只寫或只讀。 並且驅動程序本身將選擇列表中它最喜歡的第一個主機,它滿足 session_attrs 的要求。
但這種方法的問題在於它無法控制複製滯後。 您可能有某種副本,其服務時間超出您的接受範圍。 為了在replica上完整的執行讀請求,其實我們需要在Odyssey中支持無法讀時不工作的能力。
Odyssey 不得不時不時地去數據庫詢問與主數據庫的複制距離。 並且如果已經達到限制,不要讓新的請求進入數據庫,告訴客戶端你需要重新發起連接,並且可能的話,選擇另一台主機來執行請求。 這將允許數據庫快速恢復複製滯後並再次返回以響應查詢。
很難說出實施日期,因為它是開源的。 但是,我希望不會像 PgBouncer 的同事那樣 2,5 年。 這是我希望在奧德賽中看到的功能。
但是proto3上有消息協議層的prepared statement。 這是創建準備好的語句的信息以結構化形式出現的地方。 我們可以支持這樣的理解,即在某些服務器連接上,客戶端要求創建準備好的語句。 而且即使事務關閉了,我們仍然需要保持服務端和客戶端的連接。
但是這裡的對話出現了差異,因為有人說你需要了解客戶端創建了哪些準備好的語句,並在創建此服務器連接的所有客戶端之間共享服務器連接,即誰創建了這樣的準備語句。
Andres Freund 說,如果一個客戶來找你,他已經在另一個服務器連接中創建了這樣一個準備好的語句,那麼就為他創建它。 但是在數據庫而不是客戶端執行查詢似乎有點不對,但是從編寫與數據庫交互的協議的開發人員的角度來看,如果簡單地給他一個網絡連接會很方便有這樣一個準備好的請求。
我們還需要實現一項功能。 我們現在有與 PgBouncer 兼容的監控。 我們可以返回平均查詢執行時間。 但平均時間是醫院裡的平均溫度:有人冷,有人暖——平均每個人都健康。 這不是真的。
我們需要實現對百分位數的支持,這將表明存在消耗資源的緩慢請求,並使監控更容易接受。
最重要的是我要1.0版本(1.1版本已經發布了)。 事實上,現在 Odyssey 的版本是 1.0rc,即候選發布版。 我列出的所有 rake 都使用相同的版本修復,除了內存洩漏。
1.0 版對我們意味著什麼? 我們正在將奧德賽推廣到我們的基地。 它已經在我們的數據庫上運行了,但是當它達到每秒 1 個請求時,我們可以說這是一個發布版本,這是一個可以稱為 000 的版本。
社區中的一些人要求在 1.0 版本中提供更多的暫停和 SCRAM。 但這將意味著我們需要將下一個版本推出生產,因為 SCRAM 和暫停都還沒有合併。 但是,這個問題很可能會很快得到解決。
我在等你的拉取請求。 我也想听聽您對 Bouncer 有什麼問題。 讓我們討論一下。 也許我們可以實現您需要的一些功能。
我的部分到此結束,我想听聽您的意見。 謝謝你!
問題
如果我輸入自己的 application_name,它是否會被正確拋出,包括在 Odyssey 的事務池中?
奧德賽還是保鏢?
在奧德賽。 保鏢被拋出。
我們會做一個集合。
如果我的真實連接跳過其他連接,它會被傳輸嗎?
我們將製作一組列出的所有參數。 我無法確定 application_name 是否在此列表中。 似乎在那裡看到了他。 我們將設置所有相同的參數。 通過一個請求,該集合將執行客戶端在啟動期間安裝的所有內容。
感謝安德烈的報告! 好報告! 很高興奧德賽每時每刻都在發展得越來越快。 我想繼續這樣做。 我們已經要求你有一個多數據源連接,以便 Odyssey 可以同時連接到不同的數據庫,即 slave master,然後在故障轉移後自動連接到新的 master。
是的,我似乎記得那次討論。 現在有幾個倉庫。 但是它們之間沒有切換。 在我們這邊,我們必須查詢服務器它是否還活著,並了解發生了故障轉移,誰將調用 pg_recovery。 我有一個標準的方式來理解我們沒有來找主人。 而且我們必須以某種方式從錯誤中或如何理解? 也就是說,這個想法很有趣,正在討論中。 多寫評論。 如果您有懂 C 的工作人員,那麼這通常很棒。
跨副本擴展的問題也是我們感興趣的,因為我們希望讓應用程序開發人員盡可能簡單地採用複制集群。 但是這裡我想多提意見,就是怎麼做,怎麼做好。
問題也與副本有關。 原來你有一個master和幾個replicas。 很明顯,他們訪問副本的頻率低於訪問主節點的頻率,因為它們可能存在差異。 你說數據的差異可能會讓你的業務不滿意,你不去複制它。 同時,如果你很久沒有去那裡,然後開始去,那麼你需要的數據將不會立即可用。 也就是說,如果我們不斷地去主服務器,那麼緩存就會在那裡預熱,而緩存在副本中會稍微落後一點。
對,是真的。 在 pcache 中不會有你想要的數據塊,在真正的緩存中不會有你想要的關於表的信息,在計劃中不會有解析的查詢,什麼都沒有。
當你有某種集群,並在那裡添加一個新的副本時,當它開始時,它裡面的一切都是壞的,即它增加了它的緩存。
我明白了。 正確的方法是首先在副本上運行一小部分查詢,這將預熱緩存。 粗略地說,我們有一個條件,就是我們必須落後主人不超過10秒。 而且這個條件應該不是一波就包含的,但是對一些客戶來說是順利的。
是的,增加重量。
這是一個好主意。 但首先您需要實施此關閉。 首先我們需要關閉,然後我們會考慮如何打開。 這是一個很好的功能,可以順利打開。
nginx 有這個選項 slowly start
在服務器的集群中。 他逐漸增加了負荷。
是的,好主意,到時候我們會試一試。
來源: www.habr.com