Đua tốc độ với Protoco Buffer

Bài viết được sự cho phép của tác giả Nguyễn Hữu Đồng

Vốn là một thằng đam mê tốc độ, chạy xe 80,90km/h trên cao tốc như cơm bữa, trong quá trình học tập mình luôn giành thời gian tìm hiểu xem có giải pháp nào nhanh hơn giải pháp hiện tại không ? Và đương nhiên sẽ đánh đổi lại bằng nhiều như điển hình như độ phức tạp và thời gian triển khai, nhanh thì thực ra còn tuỳ thuộc vào bối cảnh, và bản chất của vấn đề cần giải quyết.

Sau tháng ngày dài làm việc cùng với JSON, chỉ biết JSON thì được ông anh giới thiệu cho thằng Protobuf, với một câu nói “nhanh lắm” mình liên bắt tay vào tìm hiểu và quả thực nó nhanh thât.

  Javascript prototype chuyên sâu

  Nếu vỗ ngực xưng tên là một javascript developer sành sỏi, mà không giải thích được prototype inheritance thì thật là kỳ

Đầu tiên nói về Protobuf, protobuf là một giao thức để tuần tự hoá dữ liệu có cấu trúc, tương tự với JSON hay XML, nhưng tốc độ thì JSON phải gọi bằng ông nội còn XML thì thôi không nói nữa, các bạn xem qua hình dưới nhé , sơ sơ thì tốc độ encode gấp 3 JSON và decode gấp 4–5 lần.

Đua tốc độ với Protoco Buffer

Nhìn bề ngoài vậy thôi chứ bản chất Protobuf nó lưu trữ data dưới dạng Binary nên làm mất khả năng đọc hiểu của loài người.

Khi làm việc với Protobuf, bạn định nghĩa các mà data được cấu trúc như thế nào,sau đó thì Proto Complier sẽ biên dịch ra mã nguồn tuỳ theo ngôn ngữ mà các bạn sử dụng, không như JSON được sử dụng rộng rãi mà mọi ngôn ngữ đều có thể áp dụng một tiêu chuẩn chung còn Protobuf là hàng nội bộ của Google nên chỉ Google mới có thể tạo ra những driver cho từng ngôn ngữ.

Protobuf rất phù hợp để làm ngôn ngữ giao tiếp giữa các server hơn là server và browser-client đơn giản là vì hầu hết browser-client giao tiếp với server bằng style REST API +JSON cộng thêm vẫn cần khả năng readable ở browser.

Mỗi file .proto gồm nhiều “message type” hiểu tương tự như struct trong go lang và class trong c++, mỗi message có thể embedded một hay nhiều message khác ví dụ trong file dứoi đây của mình, message user có thể embedded message contact.

syntax = "proto3";

package user;

message ContactProtobuf {
	string phoneNumber = 5;
	string country = 6;
}

message UserProtobuf { 
	string first_name = 1;
	string last_name = 2;
	string email = 3;
	repeated ContactProtobuf contact = 4;
}

Hãy biên dịch ra file .go xem trong file đó có gì hay. Để biên dịch các bạn phải download proto compiler và xem hướng dẫn cài đặt trong file readme tại đây và plugin cho từng ngôn ngữ, với Golang để cài đặt compiler plugin thì chỉ cần

go get -u github.com/golang/protobuf/protoc-gen-go

Để compile thì chạy lệnh

protoc -I=<include-folder-path> --go_out=<out-put> path-to-file

Trong đó -I dùng khi các bạn cần dùng các file Proto của bên thứ 3, các bạn xem tại đây. Trong trường hợp của mình mình cần biên dịch ra cùng thư mục với thư mục chứa file proto nên mình chạy lênh

protoc --go_out= . *.proto

Đua tốc độ với Protoco Buffer

Và được file user.pb.go, sau đó mình sẽ copy file này vào trong thư mục trongproject nơi chứa package user và sử dụng, cùng xem file output có gì nào.

// Code generated by protoc-gen-go. DO NOT EDIT.
// source: user.proto

package user

import (
	fmt "fmt"
	proto "github.com/golang/protobuf/proto"
	math "math"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package

type ContactProtobuf struct {
	PhoneNumber          string   `protobuf:"bytes,5,opt,name=phoneNumber,proto3" json:"phoneNumber,omitempty"`
	Country              string   `protobuf:"bytes,6,opt,name=country,proto3" json:"country,omitempty"`
	XXX_NoUnkeyedLiteral struct{} `json:"-"`
	XXX_unrecognized     []byte   `json:"-"`
	XXX_sizecache        int32    `json:"-"`
}

func (m *ContactProtobuf) Reset()         { *m = ContactProtobuf{} }
func (m *ContactProtobuf) String() string { return proto.CompactTextString(m) }
func (*ContactProtobuf) ProtoMessage()    {}
func (*ContactProtobuf) Descriptor() ([]byte, []int) {
	return fileDescriptor_116e343673f7ffaf, []int{0}
}

func (m *ContactProtobuf) XXX_Unmarshal(b []byte) error {
	return xxx_messageInfo_ContactProtobuf.Unmarshal(m, b)
}
func (m *ContactProtobuf) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
	return xxx_messageInfo_ContactProtobuf.Marshal(b, m, deterministic)
}
func (m *ContactProtobuf) XXX_Merge(src proto.Message) {
	xxx_messageInfo_ContactProtobuf.Merge(m, src)
}
func (m *ContactProtobuf) XXX_Size() int {
	return xxx_messageInfo_ContactProtobuf.Size(m)
}
func (m *ContactProtobuf) XXX_DiscardUnknown() {
	xxx_messageInfo_ContactProtobuf.DiscardUnknown(m)
}

var xxx_messageInfo_ContactProtobuf proto.InternalMessageInfo

func (m *ContactProtobuf) GetPhoneNumber() string {
	if m != nil {
		return m.PhoneNumber
	}
	return ""
}

func (m *ContactProtobuf) GetCountry() string {
	if m != nil {
		return m.Country
	}
	return ""
}

type UserProtobuf struct {
	FirstName            string             `protobuf:"bytes,1,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"`
	LastName             string             `protobuf:"bytes,2,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"`
	Email                string             `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
	Contact              []*ContactProtobuf `protobuf:"bytes,4,rep,name=contact,proto3" json:"contact,omitempty"`
	XXX_NoUnkeyedLiteral struct{}           `json:"-"`
	XXX_unrecognized     []byte             `json:"-"`
	XXX_sizecache        int32              `json:"-"`
}

func (m *UserProtobuf) Reset()         { *m = UserProtobuf{} }
func (m *UserProtobuf) String() string { return proto.CompactTextString(m) }
func (*UserProtobuf) ProtoMessage()    {}
func (*UserProtobuf) Descriptor() ([]byte, []int) {
	return fileDescriptor_116e343673f7ffaf, []int{1}
}

func (m *UserProtobuf) XXX_Unmarshal(b []byte) error {
	return xxx_messageInfo_UserProtobuf.Unmarshal(m, b)
}
func (m *UserProtobuf) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
	return xxx_messageInfo_UserProtobuf.Marshal(b, m, deterministic)
}
func (m *UserProtobuf) XXX_Merge(src proto.Message) {
	xxx_messageInfo_UserProtobuf.Merge(m, src)
}
func (m *UserProtobuf) XXX_Size() int {
	return xxx_messageInfo_UserProtobuf.Size(m)
}
func (m *UserProtobuf) XXX_DiscardUnknown() {
	xxx_messageInfo_UserProtobuf.DiscardUnknown(m)
}

var xxx_messageInfo_UserProtobuf proto.InternalMessageInfo

func (m *UserProtobuf) GetFirstName() string {
	if m != nil {
		return m.FirstName
	}
	return ""
}

func (m *UserProtobuf) GetLastName() string {
	if m != nil {
		return m.LastName
	}
	return ""
}

func (m *UserProtobuf) GetEmail() string {
	if m != nil {
		return m.Email
	}
	return ""
}

func (m *UserProtobuf) GetContact() []*ContactProtobuf {
	if m != nil {
		return m.Contact
	}
	return nil
}

func init() {
	proto.RegisterType((*ContactProtobuf)(nil), "user.ContactProtobuf")
	proto.RegisterType((*UserProtobuf)(nil), "user.UserProtobuf")
}

func init() { proto.RegisterFile("user.proto", fileDescriptor_116e343673f7ffaf) }

var fileDescriptor_116e343673f7ffaf = []byte{
	// 190 bytes of a gzipped FileDescriptorProto
	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2a, 0x2d, 0x4e, 0x2d,
	0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x01, 0xb1, 0x95, 0x7c, 0xb9, 0xf8, 0x9d, 0xf3,
	0xf3, 0x4a, 0x12, 0x93, 0x4b, 0x02, 0x40, 0xa2, 0x49, 0xa5, 0x69, 0x42, 0x0a, 0x5c, 0xdc, 0x05,
	0x19, 0xf9, 0x79, 0xa9, 0x7e, 0xa5, 0xb9, 0x49, 0xa9, 0x45, 0x12, 0xac, 0x0a, 0x8c, 0x1a, 0x9c,
	0x41, 0xc8, 0x42, 0x42, 0x12, 0x5c, 0xec, 0xc9, 0xf9, 0xa5, 0x79, 0x25, 0x45, 0x95, 0x12, 0x6c,
	0x60, 0x59, 0x18, 0x57, 0x69, 0x22, 0x23, 0x17, 0x4f, 0x68, 0x71, 0x6a, 0x11, 0xdc, 0x30, 0x59,
	0x2e, 0xae, 0xb4, 0xcc, 0xa2, 0xe2, 0x92, 0xf8, 0xbc, 0xc4, 0xdc, 0x54, 0x09, 0x46, 0xb0, 0x6a,
	0x4e, 0xb0, 0x88, 0x5f, 0x62, 0x6e, 0xaa, 0x90, 0x34, 0x17, 0x67, 0x4e, 0x22, 0x4c, 0x96, 0x09,
	0x2c, 0xcb, 0x01, 0x12, 0x00, 0x4b, 0x8a, 0x70, 0xb1, 0xa6, 0xe6, 0x26, 0x66, 0xe6, 0x48, 0x30,
	0x83, 0x25, 0x20, 0x1c, 0x21, 0x7d, 0x90, 0xe5, 0x60, 0x17, 0x4b, 0xb0, 0x28, 0x30, 0x6b, 0x70,
	0x1b, 0x89, 0xea, 0x81, 0x7d, 0x85, 0xe6, 0x8d, 0x20, 0x98, 0xaa, 0x24, 0x36, 0xb0, 0x7f, 0x8d,
	0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0xb2, 0x87, 0xe1, 0xdb, 0xfd, 0x00, 0x00, 0x00,
}

Xem qua có thể thấy, compiler đã generate ra một struct và các method như Marshal , UnMarshal để encode message sang binary và decode to message struct từ binary. Và để tạo nên một message thì ta làm như sau.

Đua tốc độ với Protoco Buffer

Đua tốc độ với Protoco Buffer

Việc decode từ Binary sang ProtoBuf cũng rất nhanh so với JSON thuần để so sánh thì bài có làm một bài test, follow thực hiện ta tạo ra một message sau đó encode sang binary rồi lại decode ra message như ban đầu, cùng thực hiện trên cả JSON và Protobuf với N lần.

package user

import (
	"encoding/json"
	"fmt"
	"time"

	"github.com/golang/protobuf/proto"
)

type Contact struct {
	PhoneNumber string `json:"phone_number`
	Country     string `json:"country"`
}

type User struct {
	FirstName string    `json:"first_name`
	LastName  string    `json:"last_name"`
	Contact   []Contact `json:"contact"`
}

func Benchmark() {
	n := 5000000
	fmt.Println("JSON - START")
	now := time.Now()

	for i := 1; i <= n; i++ {
		u := User{
			FirstName: "Dong",
			LastName:  "Nguyen",
			Contact: []Contact{
				Contact{
					PhoneNumber: "039 390 1228",
					Country:     "Viet Nam",
				},
			},
		}
		binary, _ := json.Marshal(u)
		var v User
		json.Unmarshal(binary, &v)
	}
	fmt.Println("JSON - END :", time.Now().Sub(now))
	fmt.Println("PROTOBUF - START")
	now = time.Now()
	for i := 1; i <= n; i++ {
		u := &UserProtobuf{
			FirstName: "Dong",
			LastName:  "Nguyen",
			Contact: []*ContactProtobuf{
				&ContactProtobuf{
					PhoneNumber: "039 390 1228",
					Country:     "Viet Name",
				},
			},
		}
		binary, _ := proto.Marshal(u)
		var v UserProtobuf
		proto.Unmarshal(binary, &v)
	}
	fmt.Println("PROTOBUF - END :", time.Now().Sub(now))

}

package main

import "bench/user"

func main() {
	user.Benchmark()
}

Và đây là kết quả. Thực tế thì Protobuf nhanh hơn so với JSON khoảng 4 đến 5 lần.

Đua tốc độ với Protoco Buffer

Mặc dù hơi tốn sức trong việc define các message type rồi compile nhưng thành quả nhận được rất là xứng đáng.

Đua tốc độ với Protoco Buffer

Đến đây mình xin dừng bút trong bài sau mình sẽ giới thiệu gRPC cái thứ mà nếu dùng với Protobuf thì các các server của các bạn sẽ trở thành ma tốc độ.

Bye bye các bạn, cảm ơn đã đọc bài 😀

Bài viết gốc được đăng tải tại medium.com

Có thể bạn quan tâm:

Xem thêm Việc làm Developer hấp dẫn trên TopDev