The two most important days in your life are the day you were born and the day you find out why. -Mark Twain

顯示具有 分散式系統 標籤的文章。 顯示所有文章
顯示具有 分散式系統 標籤的文章。 顯示所有文章

#102 - 分散式系統的基礎建設 - 時間與順序

為什麼時間在分散式系統中如此重要

想像一下,你和三個好朋友分別在台北、台中、高雄和花蓮,你們正在用手機的群組聊天軟體討論週末要去哪裡玩。每個人都在自己的手機上打字、傳送訊息,這些訊息會透過網路送到不同地方的伺服器,再轉發給其他人。這個看似簡單的聊天過程,其實就是一個「分散式系統」的例子。

在這個系統裡,時間扮演了一個至關重要的角色。當你看到聊天記錄時,你會期望訊息是「有順序的」。如果小明說「我們去墾丁好不好?」,小華回答「好啊,我贊成!」,但你的手機卻先顯示小華的回答,再顯示小明的提議,那就會讓人摸不著頭緒了。

時間問題在分散式系統中特別棘手,是因為這個世界上並沒有一個「絕對正確的時鐘」。每台電腦、每支手機、每個伺服器都有自己的時鐘,而這些時鐘可能快一點或慢一點。就像你家的掛鐘可能走得比學校的鐘快兩分鐘一樣,當你有上千台機器分散在世界各地時,要讓它們對「現在是幾點幾分」達成共識,是一件非常困難的事情。

讓我們看一個更嚴重的例子。想像你正在用網路銀行轉帳,你先在帳戶 A 存入一萬元,然後立刻從帳戶 A 轉帳五千元到帳戶 B。如果銀行系統搞錯了這兩個操作的順序,先處理轉帳再處理存款,那系統可能會以為你的帳戶沒有足夠的錢,導致轉帳失敗。更糟的是,如果系統允許你「先轉帳再存款」,那就等於讓你用了還沒存進去的錢,這在金融系統中是絕對不能發生的錯誤。

在資料庫系統中,時間的問題同樣關鍵。現代的大型網站通常會把資料複製到世界各地的多個資料中心,這樣才能讓使用者快速存取資料。當你在台北修改了一筆資料時,這個修改需要同步到美國、歐洲、日本的資料中心。如果有另一個使用者幾乎在同一時間也修改了同一筆資料,系統要如何判斷哪個修改應該被採用?如果不同地方的伺服器時鐘不一致,系統可能會做出錯誤的決定,導致資料錯亂。

除此之外,當分散式系統出現問題時,工程師需要查看各個伺服器的日誌檔案來找出原因。但如果這些日誌的時間戳記不準確,工程師可能會看到一個「不可能的」事件序列,例如系統先收到回應,然後才發送請求,這就像看一部倒著播放的電影,會讓人完全無法理解到底發生了什麼事。

所以,時間在分散式系統中之所以重要,是因為它直接影響到系統的正確性、可靠性和可維護性。沒有正確的時間概念,我們無法確保操作的順序、無法維護資料的一致性、無法進行有效的除錯。這就是為什麼,雖然時間看起來是個簡單的概念,但在分散式系統中卻是一個需要被認真對待的重大挑戰。

系統需要的是真實時間,還是順序?

在探討分散式系統的時間問題時,有一個很重要的觀念需要先釐清:系統真正需要的,究竟是「真實的時間」(例如現在是 2026 年 1 月 1 日下午三點二十分),還是只需要知道「事件的順序」(例如 A 事件發生在 B 事件之前)?這兩種需求看起來類似,但其實有著本質上的不同,而且需要用完全不同的方式來解決。

讓我們先從需要「真實時間」的情況開始談起。想像你正在開發一個股票交易系統,每天上午九點整開盤,下午一點半收盤。在這個系統中,時間不僅僅是用來排序交易的,它有著明確的「實際意義」。如果有人在早上八點五十分下單,系統必須要知道「開盤時間還沒到」,所以這筆交易要等到九點才能執行。又或者有人在下午一點三十五分想要下單,系統必須明確告訴他「市場已經收盤了」。在這種情況下,系統需要的是真實的、精確的時間,而不只是一個抽象的順序關係。

