proto vs gzip proto in golang

我们知道proto是基于二进制编码的,比json格式的编码要节省大量空间,那么,如果对于proto编码后的结果再进行gzip压缩,是否能产生更多空间的节省呢?gzip压缩是否具有幂等性呢?本文讨论了在golang中对这两个问题的探索和研究

gzip压缩proto编码结果

准备条件

proto定义:

// 共识规则
message ReviewRule {
  string id = 1;
  string name = 2;
  string desc = 3;
  string operator = 4;
  string tx_id = 5;
  repeated string participants = 6;
  ReviewType review_type = 7;
  google.protobuf.Struct vote = 8;
}

// 共识投票类型
enum ReviewType {
  // 全体成员投票通过
  All = 0;
  // 任意成员投票通过
  OneOf = 1;
  // 指定成员投票通过
  Designation = 2;
  // 比例成员投票通过,如30%, 50%, 70%
  Scale = 3;
}

在进行这个测试时,主要研究属性对象是ReviewRule的6,7,8,会根据长度大小生成固定长度的uuid字符串(随机),然后对ReviewRule做proto编码和proto编码后的gzip压缩。同时比对gzip解压缩后和原proto编码的字节长度是否一致,确保压缩和解压缩是对proto编码的结果无影响的。

结果比对

单位:字节Byte

随机长度 proto编码后 gzip写入 gzip压缩 gzip读取 gzip解压缩 gzip节省空间比率(%)
0 413 413 325 413 413 21.31
1 499 499 388 499 499 22.24
2 583 583 439 583 583 24.70
20 2099 2099 1240 2099 2099 40.92
200 17219 17219 8670 17219 17219 49.65
2000 168423 168423 82245 168423 168423 51.17
20000 1680423 1680423 816971 1680423 1680423 51.38

备注:gzip写入和gzip读取是为了保证在gzip处理过程中没有发生数据丢失或复写。

从上述表格中可以看到,gzip压缩后确实能在proto编码后再次降低使用的空间大小的,甚至数据量越大,压缩比越高,1.6G的数据大约可以降到800M不到,超过了50%。且解压缩后,数据大小仍然保持一致。

那么压缩的结果是否每次都能保持完全一致呢?

gzip压缩的幂等性

同样,在这个测试时,也是生成了长度为20000的随机参数ReviewRule。

结果比对

压缩次数 压缩结果base64编码结果数 压缩结果长度结果数
2 1 1
5 1 1
10 1 1
20 1 1
50 1 1
100 1 1

可以看到,对于相同的结果,压缩结果大小和base64编码后的结果不随压缩次数的增加而发生变化,因此可以推断gzip压缩是具有幂等性的,即压缩的结果每次都能保持完全一致。

附录

go版本:1.17.9
完整测试文件:

package v1

import (
    "bytes"
    "compress/gzip"
    "context"
    "encoding/base64"
    "fmt"
    "io"
    "testing"

    "github.com/google/uuid"
    "github.com/pkg/errors"
    "github.com/stretchr/testify/assert"
    "golang.org/x/sync/errgroup"
    "google.golang.org/protobuf/proto"
    "google.golang.org/protobuf/types/known/structpb"
)

func newTestReviewRule() *ReviewRule {
    rr := new(ReviewRule)
    rr.Id = uuid.New().String()
    rr.Name = "ReviewRule"
    rr.Desc = `// Code generated by protoc-gen-go. DO NOT EDIT.
    // versions:
    //  protoc-gen-go v1.28.0
    //  protoc        v3.9.0
    // source: pkg/contracts/fabric/review/v1/review.proto`
    rr.Operator = "x509::CN=hlp,OU=client,O=Hyperledger,ST=North Carolina,C=US::CN=ca.org1.example.com,O=org1.example.com,L=Durham,ST=North Carolina,C=AR"
    rr.TxId = uuid.New().String()
    return rr
}

func fillRandomParams(rr *ReviewRule, length int) error {
    rr.Participants = genStringSlice(length)
    rr.ReviewType = ReviewType(length % (len(ReviewType_name) - 1))

    vote, err := structpb.NewStruct(map[string]interface{}{
        "vote": genInterfaceSlice(length),
    })
    if err != nil {
        return errors.Wrap(err, "structpb.NewStruct")
    }
    rr.Vote = vote

    return nil
}

func Test_proto_gzip(t *testing.T) {
    rr := newTestReviewRule()
    tests := []struct {
        name   string
        length int
    }{
        {"0", 0},
        {"1", 1},
        {"2", 2},
        {"20", 20},
        {"200", 200},
        {"2000", 2000},
        {"20000", 20000},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            assert.Nil(t, fillRandomParams(rr, tt.length))

            protoBytes, err := proto.Marshal(rr)
            assert.Nil(t, err)
            var buf bytes.Buffer
            zw := gzip.NewWriter(&buf)

            i, err := zw.Write(protoBytes)
            assert.Nil(t, err)
            assert.Nil(t, zw.Close())

            fmt.Printf("name[%s]: proto[%d], gzip wi[%d], gzip[%d], ratio[%.2f], ", tt.name, len(protoBytes), i, buf.Len(), float64(i-buf.Len())/float64(i)*100)
            zr, err := gzip.NewReader(&buf)
            assert.Nil(t, err)
            defer zr.Close()

            var buf_ bytes.Buffer
            i_, err := io.Copy(&buf_, zr)
            assert.Nil(t, err)

            fmt.Printf("gzip ri[%d], read[%d]\n", i_, buf_.Len())
        })
    }
}

func genStringSlice(length int) []string {
    s := make([]string, length)
    for i := 0; i < length; i++ {
        s = append(s, genRandomString())
    }

    return s
}

func genInterfaceSlice(length int) []interface{} {
    s := make([]interface{}, length)
    for i := 0; i < length; i++ {
        s = append(s, genRandomString())
    }

    return s
}

func genRandomString() string {
    return uuid.New().String()
}

func Test_idempotent_gzip(t *testing.T) {
    rr := newTestReviewRule()
    assert.Nil(t, fillRandomParams(rr, 20000))
    protoBytes, err := proto.Marshal(rr)
    assert.Nil(t, err)
    tests := []struct {
        name      string
        frequency int
    }{
        {"2", 2},
        {"5", 5},
        {"10", 10},
        {"20", 20},
        {"50", 50},
        {"100", 100},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            stream := make(chan []byte)
            eg, egCtx := errgroup.WithContext(context.Background())
            for i := 0; i < tt.frequency; i++ {
                eg.Go(func() error {
                    var buf bytes.Buffer
                    zw := gzip.NewWriter(&buf)
                    _, err = zw.Write(protoBytes)
                    if err != nil {
                        return errors.Wrap(err, "zw.Write")
                    }

                    if err = zw.Close(); err != nil {
                        return errors.Wrap(err, "zw.Close")
                    }

                    select {
                    case <-egCtx.Done():
                        return errors.New("eg context done")
                    case stream <- buf.Bytes():
                    }

                    return nil
                })
            }

            go func() {
                eg.Wait()
                close(stream)
            }()

            base64Set := make(map[string]interface{})
            lenSet := make(map[int]interface{})
            for gzipBytes := range stream {
                base64Set[base64.StdEncoding.EncodeToString(gzipBytes)] = struct{}{}
                lenSet[len(gzipBytes)] = struct{}{}
            }
            assert.Nil(t, eg.Wait())
            assert.Len(t, base64Set, 1)
            assert.Len(t, lenSet, 1)
        })
    }
}

你可能感兴趣的:(proto vs gzip proto in golang)