摘要:原文地址從實(shí)踐到原理,帶你參透在語(yǔ)言中大放異彩,越來(lái)越多的小伙伴在使用,最近也在公司安利了一波,希望能通過(guò)這篇文章能帶你一覽的愛(ài)與恨。幀的主要作用是裝填主體信息,是數(shù)據(jù)幀。
原文地址:從實(shí)踐到原理,帶你參透 gRPC
gRPC 在 Go 語(yǔ)言中大放異彩,越來(lái)越多的小伙伴在使用,最近也在公司安利了一波,希望能通過(guò)這篇文章能帶你一覽 gRPC 的愛(ài)與恨。本文篇幅較長(zhǎng),希望你做好閱讀準(zhǔn)備,目錄如下:
簡(jiǎn)述gRPC 是一個(gè)高性能、開(kāi)源和通用的 RPC 框架,面向移動(dòng)和 HTTP/2 設(shè)計(jì)。目前提供 C、Java 和 Go 語(yǔ)言版本,分別是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持。
gRPC 基于 HTTP/2 標(biāo)準(zhǔn)設(shè)計(jì),帶來(lái)諸如雙向流、流控、頭部壓縮、單 TCP 連接上的多復(fù)用請(qǐng)求等特性。這些特性使得其在移動(dòng)設(shè)備上表現(xiàn)更好,更省電和節(jié)省空間占用。
調(diào)用模型1、客戶端(gRPC Stub)調(diào)用 A 方法,發(fā)起 RPC 調(diào)用。
2、對(duì)請(qǐng)求信息使用 Protobuf 進(jìn)行對(duì)象序列化壓縮(IDL)。
3、服務(wù)端(gRPC Server)接收到請(qǐng)求后,解碼請(qǐng)求體,進(jìn)行業(yè)務(wù)邏輯處理并返回。
4、對(duì)響應(yīng)結(jié)果使用 Protobuf 進(jìn)行對(duì)象序列化壓縮(IDL)。
5、客戶端接受到服務(wù)端響應(yīng),解碼請(qǐng)求體。回調(diào)被調(diào)用的 A 方法,喚醒正在等待響應(yīng)(阻塞)的客戶端調(diào)用并返回響應(yīng)結(jié)果。
調(diào)用方式 一、Unary RPC:一元 RPC Servertype SearchService struct{} func (s *SearchService) Search(ctx context.Context, r *pb.SearchRequest) (*pb.SearchResponse, error) { return &pb.SearchResponse{Response: r.GetRequest() + " Server"}, nil } const PORT = "9001" func main() { server := grpc.NewServer() pb.RegisterSearchServiceServer(server, &SearchService{}) lis, err := net.Listen("tcp", ":"+PORT) ... server.Serve(lis) }
創(chuàng)建 gRPC Server 對(duì)象,你可以理解為它是 Server 端的抽象對(duì)象。
將 SearchService(其包含需要被調(diào)用的服務(wù)端接口)注冊(cè)到 gRPC Server。 的內(nèi)部注冊(cè)中心。這樣可以在接受到請(qǐng)求時(shí),通過(guò)內(nèi)部的 “服務(wù)發(fā)現(xiàn)”,發(fā)現(xiàn)該服務(wù)端接口并轉(zhuǎn)接進(jìn)行邏輯處理。
創(chuàng)建 Listen,監(jiān)聽(tīng) TCP 端口。
gRPC Server 開(kāi)始 lis.Accept,直到 Stop 或 GracefulStop。
Clientfunc main() { conn, err := grpc.Dial(":"+PORT, grpc.WithInsecure()) ... defer conn.Close() client := pb.NewSearchServiceClient(conn) resp, err := client.Search(context.Background(), &pb.SearchRequest{ Request: "gRPC", }) ... }
創(chuàng)建與給定目標(biāo)(服務(wù)端)的連接句柄。
創(chuàng)建 SearchService 的客戶端對(duì)象。
發(fā)送 RPC 請(qǐng)求,等待同步響應(yīng),得到回調(diào)后返回響應(yīng)結(jié)果。
二、Server-side streaming RPC:服務(wù)端流式 RPC Serverfunc (s *StreamService) List(r *pb.StreamRequest, stream pb.StreamService_ListServer) error { for n := 0; n <= 6; n++ { stream.Send(&pb.StreamResponse{ Pt: &pb.StreamPoint{ ... }, }) } return nil }Client
func printLists(client pb.StreamServiceClient, r *pb.StreamRequest) error { stream, err := client.List(context.Background(), r) ... for { resp, err := stream.Recv() if err == io.EOF { break } ... } return nil }三、Client-side streaming RPC:客戶端流式 RPC Server
func (s *StreamService) Record(stream pb.StreamService_RecordServer) error { for { r, err := stream.Recv() if err == io.EOF { return stream.SendAndClose(&pb.StreamResponse{Pt: &pb.StreamPoint{...}}) } ... } return nil }Client
func printRecord(client pb.StreamServiceClient, r *pb.StreamRequest) error { stream, err := client.Record(context.Background()) ... for n := 0; n < 6; n++ { stream.Send(r) } resp, err := stream.CloseAndRecv() ... return nil }四、Bidirectional streaming RPC:雙向流式 RPC Server
func (s *StreamService) Route(stream pb.StreamService_RouteServer) error { for { stream.Send(&pb.StreamResponse{...}) r, err := stream.Recv() if err == io.EOF { return nil } ... } return nil }Client
func printRoute(client pb.StreamServiceClient, r *pb.StreamRequest) error { stream, err := client.Route(context.Background()) ... for n := 0; n <= 6; n++ { stream.Send(r) resp, err := stream.Recv() if err == io.EOF { break } ... } stream.CloseSend() return nil }客戶端與服務(wù)端是如何交互的
在開(kāi)始分析之前,我們要先 gRPC 的調(diào)用有一個(gè)初始印象。那么最簡(jiǎn)單的就是對(duì) Client 端調(diào)用 Server 端進(jìn)行抓包去剖析,看看整個(gè)過(guò)程中它都做了些什么事。如下圖:
Magic
SETTINGS
HEADERS
DATA
SETTINGS
WINDOW_UPDATE
PING
HEADERS
DATA
HEADERS
WINDOW_UPDATE
PING
我們略加整理發(fā)現(xiàn)共有十二個(gè)行為,是比較重要的。在開(kāi)始分析之前,建議你自己先想一下,它們的作用都是什么?大膽猜測(cè)一下,帶著疑問(wèn)去學(xué)習(xí)效果更佳。
行為分析 MagicMagic 幀的主要作用是建立 HTTP/2 請(qǐng)求的前言。在 HTTP/2 中,要求兩端都要發(fā)送一個(gè)連接前言,作為對(duì)所使用協(xié)議的最終確認(rèn),并確定 HTTP/2 連接的初始設(shè)置,客戶端和服務(wù)端各自發(fā)送不同的連接前言。
而上圖中的 Magic 幀是客戶端的前言之一,內(nèi)容為 PRI * HTTP/2.0 SM ,以確定啟用 HTTP/2 連接。
SETTINGSSETTINGS 幀的主要作用是設(shè)置這一個(gè)連接的參數(shù),作用域是整個(gè)連接而并非單一的流。
而上圖的 SETTINGS 幀都是空 SETTINGS 幀,圖一是客戶端連接的前言(Magic 和 SETTINGS 幀分別組成連接前言)。圖二是服務(wù)端的。另外我們從圖中可以看到多個(gè) SETTINGS 幀,這是為什么呢?是因?yàn)榘l(fā)送完連接前言后,客戶端和服務(wù)端還需要有一步互動(dòng)確認(rèn)的動(dòng)作。對(duì)應(yīng)的就是帶有 ACK 標(biāo)識(shí) SETTINGS 幀。
HEADERSHEADERS 幀的主要作用是存儲(chǔ)和傳播 HTTP 的標(biāo)頭信息。我們關(guān)注到 HEADERS 里有一些眼熟的信息,分別如下:
method:POST
scheme:http
path:/proto.SearchService/Search
authority::10001
content-type:application/grpc
user-agent:grpc-go/1.20.0-dev
你會(huì)發(fā)現(xiàn)這些東西非常眼熟,其實(shí)都是 gRPC 的基礎(chǔ)屬性,實(shí)際上遠(yuǎn)遠(yuǎn)不止這些,只是設(shè)置了多少展示多少。例如像平時(shí)常見(jiàn)的 grpc-timeout、grpc-encoding 也是在這里設(shè)置的。
DATADATA 幀的主要作用是裝填主體信息,是數(shù)據(jù)幀。而在上圖中,可以很明顯看到我們的請(qǐng)求參數(shù) gRPC 存儲(chǔ)在里面。只需要了解到這一點(diǎn)就可以了。
HEADERS, DATA, HEADERS在上圖中 HEADERS 幀比較簡(jiǎn)單,就是告訴我們 HTTP 響應(yīng)狀態(tài)和響應(yīng)的內(nèi)容格式。
在上圖中 DATA 幀主要承載了響應(yīng)結(jié)果的數(shù)據(jù)集,圖中的 gRPC Server 就是我們 RPC 方法的響應(yīng)結(jié)果。
在上圖中 HEADERS 幀主要承載了 gRPC 狀態(tài) 和 gRPC 狀態(tài)消息,圖中的 grpc-status 和 grpc-message 就是我們的 gRPC 調(diào)用狀態(tài)的結(jié)果。
其它步驟 WINDOW_UPDATE主要作用是管理和流的窗口控制。通常情況下打開(kāi)一個(gè)連接后,服務(wù)器和客戶端會(huì)立即交換 SETTINGS 幀來(lái)確定流控制窗口的大小。默認(rèn)情況下,該大小設(shè)置為約 65 KB,但可通過(guò)發(fā)出一個(gè) WINDOW_UPDATE 幀為流控制設(shè)置不同的大小。
PING/PONG主要作用是判斷當(dāng)前連接是否仍然可用,也常用于計(jì)算往返時(shí)間。其實(shí)也就是 PING/PONG,大家對(duì)此應(yīng)該很熟。
小結(jié)在建立連接之前,客戶端/服務(wù)端都會(huì)發(fā)送連接前言(Magic+SETTINGS),確立協(xié)議和配置項(xiàng)。
在傳輸數(shù)據(jù)時(shí),是會(huì)涉及滑動(dòng)窗口(WINDOW_UPDATE)等流控策略的。
傳播 gRPC 附加信息時(shí),是基于 HEADERS 幀進(jìn)行傳播和設(shè)置;而具體的請(qǐng)求/響應(yīng)數(shù)據(jù)是存儲(chǔ)的 DATA 幀中的。
請(qǐng)求/響應(yīng)結(jié)果會(huì)分為 HTTP 和 gRPC 狀態(tài)響應(yīng)兩種類型。
客戶端發(fā)起 PING,服務(wù)端就會(huì)回應(yīng) PONG,反之亦可。
這塊 gRPC 的基礎(chǔ)使用,你可以看看我另外的 《gRPC 入門系列》,相信對(duì)你一定有幫助。
淺談理解 服務(wù)端為什么四行代碼,就能夠起一個(gè) gRPC Server,內(nèi)部做了什么邏輯。你有想過(guò)嗎?接下來(lái)我們一步步剖析,看看里面到底是何方神圣。
一、初始化// grpc.NewServer() func NewServer(opt ...ServerOption) *Server { opts := defaultServerOptions for _, o := range opt { o(&opts) } s := &Server{ lis: make(map[net.Listener]bool), opts: opts, conns: make(map[io.Closer]bool), m: make(map[string]*service), quit: make(chan struct{}), done: make(chan struct{}), czData: new(channelzData), } s.cv = sync.NewCond(&s.mu) ... return s }
這塊比較簡(jiǎn)單,主要是實(shí)例 grpc.Server 并進(jìn)行初始化動(dòng)作。涉及如下:
lis:監(jiān)聽(tīng)地址列表。
opts:服務(wù)選項(xiàng),這塊包含 Credentials、Interceptor 以及一些基礎(chǔ)配置。
conns:客戶端連接句柄列表。
m:服務(wù)信息映射。
quit:退出信號(hào)。
done:完成信號(hào)。
czData:用于存儲(chǔ) ClientConn,addrConn 和 Server 的channelz 相關(guān)數(shù)據(jù)。
cv:當(dāng)優(yōu)雅退出時(shí),會(huì)等待這個(gè)信號(hào)量,直到所有 RPC 請(qǐng)求都處理并斷開(kāi)才會(huì)繼續(xù)處理。
二、注冊(cè)pb.RegisterSearchServiceServer(server, &SearchService{})步驟一:Service API interface
// search.pb.go type SearchServiceServer interface { Search(context.Context, *SearchRequest) (*SearchResponse, error) } func RegisterSearchServiceServer(s *grpc.Server, srv SearchServiceServer) { s.RegisterService(&_SearchService_serviceDesc, srv) }
還記得我們平時(shí)編寫(xiě)的 Protobuf 嗎?在生成出來(lái)的 .pb.go 文件中,會(huì)定義出 Service APIs interface 的具體實(shí)現(xiàn)約束。而我們?cè)?gRPC Server 進(jìn)行注冊(cè)時(shí),會(huì)傳入應(yīng)用 Service 的功能接口實(shí)現(xiàn),此時(shí)生成的 RegisterServer 方法就會(huì)保證兩者之間的一致性。
步驟二:Service API IDL你想亂傳糊弄一下?不可能的,請(qǐng)乖乖定義與 Protobuf 一致的接口方法。但是那個(gè) &_SearchService_serviceDesc 又有什么作用呢?代碼如下:
// search.pb.go var _SearchService_serviceDesc = grpc.ServiceDesc{ ServiceName: "proto.SearchService", HandlerType: (*SearchServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "Search", Handler: _SearchService_Search_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "search.proto", }
這看上去像服務(wù)的描述代碼,用來(lái)向內(nèi)部表述 “我” 都有什么。涉及如下:
ServiceName:服務(wù)名稱
HandlerType:服務(wù)接口,用于檢查用戶提供的實(shí)現(xiàn)是否滿足接口要求
Methods:一元方法集,注意結(jié)構(gòu)內(nèi)的 Handler 方法,其對(duì)應(yīng)最終的 RPC 處理方法,在執(zhí)行 RPC 方法的階段會(huì)使用。
Streams:流式方法集
Metadata:元數(shù)據(jù),是一個(gè)描述數(shù)據(jù)屬性的東西。在這里主要是描述 SearchServiceServer 服務(wù)
步驟三:Register Servicefunc (s *Server) register(sd *ServiceDesc, ss interface{}) { ... srv := &service{ server: ss, md: make(map[string]*MethodDesc), sd: make(map[string]*StreamDesc), mdata: sd.Metadata, } for i := range sd.Methods { d := &sd.Methods[i] srv.md[d.MethodName] = d } for i := range sd.Streams { ... } s.m[sd.ServiceName] = srv }
在最后一步中,我們會(huì)將先前的服務(wù)接口信息、服務(wù)描述信息給注冊(cè)到內(nèi)部 service 去,以便于后續(xù)實(shí)際調(diào)用的使用。涉及如下:
server:服務(wù)的接口信息
md:一元服務(wù)的 RPC 方法集
sd:流式服務(wù)的 RPC 方法集
mdata:metadata,元數(shù)據(jù)
小結(jié)在這一章節(jié)中,主要介紹的是 gRPC Server 在啟動(dòng)前的整理和注冊(cè)行為,看上去很簡(jiǎn)單,但其實(shí)一切都是為了后續(xù)的實(shí)際運(yùn)行的預(yù)先準(zhǔn)備。因此我們整理一下思路,將其串聯(lián)起來(lái)看看,如下:
三、監(jiān)聽(tīng)接下來(lái)到了整個(gè)流程中,最重要也是大家最關(guān)注的監(jiān)聽(tīng)/處理階段,核心代碼如下:
func (s *Server) Serve(lis net.Listener) error { ... var tempDelay time.Duration for { rawConn, err := lis.Accept() if err != nil { if ne, ok := err.(interface { Temporary() bool }); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } ... timer := time.NewTimer(tempDelay) select { case <-timer.C: case <-s.quit: timer.Stop() return nil } continue } ... return err } tempDelay = 0 s.serveWG.Add(1) go func() { s.handleRawConn(rawConn) s.serveWG.Done() }() } }
Serve 會(huì)根據(jù)外部傳入的 Listener 不同而調(diào)用不同的監(jiān)聽(tīng)模式,這也是 net.Listener 的魅力,靈活性和擴(kuò)展性會(huì)比較高。而在 gRPC Server 中最常用的就是 TCPConn,基于 TCP Listener 去做。接下來(lái)我們一起看看具體的處理邏輯,如下:
循環(huán)處理連接,通過(guò) lis.Accept 取出連接,如果隊(duì)列中沒(méi)有需處理的連接時(shí),會(huì)形成阻塞等待。
若 lis.Accept 失敗,則觸發(fā)休眠機(jī)制,若為第一次失敗那么休眠 5ms,否則翻倍,再次失敗則不斷翻倍直至上限休眠時(shí)間 1s,而休眠完畢后就會(huì)嘗試去取下一個(gè) “它”。
若 lis.Accept 成功,則重置休眠的時(shí)間計(jì)數(shù)和啟動(dòng)一個(gè)新的 goroutine 調(diào)用 handleRawConn 方法去執(zhí)行/處理新的請(qǐng)求,也就是大家很喜歡說(shuō)的 “每一個(gè)請(qǐng)求都是不同的 goroutine 在處理”。
在循環(huán)過(guò)程中,包含了 “退出” 服務(wù)的場(chǎng)景,主要是硬關(guān)閉和優(yōu)雅重啟服務(wù)兩種情況。
客戶端 一、創(chuàng)建撥號(hào)連接// grpc.Dial(":"+PORT, grpc.WithInsecure()) func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) { cc := &ClientConn{ target: target, csMgr: &connectivityStateManager{}, conns: make(map[*addrConn]struct{}), dopts: defaultDialOptions(), blockingpicker: newPickerWrapper(), czData: new(channelzData), firstResolveEvent: grpcsync.NewEvent(), } ... chainUnaryClientInterceptors(cc) chainStreamClientInterceptors(cc) ... }
grpc.Dial 方法實(shí)際上是對(duì)于 grpc.DialContext 的封裝,區(qū)別在于 ctx 是直接傳入 context.Background。其主要功能是創(chuàng)建與給定目標(biāo)的客戶端連接,其承擔(dān)了以下職責(zé):
初始化 ClientConn
初始化(基于進(jìn)程 LB)負(fù)載均衡配置
初始化 channelz
初始化重試規(guī)則和客戶端一元/流式攔截器
初始化協(xié)議棧上的基礎(chǔ)信息
相關(guān) context 的超時(shí)控制
初始化并解析地址信息
創(chuàng)建與服務(wù)端之間的連接
連沒(méi)連之前聽(tīng)到有的人說(shuō)調(diào)用 grpc.Dial 后客戶端就已經(jīng)與服務(wù)端建立起了連接,但這對(duì)不對(duì)呢?我們先鳥(niǎo)瞰全貌,看看正在跑的 goroutine。如下:
我們可以有幾個(gè)核心方法一直在等待/處理信號(hào),通過(guò)分析底層源碼可得知。涉及如下:
func (ac *addrConn) connect() func (ac *addrConn) resetTransport() func (ac *addrConn) createTransport(addr resolver.Address, copts transport.ConnectOptions, connectDeadline time.Time) func (ac *addrConn) getReadyTransport()
在這里主要分析 goroutine 提示的 resetTransport 方法,看看都做了啥。核心代碼如下:
func (ac *addrConn) resetTransport() { for i := 0; ; i++ { if ac.state == connectivity.Shutdown { return } ... connectDeadline := time.Now().Add(dialDuration) ac.updateConnectivityState(connectivity.Connecting) newTr, addr, reconnect, err := ac.tryAllAddrs(addrs, connectDeadline) if err != nil { if ac.state == connectivity.Shutdown { return } ac.updateConnectivityState(connectivity.TransientFailure) timer := time.NewTimer(backoffFor) select { case <-timer.C: ... } continue } if ac.state == connectivity.Shutdown { newTr.Close() return } ... if !healthcheckManagingState { ac.updateConnectivityState(connectivity.Ready) } ... if ac.state == connectivity.Shutdown { return } ac.updateConnectivityState(connectivity.TransientFailure) } }
在該方法中會(huì)不斷地去嘗試創(chuàng)建連接,若成功則結(jié)束。否則不斷地根據(jù) Backoff 算法的重試機(jī)制去嘗試創(chuàng)建連接,直到成功為止。從結(jié)論上來(lái)講,單純調(diào)用 DialContext 是異步建立連接的,也就是并不是馬上生效,處于 Connecting 狀態(tài),而正式下要到達(dá) Ready 狀態(tài)才可用。
真的連了嗎在抓包工具上提示一個(gè)包都沒(méi)有,那么這算真正連接了嗎?我認(rèn)為這是一個(gè)表述問(wèn)題,我們應(yīng)該盡可能的嚴(yán)謹(jǐn)。如果你真的想通過(guò) DialContext 方法就打通與服務(wù)端的連接,則需要調(diào)用 WithBlock 方法,雖然會(huì)導(dǎo)致阻塞等待,但最終連接會(huì)到達(dá) Ready 狀態(tài)(握手成功)。如下圖:
二、實(shí)例化 Service APItype SearchServiceClient interface { Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) } type searchServiceClient struct { cc *grpc.ClientConn } func NewSearchServiceClient(cc *grpc.ClientConn) SearchServiceClient { return &searchServiceClient{cc} }
這塊就是實(shí)例 Service API interface,比較簡(jiǎn)單。
三、調(diào)用// search.pb.go func (c *searchServiceClient) Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) { out := new(SearchResponse) err := c.cc.Invoke(ctx, "/proto.SearchService/Search", in, out, opts...) if err != nil { return nil, err } return out, nil }
proto 生成的 RPC 方法更像是一個(gè)包裝盒,把需要的東西放進(jìn)去,而實(shí)際上調(diào)用的還是 grpc.invoke 方法。如下:
func invoke(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error { cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...) if err != nil { return err } if err := cs.SendMsg(req); err != nil { return err } return cs.RecvMsg(reply) }
通過(guò)概覽,可以關(guān)注到三塊調(diào)用。如下:
newClientStream:獲取傳輸層 Trasport 并組合封裝到 ClientStream 中返回,在這塊會(huì)涉及負(fù)載均衡、超時(shí)控制、 Encoding、 Stream 的動(dòng)作,與服務(wù)端基本一致的行為。
cs.SendMsg:發(fā)送 RPC 請(qǐng)求出去,但其并不承擔(dān)等待響應(yīng)的功能。
cs.RecvMsg:阻塞等待接受到的 RPC 方法響應(yīng)結(jié)果。
連接// clientconn.go func (cc *ClientConn) getTransport(ctx context.Context, failfast bool, method string) (transport.ClientTransport, func(balancer.DoneInfo), error) { t, done, err := cc.blockingpicker.pick(ctx, failfast, balancer.PickOptions{ FullMethodName: method, }) if err != nil { return nil, nil, toRPCErr(err) } return t, done, nil }
在 newClientStream 方法中,我們通過(guò) getTransport 方法獲取了 Transport 層中抽象出來(lái)的 ClientTransport 和 ServerTransport,實(shí)際上就是獲取一個(gè)連接給后續(xù) RPC 調(diào)用傳輸使用。
四、關(guān)閉連接// conn.Close() func (cc *ClientConn) Close() error { defer cc.cancel() ... cc.csMgr.updateState(connectivity.Shutdown) ... cc.blockingpicker.close() if rWrapper != nil { rWrapper.close() } if bWrapper != nil { bWrapper.close() } for ac := range conns { ac.tearDown(ErrClientConnClosing) } if channelz.IsOn() { ... channelz.AddTraceEvent(cc.channelzID, ted) channelz.RemoveEntry(cc.channelzID) } return nil }
該方法會(huì)取消 ClientConn 上下文,同時(shí)關(guān)閉所有底層傳輸。涉及如下:
Context Cancel
清空并關(guān)閉客戶端連接
清空并關(guān)閉解析器連接
清空并關(guān)閉負(fù)載均衡連接
添加跟蹤引用
移除當(dāng)前通道信息
Q&A 1. gRPC Metadata 是通過(guò)什么傳輸? 2. 調(diào)用 grpc.Dial 會(huì)真正的去連接服務(wù)端嗎?會(huì),但是是異步連接的,連接狀態(tài)為正在連接。但如果你設(shè)置了 grpc.WithBlock 選項(xiàng),就會(huì)阻塞等待(等待握手成功)。另外你需要注意,當(dāng)未設(shè)置 grpc.WithBlock 時(shí),ctx 超時(shí)控制對(duì)其無(wú)任何效果。
3. 調(diào)用 ClientConn 不 Close 會(huì)導(dǎo)致泄露嗎?會(huì),除非你的客戶端不是常駐進(jìn)程,那么在應(yīng)用結(jié)束時(shí)會(huì)被動(dòng)地回收資源。但如果是常駐進(jìn)程,你又真的忘記執(zhí)行 Close 語(yǔ)句,會(huì)造成的泄露。如下圖:
3.1. 客戶端
3.2. 服務(wù)端
3.3. TCP
4. 不控制超時(shí)調(diào)用的話,會(huì)出現(xiàn)什么問(wèn)題?短時(shí)間內(nèi)不會(huì)出現(xiàn)問(wèn)題,但是會(huì)不斷積蓄泄露,積蓄到最后當(dāng)然就是服務(wù)無(wú)法提供響應(yīng)了。如下圖:
5. 為什么默認(rèn)的攔截器不可以傳多個(gè)?func chainUnaryClientInterceptors(cc *ClientConn) { interceptors := cc.dopts.chainUnaryInts if cc.dopts.unaryInt != nil { interceptors = append([]UnaryClientInterceptor{cc.dopts.unaryInt}, interceptors...) } var chainedInt UnaryClientInterceptor if len(interceptors) == 0 { chainedInt = nil } else if len(interceptors) == 1 { chainedInt = interceptors[0] } else { chainedInt = func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error { return interceptors[0](ctx, method, req, reply, cc, getChainUnaryInvoker(interceptors, 0, invoker), opts...) } } cc.dopts.unaryInt = chainedInt }
當(dāng)存在多個(gè)攔截器時(shí),取的就是第一個(gè)攔截器。因此結(jié)論是允許傳多個(gè),但并沒(méi)有用。
6. 真的需要用到多個(gè)攔截器的話,怎么辦?可以使用 go-grpc-middleware 提供的 grpc.UnaryInterceptor 和 grpc.StreamInterceptor 鏈?zhǔn)椒椒ǎ奖憧旖菔⌒摹?/p>
單單會(huì)用還不行,我們?cè)偕钇室幌拢纯此窃趺磳?shí)現(xiàn)的。核心代碼如下:
func ChainUnaryClient(interceptors ...grpc.UnaryClientInterceptor) grpc.UnaryClientInterceptor { n := len(interceptors) if n > 1 { lastI := n - 1 return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { var ( chainHandler grpc.UnaryInvoker curI int ) chainHandler = func(currentCtx context.Context, currentMethod string, currentReq, currentRepl interface{}, currentConn *grpc.ClientConn, currentOpts ...grpc.CallOption) error { if curI == lastI { return invoker(currentCtx, currentMethod, currentReq, currentRepl, currentConn, currentOpts...) } curI++ err := interceptors[curI](currentCtx, currentMethod, currentReq, currentRepl, currentConn, chainHandler, currentOpts...) curI-- return err } return interceptors[0](ctx, method, req, reply, cc, chainHandler, opts...) } } ... }
當(dāng)攔截器數(shù)量大于 1 時(shí),從 interceptors[1] 開(kāi)始遞歸,每一個(gè)遞歸的攔截器 interceptors[i] 會(huì)不斷地執(zhí)行,最后才真正的去執(zhí)行 handler 方法。同時(shí)也經(jīng)常有人會(huì)問(wèn)攔截器的執(zhí)行順序是什么,通過(guò)這段代碼你得出結(jié)論了嗎?
7. 頻繁創(chuàng)建 ClientConn 有什么問(wèn)題?這個(gè)問(wèn)題我們可以反向驗(yàn)證一下,假設(shè)不公用 ClientConn 看看會(huì)怎么樣?如下:
func BenchmarkSearch(b *testing.B) { for i := 0; i < b.N; i++ { conn, err := GetClientConn() if err != nil { b.Errorf("GetClientConn err: %v", err) } _, err = Search(context.Background(), conn) if err != nil { b.Errorf("Search err: %v", err) } } }
輸出結(jié)果:
... connection error: desc = "transport: Error while dialing dial tcp :10001: socket: too many open files" ... connection error: desc = "transport: Error while dialing dial tcp :10001: socket: too many open files" ... connection error: desc = "transport: Error while dialing dial tcp :10001: socket: too many open files" ... connection error: desc = "transport: Error while dialing dial tcp :10001: socket: too many open files" FAIL exit status 1
當(dāng)你的應(yīng)用場(chǎng)景是存在高頻次同時(shí)生成/調(diào)用 ClientConn 時(shí),可能會(huì)導(dǎo)致系統(tǒng)的文件句柄占用過(guò)多。這種情況下你可以變更應(yīng)用程序生成/調(diào)用 ClientConn 的模式,又或是池化它,這塊可以參考 grpc-go-pool 項(xiàng)目。
8. 客戶端請(qǐng)求失敗后會(huì)默認(rèn)重試嗎?會(huì)不斷地進(jìn)行重試,直到上下文取消。而重試時(shí)間方面采用 backoff 算法作為的重連機(jī)制,默認(rèn)的最大重試時(shí)間間隔是 120s。
9. 為什么要用 HTTP/2 作為傳輸協(xié)議?許多客戶端要通過(guò) HTTP 代理來(lái)訪問(wèn)網(wǎng)絡(luò),gRPC 全部用 HTTP/2 實(shí)現(xiàn),等到代理開(kāi)始支持 HTTP/2 就能透明轉(zhuǎn)發(fā) gRPC 的數(shù)據(jù)。不光如此,負(fù)責(zé)負(fù)載均衡、訪問(wèn)控制等等的反向代理都能無(wú)縫兼容 gRPC,比起自己設(shè)計(jì) wire protocol 的 Thrift,這樣做科學(xué)不少。@ctiller @滕亦飛
10. 在 Kubernetes 中 gRPC 負(fù)載均衡有問(wèn)題?gRPC 的 RPC 協(xié)議是基于 HTTP/2 標(biāo)準(zhǔn)實(shí)現(xiàn)的,HTTP/2 的一大特性就是不需要像 HTTP/1.1 一樣,每次發(fā)出請(qǐng)求都要重新建立一個(gè)新連接,而是會(huì)復(fù)用原有的連接。
所以這將導(dǎo)致 kube-proxy 只有在連接建立時(shí)才會(huì)做負(fù)載均衡,而在這之后的每一次 RPC 請(qǐng)求都會(huì)利用原本的連接,那么實(shí)際上后續(xù)的每一次的 RPC 請(qǐng)求都跑到了同一個(gè)地方。
注:使用 k8s service 做負(fù)載均衡的情況下
總結(jié)gRPC 基于 HTTP/2 + Protobuf。
gRPC 有四種調(diào)用方式,分別是一元、服務(wù)端/客戶端流式、雙向流式。
gRPC 的附加信息都會(huì)體現(xiàn)在 HEADERS 幀,數(shù)據(jù)在 DATA 幀上。
Client 請(qǐng)求若使用 grpc.Dial 默認(rèn)是異步建立連接,當(dāng)時(shí)狀態(tài)為 Connecting。
Client 請(qǐng)求若需要同步則調(diào)用 WithBlock(),完成狀態(tài)為 Ready。
Server 監(jiān)聽(tīng)是循環(huán)等待連接,若沒(méi)有則休眠,最大休眠時(shí)間 1s;若接收到新請(qǐng)求則起一個(gè)新的 goroutine 去處理。
grpc.ClientConn 不關(guān)閉連接,會(huì)導(dǎo)致 goroutine 和 Memory 等泄露。
任何內(nèi)/外調(diào)用如果不加超時(shí)控制,會(huì)出現(xiàn)泄漏和客戶端不斷重試。
特定場(chǎng)景下,如果不對(duì) grpc.ClientConn 加以調(diào)控,會(huì)影響調(diào)用。
攔截器如果不用 go-grpc-middleware 鏈?zhǔn)教幚恚瑫?huì)覆蓋。
在選擇 gRPC 的負(fù)責(zé)均衡模式時(shí),需要謹(jǐn)慎。
參考http://doc.oschina.net/grpc
https://github.com/grpc/grpc/...
https://juejin.im/post/5b88a4...
https://www.ibm.com/developer...
https://github.com/grpc/grpc-...
https://www.zhihu.com/questio...
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/31830.html
摘要:緊接上一篇,下面用作為客戶端調(diào)用的服務(wù)端。安裝插件插件可以幫助我們自動(dòng)生成客戶端封裝了的服務(wù)接口,方便我們直接引入調(diào)用,否則只生成服務(wù)請(qǐng)求響應(yīng)的實(shí)體類,用起來(lái)不太方便。打包解包客戶端發(fā)送至服務(wù)端服務(wù)端接收數(shù)據(jù)后使用對(duì)應(yīng)的實(shí)體解包服務(wù)端 緊接上一篇,下面用PHP作為客戶端調(diào)用Go的服務(wù)端。 安裝 grpc_php_plugin 插件 grpc_php_plugin插件可以幫助我們自動(dòng)生成...
摘要:谷歌思科華為等等均是的貢獻(xiàn)成員。其中谷歌云平臺(tái)和等大型云提供商成功在生產(chǎn)環(huán)境中使用了。它為良好穩(wěn)定的生產(chǎn)部署提供了一個(gè)良好的起點(diǎn)。預(yù)先準(zhǔn)備在繼續(xù)之前,我們需要準(zhǔn)備一個(gè)谷歌云平臺(tái)的賬號(hào)免費(fèi)的應(yīng)該足夠了。我們將為部署配置。 本文將帶你充分了解Etcd的工作原理,演示如何用Kubernetes建立并運(yùn)行etcd集群,如何與Etcd交互,如何在Etcd中設(shè)置和檢索值,如何配置高可用等等。 sh...
摘要:本文以機(jī)器翻譯為例,深入淺出地介紹了深度學(xué)習(xí)中注意力機(jī)制的原理及關(guān)鍵計(jì)算機(jī)制,同時(shí)也抽象出其本質(zhì)思想,并介紹了注意力模型在圖像及語(yǔ)音等領(lǐng)域的典型應(yīng)用場(chǎng)景。 最近兩年,注意力模型(Attention Model)被廣泛使用在自然語(yǔ)言處理、圖像識(shí)別及語(yǔ)音識(shí)別等各種不同類型的深度學(xué)習(xí)任務(wù)中,是深度學(xué)習(xí)技術(shù)中最值得關(guān)注與深入了解的核心技術(shù)之一。本文以機(jī)器翻譯為例,深入淺出地介紹了深度學(xué)習(xí)中注意力機(jī)制...
摘要:在本文中,我們將討論,一種本地健康檢查應(yīng)用程序的方法。標(biāo)準(zhǔn)的健康檢查工具,可以輕松查詢健康協(xié)議。選擇二進(jìn)制版本并將其下載到中在你的中指定容器的。服務(wù)器健康檢查的代碼實(shí)現(xiàn),主要部分如下完整代碼,請(qǐng)查看倉(cāng)庫(kù)。 前言 GRPC正在成為云原生微服務(wù)之間通信的通用語(yǔ)言。如果您今天要將gRPC應(yīng)用程序部署到Kubernetes,您可能想知道配置運(yùn)行狀況檢查的最佳方法。在本文中,我們將討論grpc-...
摘要:在本文中,我們將討論,一種本地健康檢查應(yīng)用程序的方法。標(biāo)準(zhǔn)的健康檢查工具,可以輕松查詢健康協(xié)議。選擇二進(jìn)制版本并將其下載到中在你的中指定容器的。服務(wù)器健康檢查的代碼實(shí)現(xiàn),主要部分如下完整代碼,請(qǐng)查看倉(cāng)庫(kù)。 前言 GRPC正在成為云原生微服務(wù)之間通信的通用語(yǔ)言。如果您今天要將gRPC應(yīng)用程序部署到Kubernetes,您可能想知道配置運(yùn)行狀況檢查的最佳方法。在本文中,我們將討論grpc-...
閱讀 3716·2021-11-23 09:51
閱讀 1378·2021-11-10 14:35
閱讀 4016·2021-09-22 15:01
閱讀 1289·2021-08-19 11:12
閱讀 386·2019-08-30 15:53
閱讀 1696·2019-08-29 13:04
閱讀 3435·2019-08-29 12:52
閱讀 3063·2019-08-23 16:14