0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
电子发烧友
开通电子发烧友VIP会员 尊享10大特权
海量资料免费下载
精品直播免费看
优质内容免费畅学
课程9折专享价
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

慢的不是Ruby,而是你的數(shù)據(jù)庫(kù)

jf_WZTOguxH ? 來(lái)源:AI前線 ? 2023-10-10 16:10 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

許多人不停抱怨 Ruby 運(yùn)行緩慢。誠(chéng)然,它的確不如人意,然而這并非致命傷,因?yàn)閱?wèn)題的根源在于你的數(shù)據(jù)庫(kù)速度緩慢,成為了瓶頸。因此,這個(gè)標(biāo)題也可以改為 “Ruby 雖慢,但對(duì)你而言無(wú)關(guān)緊要”。

在編寫(xiě)一個(gè)在現(xiàn)有的 Postgresql 數(shù)據(jù)庫(kù)中提供鍵值存儲(chǔ)的 gem,并對(duì)其進(jìn)行基準(zhǔn)測(cè)試時(shí),我不斷地念叨:Ruby 可不慢,數(shù)據(jù)庫(kù)才慢。因此,我決定搜集這些基準(zhǔn)數(shù)據(jù),以支持我的觀點(diǎn)。

在業(yè)界,這被稱(chēng)為 I/O 密集型(I/O-bound),與 計(jì)算密集型(CPU-bound)性能相對(duì)立。我所協(xié)助解決的大部分 Ruby 性能問(wèn)題都屬于前者。Ruby 的緩慢并未引發(fā)任何問(wèn)題。

Ruby 很慢,但不重要

讓我們明確一點(diǎn):Ruby 很慢。垃圾收集器、JIT 編譯器、其高度動(dòng)態(tài)的特性、更改代碼運(yùn)行時(shí)的能力等等,所有這些加在一起,都使得 Ruby 顯得較為遲緩。

然而,當(dāng)人們抱怨 “Ruby 很慢” 時(shí),當(dāng)深入研究時(shí),通??梢约?xì)分為以下三類(lèi):

Ruby 很慢,這對(duì)我們的用例來(lái)說(shuō)是個(gè)問(wèn)題。Ruby 很慢,但實(shí)際上對(duì)我們來(lái)說(shuō)并不重要。Ruby 應(yīng)用程序很慢,但實(shí)際上它是堆棧,而不僅僅是語(yǔ)言。

我想更深入地研究最后一個(gè)問(wèn)題,但在此之前,我們先解決前兩個(gè)問(wèn)題。

Ruby 每年都在提高性能,這受到了大家歡迎,但從更大的角度來(lái)看,這可能并不重要:

速度并不是減緩 Ruby 應(yīng)用的主要因素。大多數(shù)使用 Ruby 的人并不要求它更快。他們固然熱衷于免費(fèi)的提升,但并非因速度而避之不及。 ——https://www.fastruby.io/blog/ruby/performance/why-wasnt-ruby-3-faster.html
因?yàn)樾阅艽_實(shí)非常依賴(lài)于環(huán)境:
[……] 你的系統(tǒng)需要多快?它現(xiàn)在的速度又有多快?如果你能測(cè)試它目前的性能,并且了解優(yōu)秀的性能指標(biāo),那么你就應(yīng)該有信心做出改變。有時(shí)候,為了獲得其他優(yōu)勢(shì)而適度放緩某些需要是明智的決策,尤其是如果這種放緩仍在可接受的范圍內(nèi)。

——《構(gòu)建微服務(wù)》(Building Microservices)Sam Newman 著

因此通常情況下,Ruby 的速度緩慢并不重要,因?yàn)槟愕膽?yīng)用場(chǎng)景無(wú)需 Ruby 所追求的規(guī)模、速度或吞吐量。做好這種權(quán)衡是值得的。通常情況下,開(kāi)發(fā)迅速、成本低廉、發(fā)布迅速,這些都是值得為應(yīng)用程序投入額外資源(如服務(wù)器、硬件、SAAS)以保持性能可接受的。

雖然并非始終如此,但時(shí)常亦是如此。

快速基準(zhǔn)測(cè)試

為了再次驗(yàn)證 Ruby 的性能不佳,我進(jìn)行了一項(xiàng)快速的基準(zhǔn)測(cè)試,在我近期遇到的一個(gè)(簡(jiǎn)化版)實(shí)際工作中,比較了 Ruby 和 Rust 的性能:解析 CSV,從一列中提取一個(gè)數(shù)字,然后進(jìn)行桶計(jì)數(shù)(bucket-count)。這是一個(gè)簡(jiǎn)化版本(而我實(shí)際版本使用的 CSV 是這里使用的例子的十倍)。這個(gè)例子計(jì)算了一部電影的票數(shù),并對(duì)這些票數(shù)進(jìn)行分組:0 到 10 票之間,10 到 100 票之間等等。

為了進(jìn)行對(duì)比,我嘗試用 Rust 和 Ruby 創(chuàng)建了一個(gè)內(nèi)部盡可能相似的版本。結(jié)果令人失望,Ruby 和 Rust 的性能都很差勁,甚至存在一些錯(cuò)誤,而且都沒(méi)有進(jìn)行性能優(yōu)化。我確信 Ruby 和 Rust 版本都可以進(jìn)一步改進(jìn)(盡管作為 Ruby 專(zhuān)家和 Rust 新手,我已經(jīng)意識(shí)到 Rust 版本比 Ruby 版本更容易進(jìn)行進(jìn)一步優(yōu)化)。所有的基準(zhǔn)測(cè)試代碼都可以在 GitHub repo 中找到。

這并不是一項(xiàng)嚴(yán)謹(jǐn)?shù)目茖W(xué)實(shí)驗(yàn),但它揭示了一個(gè)顯而易見(jiàn)的事實(shí):Ruby 的確較慢 [1]。

Rust:

