Protocol Buffer x JSON para serialização de dados

michelaquino

Michel Aquino

Posted on August 13, 2022

Protocol Buffer x JSON para serialização de dados

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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):

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Tamanho e formato do dado serializado no Redis

O tamanho do dado é em bytes.

ProtocolBuffer

MEMORY USAGE "address-PROTO"
(integer) 312
Enter fullscreen mode Exit fullscreen mode
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"
Enter fullscreen mode Exit fullscreen mode

JSON

MEMORY USAGE "address-JSON"
(integer) 568
Enter fullscreen mode Exit fullscreen mode
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}}]}"
Enter fullscreen mode Exit fullscreen mode

Protocol Buffer como JSON

MEMORY USAGE "address-PROTO-JSON"
(integer) 576
Enter fullscreen mode Exit fullscreen mode
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\"}]}"
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
michelaquino
Michel Aquino

Posted on August 13, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related