再見MongoDB,你好PostgreSQL

jopen 10年前發布 | 13K 次閱讀 PostgreSQL

大約在5年前Olery創立了,那時的Olery只有Reputation一個產品,同時還是由一個Ruby開發代理機構開發的,經過了幾年的發展,現在的Olery除了Reputation產品之外還發布了很多其他的產品,包括:Olery Feedback、Hotel Review Data API以及一些能夠被嵌入到網頁內的部件(widget),在不久的將來,Olery將會提供更多的產品和服務。之所以能夠取得如此巨大的成功,主數據庫的作用可謂功不可沒,那么Olery的數據庫架構是如何演進的呢?最近Yorick Peterse在Olery的開發者網站上發表了一篇題為《再見MongoDB,你好PostgreSQL》的文章對此進行了介紹,本文根據此文翻譯整理而來,查看英文原文請點擊這里

最初,Olery的數據庫體系包含兩部分:使用MySQL存儲關鍵數據(用戶、通訊錄等);使用MongoDB存儲評論以及其他與之相似的數據(丟失之后能夠很容易找回的數據)。這種配置在剛開始的時候還能滿足業務需要,但是隨著公司的發展,問題就開始出現了,特別是對MongoDB的使用,這其中有一些問題源于應用程序與數據庫的交互方式,而另一些則源于數據庫本身。

例如,在某個時間點Olery必須從MongoDB中移除大約一百萬文檔然后稍后再重新插入它們。這種處理方式造成的后果就是:數據庫幾乎會被完全鎖定幾個小時,性能非常低,除非使用MongoDB的repairDatabase命令執行數據庫修復,但是由于數據庫比較大,該修復命令也需要耗費數小時的時間。另一個例子是Olery注意到應用程序糟糕的性能源于MongoDB集群,但是卻無法進一步找到問題的真實原因,無論配置什么指標、使用什么工具或者運行什么命令都找不到原因,直到Olery替換了集群的主節點才讓性能恢復正常。 無模式的問題

Olery面對的另一個核心問題是MongoDB是無模式的。無模式聽起來可能比較有趣,同時在某些情況下這樣做也有一定的好處,但是大部分情況下使用無模式的存儲引擎會引發隱式模式的問題。這些模式并不是由存儲引擎定義的,而是基于應用程序的行為和期望定義的。

例如,你可能有一個頁面集合,應用程序期望從中獲取一個字符串類型的title,這種情況下模式雖然沒有明確定義,但也清晰可見。但是如果數據結構隨著時間發生了變化,這種方式就會出現問題,特別是當舊數據沒有遷移到新數據結構上的時候。例如,假設有下面的Ruby代碼:

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

這段代碼適合所有包含title域的文檔,但是如果文檔使用了不同的域名稱(例如post_title),或者沒有類似的title域,那么這樣寫就會出現問題。為了處理這種情況,必須將代碼調整為下面這樣:

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

...

end</pre>

處理該問題的另一種方式就是在模型中定義一個模式,例如使用Mongoid。這樣做還解決了另一個問題:可重用性。如果你只有一個應用程序,那么在代碼中定義模式并不是大事,但是如果你有數十個應用程序,那么這樣做很快就會成為一個大麻煩。

無模式存儲引擎的期望是通過移除模式讓用戶使用起來更容易。實際上,用戶需要自己確保數據的一致性。在某些情況下這種方式可能比較好,但是大部分情況下這可能是一個弊端。

一個優秀數據庫的要求

