01.20 「數據庫選型」再見MongoDB,您好PostgreSQL

Olery成立於5年前。隨著時間的流逝,最初由Ruby開發機構開發的單一產品(Olery聲望)逐漸發展成為一套不同的產品和許多不同的應用程序。今天,我們不僅擁有信譽產品,還擁有Olery反饋,酒店點評數據API,可嵌入網站上的小部件以及不久的將來更多產品/服務。

在應用程序數量方面,我們也有了長足的發展。今天,我們部署了超過25種不同的應用程序(全部為Ruby),其中一些是Web應用程序(Rails或Sinatra),但大多數是後臺處理應用程序。

儘管我們可以為迄今為止所取得的成就感到非常自豪,但總會有一些隱患:我們的主數據庫。從Olery開始,我們就已經建立了一個數據庫設置,其中涉及MySQL來存儲關鍵數據(用戶,合同等),而MongoDB則用於存儲評論和類似數據(本質上是在數據丟失的情況下我們可以輕鬆檢索的數據)。儘管此設置對我們非常有用,但隨著我們的發展,特別是在MongoDB中,我們開始遇到各種問題。這些問題中的一些是由於應用程序與數據庫交互的方式所致,有些是由於數據庫本身所致。

例如,在某個時間點,我們必須從MongoDB中刪除大約一百萬個文檔,然後在以後重新插入它們。此過程的結果是數據庫幾乎處於完全鎖定狀態,持續了幾個小時,導致性能下降。直到我們執行數據庫修復(使用MongoDB的repairDatabase命令)。由於數據庫的大小,此修復程序本身也花費了數小時才能完成。

在另一個實例中,我們注意到應用程序性能下降,並設法將其追溯到我們的MongoDB集群。但是,經過進一步檢查,我們無法找到問題的真正原因。無論我們安裝了什麼度量標準,使用的工具或運行的命令,我們都找不到原因。直到我們替換了集群的主節點,性能才恢復到正常水平。

這只是兩個例子,隨著時間的流逝,我們遇到了很多這樣的情況。這裡的核心問題不僅在於我們的數據庫正在運行,而且每當我們調查數據庫時,都根本沒有跡象表明導致問題的原因。

無模式問題

我們面臨的另一個核心問題是MongoDB(或任何其他無模式存儲引擎)的基本功能之一:缺少模式。缺少模式聽起來可能很有趣,並且在某些情況下,它當然可以帶來好處。但是,對於許多人而言,無模式存儲引擎的使用會導致隱式模式的問題。這些架構不是由您的存儲引擎定義的,而是根據應用程序的行為和期望定義的。

例如,您可能有一個頁面集合,其中您的應用程序需要一個帶有字符串類型的標題字段。儘管沒有明確定義,但這裡的模式非常多。如果數據的結構隨時間而變化,這是有問題的,尤其是如果舊數據沒有遷移到新的結構中(在無模式存儲引擎中這是很成問題的)。例如,假設您具有以下Ruby代碼:

#!ruby post_slug = post.title.downcase.gsub(/\\W+/, '-')

這將適用於每個帶有標題字段且返回字符串的文檔。 對於使用其他字段名稱(例如post_title)或根本沒有標題的字段的文檔,這將不起作用。 要處理這種情況,您需要按以下方式調整代碼:

#!ruby if post.title post_slug = post.title.downcase.gsub(/\\W+/, '-') else # ... end

解決此問題的另一種方法是在模型中定義架構。例如,Mongoid是流行的Ruby MongoDB ODM,可以讓您做到這一點。但是,在使用此類工具定義架構時,應該思考為什麼他們沒有在數據庫本身中定義架構。這樣做將解決另一個問題:可重用性。如果只有一個應用程序,那麼在代碼中定義架構並不是什麼大問題。但是,當您有數十個應用程序時,這很快就會變成一團糟。

無模式存儲引擎通過消除對模式的擔心,有望使您的生活更輕鬆。實際上,這些系統只是讓您自己負責確保數據一致性。在某些情況下,這可能會解決,但我敢打賭,對於大多數情況而言,這隻會適得其反。

