本節深入伺服器的組織方式:先討論伺服器的一般設計議題,再討論伺服器叢集。
一般設計議題#
伺服器是代表一群客戶端實作特定服務的行程。本質上每個伺服器的組織方式都一樣:等待客戶端的請求進來,確保請求被處理,然後等待下一個請求。
迭代式與並行式伺服器#
- 迭代式伺服器(iterative server):伺服器自己處理請求,必要時把回應傳回請求的客戶端。
- 並行式伺服器(concurrent server):不自己處理請求,而是交給另一條執行緒或另一個行程,隨即繼續等下一個請求;由接手的執行緒或行程負責回覆。多執行緒伺服器是並行式伺服器的一例;另一種實作是為每個新請求 fork 一個新行程——許多 UNIX 系統採用這種做法。
客戶端如何找到伺服器:端點#
客戶端一律把請求送到伺服器所在機器的端點(end point),也稱埠(port);每個伺服器監聽特定端點。客戶端怎麼知道服務的端點?
- 全域指派的知名端點:例如 FTP 伺服器固定聽 TCP 埠 21、網頁的 HTTP 伺服器固定聽 TCP 埠 80。這些端點由 IANA(Internet Assigned Numbers Authority)指派;客戶端只需再找出伺服器所在機器的網路位址(可用下一章介紹的名稱服務)。
- 動態指派端點+查詢 daemon:許多服務不需要預先指派端點,例如報時伺服器可用作業系統動態指派的端點。此時可在每台跑伺服器的機器上跑一個特殊的 daemon,追蹤同機每個服務目前的端點;daemon 自己聽一個知名端點,客戶端先向它查詢端點、再聯繫目標伺服器。
- 超級伺服器(superserver):端點常與特定服務關聯,但為每個服務各養一個伺服器可能很浪費——典型 UNIX 系統上大量伺服器多數時間都在被動等待。更有效率的做法是讓單一超級伺服器代聽所有服務端點,例如 UNIX 的 inetd:它監聽多個 Internet 服務的知名埠,請求進來時 fork 一個行程處理,處理完就結束。

圖 3-11:(a) 使用 daemon 的客戶端對伺服器繫結;(b) 使用超級伺服器的客戶端對伺服器繫結
能否中斷伺服器#
設計伺服器時的另一個議題是能否、以及如何中斷它。例如使用者上傳大檔到 FTP 伺服器途中發現傳錯檔案,想中斷傳輸:
- 粗暴斷線:直接砍掉客戶端應用(連線自動斷掉)再重啟、假裝沒事——伺服器最終會以為客戶端當機而拆掉舊連線。在現今的網際網路上這招好用得很,有時甚至是唯一選擇。
- 頻外資料(out-of-band data):較好的做法是讓客戶端與伺服器能傳送「伺服器要在該客戶端其他資料之前優先處理」的資料。一種解法是伺服器另聽一個控制端點收頻外資料,同時以較低優先權聽一般資料端點;另一種是在同一條連線上送頻外資料——例如 TCP 的緊急資料(urgent data),伺服器收到時會被中斷(如 UNIX 的 signal),再檢視並處理該資料。
無狀態與有狀態伺服器#
最後一個重要設計議題是伺服器是否無狀態(stateless):
- 無狀態伺服器不保存客戶端的狀態資訊,改變自身狀態也不必通知任何客戶端。網頁伺服器就是例子:它只回應進來的 HTTP 請求,處理完就完全忘掉客戶端;它管理的檔案集合也可以逕行更動,無須通知客戶端。
- 許多無狀態設計中伺服器其實仍保有客戶端資訊(例如網頁伺服器記錄所有請求,供決定文件該不該複製、複製到哪),關鍵是這些資訊遺失也不會中斷服務——頂多損失一點效能。
- **軟狀態(soft state)**是無狀態設計的一種特殊形式:伺服器承諾替客戶端維護狀態、但僅限一段時間,期限一過就回到預設行為、丟棄相關資訊。例如伺服器承諾在一段時間內主動通知客戶端更新,逾期後客戶端就得自己輪詢。軟狀態源自電腦網路的協定設計,也同樣適用於伺服器設計。
- **有狀態伺服器(stateful server)**持續保有客戶端資訊,且需明確刪除。典型例子是允許客戶端保留檔案本地副本(甚至可更新)的檔案伺服器:伺服器維護一張(客戶端, 檔案)表格,追蹤哪個客戶端握有哪個檔案的更新權限、進而掌握最新版本。
有狀態設計常見的重要效益是客戶端感受到的讀寫效能提升;但重大代價在失效復原——伺服器當機後必須把整個狀態復原到當機前一刻(否則無法保證已處理最新的更新),而支援復原會引入可觀的複雜度(見第 8 章)。無狀態設計則完全不需特別措施:重新啟動、等請求進來即可。
延伸:會談狀態與永久狀態
Ling 等人(2004)主張應區分(暫時的)會談狀態(session state)與永久狀態(permanent state)。前述檔案伺服器的例子屬於會談狀態:與單一使用者的一串操作相關,需要維持一段時間、但非永久。會談狀態常見於三層式主從架構——應用伺服器得對資料庫伺服器連發一串查詢才能回覆客戶端。重點是會談狀態遺失並無實質傷害,客戶端重發原請求即可,因此可以用較簡單、較不可靠的方式儲存。永久狀態則是資料庫裡的那類資訊:客戶資料、購買軟體的金鑰等。不過對多數分散式系統而言,光是維護會談狀態就已構成有狀態設計,失效發生時就需要特殊措施,並得對伺服器端狀態的持久性做出明確假設。
無狀態或有狀態的選擇不該影響伺服器提供的服務。例如若檔案必須先開啟才能讀寫,無狀態伺服器就得設法模擬這個行為——常見解法(第 11 章詳述)是伺服器收到讀寫請求時先開檔、做完讀寫、立刻關檔。
伺服器想記住客戶端行為卻無法維護狀態時,常見解法是讓客戶端隨請求附上先前存取的資訊。在網頁世界裡,這些資訊由瀏覽器透明地存成 cookie——一小塊含有伺服器感興趣的客戶端專屬資訊的資料。瀏覽器從不執行 cookie,只負責保存:首次存取時伺服器把 cookie 隨網頁送回瀏覽器收好,之後每次存取同一伺服器都會附上。原理上運作良好,但瀏覽器代管 cookie 這件事往往對使用者完全隱藏——隱私也就這麼沒了。
伺服器叢集#
一般組織#
**伺服器叢集(server cluster)**簡單說就是一群透過網路相連的機器,每台跑一個或多個伺服器。這裡考慮的是以區域網路相連、通常高頻寬低延遲的叢集。多數情況下,伺服器叢集在邏輯上組織成三層:
- 第一層:(邏輯)交換器(switch),客戶端請求由此路由進來。交換器樣貌差異很大:傳輸層交換器接受 TCP 連線請求後轉給叢集內某台伺服器;也可以是一台網頁伺服器,接受 HTTP 請求後把部分工作交給應用伺服器處理、稍後收齊結果回覆 HTTP 回應。
- 第二層:應用處理伺服器。叢集運算中通常是跑在高效能硬體上、專司運算能力的伺服器;企業叢集中應用可能只需相對低階的機器,因為瓶頸不在運算而在儲存存取。
- 第三層:資料處理伺服器,特別是檔案與資料庫伺服器。視用途而定,可能跑在為高速磁碟存取配置、備有大型伺服器端快取的特化機器上。

