為什麼我喜歡資料庫?沒那麼複雜和嚇人

伍翀發表於2015-01-29

去年我在 Square 的工作中接觸到了很多種資料庫。包括:

  • 發現和解決資料庫效能問題。
  • 為新應用設計資料模型和分片策略。
  • 評估和推行新的資料庫。

起初是為需求所迫,但我很快就對資料庫著迷了。資料庫的交叉研究幾乎橫貫了計算科學的每個領域——它的理論和實現都非常複雜,而且富有挑戰性。 然而,我很快意識到這並非所有的人都像我一樣熱衷於資料庫。對於我的很多同事和朋友而言,資料庫是一個具有魔力的黑盒子系統,太嚇人太複雜了以至於不能理解。我想要改變一下這個現狀。 當談論到資料庫時,分散式系統的話題是不能忽略的。大部分現代資料庫都是分散式的,要麼是隱式的(分散式叢集資料庫),要麼是顯式的(通過應用程式級的分片連線到多個資料庫的單個應用程式)。 這篇文章是我喜歡資料庫和分散式系統的告白。它主要針對向我一樣的程式設計師,經常接觸資料庫的應用開發者。我們主要用 Java、Python、或是 Ruby 編碼,用來寫服務端的應用。本文會覆蓋到以下話題:

  • 比較和評估不同的資料庫。
  • 如何理解和充分利用你的資料庫。
  • 從大的層面上去理解資料庫是如何工作的。

首先,什麼是資料庫

在這篇文章中,任何接收並儲存資料以備將來獲取之用的軟體就是資料庫。這包括了傳統的 RDBMS 和 NoSQL 資料庫,以及如 Apache Zookeeper 和 Kafka 一樣的系統。

CAP 理論

CAP 理論。這是繼圖靈的停機問題和 P≠NP (技術上不可解)後,我最喜歡的不可解問題。CAP 理論表明,任何分散式系統最多隻能同時滿足 CP(一致性 & 分割槽容忍性), AP (可用性 & 分割槽容忍性),或者介於這兩者之間。因此,一致性和可用性之間有很有趣的權衡出現。 關於CAP定理幾個重要的誤解:

  • 傳統的“三選二”爭論是沒有意義的。你不能拋棄分割槽容忍性,因為那意味著“在分割槽中執行的操作行為是不確定的”,而且在這種情況下,資料庫並不是真正一致性的。
  • 到達 CAP 理論的限制並沒有預設給定。有很多的資料庫,它們既不是一致的,可用的,也不是分割槽容忍的。要實現 CAP 理論的限制需要精心的設計和實現。

分散式系統

如前所述,許多現代資料庫都以某種方式實現了分散式。推動資料庫的分散式化的兩個因素:

  • 為了規模上超越單機 —— 在多節點上儲存和處理資料。
  • 為了增加可用性 —— 確保資料庫不會發生單點故障。

這兩個目標是相互緊密關聯的。一般來說,通過增加機器數量來擴充套件系統會對可用性產生負面影響,因為發生單個機器故障的機會增加了。所以,實現高可用性幾乎是可擴充套件性的先決條件。

正確性和效率

正確性和效率兩者都重要,而且在分散式資料庫中兩者也是緊密關聯的。 在任何軟體中正確性都是重要的,但是對於資料庫來說它是必不可少的。因為(1)資料庫儲存資料,錯誤的資料在重啟後仍然存在。(2)資料庫被認為是軟體棧(software stack)中最值得信賴的基礎。 資料庫是正確的是什麼意思呢?許多分散式資料庫的程式碼很難懂,有驚人一致的語義。在這裡你可以做一個權衡。在一般情況下,對於效率和可用性的成本來說,更嚴格的一致性使得編寫應用程式更容易。 除了理論之外,還有實現和業務挑戰的正確性。分散式系統本質上是複雜的。像 Paxos 這種演算法是很難理解和正確實現的。隨著系統越來越複雜,更多隱蔽的故障場景會出現。像 Redis 和 ElasticSearch 就承受了由他們分散式系統的非常規設計帶來的考驗。 除了上述的權衡,效率也是很重要的,因為以前遇到的困難。我學到越多的底層編碼,我就更瞭解一臺機器美秒可以執行多少原始操作(raw operations)。在許多情況下,效率降低了複雜度,使整個系統更簡單。事實是分散式系統比底層編碼更激發我去追求效率。在給定相同負載的情況下,我更樂意選擇需要更少機器的資料庫。 最後,用於機器間協調的計算和延遲開銷會是很顯著的。總之:執行的部件越少越好。 為了從程式碼中優化出更多的效能和效率,深入更底層的抽象中是必須的旅程,包括:

  • 記憶體分配 和 垃圾收集器 [2]
  • 檔案系統排程 和 IO 裝置特性
  • 核心設定
  • 各種系統呼叫的實現細節(fork,execv,malloc)

它們中產生任何的不協調,都會導致效能不佳。你不必成為一個核心黑客(kernel hacker),但是你需要對這些元件之間的互動有一定高度的瞭解。