好的數據庫的要求

這使我想到了一個好的數據庫的要求,更具體地說是Olery的要求。對於系統,尤其是數據庫,我們重視以下方面:

  • 一致性。
  • 數據的可見性和系統的行為。
  • 正確性和明確性。
  • 可擴展性。

一致性很重要,因為它有助於設定對系統的明確期望。如果數據總是以某種方式存儲,那麼使用該數據的系統將變得更加簡單。如果在數據庫級別需要某個字段,則應用程序無需檢查該字段是否存在。數據庫即使在高壓下也應該能夠保證某些操作的完成。沒有什麼比僅插入數據而令人沮喪的了,只有在幾分鐘之後才顯示數據。

可見性適用於兩件事:系統本身以及從其中獲取數據的難易程度。如果系統出現異常,則應易於調試。反過來,如果用戶想查詢數據,這也應該很容易。

正確性意味著系統的行為符合預期。如果將某個字段定義為數字值,則不應將文本插入該字段。 MySQL的缺點是眾所周知的,因為它可以讓您準確地做到這一點,結果您可能會得到虛假數據。

可伸縮性不僅適用於性能,而且還適用於財務方面,以及系統如何滿足隨著時間變化的需求。一個系統的性能可能非常好,但是卻不能以大量金錢為代價,也不會減慢依賴於它的系統的開發週期。

遠離MongoDB

考慮到以上值,我們著手尋找MongoDB的替代者。上面提到的值通常是傳統RDBMS的一組核心功能,因此我們著眼於兩個候選對象:MySQL和PostgreSQL。

MySQL是第一個候選對象,因為我們已經在一些關鍵數據中使用它。但是MySQL並非沒有問題。例如,在將字段定義為int(11)時,您可以輕鬆地插入文本數據,MySQL會嘗試對其進行轉換。一些例子:

mysql> create table example ( `number` int(11) not null ); Query OK, 0 rows affected (0.08 sec) mysql> insert into example (number) values (10); Query OK, 1 row affected (0.08 sec) mysql> insert into example (number) values ('wat'); Query OK, 1 row affected, 1 warning (0.10 sec) mysql> insert into example (number) values ('what is this 10 nonsense'); Query OK, 1 row affected, 1 warning (0.14 sec) mysql> insert into example (number) values ('10 a'); Query OK, 1 row affected, 1 warning (0.09 sec) mysql> select * from example; +--------+ | number | +--------+ | 10 | | 0 | | 0 | | 10 | +--------+ 4 rows in set (0.00 sec)

值得注意的是,在這種情況下,MySQL會發出警告。 但是,由於警告只是警告,因此常常(即使不是總是)將它們忽略。

MySQL的另一個問題是任何表修改(例如添加列)都會導致表被鎖定以進行讀取和寫入。 這意味著使用此類表的任何操作都必須等待修改完成。 對於具有大量數據的表,這可能需要數小時才能完成,這可能導致應用程序停機。 這已導致SoundCloud等公司開發諸如lhm之類的工具來解決這一問題。

基於上述考慮,我們開始研究PostgreSQL。 PostgreSQL在很多方面做得很好,而MySQL則做不到。 例如,您不能將文本數據插入數字字段:

olery_development=# create table example ( number int not null ); CREATE TABLE olery_development=# insert into example (number) values (10); INSERT 0 1 olery_development=# insert into example (number) values ('wat'); ERROR: invalid input syntax for integer: "wat" LINE 1: insert into example (number) values ('wat'); ^ olery_development=# insert into example (number) values ('what is this 10 nonsense'); ERROR: invalid input syntax for integer: "what is this 10 nonsense" LINE 1: insert into example (number) values ('what is this 10 nonsen... ^ olery_development=# insert into example (number) values ('10 a'); ERROR: invalid input syntax for integer: "10 a" LINE 1: insert into example (number) values ('10 a');


PostgreSQL還具有以各種方式更改表的功能,而無需為每個操作鎖定表。例如,添加一個沒有默認值並且可以設置為NULL的列可以快速完成,而無需鎖定整個表。

