5分鐘學(xué)會 gRPC

image

介紹

我猜測大部分長期使用 Java 的開發(fā)者應(yīng)該較少會接觸 gRPC玖雁,畢竟在 Java 圈子里大部分使用的還是 Dubbo/SpringClound 這兩類服務(wù)框架费奸。

我也是近段時間有機(jī)會從零開始重構(gòu)業(yè)務(wù)才接觸到 gRPC 的裳瘪,當(dāng)時選擇 gRPC 時也有幾個原因:

image
  • 基于云原生的思路開發(fā)部署項目贤徒,而在云原生中 gRPC 幾乎已經(jīng)是標(biāo)準(zhǔn)的通訊協(xié)議了。
  • 開發(fā)語言選擇了 Go淆两,在 Go 圈子中 gRPC 顯然是更好的選擇住闯。
  • 公司內(nèi)部有部分業(yè)務(wù)使用的是 Python 開發(fā)瓜浸,在多語言兼容性上 gRPC 支持的非常好。

經(jīng)過線上一年多的平穩(wěn)運(yùn)行比原,可以看出 gRPC 還是非常穩(wěn)定高效的插佛;rpc 框架中最核心的幾個要點(diǎn):

  • 序列化
  • 通信協(xié)議
  • IDL(接口描述語言)

這些在 gRPC 中分別對應(yīng)的是:

  • 基于 Protocol Buffer 序列化協(xié)議,性能高效量窘。
  • 基于 HTTP/2 標(biāo)準(zhǔn)協(xié)議開發(fā)雇寇,自帶 stream、多路復(fù)用等特性蚌铜;同時由于是標(biāo)準(zhǔn)協(xié)議锨侯,第三方工具的兼容性會更好(比如負(fù)載均衡、監(jiān)控等)
  • 編寫一份 .proto 接口文件冬殃,便可生成常用語言代碼囚痴。

HTTP/2

學(xué)習(xí) gRPC 之前首先得知道它是通過什么協(xié)議通信的,我們?nèi)粘2还苁情_發(fā)還是應(yīng)用基本上接觸到最多的還是 HTTP/1.1 協(xié)議审葬。

image

由于 HTTP/1.1 是一個文本協(xié)議深滚,對人類非常友好奕谭,相反的對機(jī)器性能就比較低。

需要反復(fù)對文本進(jìn)行解析痴荐,效率自然就低了血柳;要對機(jī)器更友好就得采用二進(jìn)制,HTTP/2 自然做到了蹬昌。

除此之外還有其他優(yōu)點(diǎn):

  • 多路復(fù)用:可以并行的收發(fā)消息混驰,互不影響
  • HPACK 節(jié)省 header 空間攀隔,避免 HTTP1.1 對相同的 header 反復(fù)發(fā)送皂贩。

Protocol

gRPC 采用的是 Protocol 序列化,發(fā)布時間比 gRPC 早一些昆汹,所以也不僅只用于 gRPC明刷,任何需要序列化 IO 操作的場景都可以使用它。

它會更加的省空間满粗、高性能辈末;之前在開發(fā) https://github.com/crossoverJie/cim 時就使用它來做數(shù)據(jù)交互。

package order.v1;

service OrderService{

  rpc Create(OrderApiCreate) returns (Order) {}

  rpc Close(CloseApiCreate) returns (Order) {}

  // 服務(wù)端推送
  rpc ServerStream(OrderApiCreate) returns (stream Order) {}

  // 客戶端推送
  rpc ClientStream(stream OrderApiCreate) returns (Order) {}
  
  // 雙向推送
  rpc BdStream(stream OrderApiCreate) returns (stream Order) {}
}

message OrderApiCreate{
  int64 order_id = 1;
  repeated int64 user_id = 2;
  string remark = 3;
  repeated int32 reason_id = 4;
}

使用起來也是非常簡單的映皆,只需要定義自己的 .proto 文件挤聘,便可用命令行工具生成對應(yīng)語言的 SDK。

具體可以參考官方文檔:
https://grpc.io/docs/languages/go/generated-code/

調(diào)用

    protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    test.proto
image

生成代碼之后編寫服務(wù)端就非常簡單了捅彻,只需要實現(xiàn)生成的接口即可组去。

func (o *Order) Create(ctx context.Context, in *v1.OrderApiCreate) (*v1.Order, error) {
    // 獲取 metadata
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Errorf(codes.DataLoss, "failed to get metadata")
    }
    fmt.Println(md)
    fmt.Println(in.OrderId)
    return &v1.Order{
        OrderId: in.OrderId,
        Reason:  nil,
    }, nil
}
image