ber@berkes:db_benchmarks ? time ./target/release/movie_ratings 
Some(0..=10): ###################### - 445
Some(10..=100): ############################################################ - 1208
Some(100..=1000): ############################################################################################################### - 2229
Some(1000..=10000): ############################################# - 914
Some(10000..=18446744073709551615):  - 7

real    0m0,162s
user    0m0,146s
sys 0m0,016s

Ruby:

ber@berkes:db_benchmarks ? time ruby movie_ratings.rb 
10000..:  - 7
1000..10000: ############################################# - 914
100..1000: ############################################################################################################### - 2229
10..100: ############################################################ - 1208
0..10: ###################### - 445

real    0m1,491s
user    0m1,389s
sys 0m0,103s

Rust 版本的速度大約是 Ruby 版本的十倍,這是一個(gè)令人咋舌的差距!然而,在處理更大的數(shù)據(jù)集時(shí),這種速度差異并非呈線性增長(zhǎng),而是呈現(xiàn)出不規(guī)則的變化。其中一部分時(shí)間是由啟動(dòng)時(shí)間(在這個(gè)用例中很難測(cè)量)和 JIT 編譯器占據(jù)的,而另一部分則是 Ruby 中垃圾回收機(jī)制的任意啟動(dòng)和停止所有進(jìn)程所造成的問(wèn)題。處理大型數(shù)據(jù)集,使這成為一個(gè)真實(shí)而惱人的問(wèn)題。

但兩者的絕對(duì)差異又如何呢?Ruby 版本僅慢 1.2 秒多一點(diǎn)。這在測(cè)試和開(kāi)發(fā)過(guò)程中已經(jīng)足夠令人惱火了。當(dāng)你一遍又一遍地運(yùn)行此操作時(shí),這一天只需要幾分鐘的時(shí)間:在開(kāi)發(fā)過(guò)程中運(yùn)行大約 20 次的腳本上總共需要 1.2 秒,然后可能每周運(yùn)行一次。

雖然我只關(guān)注 CPU,但內(nèi)存也是一個(gè)重要問(wèn)題。然而,在現(xiàn)代軟件的典型用例中,內(nèi)存使用并不明顯:客戶與服務(wù)器軟件交互時(shí)會(huì)感到緩慢,但并不會(huì)直接體驗(yàn)到內(nèi)存的使用。然而,不深入探討這個(gè)問(wèn)題的主要原因是對(duì)內(nèi)存進(jìn)行基準(zhǔn)測(cè)試相當(dāng)復(fù)雜。

因此,可以說(shuō) Ruby 的確較慢,并且使用較多的資源。它做出了權(quán)衡,因此可能包括開(kāi)發(fā)在內(nèi)的整體成本更低。這取決于具體情況,沒(méi)有絕對(duì)的定論。

讓它變慢的是堆棧,而不僅僅是語(yǔ)言

讓我們來(lái)深入探討一個(gè)不容忽視的問(wèn)題:Ruby on Rails。雖然有些 Ruby 項(xiàng)目不使用 Rails,但大部分生產(chǎn)中運(yùn)行的 Ruby 代碼都是基于 Rails 開(kāi)發(fā)的。我個(gè)人主要使用 Ruby 編寫(xiě)代碼,但很少涉及 Rails(因?yàn)槲也惶矚g它),不過(guò)我是個(gè)例外。在 Ruby 開(kāi)發(fā)中,幾乎總是采用 “用 Rails 進(jìn)行 Web 開(kāi)發(fā)” 的方式。

其中一個(gè) Rails 的問(wèn)題是它與數(shù)據(jù)庫(kù)的高度耦合(也可以說(shuō)是一種好處)。Rails 專(zhuān)注于掌控?cái)?shù)據(jù)庫(kù)的一切。沒(méi)有數(shù)據(jù)庫(kù),Rails 將毫無(wú)用處,甚至可能阻礙工作進(jìn)展,而不是提供幫助 [2]。此外,Rails 專(zhuān)注于 Web 開(kāi)發(fā)。雖然你可以在 Rails 中處理非 Web 相關(guān)的任務(wù),但這毫無(wú)意義。Rails 的目標(biāo)是處理 HTTP 請(qǐng)求 - 響應(yīng)。而且,Rails 的規(guī)模相當(dāng)龐大 [3]。與 Ruby 語(yǔ)言類(lèi)似,它更側(cè)重于人機(jī)工程學(xué)(對(duì)開(kāi)發(fā)者友好度)而非性能。這是好事!然而,這也導(dǎo)致在 Rails 中性能成為一個(gè)問(wèn)題,甚至比在 Ruby 中更加突出。

因此,“堆?!?指的是 “使用數(shù)據(jù)庫(kù)的 Ruby on Rails”。由于 Rails 專(zhuān)注于 Web 開(kāi)發(fā),并且只處理 HTTP 請(qǐng)求 - 響應(yīng),我們將僅從 Web 服務(wù)的角度看待 Ruby。

為了深入分析這個(gè)問(wèn)題,我將會(huì)比較一些非 Rails、非 HTTP、純 Ruby 的腳本。

Ruby 在處理大量數(shù)據(jù)方面并不擅長(zhǎng),但從本質(zhì)上講,這正是 Web 服務(wù)所需要的。為了說(shuō)明相對(duì)性能的差異,我們進(jìn)行了一項(xiàng)實(shí)驗(yàn),比較了在不同源上寫(xiě)入和讀取一百萬(wàn)條記錄時(shí)的表現(xiàn):內(nèi)存、內(nèi)存中的 SQLite 數(shù)據(jù)庫(kù)和 Postgresql 數(shù)據(jù)庫(kù)。

顯然,這并不令人驚訝,內(nèi)存比其他任何選項(xiàng)都要快得多 [7]。在這里的 Postgresql 是一個(gè) docker 容器,只占用 CPU 資源,而且根本不需要調(diào)整配置。這與絕對(duì)數(shù)值無(wú)關(guān),所以具體設(shè)置 Postgresql 并不重要。重要的是差異的程度。