針對以上問題以及自身的業務需要,Olery認為應該從以下4個方面衡量一個數據庫:

  1. 一致性
  2. 數據以及系統行為的可見性
  3. 正確性和清晰度(explicitness)
  4. 可擴展性
  5. </ol>

    一致性是非常重要的,因為它能夠幫助一個系統建立明確的期望。如果數據始終按照某種方式存儲,那么使用這些數據的系統就會變得非常簡單。如果某個域在數據庫層面是必須的,那么應用程序就不需要檢查該域的存在性。同時即使壓力非常大,數據庫也應該能夠保證某些操作可以完成。

    可見性包括兩個方面:系統本身以及從系統中獲取數據的簡單程度。當系統出現問題的時候用戶可以很容易地調試。另外,用戶也可以很容易地檢索到所需的數據。

    正確性指系統的行為要符合期望。如果某個域被定義為數字類型,那么任何人都不能插入文本。眾所周知,MySQL在這一方面做的并不好,它沒有阻止用戶這么做,以致于數據中可能包含錯誤的內容。

    可擴展性不僅指性能,還包括財務成本以及隨著時間的推移系統是否能夠很好地處理變化的需求。

    遠離MongoDB

    基于以上標準,Olery開始尋找MongoDB的替代品,因為這些標準通常又是傳統RDBMS的核心特性,所以Olery將目光移向了MySQL和PostgreSQL。

    MySQL是第一個候選產品,之前Olery已經使用它存儲了少量的關鍵數據。但是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)</pre>

    雖然出現這種情況的時候MySQL會發出一個警告,但是這一信息通常會被忽略。

    MySQL的另一個問題就是所有的表修改(例如增加列)操作都會導致鎖表,無論是讀還是寫。這意味著在修改完成之前對該表的所有操作都必須等待。如果表的數據量非常大,修改操作可能需要數小時才能完成,這可能導致應用程序停止服務。而這也是導致SoundCloud等公司開發lhm這種工具的原因。

    鑒于以上原因,Olery開始考察PostgreSQL,與MySQL相比PostgreSQL能把很多事情做得更好。例如,用戶無法將文本值插入到數字類型的域中:

     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');</pre>

    PostgreSQL能夠以多種方式對表進行改變,不是每一個操作都需要鎖表。例如,添加一個沒有默認值同時可以設置為NULL的列能夠很快地完成,不需要鎖定整個表。

    PostgreSQL還支持很多其他的特性,例如:基于三元模型(trigram)的索引和搜索、全文檢索、支持查詢JSON、支持查詢/存儲鍵值對,支持pub/sub等。

    最重要的是PostgreSQL在性能、可靠性、正確性和一致性方面做了很好的平衡。

    使用PostgreSQL

    將整個平臺從MongoDB遷移到一個完全不同的數據庫上并不容易,為此,Olery將整個過程分為了三步:

    1. 創建一個PostgreSQL數據庫,將一小部分數據遷移過去
    2. 更新所有依賴于MongoDB的應用程序,使用PostgreSQL替代,并完成所有必須的重構工作
    3. 將生產數據遷移到新數據庫并部署新平臺
    4. </ol>

      遷移子集

      雖然有一些工具能夠處理這項工作,但是依然需要對某些數據進行轉換(例如重命名的域、類型的差異),此時需要編寫自己的工具。這些工具大部分是一次性的Ruby腳本,每個腳本執行特定的任務,例如:挪動評論、清除編碼、糾正主鍵序列等。

      盡管在遷移的過程中有一部分數據存在問題——例如某些用戶可能提交了錯誤編碼的內容,導致在被清除之前無法被導入;評論的語言名稱需要從全名(“dutch”、“english”等)修改為語言代碼以適應新的情感分析棧的需要——但是該階段并沒有遇到任何阻礙遷移的麻煩。

      更新應用程序

      更新應用程序占用了大部分時間,特別是那些嚴重依賴于MongoDB聚合框架的程序。更新的過程分為下面幾步:

      1. 使用PostgreSQL相關的代碼替換MongoDB驅動/模型設置代碼
      2. 運行測試
      3. 修復部分測試
      4. 再次運行測試,修改并重復直到所有的測試都通過
      5. </ol>

        非Rails應用程序固定使用Sequel,Rails應用程序則擺脫不了ActiveRecord。Sequel是一個非常棒的數據庫工具,它支持Olery可能會用到的大部分PostgreSQL特有的特性,雖然有時有一點繁瑣,但是它的查詢構建DSL遠比ActiveRecord要強大。

        例如,如果想計算有多少位用戶在某個區域以及每個區域的百分比,那么普通的SQL可能會是這樣:

         SELECT locale,
        count() AS amount,
        (count() / sum(count()) OVER ())  100.0 AS percentage

        FROM users

        GROUP BY locale ORDER BY percentage DESC;</pre>

        在本文的例子中該SQL會產生下面的輸出:

          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通常會需要):

         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))</pre>

        如果不喜歡使用Sequel.lit(''),還可以使用下面的語法: </p>

         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))

        雖然這兩個查詢可能有一點繁瑣,但是卻讓我們能夠更容易地部分重用,不需要使用字符串連接。

        遷移生產數據

        遷移生產數據基本上有兩種方式:

        1. 關閉整個平臺,待所有的數據都遷移完成之后再一次性上線
        2. 遷移數據的同時保持服務繼續運行
        3. </ol>

          第一種方法有一個明顯的弊端:要停止服務。第二種方式雖然不需要停止服務但是非常難處理,例如,在遷移數據的同時必須考慮正在添加的所有數據,否則就會丟失數據。

          幸運的是Olery對數據庫的大部分寫操作時間間隔都非常規律,確實會頻繁變化的數據(用戶、通訊錄等)只占一小部分,這意味著遷移它們所需的時間要比遷移評論少的多。

          該部分的基本流程是:

          1. 遷移關鍵數據(基本上包括所有絕對不能丟失的數據),例如用戶、通訊錄
          2. 遷移不太重要的數據(可以重新獲取或計算的數據)
          3. 測試所有的事情是否都已完成并運行在一組單獨的服務器上
          4. 切換生產環境到這些新服務器上
          5. 重新遷移第一步的數據,確保沒有丟失這期間創建的數據
          6. </ol>

            第2步花費的時間最長,差不多需要24小時,第1步和第5步中提到的數據遷移僅需要大約45分鐘。

            結論

            Olery大約在一個月之前就完成了遷移,目前來看除了一些積極的影響之外沒有其他副作用,某些場景下對性能的提升甚至非常顯著。例如Hotel Review Data API(運行在Sinatra上)的響應時間明顯縮短了:

            遷移發生在1月21號,大峰值是因為應用程序執行了重啟,導致其響應時間非常長,但是在21號之后平均響應時間幾乎降低了一半。

            另外,“評論持久化”部分的性能提升也非常顯著。該應用程序的責任非常簡單:保存評論數據。雖然Olery最終對該應用程序作了非常大的改變以便于完成遷移,但是這些改變非常值得:

            Scraper也變得更快了:

            雖然提升沒有評論持久化部分那么明顯,但是考慮到Scraper只使用一個數據庫檢查某條評論是否存在,這種提升也非常令人興奮。

            最后是Scraper的調度程序Scheduler:

            因為Scheduler會按照一定的時間間隔執行,所以這幅圖理解起來有點難,但是非常明顯的是在遷移之后平均處理時間降低了。

            到目前為止Olery對于這次遷移非常滿意,性能非常好,與之相關的工具也優于其他數據庫,查詢體驗也比MongoDB要好得多。雖然Olery依然有一個服務(Olery Feedback)在使用MongoDB,但是相信不久之后便會遷移到PostgreSQL上。

            來自:http://www.infoq.com/cn/articles/byebye-mongodb-hello-postgresql

             本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
             轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
             本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!