grpc是基于http/2协议的高性能的rpc框架
師傅領進門,修行在個人,跟著官方腳手架demo了grpc后,之后就需要擴展前后知識邊界,下面總結grpc的前世今生和最佳實踐。
https://www.cnblogs.com/JulianHuang/p/14441952.html
- grpc是基于http/2協議的高性能的rpc框架
- 為什么已經有http? 還需要grpc?
- 八股文都說grpc是基于http2的rpc框架,到底利用了http2的什么特性 ?
- 一個grpc內存泄漏的例子
grpc是基于http/2協議的高性能的rpc框架。
提取句式中關鍵信息:rpc框架、 http2、 高性能
1.落地賓語-rpc框架
遠程過程調用 remote process call;
程序可以像調用本地函數和本地對象一樣, 達成調用遠程服務的效果(不用意識到是遠程服務),rpc屏蔽了底層的通信細節和打解包細節。
跟許多rpc協議一樣, grpc也是基于IDL(interface define lauguage)來定義服務協議。
rpc的開發模式, 必然強調契約優先, client和server端首先約定service的結構(包括一系列方法的組合、每個方法具體簽名)。
對這個結構的描述,gRPC 默認是用Protocol Buffer去實現的。
syntax = "proto3";
option csharp_namespace = "GrpcAuthor";
package greet;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply);
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings.
message HelloReply {
string message = 1;
}
rpc框架兩個關鍵通用部件:
- channel: 通信信道
- stub: 是client的抽象,中文名叫存根。
① 建立與grcp服務端的通信信道;
② 基于步驟①的信道使用服務名Greet建立grpc服務在客戶端的存根, 就像服務Greet是本地服務一樣;
③ 用步驟②的grpc服務存根發起grpc調用GreeterClient(),就像GreeterClient是本地方法一樣。
var channel = GrpcChannel.ForAddress(
"http:///my-example-host",
new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure });
var client = new Greet.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = "world" });
rpc 是一種久遠的通信框架, http是通信協議。
2. 目前市面流行http, 為什么還需要grpc?
其實rpc框架比 http協議更早出來。
tcp協議于70年代誕生,tcp是一種可靠的、面向連接的、基于字節流的傳輸層協議。
tcp粘包?
關于tcp的定語和修飾詞也很關鍵,正因為是字節流,就如同躺在水流中的0/1串, 這些0/1串是沒有任何邊界的,應用層傳到tcp層的數據不是以消息報為單位向對面主機發送,而是以字節流的形式流淌在tcp層, 在tcp傳輸層由tcp層的協議進行切割和組包, 對端接收的時候沒能正確還原發送端的消息報。
tcp粘包問題并不是tcp協議的弊端,而是我們在應用層發送和接收數據需要對數據分段,而傳輸層是無邊界的0/1串之間的矛盾。
在70到90年代之后,互聯網并不發達,很多都是client、server點對點的傳輸,所以在那個年代rpc很活躍,client、server雙方約定服務結構,以[類本地調用]的形態通信。
90年代,隨著it產業的蓬勃發展,計算機走進了千家萬戶,90年代初期誕生了瀏覽器, 瀏覽器與上面的C/S結構最大的不同是訪問的服務端對象是千萬家不同的服務提供方。
再像C/S那樣一對一提前溝通契約就不合適了, 故瀏覽器和web服務器作為一種特殊的C/S需要約定一種【固定的、能自表述的傳輸格式】, 于是誕生了適用于B/S端的http協議。 http協議不再有rpc雙方已知的本地服務名和服務方法, 服務和服務方法被金演化成對遠程主機的資源請求。
所以,我們該問的不是“既然有HTTP協議,為什么要有RPC”,而是應該問“為什么有rpc,還需要有http”。
從上面的前世今生可以知道, rpc是通信框架,http是通信協議,rpc可以基于tcp,udp,http/2協議來實現。
grpc在眾多rpc框架中脫穎而出,取決于底層的http2基礎設施。
3. grpc到底利用了http/2的什么特性?
回過頭來看grpc的連兩個定語 ① http/2 ② 高性能。
grpc底層傳輸使用http/2,http/2兼容http1.1語義,還有如下優勢
| http2 | http1.1 |
|---|---|
| 用于數據傳輸的二進制分幀 | http1.1是基于文本協議 |
| 同一tcp連接支持流式傳輸,故支持發送多個并行請求、調用 | 應答模型:http1.1在一個tcp連接上完成[請求/響應]是串行的 |
| 減少網絡使用率的標頭壓縮 | 頭部帶有大量信息,每次都要重復發送 |
http2的二進制分幀、流式傳輸 能力支撐了grpc框架近乎本地的實時服務互調;
http2的多路復用(單tcp連接上并發多個請求,不多占文件描述符)、二進制編碼協議、頭部壓縮支撐了grpc本地互調的高性能。
這里要指出:
- HTTP/2 上的通信只需要在一個TCP連接完成,在這個連接上可見的形態是幀frame和流stream。
- 而消息(或者說業務調用,業務上的邏輯發送單位)由一或多個幀組成,這些幀可以亂序發送,然后根據每個幀首部的流標識符重新組裝。
- "gRPC雙向流通信streaming"與"http2的流式stream傳輸"是一個東西嗎?
http2流式傳輸stream是一種底層的傳輸方式,其作用是支撐單連接多路復用 。
grpc流式通信streaming,更接近業務通信級別的通信方式,grpc流式通信可用于替代高性能場景下的一元gRPC調用。
我們假設HTTP/2協議中1次RPC請求使用1個并發Stream,每個RPC消息又可 通過幀體中 Length-Prefixed Message 頭部確立了邊界,這樣,在 Stream 中連續地發送多個 DATA 幀,就可以實現流模式 RPC。
https://juejin.cn/post/7249522846211801147
tcp連接現在是性能瓶頸
底層的http2協議給予了grpc很大的性能表現,但同時也帶來了新的性能瓶頸, 現在現在壓力給到了tcp連接。
通常情況下,一個HTTP/2 tcp連接中流的數量是有限制的,一般服務器默認為100,不同的語言有不同的應對策略。
.net
.net tcp連接上默認流數量為100,當該連接中的grpc調用導致到達"連接流的限制",新的grpc調用會進行排隊,這個時候會出現因Concurrent stream=100引起的性能問題。
.NET引入了
EnableMultipleHttp2Connections可在此時產生新的連接對象。
var channel = GrpcChannel.ForAddress("https://localhost", new GrpcChannelOptions
{
HttpHandler = new SocketsHttpHandler
{
EnableMultipleHttp2Connections = true,
// ...configure other handler settings
}
});
golang
[golang tcp連接上流數量的默認限制也是100](https://github.com/grpc/grpc-go/blob/master/internal/transport/http2_client.go)defaultMaxStreamsClient =100.
可通過ServerOption: MaxConcurrentStreams修改, 從實現上默認的并發流的限制,被設計成池的概念,
類似與C# 也有利用tcp連接池來規避該問題的方案, 按下不表。
https://segmentfault.com/a/1190000041716350?utm_source=sf-similar-article
https://github.com/shimingyah/pool
我們先來分析grpc基于http2 stream概念發起調用的源碼。
創建一個gRPC 客戶端連接,會創建的幾個協程:
1)transport.loopyWriter.run 往服務端發送數據協程
2)transport.http2Client.reader 讀取服務端數據協程
http2Client的基礎結構
// 源碼所在文件:google.golang.org/grpc/http2_client.go
// http2Client 實現了接口 ClientTransport
// http2Client implements the ClientTransport interface with HTTP2.
type http2Client struct {
conn net.Conn // underlying communication channel
loopy *loopyWriter // 生產和消費關聯的隊列在這里面,所在文件:controlbuf.go
// controlBuf delivers all the control related tasks (e.g., window
// updates, reset streams, and various settings) to the controller.
controlBuf *controlBuffer // 所在文件:controlbuf.go
maxConcurrentStreams uint32
streamQuota int64
streamsQuotaAvailable chan struct{}
waitingStreams uint32
initialWindowSize int32
}
type controlBuffer struct {
list *itemList // 隊列
}
type loopyWriter struct {
// 關聯上 controlBuffer,
// 消費 controlBuffer 中的隊列 list,
// 生產 由http2Client通過controlBuffer 進行。
cbuf *controlBuffer
}
創建grpc客戶端的行為
func newHTTP2Client(connectCtx, ctx context.Context, addr resolver.Address, opts ConnectOptions, onPrefaceReceipt func(), onGoAway func(GoAwayReason), onClose func()) (_ *http2Client, err error) {
conn, err := dial(connectCtx, opts.Dialer, addr.Addr)
t.controlBuf = newControlBuffer(t.ctxDone) // 含發送隊列的初始化
if t.keepaliveEnabled {
t.kpDormancyCond = sync.NewCond(&t.mu)
go t.keepalive() // 保活協程
}
// Start the reader goroutine for incoming message. Each transport has
// a dedicated goroutine which reads HTTP2 frame from network. Then it
// dispatches the frame to the corresponding stream entity.
go t.reader()
// Send connection preface to server.
n, err := t.conn.Write(clientPreface)
go func() {
t.loopy = newLoopyWriter(clientSide, t.framer, t.controlBuf, t.bdpEst)
err := t.loopy.run()
}
}
特別說明:
每一次 gRPC 調用,客戶端均會創建一個新的 Stream,
該特性使得同一 gRPC 連接可以同時處理多個調用。請求的發送并不是同步的,而是基于隊列的異步發送。
// 源碼所在文件:internal/transport/controlbuf.go
func (l *loopyWriter) run() (err error) {
// 通過 get 間接調用 dequeue 和 dequeueAll
for {
it, err := l.cbuf.get(true)
if err != nil {
return err
}
if err = l.handle(it); err != nil {
return err
}
if _, err = l.processData(); err != nil {
return err
}
}
}
每一個gRPC客戶端連接均有一個自己的隊列,gRPC 并沒有直接限定隊列大小,所以如果不加任何限制則會內存暴漲,直到OOM發生。
這里我們結合內存泄漏的案例加深tcp連接在grpc調用中的獨特作用。
go func() {
for {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
resp, err := c.election.Leader(ctx)
if err != nil {
log.WithError(err).Errorf("get leader error. test")
} else {
log.Infof("get test leader success : %s", string(resp.Kvs[0].Value))
}
// cancel()
}
}()
pprof 顯示這個election.Leader函數導致的內存持續增長。
bug表象
etcd v3 發起的請求為grpc請求,
-
因為從closed信道能持續讀取零值,形成死循環。
-
死循環中,用于上下文釋放資源的defer cancel() 無法得到執行,因為是在函數級別。
-
每次grpc調用均形成緩慢內存泄漏。
總結
以上是生活随笔為你收集整理的grpc是基于http/2协议的高性能的rpc框架的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [python] 基于Dataset库操
- 下一篇: 【算法】Java版