(原始碼)net/rpc中一個函式呼叫的旁白
- 身份介紹
- 正題
- 收益
- 壞處
- net/RPC 原始碼剖析
- 主要名詞解釋
- 客戶端流程講解:
- 服務端流程講解:
- 初始化
- 啟動
- 從設計模式的角度理解net/rpc原始碼的設計
身份介紹
大家好,我的名字叫小R,大家也可以叫我的全名:RPC請求,我從出生開始,就背負著全村人的希望,但我知道等待我的將會是一條漫長的鏈路,雖然在整個鏈路之中,會有多名長輩(Call,Method,Client等等)為我保駕護航,但我仍舊會經歷許多的磨練(序列化+反序列化)直到將我的精神(請求引數)傳達到中央(服務端),此時我的任務就結束了,但我知道一定會有來者(回應),將我的軀體轉化成果實(回應結果)傳回我的村子里(client),無論如何,我都將在這里敘述我的故事,
正題
RPC,全稱 remote procedure call,中文名叫遠程函式呼叫,做的事情非常簡單,就是遠程的,這里的遠程可以指的是本地其他的函式,或者是其他機器、集群、機房上的,但是歸根結底,就是客戶端和服務端的一次通信,這樣的通信還有很多,比如資料庫設計中的主從設計、計算存盤分離,都和RPC大同小異,
收益
RPC實際上最大的收益就是:由于客戶端不需要關心服務端的函式如何實作(整個程序對于客戶端來說是黑盒的),從擴容的角度來看,在服務端測可以支持無限的水平擴容,從業務側開發的角度來看,也極大地減少了業務側開發的成本,
壞處
上面說到客戶端不需要關心服務端如何實作,特別是在企業級專案中,RPC經常被用作微服務架構的通信基礎,那么實際上服務端要想保證高可用+高穩定性,就必須要做到如下幾點:
- 強大的負載均衡+高效的服務發現
- 相應的熔斷、限流措施,優雅的重試機制
- 彈性擴容、縮容機制
相比于單體架構微服務架構會將上述的幾點的影響轉化到最大,相應的也就增加了底層負載均衡等微服務水平結構的開發難度,
net/RPC 原始碼剖析
主要名詞解釋
- Client:
- Request:是鏈表結構,本質上是一次請求頭部寫入,這里僅僅用來記錄呼叫方法名、seq(seq類似一個時間戳的概念,每一次呼叫都會分配一個seq number,且后呼叫RPC的seq number永遠大于先呼叫的)以及下一個request,和資料庫中的log有點類似,這里的作用是可以用來追溯請求記錄,自定義上報埋點,或者分析網路狀況等等
- ClientCodec:Client-Server 鏈路傳輸中的編碼解碼庫,會講整合后的RPC編碼成相應的流寫入RPC Session,以便server讀取,同時對服務端回傳的回應體進行解碼,供Client讀取,
- Call:正在執行的一次RPC呼叫,
- Server:
- serviceMap:一個執行緒安全的map,后來記錄不同的服務名與服務本身的映射,
- freeReq:同Client端的Request解釋,
- freeResp:和Request作用基本相同,不贅述了,
下面我將用一張圖來講述一下鏈路中的RPC呼叫函式的執行流程

客戶端流程講解:
- 呼叫Call 函式:
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error,call函式是對外暴露給客戶端呼叫的介面
serviceMethod服務+方法名,為字串型別,有大小寫檢查,例如我想呼叫Service A中的Get方法,那么就是A.Get
args呼叫RPC方法引數,一般為結構體
reply一般為回傳,根據預期的回傳值創建相應的結構體 - Call函式會呼叫Go函式,主要作用是創建channel,保證請求執行的有序性(這里可能還有一個目的是可以通過設定channel的大小來防范針對RPC的洪泛攻擊),channel的型別是Call型別,默認大小是1,主要作用是傳遞Call和接收從服務端回傳的Call
在Go()這個方法里,主要做了這么幾件事- 將
serviceMethod, args, reply, channel封裝進Call里面 - 呼叫
send(call *Call)
- 將
func (client *Client) send(call *Call)
Send() 主要作用是- 進一步的創建request,用作鏈路追蹤和回溯 以及
- 將request和args寫入session
至此為止,呼叫一次遠程呼叫的request的全部資訊已經組裝完成,寫入session的操作是:呼叫flush()函式將組裝的的request+call寫入I/O writer,等待服務端讀取,相應步驟涉及到的函式如下:
* err := client.codec.WriteRequest(&client.request, call.Args)
* c.encBuf.Flush() // Flush writes any buffered data to the underlying io.Writer.
服務端流程講解:
初始化
我們會在代碼里手動初始服務端監聽執行緒(這個程序可能在本地,也可以是利用http協議開放ip+埠的形式啟動)
Register(new(Service))
rpc.HandleHTTP()
l, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
go http.Serve(l, nil)
所以實際上很清楚(low)了,RPC雖然啟用的是tcp的協議,但是本質上還是套了一個http的殼,創建TCP連接,利用http的調度去監聽TCP的listener,所以個人感覺RPC實際上是一種特殊的http,或者可以說RPC的application layer是基于http的
啟動
我們的遠端RPC呼叫通常是按這樣的順序去執行的
func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request)func (server *Server) ServeConn(conn io.ReadWriteCloser)func (server *Server) ServeCodec(codec ServerCodec)func (s *service) call(server *Server, sending *sync.Mutex, wg *sync.WaitGroup, mtype *methodType, req *Request, argv, replyv reflect.Value, codec ServerCodec)
呼叫ServeHTTP的時候會創建一個connection(顧名思義,用來接受http報文的連接),這個connection會被當作入參傳入到ServeConn中,在ServeConn中會創建一個gobServerCodec的結構體,并作為入參傳入到ServeCodec,ServeCodec主要是為了解碼request和編碼response而生的,解碼request之后,會呼叫Call函式去執行相應的代碼,并回傳Response,Response同客戶端中Request的編碼寫入邏輯類似,就不再贅述了,
從設計模式的角度理解net/rpc原始碼的設計
假如說讓我從0開始,不借助任何的資料,我是萬萬設計不出這樣高內聚低耦合的代碼,其實看別人的代碼能理解只是第一步,最重要的是要學習這個代碼的設計思路,有時候可能好的代碼能比你只多傳一個引數,但少寫幾十行兼容
下面我來列舉一下我任何設計的比較好的代碼塊,
同時也希望各位大佬能在留言區討論
ServeCodeC和ServeConn的拆分:之所以這樣拆分,是保證了對于上層ServeConn來說,不需要關心底層的解碼編碼邏輯,同時如果底層換了一套這樣的邏輯,我們也不需要對上層有過多的更改,總結起來:在設計代碼的時候,我們需要考慮,如果下層代碼執行邏輯發生了顛覆性變更的時候,改動上層的成本有多少,耦合度越低的代碼,改動成本一定越小,Request的設計:request的設計我起初以為是一種在鏈路傳輸中的基本單元,后來發現不是,鏈表結構的Request更多的作用是鏈路的一個監控和追蹤,能夠更好的監控網路中的狀況,有了這個Request,我們可以根據其做更多的自定義監控機制,為上層業務以及容災容錯設計提供了一種思路和基礎,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/257779.html
標籤:其他
上一篇:Redis初識
下一篇:什么是 geobuf?
