到目前為止,我們談的分散式系統多半只在機器間傳遞資料;但有些情況下,傳遞程式——有時甚至是執行到一半的程式——能簡化分散式系統的設計。本節詳細檢視程式碼遷移是什麼:先看各種遷移方式,再討論遷移中的程式所使用的本地資源該怎麼辦,最後處理特別棘手的異質系統遷移問題。

程式碼遷移的方式#

遷移程式碼的理由#

傳統上,分散式系統的程式碼遷移以行程遷移(process migration)的形式進行——把整個行程從一台機器搬到另一台(Milojicic 等人,2000)。搬移執行中的行程既昂貴又複雜,得有夠好的理由,而這個理由向來是效能:基本想法是把行程從高負載機器移到低負載機器,就能改善整體系統效能。負載常以 CPU 佇列長度或 CPU 使用率衡量,也有其他效能指標。

不過在許多現代分散式系統中,最佳化運算能力不如減少通訊重要;加上底層平台與網路的異質性,經由程式碼遷移取得的效能改善往往基於定性推理,而非數學模型。以下是幾種典型情境:

  • 把運算搬到資料旁:主從式系統中伺服器管理龐大資料庫,若客戶端應用要做大量涉及大批資料的資料庫操作,把客戶端應用的一部分送到伺服器、只把結果傳回網路,可能好過讓網路被伺服器到客戶端的資料傳輸淹沒。
  • 反向:把伺服器的一部分搬到客戶端:許多互動式資料庫應用中,客戶端要填寫表單、再轉成一串資料庫操作。在客戶端處理表單、只把填好的表單送到伺服器,常能避免大量小訊息穿越網路——客戶端感受到更好的效能,伺服器也省下表單處理與通訊的時間。
  • 以平行性提升效能、卻不沾平行程式設計的麻煩:典型例子是網頁資訊搜尋——把搜尋查詢實作成一支小型移動程式,即行動代理人(mobile agent),在站點間移動;複製多份、分送到不同站點,可能拿到相對於單一程式實例的線性加速。

除了效能,支援程式碼遷移最重要的另一個理由是彈性

  • 傳統做法是把應用切成多個部分、事先決定每個部分在哪執行(第 2 章的多層式主從應用即由此而來)。若程式碼能在機器間移動,就能動態配置分散式系統。
  • 例子:伺服器以專有協定提供檔案系統的標準化介面。傳統上,基於該協定的客戶端實作必須在開發客戶端應用時就連結進去、事先備妥。另一種做法是等到客戶端繫結到伺服器時,才由伺服器把客戶端實作交給它——客戶端動態下載實作、完成初始化步驟、再呼叫伺服器。

圖 3-17:動態設定客戶端以便與伺服器通訊的原理:客戶端先取得所需軟體,再呼叫伺服器

動態下載客戶端軟體的模型有兩個重要優點:其一,客戶端不必預先安裝所有與伺服器溝通的軟體——需要時搬進來、不用了就丟掉;其二,只要介面標準化,主從協定與其實作可以隨時想改就改,不會影響依賴伺服器的既有客戶端應用。前提是下載與初始化程式碼的協定要標準化,且下載的程式碼能在客戶端機器上執行。

這個模型最嚴重的缺點是安全性(第 9 章詳述):盲目信任下載來的程式碼真的只實作它宣稱的介面——而它正在存取你毫無防護的硬碟,也沒把最精彩的部分寄給不知何方神聖——未必是個好主意。

程式碼遷移的模型#

「程式碼遷移」一詞其實涵蓋的範圍比「只搬程式碼」豐富得多:廣義上它處理的是把程式搬到目標機器去執行;某些情況(如行程遷移)連程式的執行狀態、待處理的 signal 與環境的其他部分也得一併搬。為了理解各種模型,採用 Fuggetta 等人(1998)的框架——把行程看成三個節段(segment):

  • 程式碼節段(code segment):構成執行中程式的指令集合。
  • 資源節段(resource segment):行程所需外部資源的參照——檔案、印表機、裝置、其他行程等。
  • 執行節段(execution segment):行程當前的執行狀態——私有資料、堆疊,當然還有程式計數器。

弱移動性與強移動性

  • **弱移動性(weak mobility)**是程式碼遷移的最低限度:只傳輸程式碼節段(或許附帶一些初始化資料)。特徵是被傳輸的程式一律從幾個預先定義的起始位置開始執行——Java applet 就是如此,永遠從頭執行。好處是簡單:只要求目標機器能執行該程式碼,本質上就是把程式碼做成可攜的。
  • 強移動性(strong mobility)則連執行節段也能傳輸:執行中的行程可以被停下、搬到另一台機器、再從中斷處接續執行。強移動性遠比弱移動性通用,實作也困難得多。

傳送端發起與接收端發起

  • 傳送端發起(sender-initiated):遷移由程式碼目前所在(或執行中)的機器發起。典型如把程式上傳到運算伺服器,或把搜尋程式送到網頁資料庫伺服器就地查詢。
  • 接收端發起(receiver-initiated):由目標機器主動發起,Java applet 是代表。

圖 3-18:程式碼遷移的各種替代方案

接收端發起比傳送端發起簡單。程式碼遷移多發生在客戶端與伺服器之間:向伺服器安全地上傳程式碼(傳送端發起)通常要求客戶端事先註冊並通過認證——伺服器必須認識所有客戶端,因為對方多半想動用伺服器的磁碟等資源,保護資源是頭等大事。反之,接收端發起的下載常可匿名進行,伺服器也不關心客戶端的資源;程式碼搬到客戶端只是為了改善客戶端效能,需要保護的資源有限(如記憶體與網路連線)。

弱移動性還有一個區分:遷移來的程式碼由目標行程執行,還是另起獨立行程執行。例如 Java applet 由瀏覽器下載後就在瀏覽器的位址空間內執行——好處是不必另起行程、免去目標機器上的行程間通訊;缺點是目標行程必須防範惡意或無心的程式碼執行。簡單的解法是讓作業系統建立獨立行程來執行遷移來的程式碼(但這不解決資源存取問題,那還是得另外處理)。

強移動性除了搬移執行中的行程(行程遷移),也可以用**遠端複製(remote cloning)**支援:複製會產生原行程的一份精確副本,跑在另一台機器上、與原行程平行執行。UNIX 系統的做法是 fork 出子行程、讓子行程在遠端機器上繼續。複製的好處是模型與許多應用既有的模式非常相近,唯一差別只是複製出的行程在另一台機器上執行——就此而言,以複製達成遷移是改善分散透明性的簡單手段。

遷移與本地資源#

前面只考慮了程式碼節段與執行節段的遷移;資源節段需要特別關照。程式碼遷移之所以困難,常是因為資源節段不能原封不動跟著搬。例如行程握有一個 TCP 埠的參照、正透過它與其他(遠端)行程通訊:搬家後就得放棄這個埠、在目的地重新申請。反之,以絕對 URL 參照檔案的情況下,參照在哪台機器上都有效。

行程對資源的繫結(三種,由強到弱)#

  • 依識別碼繫結(binding by identifier):行程恰恰需要那個被參照的資源,別無替代。例如以 URL 指向特定網站、以網際網路位址指向 FTP 伺服器;對本地通訊端點的參照同理。
  • 依值繫結(binding by value):只需要資源的——換一個能提供相同值的資源,行程執行不受影響。典型如程式依賴的 C 或 Java 標準函式庫:各站點檔案位置可能不同,重要的是內容而非特定檔案。
  • 依型別繫結(binding by type):最弱的形式,行程只表明需要某型別的資源——如螢幕、印表機等本地裝置的參照。

資源對機器的繫結(三種)#

遷移程式碼時常需改變資源參照,但不能改變行程對資源的繫結種類。參照該不該改、怎麼改,取決於該資源能不能跟著搬到目標機器:

  • 未依附資源(unattached resource):可輕易在機器間搬移,典型是只與待遷移程式相關的(資料)檔案。
  • 依附資源(fastened resource):搬移或複製並非不可能,但代價相對高昂——如本地資料庫、完整網站。理論上不依賴當前機器,實務上搬不動。
  • 固定資源(fixed resource):與特定機器或環境緊密相連、無法搬移——常是本地裝置;本地通訊端點也是一例。

三種行程對資源繫結 × 三種資源對機器繫結,得出遷移時要考慮的九種組合,各組合的處理原則如下:

  • 依識別碼繫結:資源未依附時,通常最好隨程式碼一起搬;但若資源被其他行程共享,替代方案是建立全域參照(global reference)——能跨越機器邊界的參照,如 URL。資源依附或固定時,最佳解也是建立全域參照。
  • 依值繫結:固定資源+依值繫結的組合,出現在行程假設記憶體可在行程間共享的情況——此時建立全域參照等於得實作分散式共享記憶體,多數情況並不可行或沒效率。依值繫結的依附資源典型是執行期函式庫,目標機器上通常已有副本、否則應在遷移前先複製過去;要複製的資料量龐大時(如文書處理系統的字典與同義詞庫),建立全域參照反而較好。未依附資源最簡單:複製(或搬移)到新目的地即可,除非被多個行程共享——那就只剩建立全域參照一途。
  • 依型別繫結:無論資源對機器的繫結為何,顯而易見的解法都是把行程重新繫結到目標機器上同型別的本地資源;只有在沒有這種資源時,才需要複製、搬移原資源,或建立全域參照。