另一個需要真實時間的例子是日誌記錄和稽核。假設你的公司有一個規定,要求所有對重要資料的存取都必須被記錄下來,而且這些記錄必須保留七年。當監管單位來稽查時,他們可能會問:「在 2025 年 3 月 15 日下午兩點到四點之間,有誰存取過客戶的個人資料?」在這種情況下,系統需要能夠提供準確的時間資訊。如果你的日誌只記錄了「事件 A 發生在事件 B 之前」這樣的相對順序,而沒有記錄真實的時間,那就完全無法回答這個問題了。

金融交易也是一個對真實時間要求極高的領域。金融法規通常會規定,所有的交易都必須精確記錄到毫秒甚至微秒的層級。這不僅是為了合規性,也是為了公平性。在高頻交易的世界裡,晚了一毫秒可能就意味著錯過了一個獲利機會,或者在價格變動前來不及出場。更重要的是,當發生糾紛時,精確的時間戳記可以作為法律證據。例如,如果有人宣稱他在價格暴跌前就已經下了賣單,但交易系統顯示那筆賣單的時間戳記是在價格暴跌之後,那這個時間戳記就成為了判斷真相的關鍵證據。

然而,並不是所有的系統都需要真實的時間。在很多情況下,我們真正在意的只是「事件的順序」。讓我們回到一開始的聊天室例子。當你在看聊天記錄時,你在意的是訊息的順序是否合理,而不是每則訊息精確是在哪一秒發送的。如果小明說「我們去墾丁好不好?」,然後小華說「好啊」,接著小美說「+1」,你關心的是這三則訊息的順序,而不是它們是在下午兩點零三分十五秒、兩點零三分十八秒、還是兩點零三分二十秒發送的。事實上,就算這些訊息的發送時間只相差零點幾秒,在人類的感知中幾乎是「同時」,但我們仍然希望它們能按照正確的因果順序顯示。

版本控制系統是另一個只需要順序而不需要真實時間的例子。當你使用 Git 管理程式碼時,系統需要知道的是「哪個版本是基於哪個版本修改的」。假設你從版本 A 創建了一個分支,做了一些修改形成版本 B,然後又做了更多修改形成版本 C。在這個過程中,重要的是 A→B→C 這個順序關係,而不是每個版本精確是在什麼時候創建的。即使兩個版本在實際時間上只相差一秒鐘,只要我們知道哪個版本是基於哪個版本,就能正確地進行合併和衝突解決。

資料庫的因果一致性也是一個很好的例子。假設你在社群媒體上發了一則貼文,然後有人留言,接著又有人回覆那則留言。這裡有一個清楚的因果鏈:貼文必須先存在,才能有留言;留言必須先存在,才能有回覆。資料庫需要確保這個因果順序被正確維護,這樣使用者才不會看到「對一則不存在的貼文的留言」這種不合理的情況。但資料庫不需要知道貼文是在「下午三點零五分二十三秒」發布的,它只需要知道「貼文發生在留言之前」就夠了。

分散式系統中的互斥鎖(mutex)也只需要順序資訊。當多個程序想要同時存取同一個資源時,它們需要競爭一個鎖。系統需要決定「誰先取得鎖」,但這個「先」指的是邏輯上的先後順序,而不是實際時間上的先後。如果程序 A 和程序 B 幾乎同時請求鎖,可能程序 B 的請求在實際時間上早了零點零一秒,但因為網路延遲,程序 A 的請求先到達,那麼讓程序 A 先取得鎖是完全合理的。重要的是系統有一個一致的方式來判斷順序,而不是這個順序是否精確反映了「實際時間」。

這兩種需求的差異,也導致了完全不同的解決方案。當系統需要真實時間時,我們必須投資在精確的時鐘同步技術上,例如使用 GPS 衛星、原子鐘,或者像 Google 的 TrueTime 這樣的基礎設施。這些解決方案通常很昂貴,而且有其物理限制,例如光速限制了訊號傳遞的速度,所以不同地點的時鐘永遠無法做到「絕對同步」。

相反地,當系統只需要順序資訊時,我們可以用「邏輯時鐘」來解決問題。邏輯時鐘不依賴實際的物理時間,它只是一種為事件賦予順序的機制。這種方法的好處是,它完全不受物理時鐘誤差的影響。即使兩台機器的系統時鐘相差了一分鐘,邏輯時鐘仍然能正確地記錄事件的因果順序。而且邏輯時鐘的實作通常非常簡單,不需要昂貴的硬體或複雜的同步協定。

理解這個區別是非常重要的,因為它直接影響到系統的設計決策。如果你誤以為系統需要真實時間,但其實只需要順序,那你可能會花費大量的資源去建置精確的時鐘同步系統,卻發現這些投資其實是不必要的。反過來說,如果你的系統確實需要真實時間,卻只用邏輯時鐘來處理,那系統就無法滿足需求,可能會在稽核、合規或關鍵業務邏輯上出現問題。

所以,在設計分散式系統時,第一個要問的問題就是:我的系統真的需要知道「現在幾點」嗎?還是我只需要知道「誰先誰後」?回答這個問題,將會引導你走向完全不同的技術方案。

Lamport Timestamp:用邏輯建立秩序

在了解了系統有時候只需要順序而不需要真實時間之後,我們來看一個非常優雅的解決方案:Lamport Timestamp(Lamport 時間戳)。這個方法是由計算機科學家 Leslie Lamport 在 1978 年提出的,它的美妙之處在於,用一個極其簡單的機制,就解決了分散式系統中事件排序的難題。

Lamport Timestamp 要解決什麼問題?

讓我們先想像一個具體的情境。假設你正在開發一個多人協作的線上文件編輯系統,就像 Google Docs 那樣。當多個使用者同時編輯同一份文件時,每個人的修改都需要被同步到其他人那裡。假設小明在台北刪除了第三行的文字,幾乎同時,小華在高雄修改了第五行的文字。這兩個操作需要被同步到所有使用者的畫面上,但問題是:系統要如何決定這兩個操作的順序?

如果系統依賴各自電腦的時鐘,就會遇到麻煩。假設小明的電腦時鐘比標準時間快了兩秒,小華的電腦時鐘是準確的。小明在「他的時間」下午三點整進行刪除操作,小華在「他的時間」下午三點零一分進行修改操作。如果系統單純比較時間戳,會認為小明的操作發生在前。但實際上,因為小明的時鐘快了兩秒,他的操作在真實時間上可能是在下午兩點五十八分,而小華的操作是在下午三點零一分,所以小華的操作實際上晚了三分鐘。

更複雜的情況是,這兩個操作可能根本沒有因果關係。它們是兩個人在各自的電腦上獨立進行的操作,沒有誰影響誰。在這種情況下,不管以什麼順序執行,只要所有使用者看到的順序一致,系統就是正確的。Lamport 發現的關鍵洞察是:在分散式系統中,我們真正需要的不是「絕對的時間」,而是「因果關係」。如果事件 A 導致了事件 B(例如,小明發送了一則訊息,小華收到後回覆),那麼系統必須確保 A 被排在 B 之前。但如果兩個事件沒有因果關係(例如兩個人同時在不同地方編輯文件),那麼它們的順序其實並不重要,只要所有人看到的順序一致就好。

Lamport Timestamp 的核心概念

Lamport 提出了一個叫做「發生在之前」(happens-before)的關係。這個關係用符號「→」來表示,讀作「A 發生在 B 之前」。這個關係有三個簡單的規則:

第一個規則是關於同一個程序內的事件。如果在同一台電腦、同一個程式裡,事件 A 在事件 B 之前執行,那麼 A→B。這是最直觀的規則,就像你先打開一個檔案,再編輯它,「打開」一定發生在「編輯」之前。

第二個規則是關於訊息傳遞。如果事件 A 是「發送一則訊息」,事件 B 是「接收這則訊息」,那麼 A→B。這也很直觀,你不可能在訊息被發送之前就收到它。這個規則建立了不同程序之間的因果關係。

第三個規則是遞移性。如果 A→B,而且 B→C,那麼 A→C。就像如果你知道小明比小華高,小華比小美高,那麼你就可以推論出小明比小美高。

如果兩個事件之間沒有 happens-before 關係,那麼它們就是「並發的」(concurrent)。注意,這裡的並發不一定表示它們在實際時間上同時發生,而是表示它們之間沒有因果關係,系統無法判斷誰先誰後。

Lamport Timestamp 的運作方式

有了 happens-before 的概念之後,Lamport 設計了一個非常簡單的演算法來給每個事件分配一個時間戳,而且這個時間戳能夠反映 happens-before 關係。這個演算法的規則簡單到令人驚訝:

每個程序維護一個本地的計數器,我們稱它為 C。這個計數器一開始是 0。每當這個程序要執行任何事件(不管是本地的計算、發送訊息、還是接收訊息),它都會先把計數器加 1,然後用新的計數器值作為這個事件的時間戳。

當程序發送訊息時,它會把當前的時間戳附加在訊息裡一起發送出去。當另一個程序接收到這則訊息時,它會看到訊息裡的時間戳,然後把自己的計數器更新為「自己的計數器」和「訊息的時間戳」中較大的那個,再加 1。

為什麼要取較大值再加 1 呢?這是為了確保接收訊息的時間戳一定大於發送訊息的時間戳,從而維持因果順序。讓我們用一個例子來說明。

假設有兩個程序 P1 和 P2。一開始,P1 的計數器是 0,P2 的計數器也是 0。P1 執行了一個本地事件,它把計數器加 1,所以這個事件的時間戳是 1。接著 P1 又執行了一個本地事件,計數器變成 2,時間戳是 2。然後 P1 發送一則訊息給 P2,發送前先把計數器加 1 變成 3,所以這則訊息帶著時間戳 3。

在 P2 這邊,它在收到訊息之前,自己的計數器還是 0。當它收到帶著時間戳 3 的訊息時,它會計算 max(0, 3) + 1 = 4,所以接收這則訊息的事件時間戳是 4。如果 P2 接下來執行一個本地事件,計數器會變成 5,時間戳就是 5。

這個機制保證了一個重要的性質:如果事件 A 發生在事件 B 之前(根據 happens-before 關係),那麼 A 的時間戳一定小於 B 的時間戳。在我們的例子中,P1 發送訊息(時間戳 3)確實發生在 P2 接收訊息(時間戳 4)之前,而 3 < 4,符合我們的期望。

用程式碼理解 Lamport Timestamp

讓我們用一段簡單的 Python 程式碼來實作 Lamport Timestamp,這樣能更具體地理解它的運作方式:

class LamportClock:
    def __init__(self, process_id):
        """初始化 Lamport 時鐘
        
        參數:
            process_id: 程序的識別編號,用於辨識是哪個程序
        """
        self.time = 0  # 本地時間戳計數器
        self.process_id = process_id
    
    def local_event(self):
        """處理本地事件(例如執行計算、修改資料)
        
        回傳:
            這個事件的時間戳
        """
        self.time += 1
        print(f"程序 {self.process_id}: 本地事件,時間戳 = {self.time}")
        return self.time
    
    def send_message(self, message_content):
        """發送訊息給其他程序
        
        參數:
            message_content: 訊息內容
            
        回傳:
            包含訊息內容和時間戳的元組
        """
        self.time += 1
        timestamp = self.time
        print(f"程序 {self.process_id}: 發送訊息 '{message_content}',時間戳 = {timestamp}")
        return (message_content, timestamp)
    
    def receive_message(self, message_content, received_timestamp):
        """接收來自其他程序的訊息
        
        參數:
            message_content: 訊息內容
            received_timestamp: 訊息攜帶的時間戳
            
        回傳:
            接收事件的時間戳
        """
        # 這是關鍵:取兩個時間戳的最大值,再加 1
        self.time = max(self.time, received_timestamp) + 1
        print(f"程序 {self.process_id}: 接收訊息 '{message_content}',時間戳 = {self.time}")
        return self.time
現在讓我們模擬兩個程序的互動:
# 建立兩個程序的 Lamport 時鐘
process_1 = LamportClock(1)
process_2 = LamportClock(2)

# 程序 1 執行一些本地事件
process_1.local_event()  # 時間戳 = 1
process_1.local_event()  # 時間戳 = 2

# 程序 2 也執行本地事件
process_2.local_event()  # 時間戳 = 1

# 程序 1 發送訊息給程序 2
message, timestamp = process_1.send_message("你好")  # 時間戳 = 3

# 程序 2 接收訊息
process_2.receive_message(message, timestamp)  # 時間戳 = max(1, 3) + 1 = 4

# 程序 2 繼續執行本地事件
process_2.local_event()  # 時間戳 = 5
執行這段程式碼會輸出:
程序 1: 本地事件,時間戳 = 1
程序 1: 本地事件,時間戳 = 2
程序 2: 本地事件,時間戳 = 1
程序 1: 發送訊息 '你好',時間戳 = 3
程序 2: 接收訊息 '你好',時間戳 = 4
程序 2: 本地事件,時間戳 = 5

從這個輸出可以看到,程序 2 在接收訊息時,它的時間戳從 1 跳到了 4。這是因為它需要確保接收事件的時間戳(4)大於發送事件的時間戳(3),從而維持因果順序。 一個更複雜的例子 讓我們看一個更複雜但更貼近實際情況的例子。假設有三個程序在協同工作:

# 建立三個程序
p1 = LamportClock(1)
p2 = LamportClock(2)
p3 = LamportClock(3)

# P1 執行一些工作
p1.local_event()  # P1: 1
p1.local_event()  # P1: 2

# P1 發送訊息給 P2
msg1, ts1 = p1.send_message("任務資料")  # P1: 3

# P2 在收到訊息前,也在做自己的工作
p2.local_event()  # P2: 1
p2.local_event()  # P2: 2

# P2 接收 P1 的訊息
p2.receive_message(msg1, ts1)  # P2: max(2, 3) + 1 = 4

# P2 處理後,發送結果給 P3
msg2, ts2 = p2.send_message("處理結果")  # P2: 5

# P3 一直在做自己的事
p3.local_event()  # P3: 1
p3.local_event()  # P3: 2
p3.local_event()  # P3: 3
p3.local_event()  # P3: 4

# P3 接收 P2 的訊息
p3.receive_message(msg2, ts2)  # P3: max(4, 5) + 1 = 6

在這個例子中,我們可以觀察到幾個重要的現象。首先,P1 的第二個本地事件(時間戳 2)和 P2 的第二個本地事件(也是時間戳 2)雖然有相同的時間戳,但它們是並發的,因為它們之間沒有訊息傳遞,也就沒有因果關係。Lamport Timestamp 並不會試圖區分這兩個事件的先後,因為在邏輯上,它們的順序並不重要。

其次,注意 P3 在接收訊息前已經執行了四個本地事件,時間戳到了 4。但當它接收到時間戳為 5 的訊息時,它的時間戳立刻跳到 6。這確保了「P2 發送訊息」(時間戳 5)這個事件,在因果順序上一定排在「P3 接收訊息」(時間戳 6)之前。

Lamport Timestamp 的優勢與限制

