雖然我們還有Zookeeper的ZAB共識演算法還沒看,
但是RPC算是很基礎的分散式系統溝通方法,在Raft
裡面也是直接使用並將Spec寫在論文中,因此我們先來看一下分散式系統怎麼溝通的吧。
其實老實說我是碩班讀Paper才第一次看到這個名詞,也才知道原來有這樣的一種方法,不知道為什麼我大學從沒聽過這個,可能是我漏聽了吧(?
一般來說在寫code的時候,一般都會用functions或是Class來讓程式模組化提供「功能」。而這些functions與物件通常都是在自己機器上執行,甚至可以說這些功能在程式被編譯成Binary code或是開始執行後一般來說是不會提供給別的程式甚是另一台機器上的程式使用的。
而Remote Procedure Call(RPC)便是將本地程式的function或是物件裡的method,外顯出來,利用TCP/UDP/HTTP各種網路通訊方法,讓別台機器上面運行的程式可以透過網路呼叫你的這個function或是method。
RPC的目的:
正如上面的敘述,RPC簡單說就是一台機器上面的程式將自己的function expose出來,透過網路介面接受另外一台機器上面的程式呼叫。
因此我們稱提供functions的為server,呼叫的為client。
步驟也非常直觀,
此方法將function抽象出來,對Client來說他以為他是呼叫本地function(寫code時也是一樣的感覺),然後得到return值
但是其實底層是將參數傳到另外一台機器上並使用那一台機器的function得到結果回傳。
但是回到分散式系統的本質,哈哈沒錯,就是網路一定會出包!
像這樣有經過網路的設計或是協定一定是要考量錯誤處理的部份。
除此之外另一件事情就是如同使用本地的function一樣,function的呼叫可能是synchronous的,因此如果處理到一半另外一邊斷線了,怎麼辦呢?
(一路看到這裡應該有底了: 斷線的101種解法...Timeout xD)
我們整理一下可能的錯誤:
從Client角度來說,他甚至沒辦法區分上面的錯誤,也不知道發生什麼事。
第一種方法就是Client呼叫後如果超過某個時間沒收到回覆,就再retry一次。
並設定retry次數,如果超過次數就返回失敗。
問題:
可能Client -> Server的通訊是正常的,但是回來的通訊出問題,因此Server每次回傳結果都失敗 => 浪費Server資源
如果此RPC操做並不是idempotent,這個比較嚴重。比方說此RPC code是對一個k-v store server呼叫set:
Best Effort的要求是function為idempotent,不改變狀態,類似functional programming那種概念或是Amazon Lambda function,才會採用Best Effort。
如果我們重新看一次上面錯誤的範例,其實除了function為idempotent的方式外,還有另外一種解決方法。
那就是讓Server判斷此次呼叫是否跟之前的一樣,也就是Server可以判斷是否是重複呼叫,如果是,就直接返回暫存的結果,而不重新執行function。
也因此Client的呼叫可以帶上ID,也就是跟參數一起被encoding的metadata,例如: (Client IP, Timestamp)
因為同一個呼叫不會被重複呼叫兩次,所以稱為At Most Once。
但是這會產生兩個問題:
什麼時候可以刪除掉暫存的訊息?
Server如果必須暫存所有的結果這是很佔空間的,尤其分散式系統有很多Clients會呼叫Server的function。
如果上一個RPC Server還沒處理完,Client已經Retry了怎麼辦?
如果Server 失效重啟怎麼辦?
Golang的RPC為 "At-Most-Once"
步驟:
package main
import (
"log"
"net"
"net/rpc"
)
// 作為rpc的server func,必須接受兩個參數 (duck typing) (request string, reply *string)
// 另外返回值為error,必須為公開的方法
type HelloService struct{}
func (h *HelloService) Hello(request string, reply *string) error {
*reply = "hello: " + request
return nil
}
func main() {
// rpc.Register會將object中所有滿足rpc規則的方法註冊為rpc函數
// 所有註冊的方法會被放在HelloService服務空間下。
rpc.RegisterName("HelloService", new(HelloService))
// 建立聆聽一個tcp連線,
listner, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
for {
conn, err := listner.Accept()
if err != nil {
log.Fatal(err)
}
// rpc.ServeConn在該連線上提供client rpc函數
go rpc.ServeConn(conn)
}
}
package main
import (
"fmt"
"log"
"net/rpc"
)
func main() {
// rpc.Dial 連接該rpc服務
conn, err := rpc.Dial("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
var reply string
// 透過conn.Call來使用rpc方法。
// conn.Call(serviceMethod, args, reply)
conn.Call("HelloService.Hello", "Jack", &reply)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply)
}
以上就是關於RPC的簡介啦
Ref: