天天看點

遠端接口設計經驗分享遠端接口設計經驗分享

分布式架構是網際網路應用的基礎架構,很多新人入職以來就開始負責編寫和調用阿裡的各種遠端接口。但如同結婚一般,用對一個正确的接口就如同嫁一個正确的人一樣,往往難以那麼順利的實作,或多或少大家都會在這個上邊吃虧。

每年雙十一系統調用複盤的時候,我都會聽到以下聲音

你們調我的接口報錯了竟然不會自己重試?

我的傳回值應該從這裡取

我傳回issuccess() == true,不代表業務成功,你還需要判斷error_code

這個error_code沒說全部都要重試啊!

這個error_code必須要重試!

還有很多了,本文的目标就是幫助大家思考,如何設計自己的遠端接口,讓接口做到<code>健壯</code>、<code>易用</code>,節省大家在這塊泥潭中所掙紮的時間。

ps:本例子的代碼可以見 excavatore-demo

...

蒼老師

上課!大家好,我是你們的蒼老師。今天就由我來給大家講講如何編寫一個健壯的遠端接口。老師将在這裡給大家設計一個集中式的日志系統。

雖然這個系統的存在不合理,但這是能找到的最簡單例子,是以不要在課堂上就系統的合理性展開讨論,否則老師會生氣的喲~

遠端接口設計經驗分享遠端接口設計經驗分享

一個集中性的日志伺服器,要求應用通過日志系統提供的日志服務,将所有日志集中統一的輸出到固定的檔案中。

系統架構圖

遠端接口設計經驗分享遠端接口設計經驗分享

小明

遠端接口設計經驗分享遠端接口設計經驗分享

這很簡單嘛,根據系統的要求和架構特性,我很快就能寫出接口定義,老師你看。“如果方法順利無異常傳回,則說明日志已經被成功寫入了日志檔案”

非常好,但這種接口隻能用在單機版的程式中,如果遇到遠端調用的場景就不适用了。要了解這個事實,首先大家就要知道遠端調用的大概實作原理。

遠端接口設計經驗分享遠端接口設計經驗分享

rpc(remote procedure call)遠端過程調用,一種通過網絡從遠端計算機程式上請求服務,而不需要了解底層網絡技術的技術實作。

rpc采用c/s模式。請求程式就是一個客戶機,而服務提供程式就是一個伺服器。首先,客戶機調用程序發送一個有程序參數的調用資訊到服務程序,然後等待應答資訊。在伺服器端,程序保持睡眠狀态直到調用資訊的到達為止。當一個調用資訊到達,伺服器獲得程序參數,計算結果,發送答複資訊,然後等待下一個調用資訊,最後,用戶端調用程序接收答複資訊,獲得程序結果,然後調用執行繼續進行。 以上資訊摘錄自百度百科
遠端接口設計經驗分享遠端接口設計經驗分享

請求過程

<code>用戶端函數</code>将參數傳遞到<code>用戶端句柄</code>。

<code>用戶端句柄</code>将請求序号、遠端方法、參數等資訊封裝到請求對象中,并完成請求對象序列化形成請求封包,通過<code>網絡用戶端</code>發送請求封包。

請求封包通過<code>網絡用戶端</code>與<code>網絡服務端</code>所約定的協定(http、rmi或自定義)進行通訊。

<code>網絡服務端</code>收到請求封包之後,通過反序列化,從請求對象中解析出遠端方法、參數等資訊,并根據這些資訊找到<code>伺服器句柄</code>。

通過<code>伺服器句柄</code>完成<code>伺服器函數</code>的本地調用過程

自此,整個請求流程完成。

應答過程

<code>伺服器函數</code>執行的過程将結果傳回<code>伺服器句柄</code>,傳回的結果可能是正常傳回,也可能是以抛異常的形式傳回。

<code>伺服器句柄</code>根據傳回的值與請求序号封裝到應答對象中,并完成應答對象的序列化,形成應答封包,通過<code>網絡服務端</code>發送應答封包。

應答封包通過<code>網絡服戶端</code>與<code>網絡客務端</code>所約定的協定(http、rmi或自定義)進行通訊。

