gRPC的大訊息傳輸

0x5010發表於2018-08-06

概述

使用gRPC的一個問題是,它的預設最大訊息大小預設被設定為4MB,那麼當資料量太大時該怎麼辦?

options

可以通過在建立Server的時候,配置相關的引數來擴大限制最小訊息大小的值。

s := grpc.NewServer(grpc.MaxRecvMsgSize(size), grpc.MaxSendMsgSize(size))

MaxRecvMsgSizeMaxSendMsgSize分別設定伺服器可以接收的最大訊息大小和可以傳送的最大訊息大小(以位元組為單位)。不設定的話預設都是4MB

雖然可以配置,但這種行為是一種滑坡謬誤,可能會導致不斷修改增加服務端客戶端最大訊息大小,而且每次請求不一定都需要全部的資料,會導致很多效能和資源上的浪費。

chunk

自然地將資料分成更小的塊並使用gRPC流方法(stream)對其進行流式傳輸是一個不錯的選擇。

首先一個proto,是一個返回流式訊息型別的rpc service

syntax = "proto3";

package pb;

service Chunker {
    rpc Chunker(Empty) returns (stream Chunk) {}
}

message Empty{}

message Chunk {
    bytes chunk = 1;
}

實現serverChunker邏輯。流式訊息的大小設定為64KB

const chunkSize = 64 * 1024

type chunkerSrv []byte

func (c chunkerSrv) Chunker(_ *pb.Empty, srv pb.Chunker_ChunkerServer) error {
    chunk := &pb.Chunk{}
    n := len(c)
    for cur := 0; cur < n; cur += chunkSize {
        if cur+chunkSize > n {
            chunk.Chunk = c[cur:n]
        } else {
            chunk.Chunk = c[cur : cur+chunkSize]
        }
        if err := srv.Send(chunk); err != nil {
            return err
        }
    }
    return nil
}

然後把gRPC服務端執行起來,使用隨機填充128M的資料來方便測試。

func main() {
    listen, err := net.Listen("tcp", ":8888")
    if err != nil {
        log.Fatal(err)
    }
    s := grpc.NewServer()
    blob := make([]byte, 128*1024*1024) // 128M
    rand.Read(blob)
    pb.RegisterChunkerServer(s, chunkerSrv(blob))
    log.Println("serving on localhost:8888")
    log.Fatal(s.Serve(listen))
}

編寫個客戶端請求一下。

func main() {
    conn, err := grpc.Dial("localhost:8888", grpc.WithInsecure())
    if err != nil {
        log.Fatal(err)
    }
    client := pb.NewChunkerClient(conn)
    stream, err := client.Chunker(context.Background(), &pb.Empty{})
    if err != nil {
        log.Fatal(err)
    }

    var blob []byte
    for {
        c, err := stream.Recv()
        if err != nil {
            if err == io.EOF {
                log.Printf("Transfer of %d bytes successful", len(blob))
                // Transfer of 134217728 bytes successful
                return
            }
            log.Fatal(err)
        }
        blob = append(blob, c.Chunk...)
}

使用分塊傳輸編碼,資料分解成一系列資料塊,並以一個或多個塊傳送,這樣客戶端自己再拼接成完整的資料,無論多少資料都可以不用修改配置。

完整程式碼

range

在資料量大的情況,不是每次都需要請求全量的資料。基於之上可以借鑑httprange協議來分片的取獲取資源。同樣的在Chunkerproto基礎上修改,在請求的時候能傳入零個(代表全部獲取)或多個Range來分片獲取資源。

syntax = "proto3";

package pb;

service RangeChunker {
    rpc Range(Res) returns (stream Chunk) {}
}

message Res {
    repeated Range r = 1;
}

message Range {
    int32 start = 1;
    int32 stop = 2;
}

message Chunk {
    bytes chunk = 1;
}

服務端的實現主要是Range的解析,這裡實現和httprange類似,使用0-99代表前100位元組而不是0-100,並簡化了很多,比如只保留了stop設定-1時代表最後一個位元組,其他的負數操作都沒有實現。需要的話可以自行修改rangeLimit

const chunkSize = 64 * 1024

type chunkerSrv []byte

func (c chunkerSrv) Range(r *pb.Res, srv pb.RangeChunker_RangeServer) error {
    chunk := &pb.Chunk{}
    ranges := c.parseRanges(r)

    for _, rr := range ranges {
        start, stop := rr[0], rr[1]
        for cur := start; cur < stop; cur += chunkSize {
            if cur+chunkSize > stop {
                chunk.Chunk = c[cur:stop]
            } else {
                chunk.Chunk = c[cur : cur+chunkSize]
            }
            if err := srv.Send(chunk); err != nil {
                return err
            }
        }
    }
    return nil
}

func (c chunkerSrv) parseRanges(r *pb.Res) [][2]int {
    n := len(c)
    ranges := [][2]int{}
    rs := r.GetR()
    if len(rs) == 0 {
        return [][2]int{[2]int{0, n}}
    }
    for _, rr := range rs {
        start, stop := rangeLimit(rr, n)
        if start == -1 {
            return nil
        }
        ranges = append(ranges, [2]int{start, stop})
    }
    return ranges
}

func rangeLimit(r *pb.Range, llen int) (int, int) {
    start, stop := int(r.Start), int(r.Stop)+1
    if stop > llen || stop == 0 {
        stop = llen
    }
    if start < 0 || stop < 0 || start >= stop {
        return -1, -1
    }
    return start, stop
}

客戶端請求也很簡單。

stream, err := client.Range(context.Background(), &pb.Res{
    R: []*pb.Range{
        {0, 99},
        {100, 199},
        {200, -1},
    },
})

完整程式碼

這樣我們就可以只請求資源的某個部分,基於此之上還可以並行請求,斷點續傳等。

原文連結:https://blog.keyboardman.me/2018/08/06/large-messages-with-grpc/

相關文章