서비스/마이크로서비스 간의 통신을 생각할 때 가장 먼저 떠오르는 옵션은 오래된 JSON입니다. 형식에는 다음과 같은 장점이 있으므로 이는 이유가 없습니다.
JSON을 사용하는 것은 기업의 일상 생활에서 개발되는 대부분의 API에 권장되는 사항입니다. 그러나 성능이 중요한 경우에는 다른 대안을 고려해야 할 수도 있습니다. 이 게시물의 목적은 애플리케이션 간 통신과 관련하여 JSON에 대한 두 가지 대안을 보여주는 것입니다.
그런데 JSON의 문제점은 무엇인가요? 장점 중 하나는 "사람이 쉽게 읽을 수 있다"는 점이지만, 이는 성능상 약점이 될 수 있다. 사실은 JSON 콘텐츠를 우리가 사용하는 프로그래밍 언어에 의해 알려진 일부 구조로 변환해야 한다는 것입니다. 이 규칙의 예외는 JSON이 기본이므로 JavaScript를 사용하는 경우입니다. 하지만 예를 들어 Go와 같은 다른 언어를 사용하는 경우 아래의 (불완전한) 코드 예에서 볼 수 있듯이 데이터를 구문 분석해야 합니다.
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) }
이 문제를 해결하기 위해 프로토콜 버퍼와 플랫 버퍼라는 두 가지 대안을 테스트할 수 있습니다.
공식 웹사이트에 따르면 Google에서 만든 Protobuf(프로토콜 버퍼)는 다음과 같습니다.
프로토콜 버퍼는 구조화된 데이터를 직렬화하기 위한 Google의 언어 중립적이고 플랫폼 중립적이며 확장 가능한 메커니즘입니다. XML이라고 생각하면 되지만 더 작고 빠르며 더 간단합니다. 데이터를 한 번 구조화하는 방법을 정의합니다. 그런 다음 특별히 생성된 소스 코드를 사용하여 다양한 언어를 사용하는 다양한 데이터 스트림에 구조화된 데이터를 빠르게 쓰고 읽을 수 있습니다.
일반적으로 gRPC와 함께 사용되지만(반드시 그런 것은 아님) Protobuf는 JSON의 텍스트 형식에 비해 성능을 크게 향상시키는 바이너리 프로토콜입니다. 하지만 JSON과 동일한 문제가 '고통'됩니다. 즉, 이를 우리 언어의 데이터 구조로 구문 분석해야 합니다. 예를 들어 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) }
바이너리 프로토콜을 채택하면 성능이 향상되지만 여전히 데이터 구문 분석 문제를 해결해야 합니다. 세 번째 경쟁자는 이 문제를 해결하는 데 중점을 두고 있습니다.
공식 홈페이지에 따르면:
FlatBuffers는 C++, C#, C, Go, Java, Kotlin, JavaScript, Lobster, Lua, TypeScript, PHP, Python, Rust 및 Swift를 위한 효율적인 크로스 플랫폼 직렬화 라이브러리입니다. 처음에는 게임 개발 및 기타 성능이 중요한 애플리케이션을 위해 Google에서 만들어졌습니다.
처음에는 게임 개발을 위해 만들어졌지만 이 게시물에서 연구하는 환경에 완벽하게 들어맞습니다. 장점은 바이너리 프로토콜일 뿐만 아니라 데이터를 구문 분석할 필요가 없다는 것입니다. 예를 들어 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()))
하지만 JSON에 대한 두 가지 대안이 얼마나 더 성능이 좋나요? 조사해보자...
첫 번째로 든 질문은 '이것을 실제 시나리오에 어떻게 적용할 수 있을까?'였습니다. 저는 다음과 같은 시나리오를 상상했습니다.
내부 마이크로서비스 아키텍처를 갖추고 수백만 명의 고객이 매일 액세스하는 모바일 애플리케이션을 보유하고 있으며 감사 목적으로 사용자 및 시스템에서 생성된 이벤트를 저장해야 하는 회사입니다.
실제 시나리오입니다. 너무 현실적이라 제가 일하는 회사에서도 매일 같이 생활하고 있어요 :)
참고: 위의 시나리오는 단순화된 것이며 팀 애플리케이션의 실제 복잡성을 나타내지는 않습니다. 교육적인 목적으로 사용됩니다.
첫 번째 단계는 프로토콜 버퍼와 플랫 버퍼에서 이벤트를 정의하는 것입니다. 둘 다 스키마를 정의하기 위한 자체 언어를 가지고 있으며 이를 사용하여 사용할 언어로 코드를 생성할 수 있습니다. 각 구성표에 대한 자세한 내용은 문서에서 쉽게 찾을 수 있으므로 다루지 않겠습니다.
event.proto 파일에는 프로토콜 버퍼 정의가 있습니다.
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; }
event.fbs 파일에는 Flatbuffers에 해당하는 파일이 있습니다.
namespace events; table Event { type: string; subject:string; source:string; time:string; data:string; } root_type Event;
다음 단계는 이러한 정의를 사용하여 필요한 코드를 생성하는 것입니다. 다음 명령은 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
그 결과 각 형식의 데이터를 조작하는 Go 패키지가 생성되었습니다.
요구사항이 충족되면 다음 단계는 이벤트 API를 구현하는 것이었습니다. main.go는 다음과 같습니다.
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 }
더 나은 정리를 위해 각 기능을 구분하는 파일을 만들었습니다.
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 중국어 웹사이트의 기타 관련 기사를 참조하세요!