圖 3-12:三層式伺服器叢集的一般組織
並非所有叢集都嚴格三層:常見每台機器自帶本地儲存、把應用與資料處理整合在單一伺服器,形成兩層式架構——例如以叢集處理串流媒體時,常部署兩層式架構、每台機器都是專責媒體伺服器。
叢集提供多種服務時,不同機器可能跑不同的應用伺服器,交換器必須能區分服務、否則無法把請求轉給正確的機器。實務上許多第二層機器只跑單一應用——原因除了軟硬體相依性,還有不同應用常由不同管理者管理、彼此不想干涉對方的機器。結果可能是某些機器暫時閒置、另一些卻請求爆量。有用的對策是暫時把服務遷移到閒置機器;Awadallah 與 Rosenblum(2004)提出用虛擬機器讓程式碼較容易遷移到實體機器。
交換器與 TCP handoff。叢集的重要設計目標是隱藏「有多台伺服器」的事實——遠端客戶端應用不需知道叢集內部組織。這種存取透明性一律經由單一存取點提供,通常以專用機器等硬體交換器實作(為了可擴縮性與可用性,叢集也可有多個存取點、各由一台專用機器實現;此處只考慮單一存取點):
- 標準存取方式是建立一條 TCP 連線,應用層請求在會談中傳送、拆線即結束。傳輸層交換器接受進來的 TCP 連線請求後,把連線**交遞(hand off)**給某台伺服器——即所謂 TCP handoff。
- 交換器收到 TCP 連線請求後,找出最適合處理的伺服器並把請求封包轉給它。該伺服器直接回覆確認給客戶端,但把交換器的 IP 位址填入 IP 封包的來源欄位——這種偽裝(spoofing)是必要的:客戶端在等交換器回應,而不是某台它從沒聽過的伺服器。TCP handoff 的實作顯然需要修改作業系統層級。

圖 3-13:TCP handoff 的原理
- 交換器藉由決定把請求轉給誰,實質上扮演負載分配的角色。最簡單的政策是 round robin:每次從清單挑下一台。也可以更聰明——若叢集提供多種服務且交換器能區分(例如服務以埠號區分,仍可在傳輸層做到),就能做有依據的轉發;更進一步是讓交換器檢視請求的酬載(payload),但前提是知道酬載長什麼樣——例如網頁伺服器的情況,交換器可預期 HTTP 請求並據以決定由誰處理。這種內容感知的請求分配(content-aware request distribution)將在第 12 章討論網頁系統時再談。
分散式伺服器#
前述叢集大多是靜態配置的,常有一台獨立的管理機器追蹤可用伺服器、把資訊傳給交換器等其他機器。單一存取點壞掉,整個叢集就無法使用。可以公開多個存取點的位址——例如 DNS 對同一主機名稱回傳多個位址——但客戶端還是得在某個位址失效時多試幾次,也沒解決存取點是靜態的問題。
穩定的長壽存取點是客戶端與伺服器都想要的;另一方面又希望叢集(含交換器)的配置有高度彈性。這催生了**分散式伺服器(distributed server)**的設計(Szymaniak 等人,2005):本質上是一組可能動態變化的機器、存取點也可能變動,對外卻始終呈現為單一的強大機器。基本理念是:與其依賴單一機器的可用性,把較簡單的機器透明地組成叢集(甚至可由終端使用者的機器動態組成,如協作式分散式系統的情況),可能達到比任何單一元件更高的穩定性。
穩定存取點的做法是利用現成的網路服務——IPv6 行動支援(MIPv6):
- MIPv6 中,行動節點有一個家網路(home network),在那裡擁有穩定的家位址(home address, HoA);家網路上有一台特殊路由器家代理(home agent),負責在節點外出時代轉流量。節點接上外地網路時會取得暫時的**轉交位址(care-of address, CoA)**並回報給家代理,此後所有流量都被轉送到行動節點。與行動節點通訊的應用程式只看得到家位址,永遠看不到轉交位址。
- 套用到分散式伺服器:為叢集指派一個唯一的**聯繫位址(contact address)**作為伺服器終身對外通訊的位址。任一時刻由一個節點以該位址擔任存取點,但這個角色可以輕易易手——存取點把自己的位址登記為家代理處的轉交位址,所有流量便導向它,再由它把請求分配給目前參與的節點。存取點失效時,另一個節點回報新的轉交位址即可完成簡單的故障轉移(fail-over)。
這種基本配置會讓家代理與存取點成為潛在瓶頸——所有流量都得經過這兩台機器。解法是 MIPv6 的路由最佳化(route optimization):行動節點回報轉交位址 CA 時,家代理可把 CA 轉給客戶端,客戶端本地存下(HoA, CA)對,之後通訊直接轉送到 CA——應用程式仍使用家位址,由底層 MIPv6 支援軟體翻譯成 CA。
路由最佳化可以讓不同客戶端各自以為在跟單一伺服器通訊,實際上卻各自對到分散式伺服器的不同成員節點:存取點把客戶端 C1 的請求轉給節點 S1(位址 CA1)時,附上足夠資訊讓 S1 發起路由最佳化程序,最終讓 C1 相信轉交位址是 CA1、存下(HA, CA1)。過程中存取點(及家代理)以隧道方式代轉 C1 與 S1 之間的多數流量,避免家代理以為轉交位址變了、得以繼續與存取點溝通;其他客戶端此時進來的請求先在存取點掛起,之後 C2 的請求可轉給 S2(位址 CA2)、讓 C2 存下(HA, CA2)。結果是不同客戶端直接與分散式伺服器的不同成員通訊,每個客戶端應用卻都保有「這台伺服器位址是 HA」的錯覺。

圖 3-14:分散式伺服器中的路由最佳化
管理伺服器叢集#
伺服器叢集對外該像一台電腦,也的確常是如此;但輪到管理叢集時,情況截然不同。
常見做法#
- 最常見的做法是把單機的傳統管理功能延伸到叢集。最原始的形式:管理者從遠端客戶端登入某節點,執行本地管理指令來監控、安裝、更動元件。
- 進一步是隱藏登入節點這件事,改在管理機器上提供介面,可對一台或多台伺服器收集資訊、升級元件、增刪節點等。主要優點是容易提供作用於一群伺服器的集體操作。這種管理方式實務上廣泛使用,例如 IBM 的 Cluster Systems Management。
叢集一旦成長到數十節點以上,這套做法就行不通了。許多資料中心得管理上千台伺服器、分屬多個協同運作的叢集,靠集中式管理伺服器根本不可行。超大叢集也需要持續的修復管理(含升級):設伺服器故障機率為 p 且故障彼此獨立,N 台伺服器全部無故障的機率是 (1−p)^N——p=0.001、N=1000 時,所有伺服器都正常運作的機率只有 36%。實務上對超大型叢集的支援幾乎都是臨機應變(ad hoc),有一些經驗法則(Brewer, 2001),但沒有系統性的方法;叢集管理仍在襁褓期,可以預期前一章討論的自我管理方案累積更多經驗後終將進入這個領域。
範例:PlanetLab#
PlanetLab 是一個有點特別的叢集:協作式分散式系統,由不同組織各捐出一台或多台電腦,合計數百節點,形成一個一層式伺服器叢集——存取、處理、儲存都可在每個節點上獨立進行。它的管理在本質上就幾乎必須是完全分散的。
節點架構(Peterson 等人,2005;Bavier 等人,2004)。每個節點(最容易想成一台電腦,也可以本身是一個叢集)有兩個重要元件:
- 虛擬機器監視器(VMM):一個強化過的 Linux 作業系統,強化主要是為了支援第二個元件。
- vserver:最好想成「一群行程在其中執行的獨立環境」。不同 vserver 的行程完全獨立,不能像一般行程那樣直接共享檔案、主記憶體、網路連線等資源;每個 vserver 有自己的一套軟體套件、程式與網路設施——例如一個 vserver 提供 Python 1.5.2 搭配老版 Apache(httpd 1.3.1)的環境,另一個支援最新版。就此而言把 vserver 叫「伺服器」有點名不副實:它其實只是把行程群彼此隔離。Linux VMM 確保 vserver 彼此分離,隔離是嚴格的——不同 vserver 裡的兩個行程可以有相同的使用者 ID,卻不代表出自同一使用者。這種分離大幅簡化了「不同組織的使用者拿 PlanetLab 當測試平台、實驗完全不同的分散式系統與應用」的支援。

圖 3-15:PlanetLab 節點的基本組織
為支援這類實驗,PlanetLab 引入 slice 的概念:一組 vserver、每個跑在不同節點上。slice 可視為一個虛擬伺服器叢集,由一組虛擬機器實現。
PlanetLab 管理上有三個突出的難題:
- 節點屬於不同組織:每個組織應能規定誰可以在其節點上跑應用,並適當限制資源用量。
- 現有的各種監控工具都假設特定的軟硬體組合,且都是為單一組織內使用而打造。
- 同一節點上不同 slice 的程式不得互相干擾——類似作業系統裡的行程獨立性問題。
資源管理與 slice 的建立:
- 每個節點有一個節點管理器(node manager),以獨立的 vserver 實作,唯一任務是在所管理的節點上建立其他 vserver 並控制資源配置。節點管理器不做政策決定,只是機制。
- 資源追蹤靠資源規格(resource specification, rspec):指定某段時間區間內配置的資源——磁碟空間、檔案描述子、進出頻寬、傳輸層端點、主記憶體、CPU 用量等。rspec 以全域唯一的 128 位元識別碼**資源能力(resource capability, rcap)**識別,節點管理器可據以在本地表格查出對應 rspec。
- 資源繫結於 slice:要用資源就得建立 slice。每個 slice 關聯一個服務提供者(service provider)——可視為在 PlanetLab 擁有帳號的實體;slice 以(principal_id, slice_tag)對識別,前者標識提供者、後者由提供者自選。
- 每個節點跑一個 slice 建立服務(slice creation service, SCS),可請求節點管理器建立 vserver 並配置資源。節點管理器不能直接經網路聯繫,使其專注於本地資源管理。SCS 也不接受任何人的建立請求——只有特定的 **slice 授權機構(slice authority)**有資格;每個 slice 授權機構握有一批節點的存取權(最簡單的模型是單一授權機構可在所有節點上請求建立 slice)。
- 服務提供者向 slice 授權機構請求在一批節點上建立 slice;提供者須先經認證與註冊為 PlanetLab 使用者(實務上透過網頁服務聯繫授權機構)。
這套程序顯示 PlanetLab 的管理是透過中介者進行的。slice 授權機構是一類重要中介者,其在各節點建立 slice 的憑證是頻外(out-of-band)取得的——實質上是去找各站點的系統管理者談,耗時費工、不可能由終端使用者自行進行。除 slice 授權機構外還有管理授權機構(management authority):前者專管 slice,後者負責看顧節點——確保轄下節點跑著基本的 PlanetLab 軟體並遵守 PlanetLab 的規則;服務提供者信任管理授權機構會提供行為良好的節點。
延伸:PlanetLab 各實體間的管理(信任)關係
以信任關係描述(Peterson 等人,2005),各實體的關係如下:
- 節點擁有者把節點交給某管理授權機構管轄,可視情況限制用途。
- 管理授權機構提供把節點加入 PlanetLab 所需的軟體。
- 服務提供者向管理授權機構註冊,信任其提供行為良好的節點。
- 服務提供者聯繫 slice 授權機構,在一批節點上建立 slice。
- slice 授權機構須認證該服務提供者。
- 節點擁有者提供 slice 建立服務給 slice 授權機構建立 slice——實質上是把資源管理委派給 slice 授權機構。
- 管理授權機構把建立 slice 的工作委派給 slice 授權機構。

圖 3-16:PlanetLab 各實體之間的管理關係
這些關係解決了「以受控方式委派節點、讓節點擁有者能信賴妥善且安全的管理」的問題。
監控:需要統一的方式讓使用者了解自己的程式在特定 slice 內表現如何。PlanetLab 的做法很簡單:每個節點配備一組感測器(sensor),各自回報 CPU 用量、磁碟活動等資訊。感測器可以任意複雜,但重點是一律以「每節點」為單位回報;資訊經由網頁伺服器提供——每個感測器都能以簡單的 HTTP 請求存取。這種監控法仍相當原始,應視為進階監控方案的基礎——原則上沒有理由不能用第 2 章討論的 Astrolabe 來彙整跨節點的感測讀數。
程式間的保護:PlanetLab 用 Linux 虛擬伺服器(vserver)隔離 slice。vserver 的核心想法是讓應用程式在自己的環境中執行,包含單機上通常共享的所有檔案——這種分離用 UNIX 的 chroot 指令即可相對容易地達成(它改變應用程式尋找檔案的檔案系統根目錄,只有超級使用者能執行)。當然還需要更多:Linux 虛擬伺服器不只分離檔案系統,也分離通常共享的行程資訊、網路位址、記憶體用量等。結果是一台實體機器被切分成多個單元,每個單元對應一個完整的 Linux 環境、與其他部分隔離。