Heim  >  Artikel  >  Backend-Entwicklung  >  JSON vs. FlatBuffers vs. Protokollpuffer

JSON vs. FlatBuffers vs. Protokollpuffer

WBOY
WBOYOriginal
2024-08-08 01:31:54442Durchsuche

Wenn wir über die Kommunikation zwischen Diensten/Microservices nachdenken, fällt uns als Erstes das gute alte JSON ein. Und das nicht ohne Grund, denn das Format hat Vorteile, wie zum Beispiel:

  • ist sowohl für Computer als auch für Menschen leicht lesbar;
  • Alle modernen Programmiersprachen können JSON lesen und generieren;
  • Es ist viel weniger ausführlich als die vorherige Alternative, Jurassic XML.

Die Verwendung von JSON ist die Empfehlung für die überwiegende Mehrheit der APIs, die im täglichen Leben von Unternehmen entwickelt werden. Aber in einigen Fällen, in denen die Leistung von entscheidender Bedeutung ist, müssen wir möglicherweise andere Alternativen in Betracht ziehen. Ziel dieses Beitrags ist es, zwei Alternativen zu JSON aufzuzeigen, wenn es um die Kommunikation zwischen Anwendungen geht.

Aber was ist das Problem mit JSON? Einer seiner Vorteile besteht darin, dass es „für Menschen leicht lesbar“ ist, dies kann jedoch ein Schwachpunkt in der Leistung sein. Tatsache ist, dass wir den JSON-Inhalt in eine Struktur konvertieren müssen, die der von uns verwendeten Programmiersprache bekannt ist. Eine Ausnahme von dieser Regel besteht bei der Verwendung von JavaScript, da JSON darin nativ ist. Wenn Sie jedoch eine andere Sprache verwenden, zum Beispiel Go, müssen wir die Daten analysieren, wie wir im folgenden (unvollständigen) Codebeispiel sehen können:

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"`
}

var e event
err := json.NewDecoder(data).Decode(&e)
if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
}

Um dieses Problem zu lösen, können wir zwei Alternativen testen: Protocol Buffers und Flatbuffers.

Protokollpuffer

Protobuf (Protocol Buffers), erstellt von Google, ist laut der offiziellen Website:

Protokollpuffer sind Googles sprachneutraler, plattformneutraler und erweiterbarer Mechanismus zur Serialisierung strukturierter Daten – denken Sie an XML, aber kleiner, schneller und unkomplizierter. Sie legen einmalig fest, wie Ihre Daten strukturiert sein sollen. Anschließend können Sie speziell generierten Quellcode verwenden, um Ihre strukturierten Daten in verschiedenen Sprachen schnell in und aus verschiedenen Datenströmen zu schreiben und zu lesen.

Protobuf wird im Allgemeinen in Verbindung mit gRPC verwendet (aber nicht unbedingt) und ist ein Binärprotokoll, das die Leistung im Vergleich zum Textformat von JSON erheblich steigert. Aber es „leidet“ unter dem gleichen Problem wie JSON: Wir müssen es in eine Datenstruktur unserer Sprache parsen. Zum Beispiel in Go:

//generated code
type Event struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Type    string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
    Subject string `protobuf:"bytes,2,opt,name=subject,proto3" json:"subject,omitempty"`
    Source  string `protobuf:"bytes,3,opt,name=source,proto3" json:"source,omitempty"`
    Time    string `protobuf:"bytes,4,opt,name=time,proto3" json:"time,omitempty"`
    Data    string `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"`
}

e := Event{}
err := proto.Unmarshal(data, &e)
if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
}

Die Einführung eines Binärprotokolls bringt uns einen Leistungsgewinn, aber wir müssen noch das Problem der Datenanalyse lösen. Unser dritter Konkurrent konzentriert sich auf die Lösung dieses Problems.

Flatbuffer

Laut der offiziellen Website:

FlatBuffers ist eine effiziente plattformübergreifende Serialisierungsbibliothek für C++, C#, C, Go, Java, Kotlin, JavaScript, Lobster, Lua, TypeScript, PHP, Python, Rust und Swift. Es wurde ursprünglich bei Google für die Spieleentwicklung und andere leistungskritische Anwendungen entwickelt.

