一種更緊湊的數據格式:Protobuf 入門

Protocol buffers,簡稱 Protobuf,是由 Google 設計的結構化序列化資料技術。對網路應用來講,常常需要在不同的伺服端、客戶端之間交換資料,這些資料格式有 XML、JSON 等等之類。XML 特點是完整,便於記載更多的 meta data,但格式複雜,需要更強的效能來解析,傳輸時也需要更多頻寬;而 JSON 是 JavaScript 用於表示物件的語法,相對 XML 來得簡潔,隨著 JavaScript 普及,也變成現在常用的輕量級資料交換格式。

儘管 XML 跟 JSON 在當前網路應用中已經相當常見,但用不同語言開發的伺服端程式,都需要分別實現自己語言的 XML 或 JSON 解析,同時,這些格式定義也是團隊協作的痛點。假設 A 團隊開發某支應用,因為開發時沒有定義文件,只用 Email 跟 B 團隊說明,如果說明得不夠清楚,就會導致 B 團隊在交換資料時格式錯誤,需要好幾個來回,才能釐清彼此應該實現的格式。

Protobuf 對這些問題有它的看法,本文會使用 Protobuf 來序列化資料,搭配 Golang 寫個簡單的讀檔寫檔程式,體會一下 Protobuf 的設計特點。

Prepare Environment

Prorobuf 使用前,需要依據格式定義文件 proto file 來編譯訊息,編譯用工具是 protoc,可以到官網下載,或使用

choco install protoc

來安裝,安裝後輸入

PS C:\Users\ken> protoc --version
libprotoc 3.12.3

來確認版本。版本號很重要,跟 Google 其他的工具一樣,不同版本間可能有相容問題,使用時盡量依照語意化版本的方式選用對應版本。

Protobuf 用於結構化訊息的方法,是先將格式定義好,再用定義好的格式來產生序列化訊息,可以參考官方說明

You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.

因為包含 Code Generator,Protobuf 可以相容多種語言,常見的 C++、C#、Java 等等都在支援中。

參照 Golang 的專案目錄,建個 pb 來放置 proto file

project
├── cmd
├── pkg
├── scripts
|   └── build_win.bat
├── pb
|   └── person.proto
|── go.mod
└── README.md

檔案內容是

syntax = "proto3";

package person;

message Person {
    string name = 1;
    string address = 2;
    string gender = 3;
    int32 age = 4;
}

syntax 是版本,用於跟 proto2 區別;package 是這份 proto file 所屬的套件名稱;message 用於定義訊息結構。假設訊息結構是 Person,其中包含姓名、地址、性別、年齡等欄位,用於表示 Person 的資料。各欄位等號後數字是用於區別欄位,如果開發中需要新增欄位,可以直接在 field 中使用新數字,即可無痛向後相容。

Use Protobuf Command Line Tool

材料準備好後,來試著用 protoc 序列化訊息。前面已經定義好的格式了,這邊將需要傳輸的內容寫在 sample.txt

name: "Ken Chen"
address: "New Taipei City"
gender: "Male"
age: 18

引號後面是 proto 欄位的值,格式類似 JSON,可以參考官方說明。

然後用 protoc 來序列化訊息

protoc --encode person.Person ./pb/person.proto > encode.txt < sample.txt
type encode.txt

Ken ChenNew Taipei CityMale

最底下那串是序列化後的訊息,可以看到 string 還留著,但 field 跟 number 都被序列化成 byte 訊息了。

這些序列化完成的訊息,可以再用 protoc 反序列化回來,變成人眼可讀

protoc --decode person.Person ./pb/person.proto < encode.txt
name: "Ken Chen"
address: "New Taipei City"
gender: "Male"
age: 18

我們將傳輸內容序列化、寫進檔案、讀出、反序列化,這就是個簡單的傳輸過程。

Prepare Golang Environment

驗證 Protobuf 的使用流程後,接著要將 Protobuf 的機制放到自行開發的應用程式中。

先下載 Golang 的 Protobuf 套件

go get google.golang.org/protobuf/cmd/protoc-gen-go

修改 proto file,讓它帶有 Golang Package 的資訊

syntax = "proto3";

option go_package = "example/pkg/pb/person";

package person;

message Person {
    string name = 1;
    string address = 2;
    string gender = 3;
    int32 age = 4;
}

使用 protoc 中的 Code Generator 來產生 go 的程式碼

protoc --go_out=./ --go_opt=paths=source_relative ./pb/*.proto

go_out 是產出文件的路徑;我們將 pb 的所有 proto file 都產生一份對應的 go 程式碼

可以看看產出文件 person.pb.go 的開頭

// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
//  protoc-gen-go v1.25.0
//  protoc        v3.12.3
// source: pb/person.proto

package person

import (
    proto "github.com/golang/protobuf/proto"
    protoreflect "google.golang.org/protobuf/reflect/protoreflect"
    protoimpl "google.golang.org/protobuf/runtime/protoimpl"
    reflect "reflect"
    sync "sync"
)

如第一行寫的,這是產出文件,看看就好,不要修改它。如果有一些客製化應用,希望在既有的 Protobuf struct 加上自己的 method,建議可以用 go package 的機制,在同目錄底下放 patch.go 文件來新增。

把產出的文件放到專案路徑 pkg,變成

project
├── cmd
├── pkg
|   └── person
|      └── person.pb.go
├── scripts
|   └── build_win.bat
├── pb
|   └── person.proto
|── go.mod
└── README.md

Read/Write Data in Golang

可以來寫點應用了,在 cmd 下新增 go 程式碼

project
├── cmd
|   └── proto
|      └── main.go
├── pkg
|   └── person
|      └── person.pb.go
├── scripts
|   └── build_win.bat
├── pb
|   └── person.proto
|── go.mod
└── README.md

內容是

package main

import (
    "example/pkg/person"
    "fmt"
    "io/ioutil"

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

func main() {
    data, _ := ioutil.ReadFile("encode.txt")
    msg := person.Person{}
    err := proto.Unmarshal(data, &msg)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(msg.Name, msg.Gender, msg.Address, msg.Age)
}

由於前面已經用 protoc 產生序列化後的檔案 encode.txt,這裡可以讀取該檔來驗證程式。example/pkg/person 是引入剛剛產出的 go 套件。使用 ioutil 讀檔,再用 proto.Umarshal 反序列化成 struct,最後印出。

編譯並執行,看看結果

.\bin\proto.exe
Ken Chen Male New Taipei City 18

可以看到欄位值跟 sample.txt 相同。

接著來寫檔,修改 main.go

func main() {
    // write file
    msgWrite := person.Person{}
    msgWrite.Age = 22
    msgWrite.Gender = "Female"
    msgWrite.Name = "Cythia"
    msgWrite.Address = "Boston ,US"
    dataWrite, err := proto.Marshal(&msgWrite)
    if err != nil {
        fmt.Println(err)
    }
    err = ioutil.WriteFile("encode.txt", dataWrite, os.ModePerm)
    if err != nil {
        fmt.Println(err)
    }

    // read file
    data, _ := ioutil.ReadFile("encode.txt")
    msg := person.Person{}
    err = proto.Unmarshal(data, &msg)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(msg.Name)
    fmt.Println(msg.Gender)
    fmt.Println(msg.Address)
    fmt.Println(msg.Age)
}

寫檔的順序跟讀檔相反,先建立 Protobuf struct,填入各欄位,用 proto.Marshal 序列化,再用 ioutil.WriteFile 將序列化後的資訊寫進檔案中。寫入檔案同樣是 encode.txt,底下再用讀檔將內容讀出。

編譯並執行,觀察結果

.\bin\proto.exe
Cythia
Female
Boston ,US
22

檔案內容由 Ken 改為 Cythia,寫入成功。

Compare with JSON

既然都有現成的資料了,可以再跟 JSON 比較,看看兩者序列化後的大小差多少。

由於 Protobuf 相容 JSON 的格式,這件事變得很容易,可以看 person.pb.go 的內容

type Person struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Name    string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"`
    Gender  string `protobuf:"bytes,3,opt,name=gender,proto3" json:"gender,omitempty"`
    Age     int32  `protobuf:"varint,4,opt,name=age,proto3" json:"age,omitempty"`
}

在 Person 這個 struct 中,Name、Address、Gender、Age 等欄位有同時打上 protobuf 跟 json 兩種 tag,因此要轉換 struct 為 json,只需要直接使用 Golang 標準庫中的 json 庫即可。

修改 main.go

// write json
dataJSON, err := json.Marshal(&msgWrite)
if err != nil {
    fmt.Println(err)
}
err = ioutil.WriteFile("encode_json.txt", dataJSON, os.ModePerm)
if err != nil {
    fmt.Println(err)
}

執行後可以看到多出 encode_json.txt 這個檔案,內容是

{"name":"Cythia","address":"Boston ,US","gender":"Female","age":22}

來比較 Protobuf 跟 JSON 序列化後的差異,用

dir *.txt /s

得到

2020/06/26  下午 02:42                30 encode.txt
2020/06/26  下午 02:42                67 encode_json.txt

Protobuf 只要 30 bytes,而 JSON 需要 67 bytes,相差一倍多,難怪 Google 宣稱它又小又快。

小結

Protobuf 對網路應用來講,最主要的優點就是體積更小,傳輸更快,在高密度的資料交換場景,例如微服務組成的大型應用中,Protobuf 能有效提高傳輸速度。讓人訝異的是,根據這篇文章,Protobuf 在解析使用的資源居然還低於 JSON,真要說不方便的地方,大約是人眼不可讀這點。

除了傳輸與效能上的特點,Protobuf 將資料格式文件化,無意中也防止兩個不同的應用程序無法交換,對多人協作來講有其優勢。麻煩的地方可能是 proto file 需要額外版控,而 Git 目前的子版本版控技術用起來不是很方便,無論 submodule 或 subtree 都有限制,以 subtree 來講,會需要開發者先到 proto file 的 Repo 中修改檔案,加入並提交後,再到專案開發資料夾用 subtree 拉下來。可以預期在開發初期,proto file 會頻繁變更,衍生出許多隱形的開發成本。

Google 怎麼處理這問題呢?他們不用處理,因為 Google 的版控 Policy 是 Monorepo,所有的程式碼都放在一起,不需要做子版本版控。這好像是某種康威定律的佐證。

Reference

Read more

Weekly Issue 第 5 期:OpenAI 的企業文化

我一直都喜歡看科技公司的願景與文化,原因是,我想知道別人是如何看待自己的使命,又是用什麼方式打造它。願景通常在官網都會有,但想要知道文化,只能聽內部人講講了。 Palantir 前陣子因為它不同於矽谷的文化,而引起很多討論。受此影響,前 OpenAI 的員工在離職創辦公司後,也發文談論他所見到的 OpenAI。最讓我震撼的是,他們幾乎沒有資金困擾,想的都是如何打造出色的 AI 模型。 🗞️ 熱門新聞 Reflections on OpenAI 前員工談 OpenAI 的內部文化。 讀起來最大的感觸是,有些價值觀、觀點、實踐,只有在世界級的公司跟資源下,才有可能建立起來。讓每個團隊各自為政,看誰能端出最好的成果,這對新創(特別是沒拿創投)實在太奢侈了。 我相信這種經歷會變成是「可以帶著走的饗宴」,那種衝擊也是最寶貴的。 AI Open Source Productivity METR 前陣子發了一篇研究,說使用 AI

By Ken Chen

Weekly Issue 第 4 期:Canonical 的面試經驗

這星期看了比較多職涯相關的內容,最讓我驚訝的是 Canonical 的面試流程,當我分享這則新聞後,有更多朋友紛紛補充他們的面試經驗:需要經歷三個 Tier,每個 Tier 都有三關,而內容甚至還包括問人選「高中成績」與「大學生活」。 我很難想像一家做 Linux 發行版的公司,會如此草率對待人選,這讓我對他們家的產品有了很大的問號。 🗞️ 熱門新聞 My experience with Canonical's interview process 這是一篇 Canonical 的面試經歷(如果你不知道什麼是 Canonical,就是開發 Ubuntu 的公司)。 整個過程讓人非常驚訝,甚至還需要人選回答「高中成績」,而在面試中做筆記居然是扣分項。我看完後有股移除 Ubuntu 的衝動。真的太扯啦。 What happens when engineers work

By Ken Chen

Weekly Issue 第 3 期:Cloudflare 宣布內容獨立日

最近用了很多 Cloudflare 的產品,像是 Zero Trust、WARP,還有 Cloudflare Tunnel。每次的體驗都讓我嘖嘖稱奇,好像它們預判了我的需求一樣。這家公司始終追求著「更好的網路」這個目標,內容付費又是另一個例子。 🗞️ 熱門新聞 Content Independence Day: no AI crawl without compensation! 賽博佛陀 Cloudflare 又來普渡眾生了。這次是針對 AI 爬蟲收費。 「網路正在改變。它的商業模式也將改變。在這個過程中,我們有機會從過去 30 年網路的優點中學習,並為未來的網路創造更好的環境。 」 Cloudflare 真的很有意思,連思考的角度都很有趣。 內容當然是有價的,只是價格會怎麼支付呢?在現代的內容創作,這題變得非常複雜。 Folklore.org: Joining Apple Computer

By Ken Chen

Weekly Issue 第 2 期:Linux 基金會啟動 FAIR 專案

有些產品看到會覺得行不通,有些產品則相反,只要聽到就覺得是個好主意。Sentry 的產品通常都是後者。我猜有部分,也是因為它們的產品都指向同一個使命:可除錯性。 🗞️ 熱門新聞 Linux Foundation Announces the FAIR Package Manager Project for Open Source Content Management System Stability Linux 基金會啟動 FAIR 專案,為 WordPress 外掛程式提供替代方案。 底下的 Supporting Quotes 可以看看,講話都很客氣,左一句「去中心化」右一句「透明的治理架構」,在講什麼大家都很清楚 😜 。 Uber 與 Airbnb 重塑 VC 玩法,一文看懂 a16z 創辦人

By Ken Chen