如何提高數據庫性能的系統設計方案

簡介

一個有趣的面試問題,我已經聽到並問過很多次了。

"你將如何提高數據庫的性能?"

這個問題可能有很多答案,因爲我想深入瞭解每個答案,所以我將分別寫三篇文章,每篇都針對某一類答案。

這個要更注重架構層面的變化,管理服務等。他們會更關注雲計算架構師或對系統設計概念有良好了解的人。

第三組答案將更注重於數據庫和操作系統的配置。

請記住,這是一個非常廣泛的話題,這是我對如何回答這個問題的看法,我將提供進一步閱讀的鏈接,並儘可能多地提供實際的例子。

問題

問題是,"我的數據庫越來越慢,你將如何提高數據庫的性能?". 在這篇文章中,我假設是一個 SQL 數據庫,特別是 Postgres,但這些解決方案是通用的,應該主要適用於任何其他數據庫。

在你向下滾動之前,想一想你會怎麼回答,如果你發現我的文章中沒有包括這個問題,請在評論中告訴我。

可能的答案

請記住,每個答案都是有取捨的。根據不同的情況和問題陳述,有些答案可能不相關。我將嘗試解釋每個答案的取捨。

垂直擴展數據庫

縱向擴展數據庫意味着增加你的數據庫實例的大小。這可能意味着增加內存、CPU、網絡帶寬、存儲等。

很多人都會討厭我說這是一個可行的解決方案,但我覺得在很多情況下,這是一個可行的,甚至是唯一的解決方案。但在我闡述我爲什麼這麼想之前,請允許我再解釋一下什麼是垂直縮放,以及爲什麼它不總是被認爲是一個好的解決方案。

垂直擴展只是意味着改進你的數據庫服務器。這讓你有更多的資源可以利用,並且是一個快速解決你所面臨的與擴展有關的大多數問題的方法。然而,最大的缺點是它不具有可持續性,如果經常這樣做,會增加技術債務。這意味着,如果你不斷增加數據庫資源,你可能很快就會達到無法提供更高容量數據庫的地步。現在你需要考慮一個系統來提高你的性能,如果你到現在還在不斷地增加你的系統資源,現在要遷移這樣一個沉重的系統就比較困難了。

除此以外,當你擁有更多的資源時,成本往往會迅速上升。還有許多其他原因導致垂直擴展會產生問題,例如,在你的系統中產生一個單點故障,更難進行災難恢復,難以進行修補 / 更新,等等。

有了所有這些缺點,你會認爲垂直縮放絕不是正確的解決方案,但它也有一個巨大的優勢,那就是時間。垂直擴展是非常快速和容易做到的。只要扔更多的錢,你就能得到更高的性能。

根據我的經驗,當你必須快速修復一個問題時,在你和你的團隊調試問題並找到一個成本更低、更可持續的解決方案時,支付幾天的費用是可以的。重要的是要記住,作爲工程師,我們希望創造完美的解決方案,但我們爲之工作的客戶對正常運行時間和成本等指標更感興趣。

我想是 Tim Peters 在 "_Python 的本質 _" 中寫了以下內容。

" 特殊情況並不特殊,不足以打破規則。
雖然實用性勝過純潔性。"

Special cases aren’t special enough to break the rules.
Although practicality beats purity

代理層面的連接池

在上一篇文章中,我們討論了連接池如何幫助你的應用程序運行更多的併發事務。

爲了進一步討論這個問題,讓我們看看一個應用實例。一個簡單的 REST API 連接到一個 Postgres 數據庫。

由於一個交易可以在任何時候流經連接,連接池或在應用層面上維護大量的連接有助於向數據庫發送大量的交易。

上圖顯示了我們現在可以發送八個事務,因爲我們現在在應用程序和數據庫之間有三個連接。我在之前的文章中更深入地討論了這個問題.

然而,這又產生了另一個問題,你的數據庫現在需要管理三個連接而不是一個。雖然這對你的應用程序是一個巨大的推動,但這給你的數據庫增加了更多的工作。當你的應用程序被水平擴展時,例如在 docker 容器中容器化或作爲 Lambda 函數運行時,這個問題就會惡化。

你可以想象,運行幾十或幾百個容器 / 無服務器函數,每個都有 5-10 個連接,會對數據庫造成多大的負荷。

當運行默認的 Postgres docker 鏡像時,我得到的變量 max_connections 的值是 100。這是可配置的,但增加更多的連接需要更多的內存,所以你的數據庫的連接數是有限制的。

所以,我們有一個問題。在應用端有大量的連接對應用來說是件好事,因爲它現在可以併發發送更多的事務,但所有這些連接都會漏到數據庫中,而數據庫現在必須承擔所有這些連接的成本。橫向擴展數據庫也不是一種選擇,因爲數據庫很難橫向擴展。

實際真正的問題是,數據庫在做兩件事。一件是存儲、檢索和插入數據的實際責任,另一件是存儲大量的連接。

那麼,解決方案是什麼?在中間添加一個代理來處理連接!

代理可以作爲你的數據庫的一種漏斗。它可以承擔起管理所有與應用服務器的數據庫連接的重任,而只將其中的幾個連接暴露給你的數據庫。

代理還有很多其他的優勢,比如災難恢復和幫助故障轉移,安全,使你的應用程序更健壯等等,但由於這篇文章是專門針對性能的,我就不多說其他的優勢了。

當你有大量的數據庫連接時,數據庫代理可能是有用的,這通常會發生在你有大量的應用程序運行實例時。常見的例子是運行無服務器功能或運行大量的 Docker 容器。另外,即使你有較多的數據庫連接,代理也只能在你沒有大量的事務或連接大部分是空閒的情況下工作。代理只是把你正在執行的事務,合併到一個較小的連接池上,如果你想支持極高數量的併發事務,那麼代理可能不是你正在尋找的解決方案。

數據庫代理還爲你的系統增加了另一個組件,從而增加了其複雜性和成本。有很多數據庫代理可供選擇,像 RDS 代理這樣的管理服務在它所提供的豐富功能方面是驚人的,而且很容易設置,但比 ProxySQL 這樣的開源代理的成本要高。

使用消息隊列的異步通信

當你按部就班地進行操作時,你是同步進行的,這意味着你首先執行步驟 1,等待它完成,然後是步驟 2,等待步驟 2 完成,然後是步驟 3,以此類推。讓我們舉一個簡單的例子,一個連接到數據庫的 REST API。API 收到一個更新數據庫中某些數據的 POST 請求,它在數據庫中執行一個命令,等待數據庫發送一個響應,然後向用戶返回一個適當的響應。這是一個同步的流程。

同步流動

讓我們將其與異步通信進行對比。在異步通信中,API 將不會等待數據庫的到來。它可以簡單地返回給用戶的響應,即它已經接受了請求,而數據庫將在 API 已經對用戶作出響應後作出響應。

異步流

你可能會想,當你還沒有執行數據庫查詢的時候,你怎麼會向用戶返回一個響應。有一些用例是可以這樣做的。有時在更新或插入數據時,你可以假設數據會被插入並更新用戶,你已經得到了他 / 她的更新請求。

同步與異步的調用真的取決於你的使用情況。有時用戶需要即時反饋,但偶爾你也可以等待幾秒鐘,甚至幾分鐘來執行更新。例如,當用戶添加評論時,你會希望該評論是即時可見的,因爲這被用作不同用戶之間快速溝通的形式。然而,也許對於上調或下調,你可以不同步更新數據庫,而是將上調 / 下調添加到一個隊列中,以便以後處理。一個服務可以輪詢隊列中新的加註 / 減注請求,並可以相應地更新數據庫。

隊列可以使其更容易處理流量的峯值,因爲它可以作爲一個緩衝區來存儲請求。

缺點是數據的一致性,這取決於你的實現。由於你的數據在隊列中而不是在數據庫中停留一小段時間,這意味着它對你的 API(將查詢你的數據庫)來說基本上是不可見的,進而對你的用戶也是不可見的。你的數據一致性可能只是幾秒鐘或更多,這取決於你的實現。

另一個有趣的反問是,你將使用哪種隊列來存儲這些請求,僅僅存儲在內存中可能是一個答案,但這也有侷限性(比如成本很高),並可能導致其他副作用,比如使你的服務器具有狀態。像 Redis 這樣的東西可以是一個很好的解決方案,因爲它支持在內存中存儲數據,也支持在磁盤中持久化。

這個解決方案的一個很好的用例是來自用戶的行動,這些行動是即發即忘的(在這種情況下,用戶根本不關心迴應,例如,報告一個堆棧溢出問題,用戶不期望立即得到迴應)。

簡而言之,如果你能容忍某種程度的數據不一致,而且你主要是爲了處理不可預測的請求高峯,這是一個很好的答案。

讓我們快速談論一下實現,你通常有兩種方法來實現這個,一種是添加另一個小的工作者服務,輪詢隊列並將數據推送到你的數據庫。