客戶端也非常簡單,只需要依賴服務(wù)端代碼步淹,創(chuàng)建一個 connection 然后就和調(diào)用本地方法一樣了从隆。

這是經(jīng)典的 unary(一元)調(diào)用,類似于 http 的請求響應(yīng)模式缭裆,一個請求對應(yīng)一次響應(yīng)键闺。

image

Server stream

gRPC 除了常規(guī)的 unary 調(diào)用之外還支持服務(wù)端推送,在一些特定場景下還是很有用的澈驼。

image
func (o *Order) ServerStream(in *v1.OrderApiCreate, rs v1.OrderService_ServerStreamServer) error {
    for i := 0; i < 5; i++ {
        rs.Send(&v1.Order{
            OrderId: in.OrderId,
            Reason:  nil,
        })
    }
    return nil
}

服務(wù)端的推送如上所示辛燥,調(diào)用 Send 函數(shù)便可向客戶端推送。

    for {
        msg, err := rpc.RecvMsg()
        if err == io.EOF {
            marshalIndent, _ := json.MarshalIndent(msgs, "", "\t")
            fmt.Println(msg)
            return
        }
    }

客戶端則通過一個循環(huán)判斷當(dāng)前接收到的數(shù)據(jù)包是否已經(jīng)截止來獲取服務(wù)端消息缝其。

為了能更直觀的展示這個過程挎塌,優(yōu)化了之前開發(fā)的一個 gRPC 客戶端,可以直觀的調(diào)試 stream 調(diào)用氏淑。

image

上圖便是一個服務(wù)端推送示例勃蜘。

Client Stream

image

除了支持服務(wù)端推送之外,客戶端也支持假残。

客戶端在同一個連接中一直向服務(wù)端發(fā)送數(shù)據(jù)缭贡,服務(wù)端可以并行處理消息炉擅。


// 服務(wù)端代碼
func (o *Order) ClientStream(rs v1.OrderService_ClientStreamServer) error {
    var value []int64
    for {
        recv, err := rs.Recv()
        if err == io.EOF {
            rs.SendAndClose(&v1.Order{
                OrderId: 100,
                Reason:  nil,
            })
            log.Println(value)
            return nil
        }
        value = append(value, recv.OrderId)
        log.Printf("ClientStream receiv msg %v", recv.OrderId)
    }
    log.Println("ClientStream finish")
    return nil
}

    // 客戶端代碼
    for i := 0; i < 5; i++ {
        messages, _ := GetMsg(data)
        rpc.SendMsg(messages[0])
    }
    receive, err := rpc.CloseAndReceive()

代碼與服務(wù)端推送類似,只是角色互換了阳惹。

image

Bidirectional Stream

image

同理谍失,當(dāng)客戶端、服務(wù)端同時都在發(fā)送消息也是支持的莹汤。

// 服務(wù)端
func (o *Order) BdStream(rs v1.OrderService_BdStreamServer) error {
    var value []int64
    for {
        recv, err := rs.Recv()
        if err == io.EOF {
            log.Println(value)
            return nil
        }
        if err != nil {
            panic(err)
        }
        value = append(value, recv.OrderId)
        log.Printf("BdStream receiv msg %v", recv.OrderId)
        rs.SendMsg(&v1.Order{
            OrderId: recv.OrderId,
            Reason:  nil,
        })
    }
    return nil
}
// 客戶端
    for i := 0; i < 5; i++ {
        messages, _ := GetMsg(data)
        // 發(fā)送消息
        rpc.SendMsg(messages[0])
        // 接收消息
        receive, _ := rpc.RecvMsg()
        marshalIndent, _ := json.MarshalIndent(receive, "", "\t")
        fmt.Println(string(marshalIndent))
    }
    rpc.CloseSend()

其實就是將上訴兩則合二為一快鱼。

image

通過調(diào)用示例很容易理解。

元數(shù)據(jù)

gRPC 也支持元數(shù)據(jù)傳輸纲岭,類似于 HTTP 中的 header抹竹。

    // 客戶端寫入
    metaStr := `{"lang":"zh"}`
    var m map[string]string
    err := json.Unmarshal([]byte(metaStr), &m)
    md := metadata.New(m)
    // 調(diào)用時將 ctx 傳入即可
    ctx := metadata.NewOutgoingContext(context.Background(), md)
    
    // 服務(wù)端接收
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Errorf(codes.DataLoss, "failed to get metadata")
    }
    fmt.Println(md) 

