Protocol Buffer x JSON para serialização de dados
Michel Aquino
Posted on August 13, 2022
Atualmente o formato JSON é um dos formatos mais usados para serialização/deserialização de dados por ter suporte nativo na maioria dos navegadores, ser muito conhecido e ter uma grande variedade de bibliotecas nas mais diversas linguagens. Além disso, por ser em formato texto é legível para humanos.
Porém, há alguns problemas: não faz distinção entre inteiro e float e não especifica precisão. Isso pode ser um problema quando se está lidando com números grandes. Por exemplo, inteiros maiores que 2ˆ53 não podem ser exatamente representados em um número de ponto flutuante de precisão dupla IEEE 754, fazendo com que esses números sejam parseados de forma incorreta. Além disso, não suporta strings binárias (sequência de caracteres sem encoding)[1].
Existem outros formatos/ferramentas de serialização/deserialização como XML, Apache Thrift, Apache Avro ,Protocol Buffers entre outros, que podem ser alternativas viáveis para várias aplicações. O objetivo deste post é focar na comparação de Protocol Buffer com JSON: performance na operação de serialização/deserialização, tamanho do dado serializado e formas de uso. O teste será baseado na linguagem Go.
Protocol Buffer
Protocol Buffer é um mecanismo criado pela Google para serialização de dados estruturados, onde é agnóstico de linguagem e plataforma. É baseado em schema, tem formato binário, compatibilidade com versões antigas e novas (backward/forward compatibility), e um gerador de código para várias linguagens, como: C++, C#, Dart, Go, Java, Kotlin, Python, Ruby, Objective C, Javascript, PHP, entre outras.
Exemplo de schema:
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
}
Os números são field tags, que são usados no algoritmo de serialização/deserialização e permite a evolução do schema, garantindo compabilidade com versões antigas e novas. Após definir o schema, o código na linguagem escolhida (nesse caso Go) é gerado usando um compilador, por ex:
protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/person.proto
Para mais detalhes sobre a ferramenta, como usar, referências e exemplos visite a página https://developers.google.com/protocol-buffers.
Como foi feito a comparação
Criei uma API simples em Go onde os dados são salvos em uma instância do Redis, ambos rodando via Docker. Há endpoints POST
(salvar um dado no Redis) e GET
(bucar o dado no Redis e devolver):
-
/proto
: serializa/deserializa os dados em ProtocolBuffer baseado no schema definido -
/json
: serializa/deserializa os dados em JSON -
/protojson
: serializa/deserializa os dados em JSON usando o schema definido em ProtocolBuffer
Para fazer a comparação foi usado o mesmo dado, que está definido hardcoded aqui.
Os seguintes testes foram feitos:
- Testes de benchmark do Go, onde são comparados quantas operações de serialização e deserialização são feitos por segundo
- Testes usando a ferramenta WRK para fazer requests simultâneos nas rotas e comparar quantos requests por segundo a API suporta
- Avaliação do tamanho e formato do dado serializado no Redis
Todos os testes foram feitos usando um macbook com as seguintes especificações:
MacBook Pro
Processador: 2 GHz Quad-Core Intel Core i5
Memória: 16 GB 3733 MHz LPDDR4X
Execução dos testes
Benchmark funções
A primeira coluna refere ao nome da função de teste, a segunda à quantidade de operações realizadas no teste e a terceira quantos nanosegundos foram gastos por operação.
- Serialização
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz
BenchmarkSerializeJSON-8 42920 28149 ns/op
BenchmarkSerializeProtocolBuffer-8 44228 27277 ns/op
BenchmarkSerializeProtocolBufferAsJSON-8 30870 40263 ns/op
- Deserialização
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz
BenchmarkDeserializeJSON-8 110302 10970 ns/op
BenchmarkDeserializeProtocolBuffer-8 562112 2198 ns/op
BenchmarkDeserializeProtocolBufferAsJSON-8 69072 18385 ns/op
Benchmark requests
POST
ProtocolBuffer
Running 10s test @ http://localhost:8888/proto
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 36.22ms 24.47ms 249.28ms 71.60%
Req/Sec 29.12 13.40 161.00 76.51%
29326 requests in 10.10s, 2.10MB read
Requests/sec: 2903.42
Transfer/sec: 212.65KB
JSON
Running 10s test @ http://localhost:8888/json
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 79.52ms 169.71ms 1.09s 93.17%
Req/Sec 29.16 13.22 110.00 76.06%
26869 requests in 10.09s, 1.92MB read
Requests/sec: 2661.85
Transfer/sec: 194.96KB
ProtocolBuffer como JSON
Running 10s test @ http://localhost:8888/protojson
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 40.33ms 27.15ms 241.85ms 70.52%
Req/Sec 26.16 12.45 100.00 63.87%
26330 requests in 10.10s, 1.88MB read
Requests/sec: 2607.63
Transfer/sec: 190.99KB
GET
ProtocolBuffer
Running 10s test @ http://localhost:8888/proto
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 25.33ms 14.87ms 143.20ms 72.99%
Req/Sec 40.86 14.23 171.00 74.51%
41078 requests in 10.10s, 13.59MB read
Requests/sec: 4067.33
Transfer/sec: 1.35MB
JSON
Running 10s test @ http://localhost:8888/json
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 26.13ms 15.08ms 135.74ms 71.77%
Req/Sec 39.44 13.27 171.00 77.17%
39667 requests in 10.10s, 22.66MB read
Requests/sec: 3927.37
Transfer/sec: 2.24MB
ProtocolBuffer como JSON
Running 10s test @ http://localhost:8888/protojson
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 27.08ms 15.91ms 156.36ms 73.76%
Req/Sec 38.24 13.48 141.00 75.38%
38499 requests in 10.09s, 22.65MB read
Requests/sec: 3813.99
Transfer/sec: 2.24MB
Tamanho e formato do dado serializado no Redis
O tamanho do dado é em bytes.
ProtocolBuffer
MEMORY USAGE "address-PROTO"
(integer) 312
GET "address-PROTO"
"\np\n\bname 101\x12$2ed0bb14-6710-42a8-931f-69d7ae3d0a8e\x1a\temail 384\"\n\n\b15-99-18\"\x0b\n\a11-7-70\x10\x01\"\x0c\n\b18-64-85\x10\x02*\x0c\b\xfc\xf0\xc9\x85\x06\x10\xc4\xa7\xa8\x9e\x01\nq\n\bname 660\x12$25a4b343-98da-42b8-85fa-be3e6ea0e0d2\x1a\temail 410\"\n\n\b20-50-93\"\x0c\n\b88-56-96\x10\x01\"\x0c\n\b50-78-43\x10\x02*\x0c\b\xfc\xf0\xc9\x85\x06\x10\xb8\xc7\xaa\x9e\x01"
JSON
MEMORY USAGE "address-JSON"
(integer) 568
GET "address-JSON"
"{\"people\":[{\"name\":\"name 992\",\"id\":\"59ed3f8a-ef20-48c6-a392-52663360658c\",\"email\":\"email 122\",\"phones\":[{\"number\":\"19-31-88\"},{\"number\":\"80-46-55\",\"type\":1},{\"number\":\"33-58-89\",\"type\":2}],\"last_updated\":{\"seconds\":1622308984,\"nanos\":121239469}},{\"name\":\"name 553\",\"id\":\"0287e61b-5f63-45a9-8dff-9ebae231f31b\",\"email\":\"email 783\",\"phones\":[{\"number\":\"82-23-38\"},{\"number\":\"91-36-36\",\"type\":1},{\"number\":\"62-58-71\",\"type\":2}],\"last_updated\":{\"seconds\":1622308984,\"nanos\":121285583}}]}"
Protocol Buffer como JSON
MEMORY USAGE "address-PROTO-JSON"
(integer) 576
GET "address-PROTO-JSON"
"{\"people\":[{\"name\":\"name 284\",\"id\":\"6bd9b8a1-bf1b-4999-8bb2-11839ed5cc06\",\"email\":\"email 590\",\"phones\":[{\"number\":\"98-63-12\"},{\"number\":\"49-36-95\",\"type\":\"HOME\"},{\"number\":\"24-81-62\",\"type\":\"WORK\"}],\"lastUpdated\":\"2021-05-29T17:23:11.963003464Z\"},{\"name\":\"name 666\",\"id\":\"629fad5c-ce7b-4265-9dcc-be99aac9bf63\",\"email\":\"email 545\",\"phones\":[{\"number\":\"50-92-79\"},{\"number\":\"18-63-76\",\"type\":\"HOME\"},{\"number\":\"31-33-11\",\"type\":\"WORK\"}],\"lastUpdated\":\"2021-05-29T17:23:11.963026415Z\"}]}"
Resultados
Benchmark funções
- Serialização
Formato | Operações | ns/op |
---|---|---|
ProtocolBuffer | 42920 | 28149 |
JSON | 44228 | 27277 |
ProtocolBuffer como JSON | 30870 | 40263 |
- Deserialização
Formato | Operações | ns/op |
---|---|---|
ProtocolBuffer | 110302 | 10970 |
JSON | 562112 | 2198 |
ProtocolBuffer como JSON | 69072 | 18385 |
Benchmark requests
- POST
Formato | Req/s |
---|---|
ProtocolBuffer | 2903.42 |
JSON | 2661.85 |
ProtocolBuffer como JSON | 2607.63 |
- GET
Formato | Req/s |
---|---|
ProtocolBuffer | 4067.33 |
JSON | 3927.37 |
ProtocolBuffer como JSON | 3813.99 |
Tamanho do dado serializado no Redis
Formato | Tamanho em bytes |
---|---|
ProtocolBuffer | 312 |
JSON | 568 |
ProtocolBuffer como JSON | 576 |
Conclusão
Após os testes, é fácil concluir que o uso do Protocol Buffer no formato binário é melhor nos 3 quesitos em relação ao JSON: faz mais operações por segundo na serialização (~ +3%) e deserialização (~ +400%), a API suporta mais requests por segundo no POST
(~ +9%) e GET
(~ +3.5%) e o tamanho do dado serializado é menor (~ -55%). Por outro lado, o Protocol Buffer como JSON performou menos que o JSON, não sendo aconselhado o uso. A desvantagem é que como é um formato binário, o dado salvo não é legível. Mas entendo que é um desvantagem pequena, já que será o software que irá manipular o dado e não um humano.
É impressionante a diferença de tamanho do dado serializado (-55%). Pensando em aplicações grandes, que trafegam um grande volume de dados via rede e armazena esses dados em caches e banco de dados, é fácil perceber a grande vantagem que o uso do Protocol Buffer trás: dados serão trafegados mais rapidamente, usará menos banda de rede e usará menos espaço para armazenamento, que no final significará economia de dinheiro.
Além disso, com as ferramentas de geração de código em várias linguagens a partir do schema
definido, a integração de clients se torna mais simples, apesar de haver preocupações em relação à evolução do schema
. Outra ferramenta que é interessantre nesse ponto é o gRPC, mas isso fica pra outro post. :)
Referências
Posted on August 13, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.