給應用授權

由於不同的程式語言都有自己的優缺點,資料庫也有自己獨特的特點。完全理解它們是很重要的。它可以讓你實現高效和複雜的應用程式,同時委託大部分複雜易錯的工作給資料庫。 一般來說,如果你沒有嚴格的效能或可用性要求,那麼傳統 RDBMS 是一個好選擇。ACID 的保證是非常強大的,而且工具也很不錯。分片 RDBMS 雖然痛苦,但它是很好理解的。MySQL 和 Postgres 是兩個普遍的選擇。 全文搜尋引擎允許你構建高階的索引和搜尋功能。整合這些系統後的最終一致性,很少是個問題,因為搜尋本身就是一個模糊操作。Lucene 和它的變種資料庫(Solr,ElasticSearch)是普遍的選擇。 訊息佇列和事件處理系統消除了那些很難正確和高效實現的程式碼。Kafka、Storm、Spark SQL、RabbitMQ 和 Redis 是普遍的選擇。 具有跨域複製的資料庫使得區域故障切換和高可用性容易地多。在這方面沒有很多開源選擇,但是 Cassandra 可能是最成熟的一個。 一致性,leader 選舉(leader election),以及分散式鎖都是很難實現和測試的。不要自己去實現。用 Zookeeper 等,或者 raft 類庫。 現在走向細節。資料庫本身是一個抽象洩露(leaky abstraction)。他們一般在隱藏底層的複雜性上做了很好的工作,但是忽視其侷限性最終會傷到你自己。以下是一些重要的東西:

  • 理解資料庫的正確性保證。一個失敗的操作是指什麼[3]? 哪些操作是完全一致的,哪些又不是?
  • 理解資料是如何被儲存和獲取的。哪些操作是有效的,哪些又是無效的?有沒有一個查詢規劃(query planner),或者每個操作的詳細統計資訊?
  • 分片和叢集架構。理解資料是如何在叢集中分佈的。你的分片策略均衡地分佈資料了嗎?或者有沒有熱點存在?
  • 資料建模模式和反模式。

業務挑戰

一旦你的軟體棧中的一部分,資料庫和你的基礎架構保證24*7提供不中斷服務 。它就引入了獨特的業務挑戰。 運算元據庫就像在海洋中航行。無論何時你遇到了問題,你都要不讓資料庫下沉的同時解決它,即使是在風暴中心。因此,資料庫需要有:

  • 內省(introspect)和監視系統的方法。
  • 維護和管理系統的把手。
  • 複製,備份和恢復。丟失資料是極其糟糕的。機器會在某個點奔潰。由於資料庫是有狀態的,你不能簡單地只部署程式碼。

在執行的同時具備以上所有。坦白說,所有資料庫在這點上都有缺點。在完全執行的同時允許任何配置都可以修改,是一個艱難的挑戰。許多操作需要一個資料庫層面的互斥量,額外的系統資源,或者重新啟動。例如包括:

  • 在 MySQL 5.6 以前,新增一列需要全表鎖,而黑客們喜歡 pt-online-schema-change 的存在就是為了緩解這一問題。MySQL 現在支援線上模式遷移。
  • Cassandra 允許你簡單地新增、刪除、和修復節點。然而,這些操作給系統新增了額外的負載,而且需要容量空間。

此外,你不能簡單的替換一個資料庫。即使是在同一資料庫內遷移資料的任務也不簡單。遷移到另一個資料庫中更是難上加難,如果不是不可行[4]。一個應用程式的程式碼是非常容易逐步鋪開並恢復(如果需要的話)的。資料比程式碼存活地更久。資料模式和儲存的資料通常會在多個應用程式之間共享。因此,初始的資料庫系統和對應的資料模型的選擇是非常重要的。 最後,資料庫總會發生故障。不管你用的是什麼平臺/服務即架構,有些故障是避免不了的:

  • 應用程式程式碼弄髒或丟失資料的 Bug 。
  • 誤解了資料庫的安全性和一致性保證,丟失了寫操作。
  • 資料模型和資料庫不匹配 —— 例如,有一個資料集並不適合在一個獨立 shard 上。期待原子切換(compare-and-swap)到最終一致性的資料庫中工作。
  • 執行故障 —— 機器崩潰。硬碟損耗。作業系統升級。
  • 網路分割槽[6]。
  • 驚群效應 —— 單個系統故障,並級聯到其他系統。
  • 而最糟糕的是 —— “突然慢下來了”。“隨機尖峰延遲”。“每天一次偶發錯誤”。“這條記錄應該存在但是沒有了”。

對於這些卻沒有單一的解決方法。運算元據庫的藝術真的屬於維護一個高 SLA 系統的藝術,但是如果我需要給出一些技巧:

  • 應用程式開發人員理解的限制和故障模式的行為。
  • 編寫一個有彈性的應用程式。多資料中心部署。自動故障轉移。
  • 擁有一支由熟悉業務並且瞭解不同故障場景和恢復方法的工程師(網站可靠性工程師,DBAs)所組成的隊伍。