gRPC gateway

gRPC 雖然功能強(qiáng)大使用也很簡單,但對于瀏覽器止潮、APP的支持還是不如 REST 應(yīng)用廣泛(瀏覽器也支持窃判,但應(yīng)用非常少)。

為此社區(qū)便創(chuàng)建了 https://github.com/grpc-ecosystem/grpc-gateway 項目喇闸,可以將 gRPC 服務(wù)暴露為 RESTFUL API袄琳。

image

為了讓測試可以習(xí)慣用 postman 進(jìn)行接口測試,我們也將 gRPC 服務(wù)代理出去燃乍,更方便的進(jìn)行測試唆樊。

反射調(diào)用

作為一個 rpc 框架,泛化調(diào)用也是必須支持的刻蟹,可以方便開發(fā)配套工具逗旁;gRPC 是通過反射支持的,通過拿到服務(wù)名稱座咆、pb 文件進(jìn)行反射調(diào)用痢艺。

https://github.com/jhump/protoreflect 這個庫封裝了常見的反射操作。

上圖中看到的可視化 stream 調(diào)用也是通過這個庫實現(xiàn)的介陶。

負(fù)載均衡

由于 gRPC 是基于 HTTP/2 實現(xiàn)的堤舒,客戶端和服務(wù)端會保持長連接;這時做負(fù)載均衡就不像是 HTTP 那樣簡單了哺呜。

而我們使用 gRPC 想達(dá)到效果和 HTTP 是一樣的舌缤,需要對請求進(jìn)行負(fù)載均衡而不是連接。

通常有兩種做法:

  • 客戶端負(fù)載均衡
  • 服務(wù)端負(fù)載均衡

客戶端負(fù)載均衡在 rpc 調(diào)用中應(yīng)用廣泛某残,比如 Dubbo 就是使用的客戶端負(fù)載均衡国撵。

gRPC 中也提供有相關(guān)接口,具體可以參考官方demo玻墅。

https://github.com/grpc/grpc-go/blob/87eb5b7502/examples/features/load_balancing/README.md

客戶端負(fù)載均衡相對來說對開發(fā)者更靈活(可以自定義適合自己的策略)介牙,但相對的也需要自己維護(hù)這塊邏輯,如果有多種語言那就得維護(hù)多份澳厢。

所以在云原生這個大基調(diào)下环础,更推薦使用服務(wù)端負(fù)載均衡囚似。

可選方案有:

  • istio
  • envoy
  • apix

這塊我們也在研究,大概率會使用 envoy/istio线得。

總結(jié)

gRPC 內(nèi)容還是非常多的饶唤,本文只是作為一份入門資料希望能讓不了解 gRPC 的能有一個基本認(rèn)識;這在云原生時代確實是一門必備技能贯钩。

對文中的 gRPC 客戶端感興趣的朋友募狂,可以參考這里的源碼:
https://github.com/crossoverJie/ptg

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市角雷,隨后出現(xiàn)的幾起案子祸穷,更是在濱河造成了極大的恐慌,老刑警劉巖谓罗,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粱哼,死亡現(xiàn)場離奇詭異季二,居然都是意外死亡檩咱,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門胯舷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來刻蚯,“玉大人,你說我怎么就攤上這事桑嘶〈缎冢” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵逃顶,是天一觀的道長讨便。 經(jīng)常有香客問我,道長以政,這世上最難降的妖魔是什么霸褒? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮盈蛮,結(jié)果婚禮上废菱,老公的妹妹穿的比我還像新娘。我一直安慰自己抖誉,他們只是感情好殊轴,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著袒炉,像睡著了一般旁理。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上我磁,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天孽文,我揣著相機(jī)與錄音淹接,去河邊找鬼。 笑死叛溢,一個胖子當(dāng)著我的面吹牛塑悼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播楷掉,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼厢蒜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了烹植?” 一聲冷哼從身側(cè)響起斑鸦,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎草雕,沒想到半個月后巷屿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡墩虹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年嘱巾,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片诫钓。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡旬昭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出菌湃,到底是詐尸還是另有隱情问拘,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布惧所,位于F島的核電站骤坐,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏下愈。R本人自食惡果不足惜纽绍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望驰唬。 院中可真熱鬧顶岸,春花似錦、人聲如沸叫编。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搓逾。三九已至卷谈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間霞篡,已是汗流浹背世蔗。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工端逼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人污淋。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓顶滩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親寸爆。 傳聞我的和親對象是個殘疾皇子礁鲁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容