国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

從實(shí)踐到原理,帶你參透 gRPC

geekidentity / 4163人閱讀

摘要:原文地址從實(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

Server
type 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。

Client
func 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

Server
func (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í)效果更佳。

行為分析 Magic

Magic 幀的主要作用是建立 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 連接。

SETTINGS

SETTINGS 幀的主要作用是設(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 幀。

HEADERS

HEADERS 幀的主要作用是存儲(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-timeoutgrpc-encoding 也是在這里設(shè)置的。

DATA

DATA 幀的主要作用是裝填主體信息,是數(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-statusgrpc-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 Service
func (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 API
type 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.UnaryInterceptorgrpc.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

相關(guān)文章

  • grpc - 使用 golang 帶你頭擼一套 RPC 服務(wù)(二)

    摘要:緊接上一篇,下面用作為客戶端調(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)生成...

    raise_yang 評(píng)論0 收藏0
  • Etcd超全解:原理闡釋及部署設(shè)置的最佳實(shí)踐

    摘要:谷歌思科華為等等均是的貢獻(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...

    yhaolpz 評(píng)論0 收藏0
  • 關(guān)于深度學(xué)習(xí)中的注意力機(jī)制,這篇文章實(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ī)制...

    iliyaku 評(píng)論0 收藏0
  • k8s與健康檢查--grpc服務(wù)健康檢查最佳實(shí)踐

    摘要:在本文中,我們將討論,一種本地健康檢查應(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-...

    maochunguang 評(píng)論0 收藏0
  • k8s與健康檢查--grpc服務(wù)健康檢查最佳實(shí)踐

    摘要:在本文中,我們將討論,一種本地健康檢查應(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-...

    Maxiye 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<