ホームページ  >  記事  >  バックエンド開発  >  JSON vs FlatBuffers vs プロトコルバッファ

JSON vs FlatBuffers vs プロトコルバッファ

WBOY
WBOYオリジナル
2024-08-08 01:31:54241ブラウズ

サービス/マイクロサービス間の通信について考えるとき、最初に思い浮かぶのは古き良き JSON です。この形式には次のような利点があるため、これには理由がないわけではありません。

  • コンピュータと人間の両方が簡単に読むことができます;
  • 最新のプログラミング言語はすべて JSON を読み取り、生成できます。
  • 以前の代替手段である Jurassic XML よりもはるかに冗長ではありません。

企業の日常生活で開発される 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 つの方法はどれくらいパフォーマンスが優れているのでしょうか?調べてみましょう...

応用

私の頭に浮かんだ最初の疑問は、「これを実際のシナリオにどのように適用できるでしょうか?」ということでした。私は次のようなシナリオを想像しました:

毎日何百万もの顧客がアクセスするモバイル アプリケーションを備え、内部マイクロサービス アーキテクチャを備えており、監査目的でユーザーとシステムによって生成されたイベントを保存する必要がある企業。

これは本物のシナリオです。とてもリアルなので、私が働いている会社で毎日それと一緒に暮らしています:)

JSON vs FlatBuffers vs Protocol Buffers

注: 上記のシナリオは簡略化したものであり、チームのアプリケーションの実際の複雑さを表すものではありません。教育目的に役立ちます。

最初のステップは、プロトコル バッファーとフラットバッファーでイベントを定義することです。どちらもスキーマを定義するための独自の言語を持っており、それを使用して、使用する言語でコードを生成できます。各スキームの詳細については、ドキュメントで簡単に説明されているため、ここでは詳しく説明しません。

ファイル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.

Benchmark

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)

JSON vs FlatBuffers vs Protocol Buffers

Nanoseconds per operation (lower is better)

JSON vs FlatBuffers vs Protocol Buffers

Number of bytes allocated per operation (lower is better)

JSON vs FlatBuffers vs Protocol Buffers

Number of allocations per operation (lower is better)

JSON vs FlatBuffers vs Protocol Buffers

Conclusion

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 サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。