![](/images/1.png)
**這是 COSCUP 2016 的講題逐字稿,投影片在[此](http://www.slideshare.net/poga/log-65475572)**
--
自古以來,軟體工程師都在追求好維護,容易理解的軟體架構。傳統上,我們需要參與過各種大型軟體專案,從中獲取經驗,或是透過昂貴的課程,大量的論文,才能從前人的經驗中學到一些方法。
今天,我想試著透過 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,就比較容易建立出一個穩定的系統了。