本博客在http://doc001.com/同步更新。
本文主要內容翻譯自MySQL開發者Ulf Wendel在PHP Submmit 2013上所做的報告「Scaling database to million of nodes」。翻譯過程中沒有全盤照搬原PPT,按照自己的理解進行了部分改寫。水平有限,如有錯誤和疏漏,歡迎指正。
本文是系列的第一篇,本系列所有文章如下:
- 百萬節點數據庫擴展之道(1): 傳統關系型數據庫
前言
今天的數據庫世界讓人倍感迷惑。回想十年前進行Web開發,可選擇的數據庫還極為有限。然而現在,除了傳統的數據庫外,有150+的NoSQL數據庫供君選擇。這期間究竟發生了什么翻天覆地的變化,使得單節點數據庫逐步演變為數百萬節點的全球數據庫?為了解答這個疑惑,本文將引導讀者回顧這些年數據庫的那些事兒。
在正文之前,讀者應當明白,與傳統開發相比,在海量數據庫系統上進行應用開發存在一定區別,而開發海量數據庫本身則有天壤之別。
數據庫的出現
1960年代——黑暗歲月
有誰曾經接觸過大型機的日常數據交換?2000年以后呢?
這個時候的應用數據被存儲在磁帶上,數據庫還未被發明出來。公司在生產環境使用的每一個應用都有自己的數據存儲方式。開發者使用十六進制編譯器來解讀數據,制作報告時需要花費了很多額外的時間從多個應用抽取數據。從這個安全問題頻出的年代看,這是怎樣天真美好的歲月,應用安全到只有知悉所有實現細節才能保持運行的地步,就算你拿到數據也白搭。(Zz..囧)
1970年代——神跡初顯
這個時代的數據存儲的目標包括:
- 數據的內在視圖(存儲細節)和外在視圖(外在表現)分離
- 中心式存儲
- 數據一致性,高效的數據訪問
- 多用戶支持,訪問控制
(關系型)數據庫終于被發明了出來,成為解決一個公司內部不同應用的數據共享問題的有效手段。
一個數據庫系統必須確保數據一致性,并提供諸如訪問控制之類的手段保證多應用、多用戶環境下的數據安全。當然,數據的存儲效率也極為關鍵。
使用數據庫帶來的一個好處就是,用戶看到的數據外在視圖和內在視圖是完全隔離的。用戶根本不需要關心數據的存儲細節。不管內部的存儲方式如何,數據庫使用一致的SQL語言提供數據。
基本數據概念
什么是數據?
我們知道數據一致性是一個數據庫系統的核心問題,然而,究竟什么是數據?
顯然,所有的數據都會有一個類型和一個值域。例如,一個字符串是一組字母的序列,一個數卻只包含數字,它們的類型就是不一樣的。
操作符(operator)與數據結構(data structure)
數據可以分為標量(scalar)數據類型和非標量(non-scalar)數據類型兩類。一個標量數據類型只保存唯一的數據項,字符串、整數就是典型的標量數據類型。與之相反,一個非標量數據類型是包含多個數據項的類型,類就是典型的非標量數據類型。
操作符用于操作數據類型,每一個數據類型都有一組適用的操作符集合。例如,類的構造器(constructor)是用于初始化類成員的操作符。
數據結構規定了數據的組織、存儲方式。一些數據庫系統允許自定義數據結構。一個對象(object)由一個數據結構和定義在其上的一組操作符組成,而POD(plain old data)數據結構就只包含數據結構。
狀態(state)
在一個程序中,數據是動態的。數據的狀態隨時間發生變化,修改操作會導致數據狀態變化,通常這些狀態遵循一定的規則。
數據庫的數據模型
數據庫的一個數據模型定義了數據庫的數據結構、數據的存儲方式,和全局操作符。數據庫會長期運行,全局操作符實際上規定了可能發生的狀態變化。
數據庫內部使用數據庫模式(schema)描述一個特定數據模型。很少有數據庫以無模式的方式存儲數據,當然,很多的NoSQL系統的模式非常靈活。
常見數據模型分為四種:
- 關系模型(Relational data model)
- 文檔模型(Document model)
- 鍵值模型(Key-Value model)
- 寬列模型(Wide columnar model)
后三種都屬于NoSQL的范疇。接下來將簡要介紹這四種模型。
關系型數據庫
盡管關系模型的缺點被一些NoSQL所改進,但是除了這些缺點,我們不應該忘記關系模型的優點。至少到現在,關系模型仍然占有統治地位。
NoSQL的革新本質在于數據模型本身,而其它的改進多流于表面,完全可以被關系型數據庫借鑒。例如,關系型數據庫也可以實現HTTP接口;關系型數據庫也可以提供更低層次的訪問接口來繞開SQL;關系型數據庫也可以簡化管理體系;等等。
模式設計
模式設計是關系型數據庫應用開發的第一步,包含3個步驟:
- 提煉出需要存儲的信息。
- 創建實體-關系(E-R)模型:
- 實體:主題、事情、物體
- 屬性:對實體的描述、信息項、規則
- 候選鍵:能夠唯一標識一個實體的屬性集合
- 關系:實體間的關聯,1:1、1:n、n:m
- 將E-R模型轉化為物理數據模型:
- 網絡模型、關系模型、分層模型
- 關系模型:表、屬性、主鍵
- 關系模型:應用數據庫規范化法則
數據庫規范化(database normalization)
數據庫規范化的目的是降低數據表的冗余和依賴程度。數據庫規范化有很多范式,其中第一范式(first normal form, 1NF)規定:
- 一個關系的所有屬性都是不可再分的原子數據項
- 每一個屬性只包含唯一的值
該范式禁止創建嵌套的表結構,例如下圖中,在一張博客發表表內嵌套一個博客評論列表。嵌套的表結構在NoSQL中比較常見。
為了不破壞1NF,同時滿足一些必要的嵌套需求,SQL:99和SQL:2003引入了非原子數據結構。SQL:99增加了ROW和ARRAY,SQL:2003增加了MULTISET。遺憾的是,很多關系型數據庫都沒有實現這些數據類型。進一步說,如果這些數據類型得到了實現,關系型數據庫連接(join)操作將會變得非常高效,鍵值數據庫和文檔數據庫在這方面的優勢也就不那么明顯了。
查詢
關系型數據庫的查詢通過SQL語言進行,其理論基礎是關系代數。
ACID事務
關系型數據庫的事務滿足ACID:
- 原子性(atomicity)
- 一個事務的操作要么全做,要么全不做
- 在各種失效情況下也予以保證:掉電、崩潰...
- 一致性(consistency)
- 事務只能導致數據庫從一個有效狀態轉變到另外一個有效狀態
- 已定義的規則不會被違反:約束、觸發器...
- 隔離性(isolation)
- 并行執行的事務與串行執行的效果等效,不會互相干擾
- 持久性(durability)
- 一旦事務提交,就不可撤銷
- 在各種失效情況下也予以保證:掉電、崩潰...
ACID反映了數據庫管理系統(database management system,DBMS)設計和開發的目標。DBMS不僅僅保證數據被正確組織(數據模型,模式),保證數據被輕松訪問(關系代數、SQL),也需要保證多用戶環境下的數據安全。
在RDMS事務中,用戶的工作要么全做,要么都不做,不存在中間狀態。事務不會破壞任何已定義的規則,在完成時保證數據庫仍然處于一個已定義的一致狀態。事務在被提交前,不會被其它并發的事務妨礙。事務一旦提交,其結果永遠不會丟失。
并發控制
假設兩個事務同時想修改同一個數據項,需要保證它們的修改不會相互沖突。這個工作由并發控制(concurrency control)算法來完成。
并發控制算法的分類如下圖所示:
這張圖是從原PPT翻譯得到的,對該分類有疑問的請參考其它文獻。
并發控制算法可以分為悲觀算法和樂觀算法兩大類。悲觀算法在事務開始前就檢查沖突數據,提前鎖定,使事務訪問順序化。樂觀算法將沖突檢查推遲到最后進行,如果沖突,則回滾事務。
隔離級別
ANSI/ISO SQL定義了若干隔離級別,隔離級別會影響并發控制算法的效率:
- 可序列化(serializable)
- 最高級別的隔離,在事務期間對沖突數據的讀寫保持范圍鎖(range lock),即沖突事務順序進行
- 可重復讀(repeatable read)
- 沒有范圍鎖,可能存在「幻影讀(phantom read)」現象
- 授權讀(read committed)
- 可能發生「不可重復讀(non-repeatable read)」
- 未授權讀(read uncommitted)
- 允許「臟讀(dirty read)」
幻象讀:一個事務中,兩個完全相同的查詢語句執行得到不同的「結果集」。在下圖的例子中,事務1的第2次查詢語句讀到了事務2新提交的數據。
幻象讀
不可重復讀:在一次事務中,「一行數據」獲取兩遍得到不同的結果。在下圖的例子中,事務2提交成功,因此它對id為1的行的修改就對其它事務可見了,與事務1之前已經從這行讀到了另外一個「age」的值不同。
不可重復讀
臟讀:當一個事務允許讀取另外一個事務修改但未提交的數據時,就可能發生臟讀。在下圖的例子中,事務2修改了一行,但是沒有提交,事務1讀了這個沒有提交的數據。現在如果事務2回滾了剛才的修改或者做了另外的修改的話,事務1中查到的數據就是不正確的了
臟讀
物理層面
關系型數據庫將記錄存儲在「頁(page)」中,每一個頁是4~32KB大小的連續內存區域。一個頁可以包含一個或多個記錄。如果單個頁不能存儲下一個記錄的全部數據,那么將使用額外的溢出頁(overflow page)。為了優化訪問效率,關系型數據庫使用B-tree或其衍生數據結構將頁按序存儲在磁盤上。如果數據的實際存儲順序與一個索引的順序一致,那么這個索引是一個聚集索引(clustered index)。例如,InnoDB就使用聚集索引按照主鍵來組織數據表。聚集索引有助于獲得更高的順序搜索性能。
連接(join)
考慮一個數據表連接操作r?s(r、s分別是兩個數據表),常見的執行策略包括:
- 嵌套循環(nested loop)
- 通過兩層嵌套循環完成,首先掃描表r,每讀到一條記錄,就去掃描表s以查找符合要求的記錄
- 算法復雜度O(nr*ns),其中nr和ns分別是表r和表s中的記錄數量
- 對索引和連接條件無任何要求
- 塊嵌套循環(block nested loop)
- 嵌套循環的一個變種,以塊為單位而不是以記錄為單位處理關系
- 相比嵌套循環,能夠減少從硬盤傳輸數據的次數,因此,效率有所改進
- 索引嵌套循環(indexed nested loop)
- 如果內層嵌套的被連接表的連接屬性上有索引,則可以利用索引來優化符合要求記錄的查找
- 歸并連接(merge join,又稱排序-歸并-連接,sort-merge join)
- 對連接的兩個表按公共屬性排序,利用歸并排序算法尋找公共屬性相同的符合條件的記錄
- 可用于計算自然連接和等值連接
- 散列連接(hash join)
- 將連接的兩個表按照公共屬性使用相同的hash函數將記錄映射到同一空間,再對相同hash值的記錄做匹配
- 實際的實現只對一個較小的表建hash查找表,另外一個表直接匹配記錄
- 可用于計算自然連接和等值連接
原PPT只提及了nested loop和hash join,這里根據其它材料進行了補充。
關系型數據庫架構擴展
盡可能地緩存
緩存是提高數據庫性能的一個有效手段。
數據庫保存的數據有狀態的(stateful),且為硬狀態(hard-state),保證數據一致性(consistent);緩存保存的數據無狀態(stateless),且為軟狀態(soft-state),不保證數據一致性(inconsisteng)。
一個典型的緩存系統如下圖所示:
軟狀態維持一段有限的時間,在過期前需要重新刷新,否則自動失效。軟狀態可能比數據庫中的狀態滯后。相反,硬狀態一直存在,且一定正確。
緩存的無狀態特征允許緩存系統簡單地通過增加/減少資源來調整規模。
接下來的描述都是以MySQL為例展開的。
MySQL內置緩存
除了外部的緩存系統,MySQL本身也進行了兩項重要的改進:
- 內置了查詢緩存,該緩存的數據狀態與數據庫是一致的。
- 通過InnoDB提供了Memcache協議的底層記錄訪問接口,訪問速度比外部的Memcache更快,簡化了架構。
MySQL主從
MySQL對可擴展性的答案是主從復制模式。在該模式中,所有的客戶端寫請求由唯一的master進行處理。master將操作記錄到二進制日志中,該日志被異步發送給slave們,slave們重放操作,完成數據的更新。slave可以處理客戶端的讀操作,前提是,應用可以容忍極短時間的數據滯后。該模式應該和緩存結合使用。
在一個讀操作為主的環境(如Web應用)中,該模式具備極佳的橫向擴展能力,增加新slave的代價可以忽略不計。
在一個寫操作為主的環境中,該模式很難橫向擴展:
- 只有一個master處理所有的寫操作,很容易單節點故障
- 很難讀寫分離,即使使用PECL/mysqlnd_ms效果也不是很好
- MySQL要努力啊...
更多MySQL演示資料參見slideshare。
架構擴展的目標
每一個MySQL工程師都應該知道架構擴展的目標有這些:
- 可用性(availability)
- 節點失效不會對集群造成影響
- 可伸縮性
- 地理分布
- 能夠根據用戶和數據的規模伸縮
- 均衡讀/寫負載
- 分區透明
- 一切內部細節對客戶端都是透明的
MySQL架構擴展方案分類
根據欲達到的目標,可對現有的MySQL解決方案進行歸類。在這里,按照「事務執行的位置」和「節點同步發生的時間」將解決方案分為四類:
未完待續...
下一部分將介紹NoSQL理論和Amazon Dynamo,參見: