數(shù)據(jù)庫怎么選擇?
本文轉載自微信公眾號「 武培軒」,作者Alex Petrov。轉載本文請聯(lián)系 武培軒公眾號。
所有數(shù)據(jù)庫管理系統(tǒng)的主要工作都是「可靠地存儲數(shù)據(jù)」并使其對用戶可用。我們使用數(shù)據(jù)庫作為數(shù)據(jù)的主要來源,幫助我們在應用程序的不同部分之間共享數(shù)據(jù)。我們使用數(shù)據(jù)庫,而不是在每次創(chuàng)建新應用程序時尋找存儲和檢索信息的方法,也不是每次都去發(fā)明一種組織數(shù)據(jù)的新方法。這樣一來,我們就可以專注于應用程序邏輯而不是基礎設施。
數(shù)據(jù)庫是模塊化的系統(tǒng),由多個部分組成:接受請求的傳輸層、決定以最高效方式運行查詢的查詢處理器、執(zhí)行操作的執(zhí)行引擎以及存儲引擎。
存儲引擎(或數(shù)據(jù)庫引擎)是數(shù)據(jù)庫的一個軟件組件,它負責在內存和磁盤上存儲、檢索和管理數(shù)據(jù),而設計它的目的是長久保存每個節(jié)點的數(shù)據(jù)[REED78]。數(shù)據(jù)庫可以響應復雜的查詢,存儲引擎則會更細粒度地看待數(shù)據(jù)并提供一組簡單的數(shù)據(jù)操作API,允許用戶創(chuàng)建、更新、刪除和檢索數(shù)據(jù)記錄。從某個角度來看,數(shù)據(jù)庫是構建在存儲引擎之上的應用程序,它提供了表結構(schema)、查詢語言、索引、事務和許多其他有用的特性。
為了獲得靈活性,鍵和值都可以是沒有預設格式的任意字節(jié)序列。它們的排序和表示語義是在更高級別的子系統(tǒng)中定義的。例如,你可以在一個表中使用int32(32位整數(shù))作為鍵,而在另一個表中使用ascii(ASCII字符串);從存儲引擎的角度來看,這兩個鍵都只是序列化的條目。
BerkeleyDB、LevelDB(及其后代RocksDB)、LMDB(及其后代libmdbx、Sophia和HaloDB)等存儲引擎的開發(fā)都與它們現(xiàn)在所嵌入的數(shù)據(jù)庫彼此獨立。使用可插拔的存儲引擎使數(shù)據(jù)庫開發(fā)人員能夠使用現(xiàn)有存儲引擎來構建數(shù)據(jù)庫系統(tǒng),并將精力集中在其他子系統(tǒng)上。
同時,數(shù)據(jù)庫系統(tǒng)組件之間清晰的解耦為切換不同引擎提供了機會,這些引擎可能分別適用于特定的用例。例如:流行的數(shù)據(jù)庫MySQL有幾個存儲引擎,包括InnoDB、MyISAM和RocksDB(在MyRocks發(fā)行版中),而MongoDB則允許在WiredTiger、內存以及(現(xiàn)已棄用的)MMAPv1存儲引擎之間進行切換。
數(shù)據(jù)庫的比較對數(shù)據(jù)庫系統(tǒng)的選擇將會產(chǎn)生長期的影響。如果選擇的數(shù)據(jù)庫不合適(因其導致性能問題、一致性問題或運維上的挑戰(zhàn)),那么我們最好在開發(fā)周期的早期就發(fā)現(xiàn)這一點,因為遷移到不同的系統(tǒng)可能并非易事,甚至在某些情況下,你還需要對應用程序的代碼進行重大的修改。
每個數(shù)據(jù)庫系統(tǒng)都有優(yōu)點和缺點。為了降低進行高成本遷移的風險,你可以在選擇數(shù)據(jù)庫上投入一些時間,以確保其具備滿足應用程序需求的能力。
試圖根據(jù)數(shù)據(jù)庫的組件(例如:使用的存儲引擎,數(shù)據(jù)共享、復制和分布的方式等)、它們的排名(ThoughtWorks等咨詢機構或諸如DB-Engines和Database of Databases等數(shù)據(jù)庫比較網(wǎng)站所呈現(xiàn)的流行度值)或實現(xiàn)語言(C、Java或Go等)來比較數(shù)據(jù)庫,可能導致無效和不成熟的結論,這些方法只能用于高層次上的比較,并且可能出現(xiàn)例如在 HBase 和 SQLite 之間進行選擇這樣粗糙的對比。因此,即使只是對每個數(shù)據(jù)庫的工作原理和內部結構有粗淺了解,這些了解也可以很好地幫助你得出一個更可靠的結論。
每一次比較都應該從清晰界定的目標開始,因為哪怕是最小的偏差都可能使整個調查完全無效。如果你正在找尋一個非常適合當下(或者未來)的工作負載的數(shù)據(jù)庫,那么你所能做的最好的事情就是在不同的數(shù)據(jù)庫系統(tǒng)上模擬這些工作負載、測量對你很重要的那些性能指標,并比較結果。有些問題(特別是性能和可伸縮性方面的問題)只有在一段時間后或隨著容量的增長才會開始顯現(xiàn)出來。為了發(fā)現(xiàn)潛在的問題,最好在盡可能接近真實生產(chǎn)環(huán)境的環(huán)境中進行長期的運行測試。
模擬現(xiàn)實世界中的工作負載不僅能幫助你了解數(shù)據(jù)庫的運行方式,還能幫助你學習如何操作與調試數(shù)據(jù)庫,并了解其社區(qū)的友好程度和能提供幫助的程度。數(shù)據(jù)庫的選擇總是這些因素的組合,而性能通常并不是最重要的方面:使用保存數(shù)據(jù)緩慢的數(shù)據(jù)庫通常比使用會快速丟失數(shù)據(jù)的數(shù)據(jù)庫要好得多。
要比較數(shù)據(jù)庫,非常詳細地理解用例并定義當前和預期的變量是有幫助的,例如:
表結構和記錄大小
客戶端數(shù)量
查詢類型和訪問模式
讀寫查詢速率
任何這些變量中的預期變化
明確這些變量可以幫助回答以下問題:
數(shù)據(jù)庫支持所需的查詢嗎?
數(shù)據(jù)庫能夠處理我們計劃存儲的數(shù)據(jù)量嗎?
單個節(jié)點可以處理的讀寫操作有多少?
一個系統(tǒng)計劃要有多少個節(jié)點?
鑒于預期的增長率,我們如何擴展集群?
維護過程是什么?
在回答了這些問題之后,你可以構建一個測試集群并模擬你的工作負載。大多數(shù)數(shù)據(jù)庫已經(jīng)有了壓測工具,可以用來重現(xiàn)特定的用例。如果沒有標準的壓測工具用來在數(shù)據(jù)庫生態(tài)系統(tǒng)中生成現(xiàn)實中的隨機工作負載,那么這可能是一個危險的信號。如果有什么東西讓你無法使用數(shù)據(jù)庫自帶的工具,那么你可以嘗試一個現(xiàn)有的通用工具,或者從零開始實現(xiàn)一個。
如果測試結果理想,那么進一步熟悉數(shù)據(jù)庫代碼可能會有更大的幫助。為了閱讀源代碼,首先要了解數(shù)據(jù)庫的各個部分、如何查找不同組件的源代碼,然后瀏覽這些組件。即使僅對數(shù)據(jù)庫代碼庫有一個粗略的了解,也有助于你更好地理解它產(chǎn)生的日志和配置參數(shù),并幫助你在使用數(shù)據(jù)庫的應用程序中,甚至在數(shù)據(jù)庫代碼本身發(fā)現(xiàn)問題。
有些人以為,可以將數(shù)據(jù)庫當作黑匣子而無須了解其中的內容是件好事。但實踐往往表明,這樣做遲早會碰到bug、服務中斷、性能倒退或其他問題。你最好為這些問題做好準備,如果你了解并且理解數(shù)據(jù)庫的內部結構,就可以減少業(yè)務風險且更有可能快速地恢復。
用于基準測試、性能評估和比較的一個流行工具是Yahoo! Cloud Serving Benchmark(YCSB)。YCSB提供了一個框架和一組可應用于不同數(shù)據(jù)存儲的公共工作負載集。就像任何通用的東西一樣,你應該小心使用這個工具,因為使用它很容易得出錯誤的結論。為了進行公平的比較并做出明智的決定,你需要投入足夠的時間來了解數(shù)據(jù)庫將在何種實際環(huán)境下運行,并相應地調整基準測試的內容。
TPC-C基準事務處理性能委員會(Transaction Processing Performance Council,TPC)提供了一組數(shù)據(jù)庫廠商用來比較和宣傳其產(chǎn)品性能的基準。TPC-C是一個聯(lián)機事務處理(OLTP)基準,它是只讀事務和更新事務的混合,用于模擬常見的應用程序工作負載。
該基準關注的是執(zhí)行的并發(fā)事務的性能和正確性。主要性能指標是吞吐量:數(shù)據(jù)庫系統(tǒng)每分鐘能夠處理的事務數(shù)。其需要執(zhí)行事務具備 ACID 屬性并符合基準本身定義的屬性集。
此基準不專注于任何特定的業(yè)務部門,但提供了對大多數(shù)適用 OLTP 數(shù)據(jù)庫的應用都很重要的抽象操作集。它包括幾個表和實體,如倉庫、庫存、客戶和訂單,并指定了表布局、可以對表執(zhí)行的事務的細節(jié)、表的最小行數(shù)和數(shù)據(jù)持久性約束。
這并不意味著基準測試只能用于比較數(shù)據(jù)庫?;鶞士捎糜诙x和測試服務級別協(xié)議注1的詳細信息、了解系統(tǒng)要求以及容量規(guī)劃等。你在使用數(shù)據(jù)庫之前對它了解得越多,在生產(chǎn)環(huán)境中運行數(shù)據(jù)庫時節(jié)省的時間就越多。
選擇數(shù)據(jù)庫是一個長期的決定,最好跟蹤新發(fā)布的版本,了解到底發(fā)生了什么變化及其原因,并制定升級策略。新版本通常包含對 bug 和安全問題的改進及修復,但也可能會引入新的 bug、性能退化或意外行為,因此在部署新版本之前測試新版本也是至關重要的。查看數(shù)據(jù)庫開發(fā)者以前是如何處理升級的,可能會讓你對將來的情況有一個很好的了解。過去的順利升級并不能保證未來的升級也會如此順利,但過去復雜的升級可能也是未來升級亦會不容易的標志。
理解權衡取舍作為用戶,我們可以看到數(shù)據(jù)庫在不同條件下的行為,但是在使用數(shù)據(jù)庫時,我們必須做出選擇來直接影響其行為。
設計一個存儲引擎肯定比僅實現(xiàn)一個教科書上的數(shù)據(jù)結構要復雜得多:很難在一開始就將許多細節(jié)和邊界情況處理正確。我們需要設計物理數(shù)據(jù)布局和組織指針、決定序列化格式、了解數(shù)據(jù)將如何被進行垃圾收集、存儲引擎如何適應整個數(shù)據(jù)庫系統(tǒng)的語義、探索如何使其在并發(fā)環(huán)境中工作,以及最后確保在任何情況下都不會丟失任何數(shù)據(jù)。
不僅有許多事情需要決定,而且這些決定中的大多數(shù)都涉及權衡取舍。例如,如果按數(shù)據(jù)插入數(shù)據(jù)庫的順序保存,我們就可以更快地存儲它們;但是如果按字典順序檢索它們,我們就必須在將結果返回給客戶端之前對它們進行重新排序。正如你將在本書中看到的那樣,存儲引擎設計有許多不同的方法,每個實現(xiàn)都有它自己的優(yōu)點和缺點。
在研究不同的存儲引擎時,我們會討論它們的優(yōu)點和缺點。如果對于每個可以想到的用例都有一個絕對最優(yōu)的存儲引擎,那么人人都一定會使用它。但是并不存在這樣的儲存引擎,因此我們需要根據(jù)服務的工作負載和用例進行明智的選擇。
市面上有許多存儲引擎,它們使用各種數(shù)據(jù)結構并且用不同的語言實現(xiàn)—從低級語言(如C)到高級語言(如Java)。所有存儲引擎都面臨相同的挑戰(zhàn)和限制。可以將其與城市規(guī)劃相類比:我們?yōu)樘囟ǖ娜丝跀?shù)量構建一座城市,并選擇是在高度上還是面積上對這座城市進行擴展。這兩種情況都可以將同樣數(shù)量的人放入該城市,但這些方法導致了截然不同的生活方式。當在高度上建設城市時,人們住在公寓里,人口密度可能導致面積較小地區(qū)的交通流量增加;而在一個面積更大且更分散的城市中,人們更有可能住在大房子里,但通勤則需要走更遠的路。
類似地,存儲引擎的開發(fā)人員所做的設計決策使它們更適合于不同的情況:有些對低讀寫延遲進行了優(yōu)化,有些則試圖最大化存儲密度(每個節(jié)點存儲的數(shù)據(jù)量),而有些則專注于運維上的簡單性。