Lamport Timestamp 的最大優勢就是它的簡單性。每個程序只需要維護一個整數計數器,每次執行事件時加 1,接收訊息時做一個簡單的 max 運算。不需要任何複雜的同步協定,不需要存取全域的時鐘伺服器,也完全不依賴實際的物理時間。這使得它非常容易實作,而且不會有額外的網路通訊開銷。

這個方法保證了一個很強的性質:如果事件 A 發生在事件 B 之前(根據 happens-before 關係),那麼 A 的時間戳一定小於 B 的時間戳。這個性質讓我們可以用 Lamport Timestamp 來判斷因果順序,確保系統不會違反因果律,不會出現「先看到回覆,再看到原始訊息」這種不合理的情況。

然而,Lamport Timestamp 也有一個重要的限制:它只能保證單向的推論。也就是說,如果 A 發生在 B 之前,那麼時間戳(A) < 時間戳(B);但反過來說,如果時間戳(A) < 時間戳(B),我們不能推論 A 一定發生在 B 之前,因為它們也可能是並發的。

在我們之前的例子中,P1 的第二個本地事件(時間戳 2)和 P3 的第四個本地事件(時間戳 4),雖然 2 < 4,但這兩個事件其實是並發的,它們之間沒有因果關係。Lamport Timestamp 無法告訴我們這一點。如果你的系統需要能夠判斷「兩個事件是否並發」,那麼你需要使用更進階的方法,例如向量時鐘(Vector Clock)。

儘管有這個限制,Lamport Timestamp 在很多實際應用中已經足夠了。例如在分散式資料庫中,它可以用來實作因果一致性;在分散式互斥鎖中,它可以用來決定哪個程序應該先獲得鎖;在版本控制系統中,它可以用來追蹤修改的依賴關係。它的簡單性使得它成為許多系統的首選方案,而當需要更強的保證時,才會考慮使用更複雜的機制。

Lamport Timestamp 的出現,標誌著分散式系統理論的一個重要突破。它告訴我們,即使在沒有全域時鐘、沒有精確同步的情況下,我們仍然可以建立一個有序的世界。這個看似簡單的想法,影響了後來數十年的分散式系統設計,也為我們理解「時間」在分散式系統中的意義,提供了一個全新的視角。時間不再只是牆上的鐘錶指針,而是一種反映事件之間因果關係的邏輯結構。這種思維方式的轉變,正是 Lamport Timestamp 帶給我們最珍貴的啟發。

Share:

#96 分散式系統介紹

分散式系統是我念書過程裡最喜歡的課程之一,也是以前我的研究領域.這一門課通常開設在碩博士班的課程裡,因為這門課需要不少基礎的課程,如果開在大學部的話,也一定是屬於大四選修課的內容.它的先修課程包含了作業系統, 演算法, 資料庫理論, 網路理論,網路程式設計等.它有一個兄弟課程叫分散式演算法.這兩門課蠻接近的,對我而言,分散式系統是比較以工程實作導向來討論分散式系統,而分散式演算法是以數學和演算法更嚴謹的電腦理論來談許多分散式系統裡所需要的運作細節. 我剛好都修過這兩門課,所以都清楚這兩個課程的內容.由於這個部落格的目的是將較不好懂的電腦理論用較白話易懂的方式來介紹給大家,所以在分散式系統的文章裡,不會著重在分散式演算法的內容.這是分散式系統系列文章的第一篇,所以會用一個較宏觀的角度來說明什麼是分散式系統.


