image

自古以來,軟體工程師都在追求好維護,容易理解的軟體架構。傳統上,我們需要參與過各種大型軟體專案,從中獲取經驗,或是透過昂貴的課程,大量的論文,才能從前人的經驗中學到一些方法。

今天,我想試著透過 LOG 這個人人都碰過的資料結構,來解釋許多複雜系統的核心,只要你能理解 LOG,你就能設計出好理解、容易維護的系統架構。

什麼是 LOG?每個人第一次寫程式時,輸出的 “Hello World” 是一種 LOG,工作時使用 Slack 有 LOG,你的伺服器有 Access Log。LOG 就是由兩個特性組成的資料結構:

  • 訊息按照時序出現
  • 出現過的訊息不會改變

「出現過的訊息不會改變」這件事,也被叫做 Append Only。

Log 這樣的性質,常常在我們 Debug 的時候被拿出來用。很多時候,Debug 便是推論事件的因果關係,Log 的特性便能讓我們透過文字來理解系統內發生的事件的因果。

不只是 Debug,許多大型複雜架構中,都是透過 Log 來解決各種困難的問題。資料庫、分散式系統、版本控制、同步、備份、訊息傳遞、前端 UI… 都使用了 Log。

所以聽完這場,你就理解了軟體架構的真理(誤)

接下來,我會透過各種實際的例子,介紹在各領域中使用 Log 的範例。


首先我們來談談資料庫。

如果你沒聽過資料庫,這東西的用途很單純,當你需要存取大量資料,又要確保資料正確性的時候,就會用到資料庫。資料庫為了確保存入的資料的正確性,提供了相當多的工具,像是 Relation、Schema、Validation、ACID Transaction… 等等。

同時,為了能有效讀取大量資料,資料庫也會建索引(Index),也會說 SQL。

所以資料庫寫入的時候其實很忙,這時候就有個問題:如果寫入到一半當機了會怎樣?既然寫入時要做這麼多事,做到一半資料損毀不就糟了?

為了解決這個問題,資料庫在真正寫入並且進行前述的複雜操作前,會先寫下一條 LOG,記載「什麼時間」「我要對資料進行什麼修改」。

接著,才會開始真正的寫入。即使寫入到一半失敗了,也有 LOG 作為依據,檢查是否有未完成的修改,等到系統穩定後再重新寫入。

這樣的技巧,叫做 Write-ahead log,也就是在實際寫入前先寫入一條 LOG,作為驗證的依據。

這便是 LOG 第一個好用的性質,它格式簡單,寫入快速,可以作為複雜操作的前置動作,方便驗證。


讓我們繼續來聊聊資料庫。

前面提到,資料庫寫入時要做非常多的工作,所以非常的吃硬體效能。一旦單一機器無法負荷我們所需的資料量時,就需要多台硬體一起來分擔。

這時候,就進入另一個複雜的領域了。當你只寫入一次,要怎麼確保所有機器上的資料都正確被修改了?

前面的 Write-head Log 這時候就很好用。由於 LOG 記載了資料庫應該要做的修改動作,只要把這份 LOG 傳送給其他台機器,讓其他機器照著做,所有機器儲存的資料便會一樣。不需要把複雜的 Index、Relation、Schema 都傳過去,只要傳 LOG 就好了。這樣的技巧,叫做 Log Shipping。

這便是 LOG 的另一個好用性質,它可以用來代表系統的「目前狀態」。只要 LOG 一樣,系統的目前狀態就應該一樣。


接著,我們來聊聊最近很紅的 Microservice。

講到 microservice,就要提一下相對應的 Monolith。

所謂的 Monolith,是指你在開發系統時,讓所有的邏輯都共享同一份運算資源跟儲存資源。很多開發框架會鼓勵這樣的開發方式。因為這樣的架構開發容易,上手速度快。但是也有相對的缺點,擴充性差,一旦有某部分邏輯吃光了你的運算資源,整個系統都會沒有反應。

而 Microservice 提出的是,將你的邏輯按照他所屬的領域來切分。使用者登入是一個領域,交易是一個領域,金流是一個領域,報表也是一個領域。每個領域都有獨立的運算跟儲存資源。

更重要的是,可以讓每個領域有獨立的團隊負責維護,這個團隊可能小至一個人就好。因為領域間有定義好的溝通介面,也可減少團隊的溝通成本。

不過,這種架構需要維護的系統也變多,環境也變複雜,需要有較高的系統維護能力來處理,才會比較方便。

一個常見的錯誤是,有些人會讓 microservice 任意的互相溝通。這樣做其實是增加領域間的耦合度。每個 microservice 都需要知道有哪些服務會呼叫它,也要知道它的結果要傳給其他哪些服務,加上常常變動的需求,整個系統很快就會變的無法維護。

所以應該怎麼做比較好呢?比較成熟的作法是建立一個 Event Stream。每個服務之間不互相直接溝通,而是從 Event Stream 的洪流中拿出他關心的資料,運算完成後,再把結果放回 Event Stream。需要這個結果的人,自然就能從同樣的 Stream 中拿到結果。

於是,每個服務只要關心自己的邏輯是否正確,不用在意資料從哪邊來,要到哪裡去,也不需要知道其他服務的存在。

這樣的 Event Stream 也是一種 LOG。Stream 中每個訊息都記載了時間跟發生的事情。傳出去的訊息也不會再改變。

這樣的架構也引出了 LOG 的另一個好用性質:這種資料結構不管是什麼語言跟系統都很好解讀,就像 Unix 說的 Universal Interface 一樣,LOG 也可以作為一種 Universal Interface。


接著來聊聊每三個月都會重寫一次的前端架構。

如果你做過前端 UI,你會發現這是一個非常複雜的領域,隨時都有無數的 State 在改變:按鈕剛被按下,對話框跳出到一半,動畫還在跑使用者就輸入了下一個動作。因為隨時使用者都有可能進行操作,有花時間播放的動畫、也可能要透過網路擷取資料。因此,UI 一直都很難重現 BUG,也很難 Debug。

所以該怎麼做比較好呢?

最近有個東西叫 Flux,跟他的親戚 Redux 一起推的架構叫做 Unidirectional Data Flow。說的是要你把 UI 的狀態用一種單向更新的結構來表現。這樣你的 UI 元件便不需要注意太多狀態,只要關注前一個 UI State 跟下一個 UI State,就知道現在要顯示什麼了。

這樣的單向更新結構,其實就是一種 LOG。

LOG 單向更新的性質,讓狀態的改變很好理解,甚至可以把整個系統所有的狀態都寫到 LOG 裡,其他的邏輯都可以是 Pure 的,更好維護。


區塊鍊最近風風火火,每個人理解的區塊鍊都不太一樣。他跟 LOG 又有什麼關係呢?

區塊鍊要解決的是交易的核心問題。譬如用名牌換便當這件事。

我拿名牌交易成便當,這就是一個交易行為。如果其他工作人員不知道我的名牌已經換過了,我就可以把整個中研院的便當都拿光。這個狀況叫做 Double Spending。

所以要怎麼避免這個問題呢?

這其實一個 Distributed Consensus 的問題。如果所有人都知道一個交易發生了,也可以驗證這個交易的正確性,那就沒有偽造的空間。

而區塊鍊的作法,就是把交易的 LOG 分成一個一個的 block。每個 block 都替自己跟之前的 block 認證,那如果要偽造一筆交易,就需要偽造所有的交易紀錄。這樣的成本便會讓偽造得不到好處,而阻止詐欺的發生。

這代表了一件事,Log agreement 便是一種 Consensus(共識)。如果所有參與者都認可同一份 log,以及驗證 log 上發生的事情,那參與者之間就能建立共識。

很多複雜的分散式演算法也是建立在 Log agreement 上的。像是開山元老 PAXOS,主軸便是「Replicated Log」,不過這個演算法實在太難了,光是成功實做出來就能出一篇論文。因此後來有了後繼者 Raft,概念也是 Replicated Log ,但是好懂很多。

所以 LOG 也可以用來建立分散式參與者之間的共識。好像也可以拿來解決一些社會問題?


最後來聊聊最潮最兇,最近剛開獨立演唱會的大數據。

所謂的大數據,首先要夠大,像是 Petabyte 等級,一個 Excel 檔就裝的下的不在這邊的討論範圍。

這麼多的資料也來自各種地方,可能是使用者的輸入,可能是商業資料,可能是更潮的物聯網的 Sensor 送來的觀測資料,或是外部的開放資料。這些資料格式千奇白怪,要經過所謂的 ETL 的清理,才能被分析,或是輸入機器學習的系統。

要怎麼有效的處理這些雜亂的資料呢?

比較成熟的架構是像濾水器一樣,原始的水源輸入後,經過一層一層的濾心,最後流出來的就是乾淨的水。我們應該替資料建立一個管線,經過一道一道的篩選,可能是篩掉缺漏的、錯誤的資料,或是把太例外的 outlier 去除,最後得到的,才是可以分析的資料。

這樣的架構,其實就是一個 Append-only Log,這樣的架構讓每個接上的服務都只需要關注當下的資料,因為已經處理過的資料不會再改變,大幅減少複雜度,提供極佳的擴充性。


講了這麼多,其實 LOG 代表的就是一件事:Determinism。Determinism 指的是「相同的過程就會產生相同的結果」。乍聽之下是一件理所當然的事情,實際上並不是很容易就能達成。如果你的系統中有你不知道的副作用,有未掌握的外在因素,就做不到這件事情。

如果你的系統是 deterministic 的話,就會比較容易理解,容易 debug,容易擴展。

所以,一個用 log 可以表示的系統,就是一個 deterministic 的系統,就是一個穩定的系統。

下次設計系統架構前,先試著用 LOG 表示你的架構,如過可以寫出描述你的系統行為的 LOG,就比較容易建立出一個穩定的系統了。