PostgreSQL中還有許多其他有趣的功能,例如:基於Trigram的索引和搜索,全文本搜索,對JSON查詢的支持,對查詢/存儲鍵值對的支持,對發佈/訂閱的支持等等。

所有PostgreSQL中最重要的是在性能,可靠性,正確性和一致性之間取得平衡。

遷移到PostgreSQL

最後,我們決定與PostgreSQL達成和解,以便在我們關心的各個主題之間取得平衡。從MongoDB遷移整個平臺到完全不同的數據庫的過程並非易事。為了簡化過渡過程,我們將這個過程大致分為3個步驟:

設置PostgreSQL數據庫並遷移一小部分數據。更新所有依賴MongoDB來使用PostgreSQL的應用程序,以及支持此功能所需的任何重構。將生產數據遷移到新數據庫並部署新平臺。

遷移子集

在我們甚至考慮遷移所有數據之前,我們需要使用一小部分最終數據來運行測試。如果您知道即使是一小部分數據也會給您帶來很多麻煩,那麼遷移毫無意義。

雖然存在可以解決此問題的工具,但我們還必須轉換一些數據(例如,重命名字段,更改類型等),因此必須為此編寫自己的工具。這些工具大部分是一次性的Ruby腳本,每個腳本執行特定的任務,例如移交評論,清理編碼,更正主鍵序列等。

最初的測試階段並未發現任何可能阻礙遷移過程的問題,儘管我們的某些數據部分存在問題。例如,某些用戶提交的內容並非總是正確地編碼,因此,如果不先清除它們就無法導入。需要進行的另一個有趣的更改是將評論的語言名稱從其全名(“荷蘭語”,“英語”等)更改為語言代碼,因為我們的新情感分析堆棧使用語言代碼代替了全名。

更新應用

到目前為止,大部分時間都花在了更新應用程序上,尤其是那些嚴重依賴MongoDB聚合框架的應用程序。投入一些測試覆蓋率較低的舊版Rails應用程序,您將有數週的工作時間。這些應用程序的更新過程基本上如下:

  • 將MongoDB驅動程序/模型設置代碼替換為PostgreSQL相關代碼
  • 運行測試
  • 修復一些測試
  • 再次運行測試,沖洗並重復直到所有測試通過

對於非Rails應用程序,我們決定使用Sequel,而我們在Rails應用程序中堅持使用ActiveRecord(至少現在是這樣)。 Sequel是一個很棒的數據庫工具包,它支持我們可能想使用的大多數(如果不是全部)PostgreSQL特定功能。與ActiveRecord相比,其查詢構建DSL的功能也要強大得多,儘管有時可能會有些冗長。

例如,假設您要計算使用某個語言環境的用戶數量以及每個語言環境的百分比(相對於整個集合)。在普通的SQL中,這樣的查詢如下所示:

#!sql SELECT locale, count(*) AS amount, (count(*) / sum(count(*)) OVER ()) * 100.0 AS percentage FROM users GROUP BY locale ORDER BY percentage DESC;


在我們的例子中,這將產生以下輸出(使用PostgreSQL命令行界面時):

locale | amount | percentage --------+--------+-------------------------- en | 2779 | 85.193133047210300429000 nl | 386 | 11.833231146535867566000 it | 40 | 1.226241569589209074000 de | 25 | 0.766400980993255671000 ru | 17 | 0.521152667075413857000 | 7 | 0.214592274678111588000 fr | 4 | 0.122624156958920907000 ja | 1 | 0.030656039239730227000 ar-AE | 1 | 0.030656039239730227000 eng | 1 | 0.030656039239730227000 zh-CN | 1 | 0.030656039239730227000 (11 rows)


Sequel允許您使用純Ruby編寫上述查詢,而無需字符串片段(這是ActiveRecord經常需要的):

#!ruby star = Sequel.lit('*') User.select(:locale) .select_append { count(star).as(:amount) } .select_append { ((count(star) / sum(count(star)).over) * 100.0).as(:percentage) } .group(:locale) .order(Sequel.desc(:percentage))