在 1980 年代以前,電腦科學的理論已經發展的很快了,但實際上的 "商業化產品" 並還沒有大量地出現.然後在 80 年代開始,個人電腦漸漸地普及,並且 Internet 也開始大量地商業化.因此,從這個年代開始漸漸地有商業的分散式系統出現. 我們先簡單定義一下什麼是分散式系統. 用一個不是很精確的定義,分散式系統是由一群電腦一起互相合作然後達成一個共同目標或任務.舉個例子, email.發電子郵件對現在的你來說是幾乎每天都會做的事情.當你在電腦上打開郵件程式,編寫一封 email,寄給在其他位置的使用者.這封信件會從你的電腦傳送到你對應的 email server,然後再從你對應的 email server 一路展轉到目地端的 email server, 最後收件人打開他的郵件程式再把這封 email 下載到他的電腦上.所以, 整個電子郵件就是一種分散式系統,它需要一堆電腦,這些電腦可以連線用來傳送訊息,進而達成讓人們溝通的目的.分散式系統就是在討論這麼多電腦在彼此互相合作達成某個任務或目的時會遇到的問題以及相關的基礎知識和解決方式.


從這樣的定義你可以了解到現在的時代 (2022) 裡有許許多多的分散式系統.例如,WWW 可說是目前全世界最大的分散式系統. 還有許多 online game 也是分散式系統的一種. 像台灣人常用的 LINE 也是分散式系統的一種. 現在因為處理器能力大增加上軟體工具的進步,所以 AI 的應用可以漸漸地在生活上看見.如果那一天你發現某一個計程車行推出了自動駕駛計程車,不需要司機操作,完全由系統自行決定如何運行,從被客人呼叫,載客,到個計程車之間的行程調派等.這也是分散式系統的一種. 再舉另外一個例子,如果某一個國家或公司發展出先進的飛彈系統,這些飛彈能在發射之後,在空中彼此溝通,彼此協調目標,然後把所有的目標都命中. 不在是傳統的一個飛彈飛到一個固定的點或目標.別懷疑,這也是分散式系統的一種.


從這些例子,我相信你可以強烈地感受到什麼是分散式系統.這門學問基本上就是一種討論如何 "打群架" (多台電腦協同運作),用一台電腦打不過 (達不成目標),那只好找一堆電腦來一起打.要叫一台電腦做一件事,這是簡單的,但要叫一堆電腦一起做事來達成一個共同目標,這就顯的複雜許多.這也呼應了一開始所說的,分散式系統的先修科目有作業系統, 演算法, 資料庫,網路等等. 


因此,你可以知道,分散式系統是一堆電腦透過網路連線能彼此溝通進而達成一個任務.電腦不局限是電腦,也可以看成是某一個電腦程式.網路不局限於有線或無線,也不局限於區域網路或廣域網路.概念上,分散式系統的定義是可以很廣泛的.不同的任務所需要底層知識會有差別.例如,email 是一種分散式系統,不論你是用那一種電腦系統,不論你的 email 程式是用什麼語言寫的,也不論你的 email server 在世界上的什麼地方,這些都不應該是障礙.所以,為了讓某一個分散式系統能夠商業化地運行下去,必須定義許多 "標準" 好讓不同的軟體廠商可以依這些標準來產生可行的工具,這樣這個分散式系統才能成功.所以,不同分散式系統的應用就能看到不同的標準.例如, email 系統要定義出 email address 的合法格式,要定義出 email 內容的合法規格,要定義出 server 和 server 之間的溝通方式和通訊協定等,也要定義 server 和 mail client 之間的溝通方式等.從 email 系統的例子,你可以看到光是網路通信就有很多標準要完成.有許多的分散式系統不見得需要走向標準化.因為一些商業考量,有些公司喜歡有自己的應用,例如早期的 ICQ, MSN messenger, 到現在的 LINE.這類似的分散式系統是相對較封閉的分散式系統,因為是由某一家公司自己主導與開發,所以每家公司所使用的 "標準" 就不見得是一致的,各家都可能不一樣.


希望這一篇短文能幫助你了解什麼是分散式系統,下一篇文章,我們將介紹分散式系統裡會面臨到的挑戰,而整個系列文章將會圍繞在這些挑戰來介紹相關的解決方案.


Share:

#41 Load Balance 負載平衡

Load Balance 顧名思義就知道是在兩個以上的運算器環境下將每個運算器的運算負載量達成一致的目的.這樣子的概念應該在許多的情境之下,例如大型網站或是大型的資料庫等.這種類型的情境一定都有一個共同的特點,那就是 request 的 client 很多而提供 service 的 server 很少.

我們以大型網站服務為例子.最先開始架設一台網站伺服器來服務某一數量的用戶端,只要用戶端不繼續增加或是服務本身的運算邏輯沒有變的更複雜時,則一切都會相安無事.只要其中一個有增多的現象時,最終總是會面臨到該網站伺服器的資源不用使用的情況.因為每個用戶端連線都需要佔用 CPU/Memory 資源,所以既便是 CPU 再快 Memory 再多,也是會遇到上限.因此,要解決的方法有兩種,一個稱為 scale up,另一個稱為 scale out.Scale up 是指在同一個伺服器內進行硬體的升級以求在同一時間內服務更多的用戶端,例如升級 CPU,記憶體,網路卡等等.Scale out 是指再加入其他的伺服器來服務用戶端.通常來說,scale up的方式比較受限,而且一台伺服器若是要能裝入更多的 CPU 和 Memory,則價錢通常都是非常地高,所以比較經濟的做法就是 scale out.

Scale out 第一個會遇到的問題就是該如何分派工作.既然要達到負載平衡,也就是每台伺服器的運算負載量是接近一致的,那麼首要考慮的事情就是該如何達到一致.如果每個用戶端所要求的工作量都是固定的,很容易就可以做到負載量是一致的.因為只要在這些伺服器前都有一個 dispatcher 來輪流地分配工作,就可以達成負載量一致的效果,就如下圖一樣.


這種方式有一個缺點,dispatcher 將會是 single point of failure,單一失敗點,也就是說它若壞掉了,整個系統便無法運作.這種運作方式也稱為 Round-robin,輪流分配工作.在直實世界中,我們很難要求每一個用戶端都會有相同的運算工作量,因此每個用戶端送過來的需求一定會有不同的運算工作量,因此用 round-robin 的方式是很難達成負載量一致的,只能說是用戶端需求分配數量是一致的,因為很有可能某一個伺服器都會收到運算量很大的工作導致它比別的伺服器來的更加忙碌或是資源更吃緊.Round-robin 的方式算是集中式的管理,因為都是透過 dispatcher 來運作.

另外還有一種比較常見的負載平衡的方法是不需要 dispatcher,而是伺服器之間要互相溝通訊息,把自己的負載量資訊傳給其他的伺服器.所以每個一伺服器都會有一份每個伺服器負載量的資料,而且這分資料是經常地在更新.因此,當新的用戶端需求進來時,這份需求會先被某一個伺服器接收,然後該伺服器會依照這份資料來決定這份工作是要自己做還是傳給其他伺服器來做,這就看系統實作的是什麼樣的演算法.若採用最簡單的,也就是挑選負載量最小的伺服器,那這份工作就會被傳送到負載量最小的伺服器來處理.


這個方式聽起來似乎比較好,因為不會有 single point of failure 的問題,只是這樣的伺服器集合不適合有太多的伺服器在一起.我們簡單地想像一下,如果這個群組有十台伺服器,每一台伺服器在每隔幾秒鐘就要彼此交換負載量的資訊,這似乎不會是一個太麻煩的工作,但如果這群組變成是一百台或是一千台伺服器時,這樣的方法顯然會有問題,因為光是和其他 999 台伺服器完成一輪的負載量資訊交換可能就要花上一段時間和不少的 CPU 與 Memory,顯然不是一個經濟的做法,而且也不見得能儘量達成負載平衡.

其實在許多伺服器之間要對一份資料取得共識,也就是說一號伺服器上收集到的資料和 n 號伺服器上收集到的資料是一樣的,這也是一個很大的學問.往後的主題將會來討論這部份.
Share:

標籤分類