压缩负载
在 Protobuf
中,数据被序列化为二进制格式,这比文本数据的负载要小得多,但我们仍然可以在二进制数据之上应用压缩。gRPC 为我们提供了 gzip 压缩器( https://pkg.go.dev/google.golang.org/grpc/encoding/gzip ),并且对于更高级的用例,允许我们编写自己的压缩器( https://pkg.go.dev/google.golang.org/grpc/encoding )。
在深入了解如何使用 gzip
压缩器之前,需要理解一个重要的概念:无损压缩可能会导致负载大小增大。如果你的负载不包含重复数据(gzip
主要用于压缩重复数据),你发送的字节数可能会比实际需要的更多。因此,在启用 gzip
压缩之前,你需要对典型负载进行实验,看看 gzip
压缩如何影响其大小。
为了展示这个例子,我在 helpers
文件夹中包含了一个名为 gzip.go
的文件,其中包含一个辅助函数 compressedSize
。该函数返回序列化数据的原始大小和经过 gzip 压缩后的大小:
func compressedSize[M protoreflect.ProtoMessage](msg M) (int, int) {
var b bytes.Buffer
gz := gzip.NewWriter(&b)
out, err := proto.Marshal(msg)
if err != nil {
log.Fatal(err)
}
if _, err := gz.Write(out); err != nil {
log.Fatal(err)
}
if err := gz.Close(); err != nil {
log.Fatal(err)
}
return len(out), len(b.Bytes())
}
这是一个通用函数,我们可以用它来处理任何消息。我们可以首先尝试使用一个不适合压缩的消息,比如 Int32Value
。所以,在 gzip.go
文件的 main
函数中,我们将创建一个 Int32Value
实例,传递给 compressedSize
函数,并打印原始大小和压缩后的大小:
func main() {
var data int32 = 268_435_456
i32 := &wrapperspb.Int32Value{
Value: data,
}
o, c := compressedSize(i32)
fmt.Printf("original: %d\ncompressed: %d\n", o, c)
}
运行这个代码,我们应该得到以下结果:
$ go run gzip.go
original: 6
compressed: 30
压缩后的负载比原始的还大五倍。这显然是在生产环境中应避免的。显然,大多数时候我们发送的并不是如此简单的消息。让我们来看一个更具体的例子。我们将使用前面书中定义的 Task
消息:
syntax = "proto3";
package todo;
import "google/protobuf/timestamp.proto";
option go_package = "github.com/PacktPublishing/gRPC-Go-forProfessionals/helpers/proto";
message Task {
uint64 id = 1;
string description = 2;
bool done = 3;
google.protobuf.Timestamp due_date = 4;
}
然后,我们可以使用以下命令编译它:
$ protoc --go_out=. \
--go_opt=module=github.com/PacktPublishing/gRPC-Go-for-Professionals/helpers \
proto/todo.proto
接下来,我们创建一个 Task
实例,并传递给 compressedSize
函数,以查看压缩的结果:
func main() {
task := &pb.Task{
Id: 1,
Description: "This is a task",
DueDate: timestamppb.New(time.Now().Add(5 * 24 * time.Hour)),
}
o, c := compressedSize(task)
fmt.Printf("original: %d\ncompressed: %d\n", o, c)
}
如果我们运行它,我们会得到以下大小:
$ go run gzip.go
original: 32
compressed: 57
虽然比之前的例子好一些,但仍然不够高效,因为我们发送的字节数比实际需要的要多。因此,在我们看到的这些情况下,使用 gzip
压缩并不合理。
最后,让我们来看一个压缩有用的例子。假设我们大多数 Task
实例的描述都很长。例如,我们可以有如下的任务描述:
task := &pb.Task{
//...
Description: `This is a task that is quite long and requires a lot
of work.
We are not sure we can finish it even after 5 days.
Some planning will be needed and a meeting is required.`,
//...
}
然后,运行 compressedSize
函数,我们会得到以下大小:
$ go run gzip.go
original: 192
compressed: 183
从这个例子中我们可以得出一个结论:在启用 gzip
压缩之前,我们需要了解我们的数据。
接下来,让我们看看如何启用 gzip
压缩。
在服务器端(server/main.go
),这非常简单,只需添加以下导入:
_ "google.golang.org/grpc/encoding/gzip"
请注意,我们在导入前加了一个下划线,以避免编译器提示我们没有使用该导入。
这就是服务器端的所有操作。在客户端,虽然代码稍微多一点,但也是很简单的。我们可以通过添加 DialOption
来启用所有 RPC 端点的压缩,或者通过添加 CallOption
来为单个端点启用压缩( https://pkg.go.dev/google.golang.org/grpc#CallOption )。
对于第一个选项,我们可以简单地添加如下代码:
opts := []grpc.DialOption{
//...
grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)),
}
gzip
添加的导入与服务器端相同,但没有前面的下划线。
对于为单个调用添加压缩,我们可以使用 CallOption
。如果我们想为 AddTask
调用添加 gzip
压缩,可以这样写:
res, err := c.AddTask(context.Background(), req, grpc.UseCompressor(gzip.Name))
总结来说,我们看到了一直启用压缩并不是一个好主意,只有在测试过我们的数据后,我们才应该启用压缩。接着,我们看到了如何在服务器端和客户端注册 gzip 压缩器。最后,我们看到了如何全局启用压缩或为单个调用启用压缩。