ber@berkes:db_benchmarks ? ruby ruby_slow.rb 
                           user     system      total        real
Mem write              0.005277   0.000000   0.005277 (  0.005271)
Sqlite mem write       0.080462   0.000000   0.080462 (  0.080464)
Postgres write         0.665662   0.151700   0.817362 (  3.068891)
Mem read               0.002772   0.000000   0.002772 (  0.002767)
Sqlite mem read       10.323161   0.021355  10.344516 ( 10.345039)
Postgres read          8.296689   0.041118   8.337807 (  8.682667)

數(shù)據(jù)庫(kù)寫(xiě)入速度緩慢。即使經(jīng)過(guò)索引和負(fù)載狀態(tài)調(diào)優(yōu),讀取速度依舊無(wú)法改善。

然而,這一現(xiàn)象仍需深入探究原因。他們未指明導(dǎo)致緩慢的具體因素。令人意外的是,這也是 ORM 棧的一環(huán)。我選擇使用 Sequel,因?yàn)樗鄬?duì)簡(jiǎn)單,方便我們剖析問(wèn)題。

請(qǐng)見(jiàn)以下兩幅火焰圖,顯示在插入數(shù)據(jù)時(shí),Postgresql 成為瓶頸。這并不奇怪,因?yàn)榇藭r(shí)數(shù)據(jù)庫(kù)需處理大量工作。我們的表只有一項(xiàng)索引,而且是最輕類(lèi)型的索引。

數(shù)據(jù)庫(kù)寫(xiě)入速度之慢令人咋舌,以至于其他時(shí)間變得微不足道。

256956de-6744-11ee-939d-92fbcf53809c.png

258a1626-6744-11ee-939d-92fbcf53809c.png

在讀取方面,Postgresql 表現(xiàn)卓越。這歸功于其簡(jiǎn)單的查找操作,無(wú)需連接,僅使用一個(gè)索引,所需數(shù)據(jù)量也很少等等。然而,解析(處理數(shù)據(jù))卻耗費(fèi)了大量時(shí)間:DateTime::parse。換言之,DateTime::parse的性能問(wèn)題相當(dāng)顯著,以至于它在數(shù)據(jù)庫(kù)中耗費(fèi)的時(shí)間微乎其微。

我們已經(jīng)明確了堆棧中的兩大性能瓶頸:Postgresql 和 ORM。

需要明確的是:這并不意味著 Sequel 性能低下,或者 DateTime::parse 存在問(wèn)題 [8]。相反,這表明我們加入堆棧的工具越多,性能就越糟糕。再?gòu)?qiáng)調(diào)一次:這是顯而易見(jiàn)的,并不令人意外。然而,值得重申。

在對(duì)整個(gè) Rails 進(jìn)行全面基準(zhǔn)測(cè)試之前,我們先來(lái)審視一下 Rails 中的 ORM:ActiveRecord。同樣地,由于查詢(xún)操作非常簡(jiǎn)單,不涉及復(fù)雜內(nèi)容,因此在數(shù)據(jù)庫(kù)中所花費(fèi)的時(shí)間非常有限。

                           user     system      total        real
Postgres Sequel write  0.679423   0.112094   0.791517 (  2.963639)
Postgres Sequel read   8.798584   0.011155   8.809739 (  9.194935)
Postgres AR write      1.741980   0.189130   1.931110 (  4.404335)
Postgres AR read       1.551020   0.040676   1.591696 (  1.922000)

通過(guò) ActiveRecord 寫(xiě)入:

2591602a-6744-11ee-939d-92fbcf53809c.png

通過(guò) ActiveRecord 讀?。?/p>

25a81df6-6744-11ee-939d-92fbcf53809c.png

通過(guò) Sequel 讀?。?/p>

25ca5308-6744-11ee-939d-92fbcf53809c.png

通過(guò) Sequel 寫(xiě)入:

25ce2e6a-6744-11ee-939d-92fbcf53809c.png

我們可以清楚地看到,Sequel 中的 DateTime::parse 問(wèn)題依然存在。我推測(cè),ActiveRecord 采用了一種更高效的策略,將 Postgresql 中的日期時(shí)間轉(zhuǎn)換為本地 DateTime。

盡管如此,Ruby 的糟糕性能相對(duì)來(lái)說(shuō)并不重要。如果最快的數(shù)據(jù)庫(kù)查詢(xún)需要 150 毫秒,那么 Ruby 暫停 15 毫秒進(jìn)行垃圾回收并沒(méi)有太大關(guān)系。JIT 的開(kāi)銷(xiāo)、Rack 和 Rails 的 HTTP 解析和轉(zhuǎn)發(fā)的多層堆棧,除了向數(shù)據(jù)庫(kù)插入查詢(xún)耗時(shí) 190ms 之外,對(duì)整體性能影響不大。

這個(gè)例子展示了從表中獲取一條記錄的操作,雖然它并非關(guān)系型數(shù)據(jù)庫(kù)所擅長(zhǎng)的領(lǐng)域,但它揭示了 ORM 存在的實(shí)際性能問(wèn)題:缺乏連接、排序、過(guò)濾和計(jì)算等操作。

因此,即使 ORM 性能較差,數(shù)據(jù)庫(kù)仍然是主要的耗時(shí)組件。

擴(kuò)大規(guī)模

我們都曾遇到過(guò)這樣的情況:Ruby/Rails 代碼變得錯(cuò)綜復(fù)雜,設(shè)置糟糕透頂,以至于堆棧(或自定義代碼)成為瓶頸。問(wèn)題看似簡(jiǎn)單解決:只需增加額外服務(wù)器。盡管單個(gè)請(qǐng)求速度不變,但至少服務(wù)器負(fù)載不再影響其他用戶性能。應(yīng)用雖未變快,卻能容納更多用戶。

起初,這很容易實(shí)現(xiàn),直到數(shù)據(jù)庫(kù)再次成為瓶頸。寫(xiě)入關(guān)系數(shù)據(jù)庫(kù)始終是個(gè)難題:只能垂直擴(kuò)展,即增加更強(qiáng)大的數(shù)據(jù)庫(kù)服務(wù)器。至于查詢(xún)(讀?。┓矫妫梢酝ㄟ^(guò)增加復(fù)雜性來(lái)解決:讀取副本(曾稱(chēng)為 “從屬”)。幾乎所有常見(jiàn)的關(guān)系數(shù)據(jù)庫(kù)服務(wù)器都支持此方法。雖然并不簡(jiǎn)單,因?yàn)樗鼘ⅰ白罱K一致性”引入了一個(gè)設(shè)置 / 框架,這個(gè)設(shè)置 / 框架從來(lái)沒(méi)有被設(shè)計(jì)成最終一致,但這是可行的。寫(xiě)入(創(chuàng)建、插入、更新、刪除等)則不然:數(shù)據(jù)庫(kù)可能在某個(gè)時(shí)刻成為瓶頸。除非永遠(yuǎn)如此:但性能從一開(kāi)始就并非問(wèn)題。

解決 Ruby 代碼中的性能問(wèn)題輕而易舉:只需增加更多服務(wù)器。然而,解決數(shù)據(jù)庫(kù)性能問(wèn)題就沒(méi)那么容易了,因?yàn)閿U(kuò)大關(guān)系數(shù)據(jù)庫(kù)規(guī)模困難重重,甚至有時(shí)不可能。

因此,為保持代碼可擴(kuò)展性,應(yīng)盡量在代碼中保留邏輯、轉(zhuǎn)換等元素。將業(yè)務(wù)邏輯、約束、驗(yàn)證和計(jì)算推入數(shù)據(jù)庫(kù),等于放棄了最簡(jiǎn)單、通常也最經(jīng)濟(jì)的性能提升手段:“增加更多服務(wù)器”。

Rails

正如多次提到的,Rails 的復(fù)雜性導(dǎo)致了真正難以解決的性能問(wèn)題。讓我們深入探討一下。

引用 DHH 在 Rails 的一句話:

“所有花哨的優(yōu)化都是為了讓你更接近于如果你沒(méi)有使用這么多技術(shù)就會(huì)得到的性能”

https://macwright.com/2020/05/10/spa-fatigue.html——https://twitter.com/dhh/status/1259644085322670080

Rails 的內(nèi)部復(fù)雜性對(duì)性能有兩大影響。首先,它包含大量抽象,被批評(píng)為 “黑魔法”。其次,在典型的 HTTP 循環(huán)中,數(shù)據(jù)需要經(jīng)過(guò)所有這些層和所有這些復(fù)雜性,直到請(qǐng)求響應(yīng)完成。

由于 Ruby 處理數(shù)據(jù)相對(duì)較慢(參見(jiàn)下文),數(shù)據(jù)傳遞的代碼越多,結(jié)果就越慢。這對(duì)所有軟件都是如此,但 Ruby 放大了這一點(diǎn)。Rails 的 163500 行 Ruby 代碼當(dāng)然無(wú)助于加快速度。

“代碼行” 并非性能指標(biāo),但它們是一種指示。即使是最小的 Rails 項(xiàng)目也包含數(shù)十萬(wàn)行代碼,即使你只使用其中一小部分?jǐn)?shù)據(jù)。

針對(duì) Rails 的基準(zhǔn)測(cè)試已經(jīng)進(jìn)行了許多次。我現(xiàn)在將獲得更多元數(shù)據(jù),而不是繼續(xù)討論整個(gè)堆棧的 “基準(zhǔn)” 和火焰圖。少談數(shù)字,多談概念。因?yàn)閷?duì)于 Rails,我確信性能問(wèn)題是概念性的。如上所述,技術(shù)性能問(wèn)題是由 Ruby 而不是 Rails 引起的。

ActiveRecord(Rails 中的實(shí)現(xiàn),而非模式 per-sé)是對(duì)系統(tǒng)(關(guān)系數(shù)據(jù)庫(kù))的抽象,需要大量詳細(xì)知識(shí)來(lái)保持性能。ActiveRecord (模式)不僅是一個(gè)漏洞的抽象,更多地是一個(gè)抽象,隱藏了一些不應(yīng)被隱藏的細(xì)節(jié)。

更實(shí)際的情況是:幾年前我為了修復(fù)一個(gè) N+1 查詢(xún)而加入的 User.active.includes(:roles) 動(dòng)態(tài)地選擇它認(rèn)為你需要的內(nèi)容。它可能會(huì)“突然地、神奇地、動(dòng)態(tài)地”開(kāi)始構(gòu)建其他連接和查詢(xún),從而降低性能。(好吧,不是從一分鐘到下一分鐘的運(yùn)行時(shí),而是經(jīng)過(guò)小的更改)。

我曾在一個(gè)擁有百萬(wàn)級(jí)用戶的應(yīng)用程序中,導(dǎo)致數(shù)據(jù)庫(kù)服務(wù)器集群崩潰:原因在于一個(gè)無(wú)關(guān)控制器的簡(jiǎn)單更改,使 Rails 切換到一個(gè)外部連接,該連接具有巨大物化視圖,本不應(yīng)以這種方式連接(用于報(bào)告)。然而,Rails 的魔力使其從此開(kāi)始使用這一特性。每次頁(yè)面加載都會(huì)導(dǎo)致大約 2 秒鐘的數(shù)據(jù)庫(kù)查詢(xún),占用數(shù)據(jù)庫(kù)服務(wù)器上的所有 CPU 和 IO。

當(dāng)然,這是個(gè)愚蠢的錯(cuò)誤。我們沒(méi)有看到這一點(diǎn),因?yàn)樵陂_(kāi)發(fā)和測(cè)試中,性能從未下降。但我們應(yīng)該注意到的是,這種錯(cuò)誤在代碼庫(kù)中比比皆是。這些項(xiàng)目之所以繼續(xù)運(yùn)行,唯一的原因是 Heroku 服務(wù)器的巨大成本(1200 美元 / 月),能為數(shù)百訪問(wèn)者提供服務(wù)一天。這樣的錯(cuò)誤不會(huì)導(dǎo)致數(shù)據(jù)庫(kù)集群崩潰,而是逐漸累積成昂貴且性能糟糕的應(yīng)用程序。20 毫秒的減速幾乎無(wú)法衡量,數(shù)百個(gè) 20 毫秒的速度減慢在幾個(gè)月內(nèi)逐漸增加,使響應(yīng)變得令人無(wú)法接受。最糟糕的是,這些 “錯(cuò)誤” 被團(tuán)隊(duì)貼上了 “以 Rails 方式完成” 的標(biāo)簽。

Rails 里到處都是這樣的 footgun(footgun,意即傷自己的腳的槍?zhuān)琑ails 稱(chēng)其為“尖刀”。譯注:指在一個(gè)產(chǎn)品上添加一個(gè)新東西,容易讓槍打著自己腳。表明設(shè)計(jì)不好,促使用戶不敢加?xùn)|西。)。其中大部分本身是無(wú)害的。很容易以次優(yōu)的方式連接表,對(duì)未索引的列進(jìn)行排序或過(guò)濾。Active-record 充滿了一些工具,可以很容易地濫用數(shù)據(jù)庫(kù),無(wú)需警告。我開(kāi)發(fā)的 Rails 應(yīng)用程序數(shù)量驚人,其中包含某種形式的 .sort(params[:sort by]):僅在 2021 年,我就開(kāi)發(fā)了三個(gè)獨(dú)立的 Rails 應(yīng)用程序,所有這些應(yīng)用程序都可以通過(guò)使用 ?sort=some_unindexed_field 觸發(fā)請(qǐng)求來(lái)處理數(shù)據(jù)庫(kù)。雖然這個(gè)例子很極端,可能被視為安全問(wèn)題,但它說(shuō)明了讓?xiě)?yīng)用程序性能變差是多么容易。

sorting-by-un-indexed-field 示例揭示了 Rails 與數(shù)據(jù)庫(kù)的耦合如何使其許多性能問(wèn)題成為數(shù)據(jù)庫(kù)問(wèn)題。

根據(jù)我的經(jīng)驗(yàn),Rails 中的性能問(wèn)題總是:

  1. N+1 個(gè)查詢(xún)。易于檢測(cè)。難以修復(fù)(不引入大量耦合問(wèn)題)。

  2. 未優(yōu)化的連接。添加簡(jiǎn)單的 has_many 太容易了,這使得開(kāi)發(fā)人員可以在數(shù)據(jù)庫(kù)中啟動(dòng)過(guò)于繁重的查詢(xún)。一旦通過(guò)應(yīng)用程序引入和傳播,這幾乎不可能解決。總有一些代碼最終運(yùn)行類(lèi)似 User.with_access_to(project).notifications.last.sent_to 的代碼。而且它會(huì)查詢(xún)五個(gè)連接表并且連接到至少一個(gè)索引上,而這個(gè)索引并不是為此準(zhǔn)備的。導(dǎo)致大約 800 毫秒的查詢(xún)。在每次頁(yè)面加載時(shí)。

  3. 未優(yōu)化的 where、group 和 order 調(diào)用。使用難以篩選、分組或排序或優(yōu)化不佳的列。使用非索引列。

  4. 我的經(jīng)驗(yàn)法則是,每個(gè)添加或刪除的 where、has_many、group 或任何此類(lèi) active-record 方法都必須伴隨著數(shù)據(jù)庫(kù)遷移。因?yàn)橹挥挟?dāng)你已經(jīng)有了以前沒(méi)有使用過(guò)的索引時(shí),才需要為這種新的查詢(xún)方式優(yōu)化數(shù)據(jù)庫(kù)(這意味著它以前優(yōu)化得很差)。另一種情況是當(dāng)你重用現(xiàn)有索引時(shí),在這種情況下,你很可能應(yīng)該重構(gòu)以將查詢(xún)轉(zhuǎn)移到單一責(zé)任(例如,命名范圍)。

使用 Rails 人性化的 active-record API,很容易忘記你仍然只是在查詢(xún)一個(gè)復(fù)雜的關(guān)系數(shù)據(jù)庫(kù)。它需要微調(diào)、調(diào)優(yōu)和調(diào)整,以便在合理的時(shí)間內(nèi)為你提供數(shù)據(jù)。

使用 Rails,很容易累積許多小錯(cuò)誤,從而使數(shù)據(jù)庫(kù)成為瓶頸。但是,即使所有這些都在你的控制之下,高性能的數(shù)據(jù)庫(kù)調(diào)用仍然比許多其他調(diào)用慢很多。

從內(nèi)存和代碼中填充某個(gè)數(shù)組,然后從數(shù)據(jù)庫(kù)中填充該數(shù)組,速度仍然要快一千倍或更多。正如我在第一段中所展示的那樣。

所以,該怎么辦呢?我采用的一些經(jīng)驗(yàn)法則是:

  • 在可以避免的情況下,不要使用數(shù)據(jù)庫(kù)。這總是比我想象的更頻繁。我不需要將世界上 195 個(gè)國(guó)家存儲(chǔ)在數(shù)據(jù)庫(kù)中,并在顯示國(guó)家下拉列表時(shí)加入。只需硬編碼或在啟動(dòng)時(shí)輸入配置讀取。見(jiàn)鬼,也許你的電子商務(wù)網(wǎng)站的整個(gè)產(chǎn)品目錄可以是一個(gè)單獨(dú)的 YAML 啟動(dòng)時(shí)讀???這適用于比我通常認(rèn)為的更多的對(duì)象。

  • 將邏輯與數(shù)據(jù)庫(kù)分離,因?yàn)閿?shù)據(jù)庫(kù)是最慢且最難擴(kuò)展的地方。

  • 謹(jǐn)慎處理 sort()、where()、join() 等調(diào)用。如果添加(或刪除)了索引,它們必須伴隨著至少調(diào)優(yōu)索引的遷移。

  • 保持所有數(shù)據(jù)庫(kù)調(diào)用簡(jiǎn)單。盡可能少的連接,盡可能少的過(guò)濾器和排序。一般來(lái)說(shuō),數(shù)據(jù)庫(kù)可以更容易地為此進(jìn)行優(yōu)化。這也使應(yīng)用程序與實(shí)際的數(shù)據(jù)庫(kù)細(xì)節(jié)分離。

  • N+1 個(gè)查詢(xún)并不總是壞事。有時(shí)甚至是首選。因?yàn)樗鼈兪箻I(yè)務(wù)邏輯保留在代碼中。并將獲取內(nèi)容的邏輯保存在一個(gè)地方,從而允許在那里進(jìn)行性能優(yōu)化。

  • 保持對(duì)實(shí)際性能問(wèn)題的了解。根據(jù)性能是 I/O 密集型的還是計(jì)算性的,主動(dòng)擴(kuò)大規(guī)模。并祈禱它是計(jì)算性的。

內(nèi)文注釋?zhuān)?/strong>

[1] 不過(guò),我要強(qiáng)調(diào)的是:作為 Rust 新手,我花了一個(gè)多小時(shí)編寫(xiě) Rust 版本,而作為 Ruby 資深用戶(10 年以上),我只用了不到 10 分鐘。我需要運(yùn)行兩個(gè)版本 2000 多次,然后我花在開(kāi)發(fā) Rust 版本上的額外時(shí)間才能在等待它運(yùn)行的額外時(shí)間中得到回報(bào)。

[2] 我確信你可以給我展示一個(gè)項(xiàng)目,在那里你不用數(shù)據(jù)庫(kù)就可以運(yùn)行 Rails,而且這很有意義。這些案例是存在的。我遇到的一些問(wèn)題是:“我已經(jīng)知道 Rails,但不知道 Sinatra”,或者“管理要求我們?cè)陬?lèi)似的代碼庫(kù)上運(yùn)行一切”。實(shí)際上,最后一個(gè)理由不成立。大多數(shù)都是合理的理由,除了最后一個(gè):這是選擇 Rails 的一個(gè)可怕的理由。

[3] 一個(gè)快速 grep:超過(guò) 9000 個(gè)類(lèi),超過(guò) 33000 個(gè)方法;不包括所有神奇的動(dòng)態(tài)方法,比如圍繞數(shù)據(jù)庫(kù)模型的方法。這還不包括 rails 本身附帶的 70 多個(gè)依賴(lài)項(xiàng)。

[4] 一個(gè)常見(jiàn)的 Rails 應(yīng)用程序?qū)l(fā)送電子郵件,可能會(huì)生成 pdf,接收 CSV 或?qū)С?CSV,但所有交互通常都通過(guò) HTTP 進(jìn)行。我知道 Rails 只用于運(yùn)行 cron 作業(yè)、ETL 管道甚至媒體編碼的例外情況(我曾研究過(guò)),但這些確實(shí)是例外情況。

[5] 具有諷刺意味的是,在這種非 http、非 rails 的環(huán)境中,性能問(wèn)題變得不那么明確了,然而在這些情況下,人們通常會(huì)因?yàn)?ruby 的性能問(wèn)題而將其作為選項(xiàng)。這也是 Ruby 很少在 Rails(和 / 或 Web)之外使用的原因之一。

[7] 令人驚訝的是,從內(nèi)存中的 SQLite 中查找比從數(shù)據(jù)庫(kù)中查找要慢。但這說(shuō)明了另一個(gè)重要問(wèn)題:數(shù)據(jù)庫(kù)運(yùn)行在單獨(dú)的線程中,甚至可能在單獨(dú)的硬件上。因此負(fù)載是分布式的:在 SQLite 和我們的內(nèi)存示例中,一個(gè) Ruby 線程完成了所有的過(guò)濾、獲取和提升。對(duì)于外部數(shù)據(jù)庫(kù),這是偏移量。根據(jù)你的設(shè)置,Ruby 線程甚至可能在數(shù)據(jù)庫(kù)進(jìn)行查找時(shí)繼續(xù)工作。在這種情況下,經(jīng)過(guò)優(yōu)化以過(guò)濾和獲取數(shù)據(jù)的 Postgresql 可以比 SQLite-inside-ruby 更快地完成這項(xiàng)工作。在典型的生產(chǎn)設(shè)置中,Postgresql 更適合這一點(diǎn)。

[8] 請(qǐng)注意,雖然 DateTime:parse 很慢,但這個(gè)函數(shù)是用 C 編寫(xiě)的。之所以慢,并不是因?yàn)樗怯?Ruby 編寫(xiě)的,而是因?yàn)榻馕鋈绱藦?fù)雜的文本很慢。對(duì)于 Rust 中的功能相當(dāng)?shù)陌姹緛?lái)說(shuō),它可能會(huì)一樣慢。

[9] 有更多的理由說(shuō)明這是一個(gè)更好的主意。最明顯的一點(diǎn)是,你永遠(yuǎn)不能把所有的業(yè)務(wù)邏輯都放在數(shù)據(jù)庫(kù)中,即使你想這樣做。因此,你將在多個(gè)地方擁有業(yè)務(wù)邏輯,而不需要任何去往何處的結(jié)構(gòu)。所以把它放在一個(gè)地方的顯而易見(jiàn)的解決方案是……放在一個(gè)地方。唯一可以保存所有內(nèi)容的地方:你的應(yīng)用程序。


聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 數(shù)據(jù)庫(kù)
    +關(guān)注

    關(guān)注

    7

    文章

    3922

    瀏覽量

    66160
  • ruby
    +關(guān)注

    關(guān)注

    0

    文章

    44

    瀏覽量

    3671
  • postgresql
    +關(guān)注

    關(guān)注

    0

    文章

    24

    瀏覽量

    376

原文標(biāo)題:慢的不是 Ruby,而是你的數(shù)據(jù)庫(kù)

文章出處:【微信號(hào):AI前線,微信公眾號(hào):AI前線】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 0人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評(píng)論

    相關(guān)推薦
    熱點(diǎn)推薦

    分布式數(shù)據(jù)庫(kù),什么是分布式數(shù)據(jù)庫(kù)

    分布式數(shù)據(jù)庫(kù),什么是分布式數(shù)據(jù)庫(kù) 分布式數(shù)據(jù)庫(kù)系統(tǒng)是在集中式數(shù)據(jù)庫(kù)系統(tǒng)成熟技術(shù)的基礎(chǔ)上發(fā)展起來(lái)的,但不是簡(jiǎn)單地把集中式數(shù)
    發(fā)表于 03-18 15:25 ?3995次閱讀

    MySQL數(shù)據(jù)庫(kù)誤刪后的回復(fù)技巧

    在日常運(yùn)維工作中,對(duì)于數(shù)據(jù)庫(kù)的備份是至關(guān)重要的!數(shù)據(jù)庫(kù)對(duì)于網(wǎng)站的重要性使得我們對(duì) MySQL 數(shù)據(jù)庫(kù)的管理不容有失!然而是人總難免會(huì)犯錯(cuò)誤,說(shuō)不定哪天大腦短路了,誤操作把
    發(fā)表于 05-05 08:02 ?2467次閱讀
    MySQL<b class='flag-5'>數(shù)據(jù)庫(kù)</b>誤刪后的回復(fù)技巧

    多維數(shù)據(jù)庫(kù)有哪些

    多維數(shù)據(jù)庫(kù)可以簡(jiǎn)單地理解為:將數(shù)據(jù)存放在一個(gè)n維數(shù)組中,而不是像關(guān)系數(shù)據(jù)庫(kù)那樣以記錄的形式存放。因此它存在大量稀疏矩陣,人們可以通過(guò)多維視圖來(lái)觀察數(shù)
    的頭像 發(fā)表于 02-24 11:14 ?7668次閱讀
    多維<b class='flag-5'>數(shù)據(jù)庫(kù)</b>有哪些

    數(shù)據(jù)庫(kù)教程之如何進(jìn)行數(shù)據(jù)庫(kù)設(shè)計(jì)

    本文檔的主要內(nèi)容詳細(xì)介紹的是數(shù)據(jù)庫(kù)教程之如何進(jìn)行數(shù)據(jù)庫(kù)設(shè)計(jì)內(nèi)容包括了:1 數(shù)據(jù)庫(kù)設(shè)計(jì)概述 ,2 數(shù)據(jù)庫(kù)需求分析 ,3 數(shù)據(jù)庫(kù)結(jié)構(gòu)設(shè)計(jì) ,4
    發(fā)表于 10-19 10:41 ?21次下載
    <b class='flag-5'>數(shù)據(jù)庫(kù)</b>教程之如何進(jìn)行<b class='flag-5'>數(shù)據(jù)庫(kù)</b>設(shè)計(jì)

    工業(yè)大數(shù)據(jù)中的實(shí)時(shí)數(shù)據(jù)庫(kù)與時(shí)序數(shù)據(jù)庫(kù)是什么

    實(shí)時(shí)數(shù)據(jù)庫(kù)其實(shí)并不單單只是一個(gè)數(shù)據(jù)庫(kù),而是一個(gè)系統(tǒng),包括對(duì)各類(lèi)工業(yè)接口的數(shù)據(jù)采集,海量監(jiān)測(cè)數(shù)據(jù)的壓縮、存儲(chǔ)及檢索,基于監(jiān)測(cè)
    發(fā)表于 11-09 10:17 ?8617次閱讀

    數(shù)據(jù)庫(kù)和自建數(shù)據(jù)庫(kù)的區(qū)別及應(yīng)用

    數(shù)據(jù)庫(kù)是指優(yōu)化和部署在云端的數(shù)據(jù)庫(kù),阿里云和騰訊云都提供云數(shù)據(jù)庫(kù),云數(shù)據(jù)庫(kù)和自己搭建的數(shù)據(jù)庫(kù)有什么區(qū)別?有必要使用云
    的頭像 發(fā)表于 11-20 16:26 ?4949次閱讀
    云<b class='flag-5'>數(shù)據(jù)庫(kù)</b>和自建<b class='flag-5'>數(shù)據(jù)庫(kù)</b>的區(qū)別及應(yīng)用

    華為云數(shù)據(jù)庫(kù)-RDS for MySQL數(shù)據(jù)庫(kù)

    (for MySQL)為輔。 MySQL數(shù)據(jù)庫(kù)是全球最受歡迎的一種數(shù)據(jù)庫(kù),它是屬于 Oracle旗下的一款產(chǎn)品,MySQL是一種關(guān)系型數(shù)據(jù)庫(kù)管理系統(tǒng),關(guān)系數(shù)據(jù)庫(kù)
    的頭像 發(fā)表于 10-27 11:06 ?1828次閱讀

    數(shù)據(jù)庫(kù)數(shù)據(jù)恢復(fù)】MongoDB數(shù)據(jù)庫(kù)數(shù)據(jù)遷移報(bào)錯(cuò)的數(shù)據(jù)恢復(fù)案例

    MongoDB數(shù)據(jù)庫(kù)存儲(chǔ)方式是將文檔存儲(chǔ)在集合之中,而不是像Oracle、MySQL一樣的關(guān)系型數(shù)據(jù)庫(kù)。 MongoDB數(shù)據(jù)庫(kù)是開(kāi)源數(shù)據(jù)庫(kù)
    的頭像 發(fā)表于 12-06 11:46 ?1568次閱讀
    【<b class='flag-5'>數(shù)據(jù)庫(kù)</b><b class='flag-5'>數(shù)據(jù)</b>恢復(fù)】MongoDB<b class='flag-5'>數(shù)據(jù)庫(kù)</b><b class='flag-5'>數(shù)據(jù)</b>遷移報(bào)錯(cuò)的<b class='flag-5'>數(shù)據(jù)</b>恢復(fù)案例

    數(shù)據(jù)庫(kù)上云已成趨勢(shì),華為云數(shù)據(jù)庫(kù)與傳統(tǒng)數(shù)據(jù)庫(kù)對(duì)比解析

    ,并不適合海量數(shù)據(jù)存儲(chǔ),并且裝載的速度。舉個(gè)例子,當(dāng)傳統(tǒng)索引需要重新創(chuàng)建,加載的性能就會(huì)大幅度下降。為了解決此類(lèi)問(wèn)題,華為云數(shù)據(jù)庫(kù)應(yīng)運(yùn)而生。 與傳統(tǒng)數(shù)據(jù)庫(kù)不同的是,華為云提供的云
    的頭像 發(fā)表于 12-27 16:52 ?1347次閱讀
    <b class='flag-5'>數(shù)據(jù)庫(kù)</b>上云已成趨勢(shì),華為云<b class='flag-5'>數(shù)據(jù)庫(kù)</b>與傳統(tǒng)<b class='flag-5'>數(shù)據(jù)庫(kù)</b>對(duì)比解析

    數(shù)據(jù)庫(kù)建立|數(shù)據(jù)庫(kù)創(chuàng)建的方法?

    數(shù)據(jù)庫(kù)是一個(gè)存儲(chǔ)關(guān)鍵數(shù)據(jù)的文件系統(tǒng)。利用數(shù)據(jù)庫(kù)管理系統(tǒng)建立每個(gè)人的數(shù)據(jù)庫(kù)可以更好地提供安全。 數(shù)據(jù)庫(kù)建立|
    的頭像 發(fā)表于 07-14 11:15 ?1624次閱讀

    python讀取數(shù)據(jù)庫(kù)數(shù)據(jù) python查詢(xún)數(shù)據(jù)庫(kù) python數(shù)據(jù)庫(kù)連接

    python讀取數(shù)據(jù)庫(kù)數(shù)據(jù) python查詢(xún)數(shù)據(jù)庫(kù) python數(shù)據(jù)庫(kù)連接 Python是一門(mén)高級(jí)編程語(yǔ)言,廣泛應(yīng)用于各種領(lǐng)域。其中,Python在
    的頭像 發(fā)表于 08-28 17:09 ?2217次閱讀

    什么是數(shù)據(jù)庫(kù)?除了MySQL還有哪些數(shù)據(jù)庫(kù)?

    對(duì)于大多數(shù)項(xiàng)目,用 MySQL 等關(guān)系型數(shù)據(jù)庫(kù)來(lái)存儲(chǔ)數(shù)據(jù)就足夠了。但關(guān)系型數(shù)據(jù)庫(kù)不是銀彈!在某些場(chǎng)景下,比如要存儲(chǔ)的數(shù)據(jù)間沒(méi)有關(guān)系時(shí),它并
    發(fā)表于 10-13 10:20 ?1090次閱讀
    什么是<b class='flag-5'>數(shù)據(jù)庫(kù)</b>?除了MySQL還有哪些<b class='flag-5'>數(shù)據(jù)庫(kù)</b>?

    數(shù)據(jù)庫(kù)數(shù)據(jù)恢復(fù)——MongoDB數(shù)據(jù)庫(kù)介紹和數(shù)據(jù)恢復(fù)案例

    MongoDB數(shù)據(jù)庫(kù)是文檔數(shù)據(jù)存儲(chǔ)庫(kù),將文檔存儲(chǔ)在集合之中,不是像MySQL一樣的關(guān)系型數(shù)據(jù)庫(kù)。
    的頭像 發(fā)表于 11-08 15:04 ?1154次閱讀
    <b class='flag-5'>數(shù)據(jù)庫(kù)</b><b class='flag-5'>數(shù)據(jù)</b>恢復(fù)——MongoDB<b class='flag-5'>數(shù)據(jù)庫(kù)</b>介紹和<b class='flag-5'>數(shù)據(jù)</b>恢復(fù)案例

    選擇 KV 數(shù)據(jù)庫(kù)最重要的是什么?

    經(jīng)常有客戶提到 KV 數(shù)據(jù)庫(kù),但卻偏偏“不要 Redis”。比如有個(gè)做安全威脅分析平臺(tái)的客戶,他們明確表示自己對(duì)可靠性要求非常高,需要的不是開(kāi)源 Redis 這種內(nèi)存緩存庫(kù),而是 KV
    的頭像 發(fā)表于 03-28 22:11 ?919次閱讀
    選擇 KV <b class='flag-5'>數(shù)據(jù)庫(kù)</b>最重要的是什么?

    數(shù)據(jù)庫(kù)是哪種數(shù)據(jù)庫(kù)類(lèi)型?

    數(shù)據(jù)庫(kù)是一種部署在虛擬計(jì)算環(huán)境中的數(shù)據(jù)庫(kù),它融合了云計(jì)算的彈性和可擴(kuò)展性,為用戶提供高效、靈活的數(shù)據(jù)庫(kù)服務(wù)。云數(shù)據(jù)庫(kù)主要分為兩大類(lèi):關(guān)系型數(shù)據(jù)庫(kù)
    的頭像 發(fā)表于 01-07 10:22 ?499次閱讀

    電子發(fā)燒友

    中國(guó)電子工程師最喜歡的網(wǎng)站

    • 2931785位工程師會(huì)員交流學(xué)習(xí)
    • 獲取您個(gè)性化的科技前沿技術(shù)信息
    • 參加活動(dòng)獲取豐厚的禮品