サービス/マイクロサービス間の通信について考えるとき、最初に思い浮かぶのは古き良き JSON です。この形式には次のような利点があるため、これには理由がないわけではありません。
企業の日常生活で開発される API の大部分では、JSON の使用が推奨されています。ただし、パフォーマンスが重要な場合には、他の代替手段を検討する必要がある場合があります。この投稿の目的は、アプリケーション間の通信に関して JSON に代わる 2 つの方法を示すことです。
しかし、JSON には何が問題なのでしょうか?その利点の 1 つは「人間が読みやすい」ということですが、これがパフォーマンスの弱点になる可能性があります。実際には、JSON コンテンツを、使用しているプログラミング言語で既知の構造に変換する必要があります。このルールの例外は、JSON がネイティブであるため、JavaScript を使用する場合です。ただし、別の言語、たとえば Go を使用している場合は、以下の(不完全な)コード例に示すように、データを解析する必要があります。
この問題を解決するには、プロトコル バッファーとフラットバッファーという 2 つの代替案をテストできます。
Google によって作成された Protobuf (プロトコル バッファー) は、公式ウェブサイトによると次のとおりです。
プロトコル バッファーは、構造化データをシリアル化するための Google の言語中立、プラットフォーム中立の拡張可能なメカニズムです。XML を思い浮かべてください。ただし、より小さく、より速く、より簡単です。データをどのように構造化するかを一度定義します。その後、特別に生成されたソース コードを使用して、さまざまな言語を使用してさまざまなデータ ストリームとの間で構造化データをすばやく読み書きできるようになります。
通常、gRPC と組み合わせて使用されます(ただし、必ずしもそうである必要はありません)。Protobuf は、JSON のテキスト形式と比較してパフォーマンスを大幅に向上させるバイナリ プロトコルです。しかし、JSON と同じ問題に「悩まされています」。言語のデータ構造に解析する必要があります。たとえば、Go では次のようになります:
バイナリ プロトコルを採用するとパフォーマンスが向上しますが、データ解析の問題を解決する必要があります。 3 番目の競合他社は、この問題の解決に焦点を当てています。
公式サイトによると
FlatBuffers は、C++、C#、C、Go、Java、Kotlin、JavaScript、Lobster、Lua、TypeScript、PHP、Python、Rust、Swift 用の効率的なクロスプラットフォーム シリアル化ライブラリです。これは当初、ゲーム開発やその他のパフォーマンスが重要なアプリケーションのために Google で作成されました。
元々はゲーム開発用に作成されましたが、この記事で調査する環境に完全に適合します。その利点は、バイナリ プロトコルであることに加えて、データを解析する必要がないことです。たとえば、Go では次のようになります:
しかし、JSON に代わる 2 つの方法はどれくらいパフォーマンスが優れているのでしょうか?調べてみましょう...
私の頭に浮かんだ最初の疑問は、「これを実際のシナリオにどのように適用できるでしょうか?」ということでした。私は次のようなシナリオを想像しました:
毎日何百万もの顧客がアクセスするモバイル アプリケーションを備え、内部マイクロサービス アーキテクチャを備えており、監査目的でユーザーとシステムによって生成されたイベントを保存する必要がある企業。
これは本物のシナリオです。とてもリアルなので、私が働いている会社で毎日それと一緒に暮らしています:)
注: 上記のシナリオは簡略化したものであり、チームのアプリケーションの実際の複雑さを表すものではありません。教育目的に役立ちます。
最初のステップは、プロトコル バッファーとフラットバッファーでイベントを定義することです。どちらもスキーマを定義するための独自の言語を持っており、それを使用して、使用する言語でコードを生成できます。各スキームの詳細については、ドキュメントで簡単に説明されているため、ここでは詳しく説明しません。
ファイルevent.protoにはプロトコルバッファ定義があります:
そして、event.fbs ファイルには、Flatbuffers に同等のものが含まれています:
次のステップは、これらの定義を使用して必要なコードを生成することです。次のコマンドは、macOS に依存関係をインストールします:
その結果、各形式のデータを操作するための Go パッケージが作成されます。
要件が満たされたので、次のステップはイベント API の実装です。 main.go は次のようになります:
より良く整理するために、次のような各機能を分離するファイルを作成しました:
package main import ( "encoding/json" "net/http" "github.com/google/uuid" ) type event struct { ID uuid.UUID Type string `json:"type"` Source string `json:"source"` Subject string `json:"subject"` Time string `json:"time"` Data string `json:"data"` } func processJSON() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var e event err := json.NewDecoder(r.Body).Decode(&e) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } saveEvent(e.Type, e.Source, e.Subject, e.Time, e.Data) w.WriteHeader(http.StatusCreated) w.Write([]byte("json received")) } }
package main import ( "io" "net/http" "github.com/eminetto/post-flatbuffers/events_pb" "google.golang.org/protobuf/proto" ) func processPB() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { body := r.Body data, _ := io.ReadAll(body) e := events_pb.Event{} err := proto.Unmarshal(data, &e) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } saveEvent(e.GetType(), e.GetSource(), e.GetSubject(), e.GetTime(), e.GetData()) w.WriteHeader(http.StatusCreated) w.Write([]byte("protobuf received")) } }
package main import ( "io" "net/http" "github.com/eminetto/post-flatbuffers/events" ) func processFB() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { body := r.Body data, _ := io.ReadAll(body) e := events.GetRootAsEvent(data, 0) saveEvent(string(e.Type()), string(e.Source()), string(e.Subject()), string(e.Time()), string(e.Data())) w.WriteHeader(http.StatusCreated) w.Write([]byte("flatbuffer received")) } }
In the functions processPB() and processFB(), we can see how the generated packages are used to manipulate the data.
The last step of our proof of concept is generating the benchmark to compare the formats. I used the Go stdlib benchmark package for this.
The file main_test.go has tests for each format:
package main import ( "bytes" "fmt" "net/http" "net/http/httptest" "os" "strings" "testing" "github.com/eminetto/post-flatbuffers/events" "github.com/eminetto/post-flatbuffers/events_pb" flatbuffers "github.com/google/flatbuffers/go" "google.golang.org/protobuf/proto" ) func benchSetup() { os.Setenv("DEBUG", "false") } func BenchmarkJSON(b *testing.B) { benchSetup() r := handlers() payload := fmt.Sprintf(`{ "type": "button.clicked", "source": "Login", "subject": "user1000", "time": "2018-04-05T17:31:00Z", "data": "User clicked because X"}`) for i := 0; i < b.N; i++ { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/json", strings.NewReader(payload)) r.ServeHTTP(w, req) if w.Code != http.StatusCreated { b.Errorf("expected status 201, got %d", w.Code) } } } func BenchmarkFlatBuffers(b *testing.B) { benchSetup() r := handlers() builder := flatbuffers.NewBuilder(1024) evtType := builder.CreateString("button.clicked") evtSource := builder.CreateString("service-b") evtSubject := builder.CreateString("user1000") evtTime := builder.CreateString("2018-04-05T17:31:00Z") evtData := builder.CreateString("User clicked because X") events.EventStart(builder) events.EventAddType(builder, evtType) events.EventAddSource(builder, evtSource) events.EventAddSubject(builder, evtSubject) events.EventAddTime(builder, evtTime) events.EventAddData(builder, evtData) evt := events.EventEnd(builder) builder.Finish(evt) buff := builder.FinishedBytes() for i := 0; i < b.N; i++ { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/fb", bytes.NewReader(buff)) r.ServeHTTP(w, req) if w.Code != http.StatusCreated { b.Errorf("expected status 201, got %d", w.Code) } } } func BenchmarkProtobuffer(b *testing.B) { benchSetup() r := handlers() evt := events_pb.Event{ Type: "button.clicked", Subject: "user1000", Source: "service-b", Time: "2018-04-05T17:31:00Z", Data: "User clicked because X", } payload, err := proto.Marshal(&evt) if err != nil { panic(err) } for i := 0; i < b.N; i++ { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/pb", bytes.NewReader(payload)) r.ServeHTTP(w, req) if w.Code != http.StatusCreated { b.Errorf("expected status 201, got %d", w.Code) } } }
It generates an event in each format and sends it to the API.
When we run the benchmark, we have the following result:
Running tool: /opt/homebrew/bin/go test -benchmem -run=^$ -coverprofile=/var/folders/vn/gff4w90d37xbfc_2tn3616h40000gn/T/vscode-gojAS4GO/go-code-cover -bench . github.com/eminetto/post-flatbuffers/cmd/api -failfast -v goos: darwin goarch: arm64 pkg: github.com/eminetto/post-flatbuffers/cmd/api BenchmarkJSON BenchmarkJSON-8 658386 1732 ns/op 2288 B/op 26 allocs/op BenchmarkFlatBuffers BenchmarkFlatBuffers-8 1749194 640.5 ns/op 1856 B/op 21 allocs/op BenchmarkProtobuffer BenchmarkProtobuffer-8 1497356 696.9 ns/op 1952 B/op 21 allocs/op PASS coverage: 77.5% of statements ok github.com/eminetto/post-flatbuffers/cmd/api 5.042s
If this is the first time you have analyzed the results of a Go benchmark, I recommend reading this post, where the author describes the details of each column and its meaning.
To make it easier to visualize, I created graphs for the most critical information generated by the benchmark:
Number of iterations (higher is better)
Nanoseconds per operation (lower is better)
Number of bytes allocated per operation (lower is better)
Number of allocations per operation (lower is better)
The numbers show a great advantage of binary protocols over JSON, especially Flatbuffers. This advantage is that we do not need to parse the data into structures of the language we are using.
Should you refactor your applications to replace JSON with Flatbuffers? Not necessarily. Performance is just one factor that teams must consider when selecting a communication protocol between their services and applications. But if your application receives billions of requests per day, performance improvements like those presented in this post can make a big difference in terms of costs and user experience.
The codes presented here can be found in this repository. I made the examples using the Go language, but both Protocol Buffers and Flatbuffers support different programming languages, so I would love to see other versions of these comparisons. Additionally, other benchmarks can be used, such as network consumption, CPU, etc. (since we only compare memory here).
I hope this post serves as an introduction to these formats and an incentive for new tests and experiments.
Originally published at https://eltonminetto.dev on August 05, 2024
以上がJSON vs FlatBuffers vs プロトコルバッファの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。