摘要:原文地址從實踐到原理,帶你參透在語言中大放異彩,越來越多的小伙伴在使用,最近也在公司安利了一波,希望能通過這篇文章能帶你一覽的愛與恨。幀的主要作用是裝填主體信息,是數據幀。
原文地址:從實踐到原理,帶你參透 gRPC
gRPC 在 Go 語言中大放異彩,越來越多的小伙伴在使用,最近也在公司安利了一波,希望能通過這篇文章能帶你一覽 gRPC 的愛與恨。本文篇幅較長,希望你做好閱讀準備,目錄如下:
簡述gRPC 是一個高性能、開源和通用的 RPC 框架,面向移動和 HTTP/2 設計。目前提供 C、Java 和 Go 語言版本,分別是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持。
gRPC 基于 HTTP/2 標準設計,帶來諸如雙向流、流控、頭部壓縮、單 TCP 連接上的多復用請求等特性。這些特性使得其在移動設備上表現更好,更省電和節省空間占用。
調用模型1、客戶端(gRPC Stub)調用 A 方法,發起 RPC 調用。
2、對請求信息使用 Protobuf 進行對象序列化壓縮(IDL)。
3、服務端(gRPC Server)接收到請求后,解碼請求體,進行業務邏輯處理并返回。
4、對響應結果使用 Protobuf 進行對象序列化壓縮(IDL)。
5、客戶端接受到服務端響應,解碼請求體。回調被調用的 A 方法,喚醒正在等待響應(阻塞)的客戶端調用并返回響應結果。
調用方式 一、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) }
創建 gRPC Server 對象,你可以理解為它是 Server 端的抽象對象。
將 SearchService(其包含需要被調用的服務端接口)注冊到 gRPC Server。 的內部注冊中心。這樣可以在接受到請求時,通過內部的 “服務發現”,發現該服務端接口并轉接進行邏輯處理。
創建 Listen,監聽 TCP 端口。
gRPC Server 開始 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", }) ... }
創建與給定目標(服務端)的連接句柄。
創建 SearchService 的客戶端對象。
發送 RPC 請求,等待同步響應,得到回調后返回響應結果。
二、Server-side streaming RPC:服務端流式 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 }客戶端與服務端是如何交互的
在開始分析之前,我們要先 gRPC 的調用有一個初始印象。那么最簡單的就是對 Client 端調用 Server 端進行抓包去剖析,看看整個過程中它都做了些什么事。如下圖:
Magic
SETTINGS
HEADERS
DATA
SETTINGS
WINDOW_UPDATE
PING
HEADERS
DATA
HEADERS
WINDOW_UPDATE
PING
我們略加整理發現共有十二個行為,是比較重要的。在開始分析之前,建議你自己先想一下,它們的作用都是什么?大膽猜測一下,帶著疑問去學習效果更佳。
行為分析 MagicMagic 幀的主要作用是建立 HTTP/2 請求的前言。在 HTTP/2 中,要求兩端都要發送一個連接前言,作為對所使用協議的最終確認,并確定 HTTP/2 連接的初始設置,客戶端和服務端各自發送不同的連接前言。
而上圖中的 Magic 幀是客戶端的前言之一,內容為 PRI * HTTP/2.0 SM ,以確定啟用 HTTP/2 連接。
SETTINGSSETTINGS 幀的主要作用是設置這一個連接的參數,作用域是整個連接而并非單一的流。
而上圖的 SETTINGS 幀都是空 SETTINGS 幀,圖一是客戶端連接的前言(Magic 和 SETTINGS 幀分別組成連接前言)。圖二是服務端的。另外我們從圖中可以看到多個 SETTINGS 幀,這是為什么呢?是因為發送完連接前言后,客戶端和服務端還需要有一步互動確認的動作。對應的就是帶有 ACK 標識 SETTINGS 幀。
HEADERSHEADERS 幀的主要作用是存儲和傳播 HTTP 的標頭信息。我們關注到 HEADERS 里有一些眼熟的信息,分別如下:
method:POST
scheme:http
path:/proto.SearchService/Search
authority::10001
content-type:application/grpc
user-agent:grpc-go/1.20.0-dev
你會發現這些東西非常眼熟,其實都是 gRPC 的基礎屬性,實際上遠遠不止這些,只是設置了多少展示多少。例如像平時常見的 grpc-timeout、grpc-encoding 也是在這里設置的。
DATADATA 幀的主要作用是裝填主體信息,是數據幀。而在上圖中,可以很明顯看到我們的請求參數 gRPC 存儲在里面。只需要了解到這一點就可以了。
HEADERS, DATA, HEADERS在上圖中 HEADERS 幀比較簡單,就是告訴我們 HTTP 響應狀態和響應的內容格式。
在上圖中 DATA 幀主要承載了響應結果的數據集,圖中的 gRPC Server 就是我們 RPC 方法的響應結果。
在上圖中 HEADERS 幀主要承載了 gRPC 狀態 和 gRPC 狀態消息,圖中的 grpc-status 和 grpc-message 就是我們的 gRPC 調用狀態的結果。
其它步驟 WINDOW_UPDATE主要作用是管理和流的窗口控制。通常情況下打開一個連接后,服務器和客戶端會立即交換 SETTINGS 幀來確定流控制窗口的大小。默認情況下,該大小設置為約 65 KB,但可通過發出一個 WINDOW_UPDATE 幀為流控制設置不同的大小。
PING/PONG主要作用是判斷當前連接是否仍然可用,也常用于計算往返時間。其實也就是 PING/PONG,大家對此應該很熟。
小結在建立連接之前,客戶端/服務端都會發送連接前言(Magic+SETTINGS),確立協議和配置項。
在傳輸數據時,是會涉及滑動窗口(WINDOW_UPDATE)等流控策略的。
傳播 gRPC 附加信息時,是基于 HEADERS 幀進行傳播和設置;而具體的請求/響應數據是存儲的 DATA 幀中的。
請求/響應結果會分為 HTTP 和 gRPC 狀態響應兩種類型。
客戶端發起 PING,服務端就會回應 PONG,反之亦可。
這塊 gRPC 的基礎使用,你可以看看我另外的 《gRPC 入門系列》,相信對你一定有幫助。
淺談理解 服務端為什么四行代碼,就能夠起一個 gRPC Server,內部做了什么邏輯。你有想過嗎?接下來我們一步步剖析,看看里面到底是何方神圣。
一、初始化// 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 }
這塊比較簡單,主要是實例 grpc.Server 并進行初始化動作。涉及如下:
lis:監聽地址列表。
opts:服務選項,這塊包含 Credentials、Interceptor 以及一些基礎配置。
conns:客戶端連接句柄列表。
m:服務信息映射。
quit:退出信號。
done:完成信號。
czData:用于存儲 ClientConn,addrConn 和 Server 的channelz 相關數據。
cv:當優雅退出時,會等待這個信號量,直到所有 RPC 請求都處理并斷開才會繼續處理。
二、注冊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) }
還記得我們平時編寫的 Protobuf 嗎?在生成出來的 .pb.go 文件中,會定義出 Service APIs interface 的具體實現約束。而我們在 gRPC Server 進行注冊時,會傳入應用 Service 的功能接口實現,此時生成的 RegisterServer 方法就會保證兩者之間的一致性。
步驟二:Service API IDL你想亂傳糊弄一下?不可能的,請乖乖定義與 Protobuf 一致的接口方法。但是那個 &_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", }
這看上去像服務的描述代碼,用來向內部表述 “我” 都有什么。涉及如下:
ServiceName:服務名稱
HandlerType:服務接口,用于檢查用戶提供的實現是否滿足接口要求
Methods:一元方法集,注意結構內的 Handler 方法,其對應最終的 RPC 處理方法,在執行 RPC 方法的階段會使用。
Streams:流式方法集
Metadata:元數據,是一個描述數據屬性的東西。在這里主要是描述 SearchServiceServer 服務
步驟三: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 }
在最后一步中,我們會將先前的服務接口信息、服務描述信息給注冊到內部 service 去,以便于后續實際調用的使用。涉及如下:
server:服務的接口信息
md:一元服務的 RPC 方法集
sd:流式服務的 RPC 方法集
mdata:metadata,元數據
小結在這一章節中,主要介紹的是 gRPC Server 在啟動前的整理和注冊行為,看上去很簡單,但其實一切都是為了后續的實際運行的預先準備。因此我們整理一下思路,將其串聯起來看看,如下:
三、監聽接下來到了整個流程中,最重要也是大家最關注的監聽/處理階段,核心代碼如下:
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 會根據外部傳入的 Listener 不同而調用不同的監聽模式,這也是 net.Listener 的魅力,靈活性和擴展性會比較高。而在 gRPC Server 中最常用的就是 TCPConn,基于 TCP Listener 去做。接下來我們一起看看具體的處理邏輯,如下:
循環處理連接,通過 lis.Accept 取出連接,如果隊列中沒有需處理的連接時,會形成阻塞等待。
若 lis.Accept 失敗,則觸發休眠機制,若為第一次失敗那么休眠 5ms,否則翻倍,再次失敗則不斷翻倍直至上限休眠時間 1s,而休眠完畢后就會嘗試去取下一個 “它”。
若 lis.Accept 成功,則重置休眠的時間計數和啟動一個新的 goroutine 調用 handleRawConn 方法去執行/處理新的請求,也就是大家很喜歡說的 “每一個請求都是不同的 goroutine 在處理”。
在循環過程中,包含了 “退出” 服務的場景,主要是硬關閉和優雅重啟服務兩種情況。
客戶端 一、創建撥號連接// 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 方法實際上是對于 grpc.DialContext 的封裝,區別在于 ctx 是直接傳入 context.Background。其主要功能是創建與給定目標的客戶端連接,其承擔了以下職責:
初始化 ClientConn
初始化(基于進程 LB)負載均衡配置
初始化 channelz
初始化重試規則和客戶端一元/流式攔截器
初始化協議棧上的基礎信息
相關 context 的超時控制
初始化并解析地址信息
創建與服務端之間的連接
連沒連之前聽到有的人說調用 grpc.Dial 后客戶端就已經與服務端建立起了連接,但這對不對呢?我們先鳥瞰全貌,看看正在跑的 goroutine。如下:
我們可以有幾個核心方法一直在等待/處理信號,通過分析底層源碼可得知。涉及如下:
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) } }
在該方法中會不斷地去嘗試創建連接,若成功則結束。否則不斷地根據 Backoff 算法的重試機制去嘗試創建連接,直到成功為止。從結論上來講,單純調用 DialContext 是異步建立連接的,也就是并不是馬上生效,處于 Connecting 狀態,而正式下要到達 Ready 狀態才可用。
真的連了嗎在抓包工具上提示一個包都沒有,那么這算真正連接了嗎?我認為這是一個表述問題,我們應該盡可能的嚴謹。如果你真的想通過 DialContext 方法就打通與服務端的連接,則需要調用 WithBlock 方法,雖然會導致阻塞等待,但最終連接會到達 Ready 狀態(握手成功)。如下圖:
二、實例化 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} }
這塊就是實例 Service API interface,比較簡單。
三、調用// 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 方法更像是一個包裝盒,把需要的東西放進去,而實際上調用的還是 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) }
通過概覽,可以關注到三塊調用。如下:
newClientStream:獲取傳輸層 Trasport 并組合封裝到 ClientStream 中返回,在這塊會涉及負載均衡、超時控制、 Encoding、 Stream 的動作,與服務端基本一致的行為。
cs.SendMsg:發送 RPC 請求出去,但其并不承擔等待響應的功能。
cs.RecvMsg:阻塞等待接受到的 RPC 方法響應結果。
連接// 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 方法中,我們通過 getTransport 方法獲取了 Transport 層中抽象出來的 ClientTransport 和 ServerTransport,實際上就是獲取一個連接給后續 RPC 調用傳輸使用。
四、關閉連接// 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 }
該方法會取消 ClientConn 上下文,同時關閉所有底層傳輸。涉及如下:
Context Cancel
清空并關閉客戶端連接
清空并關閉解析器連接
清空并關閉負載均衡連接
添加跟蹤引用
移除當前通道信息
Q&A 1. gRPC Metadata 是通過什么傳輸? 2. 調用 grpc.Dial 會真正的去連接服務端嗎?會,但是是異步連接的,連接狀態為正在連接。但如果你設置了 grpc.WithBlock 選項,就會阻塞等待(等待握手成功)。另外你需要注意,當未設置 grpc.WithBlock 時,ctx 超時控制對其無任何效果。
3. 調用 ClientConn 不 Close 會導致泄露嗎?會,除非你的客戶端不是常駐進程,那么在應用結束時會被動地回收資源。但如果是常駐進程,你又真的忘記執行 Close 語句,會造成的泄露。如下圖:
3.1. 客戶端
3.2. 服務端
3.3. TCP
4. 不控制超時調用的話,會出現什么問題?短時間內不會出現問題,但是會不斷積蓄泄露,積蓄到最后當然就是服務無法提供響應了。如下圖:
5. 為什么默認的攔截器不可以傳多個?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 }
當存在多個攔截器時,取的就是第一個攔截器。因此結論是允許傳多個,但并沒有用。
6. 真的需要用到多個攔截器的話,怎么辦?可以使用 go-grpc-middleware 提供的 grpc.UnaryInterceptor 和 grpc.StreamInterceptor 鏈式方法,方便快捷省心。
單單會用還不行,我們再深剖一下,看看它是怎么實現的。核心代碼如下:
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...) } } ... }
當攔截器數量大于 1 時,從 interceptors[1] 開始遞歸,每一個遞歸的攔截器 interceptors[i] 會不斷地執行,最后才真正的去執行 handler 方法。同時也經常有人會問攔截器的執行順序是什么,通過這段代碼你得出結論了嗎?
7. 頻繁創建 ClientConn 有什么問題?這個問題我們可以反向驗證一下,假設不公用 ClientConn 看看會怎么樣?如下:
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) } } }
輸出結果:
... 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
當你的應用場景是存在高頻次同時生成/調用 ClientConn 時,可能會導致系統的文件句柄占用過多。這種情況下你可以變更應用程序生成/調用 ClientConn 的模式,又或是池化它,這塊可以參考 grpc-go-pool 項目。
8. 客戶端請求失敗后會默認重試嗎?會不斷地進行重試,直到上下文取消。而重試時間方面采用 backoff 算法作為的重連機制,默認的最大重試時間間隔是 120s。
9. 為什么要用 HTTP/2 作為傳輸協議?許多客戶端要通過 HTTP 代理來訪問網絡,gRPC 全部用 HTTP/2 實現,等到代理開始支持 HTTP/2 就能透明轉發 gRPC 的數據。不光如此,負責負載均衡、訪問控制等等的反向代理都能無縫兼容 gRPC,比起自己設計 wire protocol 的 Thrift,這樣做科學不少。@ctiller @滕亦飛
10. 在 Kubernetes 中 gRPC 負載均衡有問題?gRPC 的 RPC 協議是基于 HTTP/2 標準實現的,HTTP/2 的一大特性就是不需要像 HTTP/1.1 一樣,每次發出請求都要重新建立一個新連接,而是會復用原有的連接。
所以這將導致 kube-proxy 只有在連接建立時才會做負載均衡,而在這之后的每一次 RPC 請求都會利用原本的連接,那么實際上后續的每一次的 RPC 請求都跑到了同一個地方。
注:使用 k8s service 做負載均衡的情況下
總結gRPC 基于 HTTP/2 + Protobuf。
gRPC 有四種調用方式,分別是一元、服務端/客戶端流式、雙向流式。
gRPC 的附加信息都會體現在 HEADERS 幀,數據在 DATA 幀上。
Client 請求若使用 grpc.Dial 默認是異步建立連接,當時狀態為 Connecting。
Client 請求若需要同步則調用 WithBlock(),完成狀態為 Ready。
Server 監聽是循環等待連接,若沒有則休眠,最大休眠時間 1s;若接收到新請求則起一個新的 goroutine 去處理。
grpc.ClientConn 不關閉連接,會導致 goroutine 和 Memory 等泄露。
任何內/外調用如果不加超時控制,會出現泄漏和客戶端不斷重試。
特定場景下,如果不對 grpc.ClientConn 加以調控,會影響調用。
攔截器如果不用 go-grpc-middleware 鏈式處理,會覆蓋。
在選擇 gRPC 的負責均衡模式時,需要謹慎。
參考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...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/31830.html
摘要:緊接上一篇,下面用作為客戶端調用的服務端。安裝插件插件可以幫助我們自動生成客戶端封裝了的服務接口,方便我們直接引入調用,否則只生成服務請求響應的實體類,用起來不太方便。打包解包客戶端發送至服務端服務端接收數據后使用對應的實體解包服務端 緊接上一篇,下面用PHP作為客戶端調用Go的服務端。 安裝 grpc_php_plugin 插件 grpc_php_plugin插件可以幫助我們自動生成...
摘要:谷歌思科華為等等均是的貢獻成員。其中谷歌云平臺和等大型云提供商成功在生產環境中使用了。它為良好穩定的生產部署提供了一個良好的起點。預先準備在繼續之前,我們需要準備一個谷歌云平臺的賬號免費的應該足夠了。我們將為部署配置。 本文將帶你充分了解Etcd的工作原理,演示如何用Kubernetes建立并運行etcd集群,如何與Etcd交互,如何在Etcd中設置和檢索值,如何配置高可用等等。 sh...
摘要:本文以機器翻譯為例,深入淺出地介紹了深度學習中注意力機制的原理及關鍵計算機制,同時也抽象出其本質思想,并介紹了注意力模型在圖像及語音等領域的典型應用場景。 最近兩年,注意力模型(Attention Model)被廣泛使用在自然語言處理、圖像識別及語音識別等各種不同類型的深度學習任務中,是深度學習技術中最值得關注與深入了解的核心技術之一。本文以機器翻譯為例,深入淺出地介紹了深度學習中注意力機制...
摘要:在本文中,我們將討論,一種本地健康檢查應用程序的方法。標準的健康檢查工具,可以輕松查詢健康協議。選擇二進制版本并將其下載到中在你的中指定容器的。服務器健康檢查的代碼實現,主要部分如下完整代碼,請查看倉庫。 前言 GRPC正在成為云原生微服務之間通信的通用語言。如果您今天要將gRPC應用程序部署到Kubernetes,您可能想知道配置運行狀況檢查的最佳方法。在本文中,我們將討論grpc-...
摘要:在本文中,我們將討論,一種本地健康檢查應用程序的方法。標準的健康檢查工具,可以輕松查詢健康協議。選擇二進制版本并將其下載到中在你的中指定容器的。服務器健康檢查的代碼實現,主要部分如下完整代碼,請查看倉庫。 前言 GRPC正在成為云原生微服務之間通信的通用語言。如果您今天要將gRPC應用程序部署到Kubernetes,您可能想知道配置運行狀況檢查的最佳方法。在本文中,我們將討論grpc-...
閱讀 3702·2021-11-23 09:51
閱讀 1360·2021-11-10 14:35
閱讀 4008·2021-09-22 15:01
閱讀 1279·2021-08-19 11:12
閱讀 379·2019-08-30 15:53
閱讀 1690·2019-08-29 13:04
閱讀 3429·2019-08-29 12:52
閱讀 3055·2019-08-23 16:14