<code>網絡用戶端</code>收到應答封包之後,通過反序列化,從應答對象中解析出請求序号所挂鈎的<code>用戶端句柄</code>

<code>用戶端句柄</code>将傳回資料傳回到<code>用戶端函數</code>,以傳回值或抛異常的形式将資訊傳回

自此,整個應答流程完成。

一次完整的rpc調用一共分10步,每一步都有可能出錯,是以在設計一個遠端接口的時候必須充分考慮到所有的出錯可能,與用戶端約定出錯的應對方案。無論哪個環節出問題,都要求你的業務邏輯依舊保證不能錯亂!

遠端接口設計經驗分享遠端接口設計經驗分享
遠端接口設計經驗分享遠端接口設計經驗分享

不愧是蒼老師,果然 博 大精深。我明白了,因為增加了遠端通路的因素,是以原本單機中非常小的出錯機率就被放大了,這也不得不讓程式被迫感覺和處理這些通訊錯誤。

那請問遇到這些錯誤都應該怎樣進行歸納和處理呢?

通訊架構錯誤根據發生環節分可以細分為

marshell &amp; unmarshell

c/s雙方采用了不一緻的序列化/反序列化算法,導緻在通訊之前或之後無法正常取得通訊的對象。進而導緻雙方在編碼、解碼的過程中發生錯誤。

如果你的通訊架構使用了hessian那基本上你都有機會遇到過。至于序列化和反序列化的梗,都可以開個專題了。這裡就不在啰嗦。

網絡通訊錯誤

系統錯誤會導緻無法預測的異常産生,具體取決于rpc的實作方式。對于這種錯誤,唯一的處理方式隻有:另外找時間/機會重試。

業務系統錯誤分兩種情況

業務錯誤

client傳遞了違背業務規則的參數,導緻業務邏輯處理失敗。這種錯誤無論重複多少次都會得到一樣結局。

系統錯誤

server處理内部邏輯時出現了無法控制的錯誤,常見的有:

資料庫通路失敗

檔案寫入失敗

網絡通訊失敗

一般遇到這種錯誤,可以通過重試解決。

出錯情況

解決方案

是否重試

通訊架構錯誤

抛出架構異常

重試

抛出系統異常

傳回明确的錯誤碼

禁止重試

遠端接口設計經驗分享遠端接口設計經驗分享

嗯,我了解了,一個好的遠端方法定義必須考慮到上邊所羅列的異常場景,要求做到<code>明确的錯誤處理約定</code>。那請問蒼老師這個接口應該如何寫呢?

先别着急,要寫出健壯的接口,你還有幾個概念要了解。首先我們先來看這個接口的聲明。我的比你多了兩個重要的資訊<code>resultdo&lt;void&gt;</code>與<code>logexception</code>,接下來我會講解這定義這兩個類的作用

遠端接口設計經驗分享遠端接口設計經驗分享

如果你有機會重新搭建一個應用,推薦大家采用分包的政策來考慮自己的子產品組織。

遠端接口設計經驗分享遠端接口設計經驗分享

common:定義core和client所共用的内容

業務接口聲明

logservice

domain對象(這裡為了簡單,所有的do、to、dto都統一命名為do)

resultdo&lt;t&gt;

業務異常

logexception

client:富用戶端,在這一層可以組織cache、業務無關的通用校驗,這一次層并非必須。

服務用戶端實作

logserviceclient

asynclogserviceclient

core:業務服務的實作,這一層的代碼運作在服務端。

服務業務邏輯實作,同時内部按照習慣可以再次分層為(<code>service</code>、<code>manager</code>、<code>dao</code>)

logserviceimpl

遠端接口設計經驗分享遠端接口設計經驗分享

這套rpc接口聲明的理念在于:如何通過約定區分出系統異常與業務異常。區分的關鍵就在于<code>resultdo&lt;?&gt;</code>與<code>logexception</code>上

<code>info</code>方法不需要返值,但服務端需要在業務出錯的時候,将錯誤碼傳回給用戶端,以便友好的錯誤提示。是以在result對象中有兩個方法:

<code>public boolean issuccess();</code>

<code>issuccess</code>為<code>true</code>時表明業務處理成功:當用戶端擷取到這個值時,表明服務端已正确經接請求到并且成功的處理了這個請求,業務完成。這是最好的情況。

<code>issuccess</code>為<code>false</code>時表明業務處理失敗:當用戶端擷取到時,表明服務端已經正确接到請求,但業務處理失敗,失敗原因在錯誤碼<code>errorcode</code>中展現。

<code>public string geterrorcode();</code>

當服務端正确接到請求,但業務處理失敗時,失敗的原因以錯誤碼形式傳回。

這個異常主要用于收縮和屏蔽服務層的具體錯誤資訊,當服務端遇到無法處理的錯誤情況時,需要繼續向用戶端外抛,讓用戶端來擇機進行重試。用戶端亦可通過logexception快速判斷目前業務中斷的原因來自于logservice的失敗。

用戶端處理邏輯表

調用情況

issuccess

errorcode

throw logexception

throw exception

用戶端處理

架構錯誤

/

true

false

不重試

成功傳回

所有情況也不是一層不變。比如<code>業務錯誤</code>傳回錯誤碼,但有時處于性能考慮(抛異常非常消耗jvm性能),可以在接口聲明中約定部分錯誤碼也必須要進入重試。但這種場景越少越好,而且一旦做出約定,出于接口向下相容的考慮,這種需要重試的錯誤碼自聲明以來,隻能減少不能增加,否則會引起相容問題。

老師也見過有系統在resultdo中聲明了<code>public boolean isretry();</code>方法,這樣當系統發生業務錯誤的時候,是否重試的判斷就交由<code>isretry()</code>來進行判斷,這也是不錯的選擇。

遠端接口設計經驗分享遠端接口設計經驗分享

增加isretry後的用戶端處理邏輯表

isretry

老實說,這一層不是必須的,很多情況下用戶端直接使用服務端聲明的service接口足矣。但若遇到在用戶端容災、增強的場景,則serviceclient的優勢就展現出來。

遠端接口設計經驗分享遠端接口設計經驗分享
遠端接口設計經驗分享遠端接口設計經驗分享

一個好的系統約定能減少很多不必要的錯誤,但畢竟不是所有系統都是新的系統,在面臨各種<code>先人的智慧</code>時,如何讓不符合約定的遠端接口也納入約定來?

在面對<code>先人的智慧</code>時,改變現有被大量調用的接口聲明是不可能的,在這種情況下<code>存在即合理</code>,哪怕明知接口聲明或實作存在問題,你也不能去變更這個接口。接口維護原則請聽下堂課《遠端接口維護經驗分享》。

當遇到這種不在約定的接口時,需要用<code>裝飾模式</code>将不規範的接口包裝成為規範的接口。

遠端接口設計經驗分享遠端接口設計經驗分享

幾乎可以肯定的,在公司中你肯定不是第一個聲明接口的人。是以當你定出了遠端接口設計規範之後,如何面對老接口則成了一個頭疼的問題。

<code>先人的智慧</code>是無窮的,現在我們讨論的問題,我們的前輩都已經面臨并解決了(運氣不好你可能還會遇到新手練手寫的接口),隻是解決的方法各種各樣,沒有形成約定。何解?

此時可以考慮使用<code>裝飾模式</code>将不規範的接口重新包裝成符合設計規範的接口,這樣做有兩個好處:

解決老接口不規範問題

減小老接口暴露到業務代碼中的機率

這裡需要解釋下。外部接口的定義不受控制,如果此時一個service需要更新,則改動、回歸、代碼review範圍僅限于wrapper類即可,若将所有業務代碼直接引用外部的service/serviceclient類,則更新的回歸面将被放大。

是以無論對方聲明的接口是否符合約定,我都會建議用戶端不要直接使用service/serviceclient,而是wrapper一層。

遠端接口設計經驗分享遠端接口設計經驗分享

太好了,經過老師提點,我終于寫出了一個健壯的遠端接口,并知道如何與用戶端約定重試的關系。

不過我還是想問問,這種遠端的日志系統存在是否不是太合理,老師你舉這個例子是不是不太恰當?

遠端接口設計經驗分享遠端接口設計經驗分享