圖 3-19:把程式碼遷移到另一台機器時,針對本地資源參照所應採取的處理方式

建立全域參照可能不只是「用個 URL」那麼簡單,其代價有時高到令人卻步。例如一支為專用多媒體工作站即時產生高品質影像的程式:產圖是運算密集工作,因此想把程式搬到高效能運算伺服器——但對工作站建立全域參照意味著在運算伺服器與工作站之間架設通訊路徑,且兩端都要投入可觀的處理才能滿足影像傳輸的頻寬需求。淨結果可能是:只因全域參照的代價太高,搬去運算伺服器根本不划算。

延伸案例:遷移使用本地通訊端點的行程

行程正在使用本地通訊端點時要遷移,面對的是「固定資源+依識別碼繫結」的組合,基本上有兩種解法:

  1. 讓行程遷移後回頭與來源機器建立連線,並在來源機器安裝一個單純轉發所有進入訊息的行程。缺點是來源機器一故障,與已遷移行程的通訊就可能失敗。
  2. 讓所有曾與遷移中行程通訊的行程更改其全域參照,改把訊息送到目標機器上的新通訊端點。

異質系統中的遷移#

前面默默假設遷移過去的程式碼在目標機器上就能執行。同質系統中這個假設成立;但一般而言,分散式系統建立在異質的平台集合上,各有自己的作業系統與機器架構。異質環境的遷移要求每個平台都被支援——程式碼節段在每個平台上都能執行,執行節段在每個平台上都能被妥善表示。

異質性帶來的問題與可攜性問題大同小異,解法也很相似:

  • 1970 年代末,Pascal 移植問題的簡單解法是產生機器無關的中間碼、跑在抽象的虛擬機器上(Barron, 1981)——虛擬機器得在各平台上實作,但實作了 Pascal 程式就到處能跑。這個想法流行了幾年,卻始終沒有成為其他語言(尤其是 C)可攜性問題的通用解。
  • 約 25 年後,異質系統的程式碼遷移改由腳本語言與高可攜語言(如 Java)進攻,本質上採用的是同一招:全都依賴行程虛擬機器——直接直譯原始碼(腳本語言),或直譯編譯器產生的中間碼(Java)。語言開發者要成功,「在對的時間出現在對的地方」也很重要。

近期的發展開始弱化對程式語言的依賴:與其只遷移行程,不如遷移整個運算環境。基本想法是把整體環境劃分成隔間(compartmentalize),讓同一隔間內的行程擁有自己對運算環境的視野。劃分得當時,就能把一個隔間與底層系統解耦、實際搬到另一台機器:

  • 這實質上為行程提供了一種強移動性:行程可在執行途中任意時點被搬走,遷移完成後從中斷處接續。
  • 行程對本地資源有繫結所衍生的許多麻煩也可能迎刃而解——本地資源常常就是被遷移環境的一部分,繫結多半直接保留。
  • 遷移整個環境最重要的理由或許是:機器要停機時服務得以延續。例如伺服器叢集中,系統管理者要關機或換機時不必終止所有執行中的行程——把環境暫時凍結、搬到另一台機器(與既有環境並列)、再解凍即可。這是管理長時間執行的運算環境及其行程極為強大的方式。
延伸案例:虛擬機器的即時遷移(Clark 等人,2005)

Clark 等人(2005)研究虛擬化作業系統的即時(real-time)遷移,適用情境是透過單一共享區域網路緊密耦合的伺服器叢集。此時遷移涉及兩大問題:搬移整個記憶體映像、搬移對本地資源的繫結。

記憶體映像的遷移原則上有三種做法(可以組合):

  1. 把記憶體分頁推送(push)到新機器,遷移過程中被改動的分頁再重送。
  2. 停止當前虛擬機器、搬移記憶體、再啟動新虛擬機器。
  3. 讓新虛擬機器按需拉入(pull)分頁——行程立刻在新虛擬機器上開跑,記憶體分頁需要時才複製。

若遷移中的虛擬機器正在提供持續服務(live service),第二種做法會造成無法接受的停機時間;純按需的第三種做法則會大幅拉長遷移期,且遷移後要很久才把工作集搬齊,效能可能很差。作者提出預先複製(pre-copy):結合第一種做法與一段簡短的「停止並複製」階段(第二種做法的體現)——實測服務停機時間可壓到 200 毫秒以內。

本地資源方面,只處理叢集伺服器讓事情單純許多:網路只有一個,只需公告新的「網路位址對 MAC 位址」繫結,客戶端就能從正確的網路介面聯繫上已遷移的行程;若儲存又是獨立的一層,檔案繫結的遷移同樣簡單。整體效果是:我們搬的不再是行程,而是一整個作業系統在機器之間移動。