Obwohl es ursprünglich für die Spieleentwicklung entwickelt wurde, passt es perfekt in die Umgebung, die wir in diesem Beitrag untersuchen. Sein Vorteil besteht darin, dass wir die Daten nicht analysieren müssen und es sich um ein binäres Protokoll handelt. Zum Beispiel in Go:

//generated code
e := events.GetRootAsEvent(data, 0)

//we can use the data directly
saveEvent(string(e.Type()), string(e.Source()), string(e.Subject()), string(e.Time()), string(e.Data()))

Aber wie viel leistungsfähiger sind die beiden Alternativen zu JSON? Lasst uns untersuchen...

Anwendung

Die erste Frage, die mir in den Sinn kam, war: „Wie kann ich das in einem realen Szenario anwenden?“ Ich habe mir folgendes Szenario vorgestellt:

Ein Unternehmen mit einer mobilen Anwendung, auf die täglich Millionen von Kunden zugreifen, mit einer internen Microservices-Architektur und das von Benutzern und Systemen generierte Ereignisse zu Prüfzwecken speichern muss.

Das ist ein echtes Szenario. So real, dass ich jeden Tag in der Firma, in der ich arbeite, damit lebe :)

JSON vs FlatBuffers vs Protocol Buffers

Hinweis: Das obige Szenario ist eine Vereinfachung und spiegelt nicht die tatsächliche Komplexität der Teamanwendung wider. Es dient pädagogischen Zwecken.

Der erste Schritt besteht darin, ein Ereignis in Protocol Buffers und Flatbuffers zu definieren. Beide verfügen über eine eigene Sprache zum Definieren von Schemata, mit deren Hilfe wir dann Code in den von uns verwendeten Sprachen generieren können. Ich werde nicht auf die Details der einzelnen Schemata eingehen, da diese leicht in der Dokumentation zu finden sind.

Die Datei event.proto hat die Protokollpufferdefinition:

syntax = "proto3";
package events;

option go_package = "./events_pb";

message Event {
    string type = 1;
    string subject = 2;
    string source = 3;
    string time = 4;
    string data = 5;
}

Und die Datei event.fbs hat das Äquivalent in Flatbuffers:

namespace events;

table Event {
    type: string;
    subject:string;
    source:string;
    time:string;
    data:string;
}

root_type Event;

Der nächste Schritt besteht darin, diese Definitionen zu verwenden, um den erforderlichen Code zu generieren. Die folgenden Befehle installieren die Abhängigkeiten auf macOS:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
brew install protobuf
protoc -I=. --go_out=./ event.proto
brew install flatbuffers
flatc --go event.fbs

Das Ergebnis ist die Erstellung von Go-Paketen zur Bearbeitung von Daten in jedem Format.

Nachdem die Anforderungen erfüllt waren, bestand der nächste Schritt in der Implementierung der Event-API. Das main.go sah so aus:

package main

import (
    "fmt"
    "net/http"
    "os"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
    "github.com/google/uuid"
)

func main() {
    r := handlers()
    http.ListenAndServe(":3000", r)
}

func handlers() *chi.Mux {
    r := chi.NewRouter()
    if os.Getenv("DEBUG") != "false" {
        r.Use(middleware.Logger)
    }
    r.Post("/json", processJSON())
    r.Post("/fb", processFB())
    r.Post("/pb", processPB())
    return r
}

func saveEvent(evType, source, subject, time, data string) {
    if os.Getenv("DEBUG") != "false" {
        id := uuid.New()
        q := fmt.Sprintf("insert into event values('%s', '%s', '%s', '%s', '%s', '%s')", id, evType, source, subject, time, data)
        fmt.Println(q)
    }
    // save event to database
}

Zur besseren Organisation habe ich Dateien zur Trennung der einzelnen Funktionen erstellt, die wie folgt aussahen:

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

Das obige ist der detaillierte Inhalt vonJSON vs. FlatBuffers vs. Protokollpuffer. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn