遠程過程調用(RPC)范式的出現可以追溯到40年之前。時至今日,它仍是在編寫分布式應用時使用率最高的一種編程模型。只是近些年來,人們對於RPC技術的質疑與批評聲逐漸多了起來。Steve Vinoski在2008年曾尖銳地指出,之所以RPC仍然能夠得到諸多開發者的支持,其原因只有一個:舒適感!Vinoski完全不認可這種思想,他表示:
“開發者的舒適感真的比正確性、可伸縮性、性能、關注分離、可擴展性以及附加的復雜性還要重要嗎?”
盡管面臨著這些尖銳的批評,但RPC的歷史地位是不容置疑的,而它在現代化的應用中仍能夠占據一席之地,成為分布式計算中一種重要的編程模型。正在攻讀博士學位的Christopher Meiklejohn近來開設了一系列博客文章以回顧分布式計算中的各種編程模型與語言,在其中一篇文章中對RPC進行了詳盡的回顧與展望。
概述
簡單來說,一台機器上的程序對另一台機器上的子程序的調用就是一次RPC調用。在調用過程中,主程序不需要操心與遠程執行相關的任何代碼,與本地調用相比,其唯一區別就在於需要提供遠程節點的標識。最早為人所知並接受的RPC實現是由Sun提供的SunRPC機制,使用在其網絡文件系統(NFS)中。
除此之外,常見的RPC機制還包括Java的RMI、DCOM、XML-RPC、SOAP、CORBA,以及Google的gRPC等等。
RPC的早期發展
RPC思想最早的原型可追溯至1974年所發布的RFC 674草案 —— “過程調用協議文檔第2版”,該草案當時的目標是為因特網上的全部70個節點定義一種共享資源的通用方式,在該草案中引入了過程調用范圍第2版(PCP)的概念。而在第二年發布的RFC 684草案 —— “對以過程調用作為網絡協議的評論”中,首次分析了RPC這種編程范式存在的三大問題以及這些問題與分布式系統的本質問題之間的關聯。這三大問題可以簡要地概述如下:
伴隨著這三大問題的是使用這種編程范式時的一系列麻煩,這些麻煩在RPC的40多年發展歷史中始終陰魂不散,包括:如何從故障或錯誤中恢復;如何始終保證操作的正確順序;RPC范式強制使用者進行同步編程方式;RPC的調用-響應模型使得因系統過載而導致消息無法正常處理時,對優先級的排列變得相當困難。
隨後發布的RFC 707草案繼承了RFC 684的思想,並提出了一個新問題,即各種服務,例如TELNET與FTP之間的資源共享問題。因為這些服務各自具有不同的接口,因此使用者必須了解他們的操作端口與命令。該草案的作者提出了一個建議:為遠程過程的執行定義一個通用的接口,該接口接受一個參數列表,並依然遵循RPC的調用-響應模型。雖然這一提議並未解決RPC 684中所提出的問題,但這一模型在之後依然得到了許多系統的采納。
CORBA
CORBA是對面向對象語言的一種抽象,允許開發者進行跨機器、跨語言的通信。CORBA通過接口定義語言(IDL)指定遠程對象的接口。IDL用於生成遠程系統中的對象接口在本機中的樁代碼,並且在實際的語言實現與抽象接口之間生成映射關系。
CORBA的試圖為應用開發者帶來幾點益處:不依賴於具體的語言、操作系統以及架構;將IDL中的抽象類型映射為具體實現所帶來的靜態類型特性;以及對象在不同機器之間的傳輸。CORBA承諾,通過使用映射,遠程方法調用的使用就與本地調用一樣簡單,甚至與分布式系統相關的異步也可被映射為本地異常進行處理。
但是,Vinoski在2003年表示,僅基於透明性這一點對於編程語言與抽象進行評估的方式是有缺陷的。在他看來,IDL映射的目的在於將中間件抽象直接合並至編程語言的領域中,通過這種透明性減少編程語言與中間件這兩者之間的阻抗失調。但問題在於,不恰當的透明性可能會掩蓋分布式計算中出現的某些問題,例如並發與部分失敗相關的問題。
對RPC范式的批評
Tanenbaum與van Renesse對RPC范式提出了尖銳的批評,他們認為將遠程調用與本地調用一視同仁的思想在本質上就是錯的,RPC試圖打造的透明性也是根本不可能實現的。他們認為為遠程訪問專門設計一種協議是更好的做法。
Tanenbaum與van Renesse的批評意見涵蓋了RFC 684草案中已經提到的幾點內容:延遲、缺乏並行性、異常處理以及故障檢測等等。此外,他們還提出了一些批評意見:
單線程服務器
如果服務器無法立即向客戶端發送響應,比如它正在等待來自另一台服務器的輸入。在這種情況下,不僅服務器端產生了阻塞,客戶端也無法繼續執行本地計算過程。
兩軍問題
怎樣才能讓兩台服務器對於某個RPC的成功執行以及收到響應的結果達成一致呢?雖然某一方可以向對方發送確認信息,但對方還得向這個確認信息發送另一個確認信息以再次確認。因此無論發送幾次確認都無法實現100%的一致性。這一主題其實也是一致性問題的核心,許多與分布式系統相關的文獻對其進行了更深入的探討。
參數
Tanenbaum與van Renesse也敘述了參數傳遞與參數封送的問題,這一問題在CORBA等有可能包含引用的對象系統中顯得更為嚴重。在這種情況下,為了保證引用的有效性,必須使用某種特定的分布式引用。
冪等性
最後一個問題是如何跨網絡表達只執行一次的語義,作者在此處強調了冪等性(idempotence)的重要性。簡單來說,具有冪等性的操作即使經過多次執行,其結果與只執行一次也沒有區別。舉例來說,HTTP中的PUT就具有冪等性的語義,而POST則不具有這一語義。作者提到了一個可能發生的場景:假設服務器在完成某個操作之後突然崩潰而來不及發送確認信息,客戶端就有可能在超時之後再次發送這個實際上已經完成的請求,如果此時服務器完成了重啟,就有可能再次執行這一操作。而如果該操作不滿足冪等性,就可能產生一些意外的副作用。
分布式計算備忘錄
Jim Waldo和Sam Kendall等人共同撰寫了一篇非常有名的論文“分布式計算備忘錄”,這篇論文在Reddit上被人推薦為“每個程序員都應當至少讀上兩篇”的論文。在這篇論文中,作者表示“忽略本地計算與分布式計算之間的區別是一種危險的思想”,特別指出了Emerald、Argus、DCOM以及CORBA的設計問題。作者將這些設計問題歸納為“三個錯誤的原則”:
十年一輪回的錯誤
Waldo表示,每過10年,人們就會再次嘗試將本地計算與遠程計算的設計揉合在一起,再一次犯下相同的錯誤。他再次強調:本地計算與遠程計算的本質是完全不同的。
延遲
最明顯的區別就在於延遲問題:如果忽略了延遲問題,軟件的性能就會受到直接影響。Waldo表示,“依賴於底層硬件速度的逐步提高”是錯誤的,一些實際的問題是很難通過測試找出的。性能分析是一個復雜的問題,在某一時刻表現良好的設計未必永遠是合適的。
內存訪問
Waldo對內存訪問的批評是特定於CORBA與它的繼任者的:對象可能會引用在同一地址空間內的指針,但一旦對象產生了移動,這些指針就會變得無效化。他認為處理這一問題的一種途徑是使用分布式共享內存,但在實踐上更常見的做法是使用封送或CORBA引用替換技術。
局部故障
作者在最後談到了一個最本質的問題:局部故障。在本地計算中,故障都是可檢測的。而在分布式計算中,相互獨立的組件可能會產生故障,並且故障可能是局部的。
在文章的開頭部分曾經提到了Vinoski對於RPC的批評,他認為選擇RPC的唯一原因在於開發者的舒適感。在提出這一說法幾年之後,他提出了幾個非常重要的論點:
分布式編程語言
當我們在談到分布式編程語言時,多數開發者所想到的其實只是如何用一般性的編程語言去構建分布式系統。實際上,只要某種語言支持並發元素,並且能夠打開一個網絡套接字,那麼就能夠構建一個分布式系統。而真正的分布式編程語言為分布式特性提供了第一等的支持。像Go這樣的語言更像是一種並發語言,它為並發提供了第一等的支持。���然並發是分布式中的一個重要部分,但他們畢竟還是不同的主題。
而Erlang則為分布式提供了第一等的支持,它雖然同樣使用了RPC機制,但更傾向於在進程之間使用異步消息傳遞方式。受到這一設計優秀表達能力的激勵,Distributed Process與Akka等框架也隨之出現,以提供Erlang風格的語義能力。