PS:在 Square ,我們有一個超酷的線上資料儲存(ODS)團隊把這些問題從我們這裡抽離出去。

基礎構建模組

通過資料庫提供的抽象真的很神奇。資料提取(ingestion),查詢,複製,以及故障恢復都在同一個包裡?但是當你習慣了它,你就開始認識到有一些基礎的構建模組 —— 在所有資料庫之間共享的通用模式和元件。 首先,資料檢索降低了以下中的一個:

  • 鍵值查詢(雜湊表)
  • 範圍查詢(樹和 LSM 樹)
  • 檔案偏移量查詢(Kafka,HDFS)

在本文的最後,我想說的是,電腦並不懂 SQL、索引、聯接、或是其他的花哨的裝飾。上層的操作需要被翻譯成機器能執行的東西。 對於可持久化資料結構,B-trees, hash tables,和 LSM樹 (log-structured-merge-trees)都是很普遍的選擇。很可能你的資料儲存在其中的一個,除非它需要一些特定的查詢(例如:地理空間查詢)。LSM 樹是一種流行的現代的選擇,在BigTableHBase, Cassandra, LevelDB, 以及 RocksDB 中都有使用,因為其一流的寫入效能和合理的讀取效能。 最後,還有流行的模式和演算法用於整個不同的系統: Paxos, Raft一致性雜湊Quorum 讀/寫Merkel 樹, 以及 Vector Clocks 都是一些基本的構建模組。

總結

這篇文章是對一些話題的簡單、高度概括。還有很多話題我沒有涉及到,諸如對不同工作流的優化(OLAP、OLTP、批處理)和資料庫的UX(查詢語言、傳輸協議、客戶端類庫),這些都是同等重要的。不同語義一致性的影響,諸如sequential consistency, read your own write, at least once delivery 都是非常有趣的。 關於資料庫最棒的事情就是它是一個非常成熟的抽象。它大部分都在工作,而作為一名應用開發者,你可以不用思考就能非常容易地儲存和讀取資料。這絕對是值得慶祝的,但是為了這足夠先進的技術脫層皮也確實是值得的。 我希望有更多的人能被這個主題深深吸引,並充分利用它。

參考文獻和旁註

[1] 你不能犧牲分割槽容忍性 —— http://codahale.com/you-cant-sacrifice-partition-tolerance/。Aphy’rs Jepsend 的文章是一個很好的入門資源 —— http://aphyr.com/tags/Jepsen [2] 現代資料庫頻繁地利用OS檔案系統快取顯著地加速檔案系統的訪問。未使用的記憶體自動地被用作快取。這樣一個系統的推薦生產配置對於沒經驗的人來說是不尋常的,機器有90%的記憶體是空閒的。 [3] 在基於類似 dynamo 的 quorum 系統中的常見缺陷是寫故障時沒有給出任何資訊。當沒有按時寫入到內部副本時,寫操作可以在客戶端外部失敗。因此,失敗的寫操作是非常容易成功。最糟糕的是,在最後寫成功(last-write-wins)解決策略和傾斜的系統時鐘下,以後的寫入操作可以被這次的寫入故障覆蓋。 [4] 在資料庫之間遷移涉及到了在多個資料庫之間同時做讀寫操作。資料的所有者(俗稱“真相來源”)是不確定的,而在新舊系統之間做同步可能會有資料的丟失。 [5] 在初創公司的基礎架構中,應用程式的 bug 可能是可靠性和可用性最大的罪犯。擴充套件和效能問題是可以被預測到的,而當你有一把伺服器的時候,硬體故障通常不能被預見。然而,每天都會有新程式碼部署上線。 [6] 從網路是可靠的 —— 網路分割槽比你想象的還要常見。從根本上說,沒有辦法區分高延遲、網路分割槽、GC 停頓、和機器故障 —— 他們的表現都是低速連線。在 ElasticSearch 中這是一個普遍面臨的問題。一個節點正在遭受一個大 GC 停頓,而整個叢集認為這個節點掛了,然後試圖重新分配資料,一連串的問題出現了。

多資料庫

一旦一個系統與多個資料庫有互動了,系統就是最終一致性了。你不能在同一時刻併發地修改多個資料庫,除非你實現了兩階段提交協議(2PC)。這類似於“原子操作的成分是不是原子”。

關於刪除

在任何的分散式系統中,刪除資料是困難且危險的。資料到處都被複制,不管是資料庫還是應用程式。沒有一個適當的協調,被刪除的資料被恢復回來是不可能的。一種典型的處理方法是寫一個tombstones記錄來代表刪除,然而, tombstones 有他們自己的問題 :

  • Tombstones 佔據硬碟空間。為了回收硬碟空間,tombstones 必須到期。如果 tombstones 在完全複製前過期且被刪除了,已刪除的記錄可以被複制回來。
  • 你可以用一箇舊的 tombstone 來刪除一個將來的寫操作。這被通俗地稱為厄運石頭(doomstone)。搞笑的是,這是一個現實存在的問題。

相關文章