為什麼要離開單一機器?
你目前學到的一切,都活在單一一台電腦裡。一個核心管理一組實體記憶體、一顆磁碟、一群行程,全都由一個可信的權威看顧著。那個世界很舒服,因為它有兩份隱藏的禮物。第一,有共用記憶體:任兩個行程可以被交給同一份實體 RAM,立刻看見彼此的寫入。第二,有共用時鐘:一顆硬體計時器驅動整台機器,所以「之前」和「之後」從不含糊。我們即將把這兩份禮物都送出去。
我們這麼做,是因為一台機器不夠用。我們想服務的使用者多過一個機箱所能承受,於是加上機器、把負載攤開。我們想讓服務在某台機器起火時仍持續運轉,於是把副本存在別處。我們想讓資料住在使用它的人身邊,散落在全球各地。一個分散式系統就是許多獨立的電腦,僅靠一個網路相連,協同合作好讓自己看起來像一個一致的服務。獎賞是規模、存活與觸及;代價則是那兩份禮物沒了。
你剛剛失去的那兩份禮物
失去共用記憶體,你就失去了最愛的工具。回到並行那個階段,一個執行緒用號誌保護共享資料——碰它之前 wait(S)、之後 signal(S)——而一把互斥鎖讓兩個執行緒不會同時進入同一個臨界區間。那些招數每一個都假設了「一份雙方都能碰的記憶體」。跨機器之間並沒有這種記憶體。機器 A 要告訴機器 B 任何事,唯一的辦法就是透過網路送出一則訊息,這個方式我們稱為訊息傳遞。你無法窺看另一台機器的 RAM;你只能發問,然後等待一個也許永遠不會來的答覆。
失去共用時鐘,你就失去了「何時」的意義。每台機器都有自己的石英晶體,以自己略微不準的速率滴答,每天和鄰居漂移開幾毫秒。所以如果機器 A 把一個事件蓋上 10:00:00.100 的時戳、機器 B 把另一個蓋上 10:00:00.090,你真的無法斷定 B 先發生——它們的時鐘根本就意見不合。沒有一個全域的「現在」可以查詢。光是這一個事實,就會摧毀任何暗自假設「事件有一個全序」的演算法,而修補它正是邏輯時鐘存在的全部理由,也是本階段第四篇導覽專門要講的。
延遲、遺失,以及最殘忍的那個:部分失效
訊息要花時間,而且時間並不固定:一個封包也許一毫秒就到,也許要一秒,全看壅塞與路由。訊息也會遺失——一個交換器溢位、一條線被扯掉——所以「我送出去了」不等於「它到了」。正是這些危害,讓分散式程式設計者彼此告誡所謂的分散式運算的謬誤:那些誘人的謊言,像是「網路是可靠的」、「延遲為零」、「頻寬無限」。信了其中任何一個,寫出來的程式在你筆電上能跑,到了正式環境就垮掉。
但最深的難處是部分失效,它在單一機器上沒有對應物。在一台電腦裡,東西往往「全有或全無」地失效:要嘛機器運轉、你的程式在跑,要嘛它當機、一切一起停下。在分散式系統裡,一個節點可能死掉,而其餘的繼續走。更糟的是,當你送一個請求給某個節點、卻什麼回音也沒等到,你無法分辨三件事中哪一件發生了:節點當機了;節點好端端只是慢;或者節點完美地把事做完了、只有回覆在路上遺失。這種模稜兩可——一段沉默可能代表上述任一種——是整個領域裡最難對付的一個事實。
Client sends a request to a Server, then waits ... client ---- request ----> server client <... silence .... (nothing comes back) What does the silence mean? Three indistinguishable cases: (a) server crashed before doing the work -> NOT done (b) server is alive but slow / overloaded -> maybe done later (c) server did the work, the REPLY was lost -> ALREADY done The client cannot tell (a), (b), (c) apart from silence alone. This is partial failure -- and it forces retries to be careful.
分散式系統會長成的樣子
面對那些難處,我們究竟怎麼安排這些機器?最常見的樣子是主從模型:有些機器扮演伺服器,持有資料、做粗重的工作,另一些扮演用戶端,送出請求、呈現結果。你的瀏覽器向一台網頁伺服器要一個頁面,就是最日常的畫面。它好推理,因為權威是集中的——但同一個中心在重載下是瓶頸,若它死了又是單一故障點。
另一種樣子則把權威攤開。在點對點模型裡,每台機器都是平等的同儕,既發問也回答,沒有享有特權的中心——想想檔案分享群集,每個下載者同時也是上傳者。同儕能優雅地擴充、沒有單一故障點,但協調一群平等者著實更難:沒有人說了算,同儕就得設法在彼此之間達成一致。這個一致性問題會在本階段尾聲以共識的名義回來,而它最尖銳的極限就是CAP 定理,也是第五篇導覽的壓軸。
與失去共存:前方的工具箱
本階段其餘的部分,是一套熬過這些失去的工具箱,而每一篇導覽各攻克一項。下一篇用遠端程序呼叫把網路弄得幾乎隱形:你呼叫一個函式,它就靜靜地在另一台機器上執行,彷彿它在本機。第三篇讓機器透過網路共用檔案,並問出那個尷尬的問題——快取:若每個節點為了速度各自保留一份副本,它們要怎麼維持一致?第四篇在沒有共用時鐘的情況下,重建一個堪用的時間觀念,靠的是「先發生」關係與邏輯時鐘。第五篇則正面對決一致這件事本身:共識、CAP 定理,以及靠保留副本來熬過失效。
在那幾個答案底下,安靜地坐著一個想法:複製——把同一份資料保留在不只一台機器上。複製是分散式系統熬過部分失效的方法:若某一份副本所在的機器死了,另一份副本來回答。但複製也是最深張力的源頭,因為你一旦有了兩份副本,就必須讓它們同步,而讓它們同步需要通訊,而通訊我們剛剛才承認是又慢又不可靠的。這個張力——為了存活而有副本,卻又有可能彼此意見相左的副本——正是貫穿其餘每一篇導覽的那條線。
帶著一個誠實的提醒往前走:分散式並不自動代表更好。加上機器會讓出錯的方式倍增,而且它無法廢除物理——一個橫越大洋的請求永遠要花上數十毫秒。我們走向分散式,是當一台機器真的給不了我們所需的規模、存活或觸及時,而且我們是睜大眼睛、以複雜度作為代價在付帳。把這份誠實放在身邊;這正是先前的階段所秉持的同一種精神——它承認分頁是拿外部碎裂換內部碎裂,也承認微核心是以額外的訊息傳遞為代價來買進穩健。