如果您不喜歡使用Sequel.lit('*'),也可以使用以下語法:

#!ruby User.select(:locale) .select_append { count(users.*).as(:amount) } .select_append { ((count(users.*) / sum(count(users.*)).over) * 100.0).as(:percentage) } .group(:locale) .order(Sequel.desc(:percentage))


雖然這兩個查詢可能都比較冗長,但它們更易於重用部分查詢,而不必訴諸字符串連接。

將來,我們可能還會將Rails應用程序移至Sequel,但是考慮到Rails與ActiveRecord緊密相連,我們尚不確定是否值得花時間和精力。

遷移生產數據

最終,這使我們進入了遷移生產數據的過程。基本上有兩種方法可以執行此操作:

  • 關閉所有平臺,並在所有數據遷移後使其重新聯機。
  • 在保持運行的同時遷移數據。

選項1有一個明顯的缺點:停機時間。另一方面,方法2不需要停機,但是很難處理。例如,在此設置中,您在遷移數據時必須考慮添加的所有數據,否則會丟失數據。

幸運的是,Olery具有相當獨特的設置,因為對數據庫的大多數寫入操作僅在相當固定的時間間隔內進行。確實更改頻率更高的數據(例如用戶和合同信息)是相當少量的數據,這意味著與我們的評論數據相比,遷移所需的時間要少得多。

這部分的基本流程是:

  • 遷移關鍵數據,例如用戶,合同,基本上是我們以任何方式無法承受的所有數據。
  • 遷移不太重要的數據(我們可以重新刮擦,重新計算的數據等)。
  • 測試是否一切正常並在一組單獨的服務器上運行。
  • 將生產環境切換到這些新服務器。
  • 重新遷移步驟1的數據,確保在此期間創建的數據不會丟失。

第2步花費了迄今為止最長的時間,大約是24小時。另一方面,遷移步驟1和5中提到的數據僅花費了大約45分鐘。

結論

自我們完成遷移以來已經快一個月了,到目前為止,我們感到非常滿意。到目前為止,所產生的影響不過是積極的,在各種情況下甚至導致我們應用程序的性能大大提高。例如,由於遷移,我們的酒店評論數據API(在Sinatra上運行)最終獲得了比以前更低的響應時間:

「數據庫選型」再見MongoDB,您好PostgreSQL

遷移是在1月21日進行的,最大的高峰只是應用程序執行了硬重啟(導致該過程中的響應時間稍慢)。 21日之後,平均響應時間幾乎縮短了一半。

我們看到性能大幅提高的另一種情況就是所謂的“審查持久性”。 這個應用程序(作為守護程序運行)的目的很簡單:保存評論數據(評論,評論等級等)。 儘管我們最終對該應用程序進行了一些非常大的更改以進行遷移,但結果卻非常有益:

「數據庫選型」再見MongoDB,您好PostgreSQL

我們的刮板(scrapers )也最終更快了:

「數據庫選型」再見MongoDB,您好PostgreSQL

區別並不像複審持久性那麼大,但是由於抓取工具僅使用數據庫來檢查是否存在複審(相對較快的操作),因此這並不奇怪。

最後是計劃抓取過程的應用程序(簡稱為“調度程序”):

「數據庫選型」再見MongoDB,您好PostgreSQL

由於調度程序僅按特定的間隔運行,因此該圖有些難以理解,但是遷移後的平均處理時間明顯減少了。

最後,我們對到目前為止的結果非常滿意,我們當然不會錯過MongoDB。 性能非常好,與之相比,圍繞它的工具使其他數據庫顯得蒼白,與MongoDB相比(尤其是對於非開發人員而言),查詢數據要輕鬆得多。 儘管確實有一個服務(Olery Feedback)仍在使用MongoDB(儘管是一個單獨的相當小的集群),但我們打算將來也將其遷移到PostgreSQL。


原文:https://developer.olery.com/blog/goodbye-mongodb-hello-postgresql/

本文:http://jiagoushi.pro/goodbye-mongodb-hello-postgresql

討論:請加入知識星球或者微信圈子【首席架構師圈】


分享到:


相關文章: