告別關聯式資料庫的舊時代
我們可能不小心錯過了什麼重要的資料庫創新?
今年的產業新聞:蘋果的 M1 使用 ARM 架構,Oracle 的伺服器也使用 Ampere 開發的 ARM 架構的 CPU 。Intel 無敵一時的 x86 受到挑戰。
這背後的故事是什麼呢?有一種 CPU 的設計叫 RISC ,它比現在主流的 CISC CPU 的設計來得更加輕量,因為它只做了關鍵且必要的事。也由於這個輕量的設計,讓它在耗電可以更省、CPU clock rate 上可以更快。整體來講,現在的時代,RISC 可以視為是比 CISC 更好的設計。[1]
為什麼呢?因為早期,組語是用人類的手工在寫的,所以 CPU 的指令集要設計地儘量高階,把複雜性放入 CPU 的抽象層裡,才能提高人類徒手開發組語的效率。但是,後來人類只寫高階語言了,組語都是被編譯器編譯出來的。在這種前提之下,RISC 就顯然是比較合理的設計,設計相對簡單的硬體才更容易做最佳化。
一言以蔽之,CISC CPU 可以視為是:「舊時代因為硬體效能差需要人類徒手寫組語,才顯得有道理的設計。」因為數十年前的硬體效能限制,而導致的特殊設計,其實不只是在 CPU 的領域出現,其實也同樣出現在應用軟體的核心 — — 關聯式資料庫。
舊時代的硬碟
在關聯式資料庫剛發展的年代,資料庫儲存資料的重要媒介 — — 硬碟,它的價格非常的高。也因此,舊時代關聯式資料庫的設計沒有例外,每一家廠商在儲存資料到硬碟上時,都把資料庫設計成 mutable database 。
什麼是 mutable database 呢?比方說,我在資料庫建立了一張 table ,然後,插入兩個 row ,並且修改了其中一個 row ,總共做了 3 個操作。過了兩天,我想取消最後一個操作。資料庫表示:「不好意思,我提供的 API 裡,並沒有回到過去、或是取消操作這一類的設計。」
相對於 mutable database 的設計則是 immutable database 。同樣的需求,如果是交給 immutable database 的話,它則是輕易地達成。 mutable database 的底層所儲存的資料,是過去到現在所有的操作所建構的結果,是當下的狀態。所以,資料庫的狀態,它只能前進,不能後退。另一方面,immutable database 的底層,儲存的資料,則是過去到現在所有的操作本身,換言之,它所記錄的,不只是某個時刻的狀態,而是一連串足以推導出任意時刻資料庫狀態的日誌 — — 事件溯源 (event sourcing) 的 immutable log。
這個 mutable database 的特性,在舊時代因為相對節省硬碟空間,是很有道理的設計。然而,在新時代,因為硬碟已經不再是最重要的限制因素了,我們應該來檢驗,為了這個設計,付出了什麼代價。
回溯歷史難
我開發軟體時,應用的關聯式資料庫是 Datomic 。Datmic 是 immutable database ,本身提供的功能就有「回溯歷史」。所以,如果我想要知道資料庫昨日某個時間點的狀態,只要把昨日的某個時間點傳入資料庫,資料庫就會立刻倒轉到那個時間點,讓我可以用一般正常的方式來做查詢 (Query)。
同樣的事情,如果我想要用一般 SQL-based 關聯式資料庫來做的話,就困難許多。在 SQL-based 關聯式資料庫,回溯歷史的這種需求一般來講,工程師有三種作法:
作法一:自行針對需要回溯歷史的資料表,建立對應的一張歷史記錄表。這種作法非常的耗人力。因為如果需要回溯的資料表有 n 張,往往就需要多建立 n 張的歷史記錄表。
作法二:應用 SQL 2011 版之後的 as-of
語法。不過,這邊講一個 2021 年的台灣軟體業實務,我認識的軟體工程師裡,如果不討論 fb backend 板板主這種例外,很多人並不知道 SQL 2011 版…。此外,支援 as-of
語法的資料庫,據我所知,目前有充分支援,只有少數幾家付費的資料庫。
作法三: 這個是台商最常用的,一行程式碼都不用寫。每天或是每小時去備分資料庫。如果需要回溯,就把之前備分下來的資料庫備分檔載入。缺點也很明顯:一方面超級耗硬碟空間、另一方面,記錄的時間跨度也超大,由於光是備分本身可能也需要不少時間,很有可能是一個小時才能備分一次。
修正錯誤更難
修正錯誤的問題與回溯歷史的問題,基本上是一體兩面,但是,這個問題卻又更嚴重。儘管回溯歷史是很好用的功能,但是,真的沒有,在簡單的應用情境,倒也還好。另一方面,回溯歷史某種程度來講,還比較可以用上述的作法一與作法三來當「替代方案」。在修正錯誤的例子裡,如果用作法三來處理的話,往往就會在修正錯誤的同時,也造成了資料庫的異常回溯。
這邊既然談到了修正錯誤,就很值得把 Lambda Architecture 一併納入討論。Lamda Architecture 是一種用來處理分散式系統、大數據收集的資料處理設計 Pattern 。在分散式資料處理的情境,由於 CAP theorem 存在的關系,資料庫往往只能做到 eventual consistency 。網路的延遲等因素,都很容易造成所謂的 network fault 。於是,許多的分散式資料庫的設計,都強調自身可以如何地對這些的網路分裂錯誤 (network partition fault) 做到容錯設計 (fault tolerance) 。然而在該文章裡,卻強調了一點:「比網路分裂錯誤的容錯設計更重要的設計,當然是針對人為錯誤的容錯設計。」
Lambda Architecture 的設計概念,自然也是一樣,應用了 immutable log 做為底層來做錯誤的修正 — — 不管是網路的錯誤或是人為的錯誤。
高速平行寫入超難
由於現行大多數的關聯式資料庫設計,是以 mutable database 為原則來設計的。當應用在 Data intensive application 的情境時,很容易在資料平行寫入撞到效能的門檻。什麼是 Data intensive application 的情境呢?比方說,有一億人要同時登入一個網站去買火車票之類的情境。
為什麼會有效能的門檻呢?當大量平行寫入發生時,要避免兩個同時發生的交易 (transaction) 的衝突 (collision) 發生,需要把一個交易應用在現在的系統狀態上,與另一個交易應用在系統狀態上,兩者加以比較,才能達成。要做到上述的事,就會產生所謂的鎖 (locking)。就是這些鎖,讓系統的效能被消耗掉。
在 mutable database 的時間觀念裡,系統永遠只有一個正確的時間,就是當下資料庫的時間。任兩筆交易如果發生的時間很近,就會需要協調。要協調的話,兩個交易要先一起走到那個時間點上,兩個交易一起先讀取資料庫,取得系統現在的狀態,再來探討,兩個交易寫入的狀態會不會有衝突。在上述的 mutable database 情境,鎖往往是在資料庫讀取時就開始加上,直到完成寫入才釋放。
而在 immutable database 的時間觀念裡,系統可以有一個正確的當下,跟無數的正確的過去時間,因為每一筆成功發生的交易,都等同於對系統刻下了一個正確的過去時間點。也因為上述的特性,這種資料庫可以相對容易地去設計所謂的「讀寫分離」。於是,任兩筆交易如果要協調,不需要先一起走到同一個時間點上,它們大可以在不同的歷史時間點上,各自先行讀取資料庫,取得系統某個時間的狀態,完成了它們認為合理的寫入資料之後,再來探討,兩個交易寫入的狀態會不會有衝突。在上述的 immutable database 情境,鎖往往是只有在資料庫要寫入的時刻才加上的,讀取的這段時間,則不用上鎖。
迷思與總結
本文探討到了一些概念:immutable log、事件溯源、讀寫分離。很多工程師會認為,這些東西是在大公司,對軟體開發有高標準時,才需要的技巧。或者是在 DDIA (Design data intensive application) 的情境時才值得做的投資。
我認為不是這樣子。回溯歷史、修正錯誤的需求,在任何規模的開發團隊都存在,尤其是修正錯誤。要做到這些也不需要超高的開發成本,只需要一位願意踏入新時代的工程師。
註1 : 有讀者認為我對於 CPU 的觀點有錯誤。而且也提供了一定的參考資料。嗯,請大家日後討論 CPU 時,不要以本篇做為重要的參考文。
2024/01 更新:
新文章已經轉換到 https://replware.substack.com 發表,歡迎讀者訂閱