或者你可以使用一個插件,如果你能找到一個,避免額外的服務。This 是一個很好的起點。

改變你的數據庫

數據庫通常是爲特定的使用情況而建立的。當然,你可以嘗試以一種它們並不打算被使用的方式來使用它們,它可能會起作用,但你可能會面臨性能、數據完整性、一致性等問題。

例如,SQL 數據庫有很多很好的功能,比如對 ACID 的良好支持,創建關係以表格形式存儲數據的能力,連接關係的能力,等等。然而,如果你的數據是非結構化的,將其存儲在 SQL 數據庫中並不容易(不過也不是不可能,有一些數據模型如 EAV 這樣的數據模型也存在於非結構化數據的模型中)。如果你的要求是存儲非結構化數據,基於文檔的數據庫可能會更好地滿足你的需求。

除了你想存儲的數據外,還有其他因素需要考慮,例如,性能。某些數據庫爲特定的使用情況提供更好的性能。

例如,列式數據庫將以面向列的方式存儲數據,並且能夠比 SQL 或文檔數據庫更快地執行列聚合查詢。因此,如果你想獲取所有行的列和 / 或對其執行聚合功能,像 Cassandra 或 Redshift 這樣的東西會比 Postgres 或 Mongo 快很多。

除此之外,一些數據庫將數據存儲在內存中而不是磁盤中。從內存中檢索數據比從磁盤中檢索數據要快得多,所以這些數據庫的數據檢索速度明顯要快。Redis 就是一個很好的例子。不過它們也有缺點,比如不支持存儲關係型數據,或者因爲數據現在存儲在內存中而不是磁盤中,所以存儲數據的成本更高。我在以前的文章中寫了很多關於 Redis 的內容,有很多實用的項目,所以請查看更多關於 Redis 的內容。here.

簡而言之,數據庫是爲特定的使用情況而建立的,有些是爲了解決特定的問題。嘗試找到一個能很好地解決你的問題的數據庫總是一個好主意。

這種解決方案的缺點是,你需要將你的數據從一個數據庫遷移到另一個數據庫,而數據遷移並不簡單或直接。你還需要在應用層面上做重大改變,以使用新的數據庫。這將需要開發和測試時間。

添加一個輔助數據庫

很多時候,一個數據庫並不能滿足你的所有要求。當你想使用多個數據庫時,有幾個很好的例子可以說明。

例如,也許你想存儲具有 ACID 屬性的關係型數據,但也想讓更多的流行數據能夠非常迅速地被使用。這方面的一個很好的例子可能是我們的 Stackexchange 數據,我一直在用它做例子。

簡而言之,這就是我們數據的使用情況。

95% 的請求是針對同樣的前 10% 的帖子。事實上,50% 以上的請求是針對前 1% 的帖子。

我在我的文章中更深入地談到了這個問題。previous post 如果你有興趣的話。

簡單的想法是,我們需要將流行的數據存儲在一個緩存中,比如 Redis,其餘的數據存儲在 Postgres。我們不能把所有的數據都存儲在 Redis 中,因爲 Redis 的存儲可能相當昂貴。

根據用戶如何使用我們的服務,我們可以根據用戶如何使用我們的服務來定義數據如何被髮送到 Redis 和 Postgres。例如,根據使用情況的統計,我們發現大多數帖子在一天內很受歡迎,然後就很少再被請求。我們可以有一個簡單的架構,最初我們將帖子存儲到 Redis 和 Postgres,並每 12 小時運行一個 cron 工作,簡單地將超過一天的帖子轉移到 Postgres。由於現在大多數的檢索都發生在 Redis 上,我們的 Postgres 服務器也可以更容易地處理它所得到的請求。

這就是流程的模樣。

這裏的缺點是使你的系統更加複雜,一般來說成本也更高。除此之外,你還必須考慮如何處理每個數據庫中的數據,如果用戶更新了數據,需要在多個數據庫中如何更新,如何快速運行你的 cron 或你想出的其他解決方案。

一般來說,這使你的系統變得複雜,併爲更多的問題打開了空間,儘管如果使用得當,那麼這可以成爲修復性能甚至增加更好功能的一個偉大的解決方案。

添加讀取副本

正如我們已經看到的,很多應用程序是重讀的。這意味着我們在數據庫上得到的大多數請求都是讀請求,而不是寫請求。這實際上是一件好事,因爲擴展大多數數據庫以處理更多的讀取請求是比較容易的。

我們可以創建複製主數據庫的讀副本。這是一個獨立的數據庫,甚至可以在不同的服務器上運行。在不同實例上運行的多個數據庫可以通過網絡交換數據並進行通信。

架構成爲

主數據庫爲寫保留,讀副本可以處理讀。你甚至可以在同一個數據庫中添加多個讀副本,以服務於更多的數據庫讀。

那麼,這如何提高性能呢?你的讀取請求(佔你流量的大部分)現在可以被分割成多個數據庫,每個數據庫都運行在不同的硬件上,有自己的 CPU、內存和網絡帶寬。

你需要回答的一個基本問題是如何同步這些數據庫。通常有兩種選擇,對每個請求進行同步更新,主數據庫的每一次更新都會首先同步到讀取的副本,然後將響應返回給用戶。

或者做一個異步更新,更新只是寫到你的主數據庫,在某些時候,你把你的讀副本數據庫同步到你的主數據庫。

你可能已經猜到了,這也造成了很大的弊端。例如,如果你異步同步你的讀取副本,你的數據庫可能不同步。然而,同步請求會使進行數據庫更新的時間增加一倍,因爲它們現在應該發生在兩個數據庫上。

如果數據的一致性不是特別重要,而且你的大部分流量都是重讀的,那麼添加異步更新的讀副本可以是一個很好的工具。

在回答問題前先反問

在回答這個問題之前,你一般應該問幾個反面的問題,以幫助更好地理解這個問題。這些可以幫助你衡量系統中的瓶頸問題。整個系統可能相當複雜,可能有很多原因導致數據庫開始表現不佳。爲了更好地瞭解原因,並更好地理解數據庫的要求,你可以向面試官提出一些問題,這些問題可以幫助你找出最佳解決方案。

由於這一部分需要對上面的答案有一定的瞭解,所以我在討論了可能的答案後將其列入,但你在回答之前可能應該提出反問。

是讀取性能慢還是寫入性能慢?

一個非常重要的因素可以推動你的決策,就是有關數據庫的讀寫性能如何。有些解決方案可能會提高讀取性能(如添加讀取副本),有些可能會提高寫入性能。

瞭解用戶如何使用你的服務

這對於做出所需的一致性、性能要求和可用性的決定至關重要。很多修復性能的方法可能會影響你的數據庫的一致性。例如,增加一個隊列並以異步方式而不是同步方式進行更新會影響你的數據庫的一致性。

瞭解用戶模式,用戶何時使用你的服務也很重要。一個監測服務可能會被 24 小時持續使用,但一個電子商務網站可能會在一天中的特定時段看到非常高的峯值。

像在你的數據庫前面使用隊列,擴大你的緩存,增加更多的讀取副本等解決方案可以很好地處理高峯期的流量,而縮小你的緩存,在用戶不使用你的服務時刪除讀取副本以節省成本。

用戶數量

大量的消費者通常意味着大量的連接,這可能會嚴重影響數據庫。如果達到其連接數的限制,數據庫將拒絕爲新的連接提供服務,這可能會影響數據庫的性能。

如果你的數據庫有很多消費者(例如,大量的無服務器功能或在 ECS、EKS、GKE 等容器編排服務上運行的容器),你可能需要採用一種解決方案來解決這個問題。

你有很多選擇,添加隊列、添加數據庫代理、添加讀複製、或分片數據庫都是可能的解決方案。

時間線?

推動我決策的一個關鍵因素是時間表。如果數據庫表現不佳,用戶感到沮喪並轉而使用競爭對手的產品,那麼就必須儘快找到一個解決方案,即使它帶有技術債務或更高的成本。

這是一個很好的例子,說明垂直縮放可能比水平縮放更好。

瞭解成本計算

重要的是要知道這些改進如何影響你公司的錢包。

例如,使用像 RDS 這樣的管理服務,將減少開發人員的時間和減少錯誤,但也會更昂貴。

在可能的情況下,確保你的解決方案也是具有成本效益的,這一點很重要。最好是在你可以的時候考慮開源服務並託管它們,只有在你認爲使用開源服務可能不可行或可能更復雜的時候才考慮託管服務。

總結

我還想介紹幾個要點,那就是分片和緩存,但是,這篇文章已經變得非常長了,所以不會在這篇文章中介紹。但是,如果你有興趣,你可以對這些要點進行更多的研究,以更好地理解它們。

來源:

https://www.toutiao.com/article/7114095072597295631/?log_from=843b4ae16e0f_1656983884145

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/rnejtLaTchFRXAVhOnTB_w