В настоящее время формат JSON является одним из наиболее используемых форматов для сериализации/десериализации данных, поскольку имеет встроенную поддержку в большинстве браузеров, очень хорошо известен и имеет широкий спектр библиотек на нескольких языках. Кроме того, будучи в текстовом формате, он легко читается человеком.
Однако есть некоторые проблемы: он не различает integer и float и не указывает точность. Это может стать проблемой, когда вы имеете дело с большими числами. Например, целые числа больше 2ˆ53 не могут быть точно представлены в формате IEEE 754 с плавающей запятой двойной точности, что приводит к неправильному разбору этих чисел. Кроме того, он не поддерживает двоичные строки (последовательность символов без кодировки)[1].
Существуют и другие форматы/инструменты сериализации/десериализации, такие как XML, Apache Thrift, Apache Avro, Protocol Buffers и другие, которые могут быть жизнеспособными альтернативами для различных приложений. Цель этого поста — сфокусироваться на сравнении Protocol Buffer с JSON: производительность в операциях сериализации/десериализации, размер сериализованных данных и способы использования. Тест будет основан на языке Go.
- Буфер протокола
- Как проводилось сравнение
- Выполнение тестов
- Контрольные функции
- Контрольные запросы
- ПОСТ
- ПОЛУЧИТЬ
- Размер и формат данных, сериализованных в Redis
- ProtocolBuffer
- JSON
- Буфер протокола в формате JSON
- Результаты
- Контрольные функции
- Контрольные запросы
- Размер сериализованных данных в Redis
- Заключение
- Ссылки
Буфер протокола
Protocol Buffer — это механизм, созданный компанией Google для сериализации структурированных данных, при этом он не зависит от языка и платформы. Он основан на схемах, имеет бинарный формат, обратную/прямую совместимость и генератор кода для нескольких языков, таких как: C++, C#, Dart, Go, Java, Kotlin, Python, Ruby, Objective C, Javascript, PHP и др.
Пример схемы:
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
}
Числа — это теги полей, которые используются в алгоритме сериализации/десериализации и позволяют схеме развиваться, обеспечивая совместимость со старыми и новыми версиями. После определения схемы, код на выбранном языке (в данном случае Go) генерируется с помощью компилятора, например:
protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/person.proto
Для получения более подробной информации об инструменте, его использовании, ссылок и примеров посетите сайт https://developers.google.com/protocol-buffers.
Как проводилось сравнение
Я создал простой API на Go, где данные сохраняются в экземпляре Redis, оба запускаются через Docker. Существуют конечные точки POST
(сохранить данные в Redis) и GET
(загрузить данные в Redis и вернуть):
Для сравнения использовались те же данные, которые заданы здесь в жесткой кодировке.
Были проведены следующие тесты:
- Пройдите эталонные тесты, в которых сравнивается количество операций сериализации и десериализации, выполняемых в секунду.
- Тесты с использованием инструмента WRK для выполнения одновременных запросов на маршрутах и сравнения количества запросов в секунду, поддерживаемых API
- Оценка размера и формата сериализованных данных в Redis
Все тесты проводились на компьютере macbook со следующими характеристиками:
MacBook Pro
Processador: 2 GHz Quad-Core Intel Core i5
Memória: 16 GB 3733 MHz LPDDR4X
Выполнение тестов
Контрольные функции
В первом столбце указано название тестовой функции, во втором — количество операций, выполненных в тесте, а в третьем — сколько наносекунд было потрачено на одну операцию.
- Сериализация
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
- Де-сериализация
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
Контрольные запросы
ПОСТ
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 как 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
ПОЛУЧИТЬ
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 как 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
Размер и формат данных, сериализованных в Redis
Размер данных указывается в байтах.
ProtocolBuffer
MEMORY USAGE "address-PROTO"
(integer) 312
GET "address-PROTO"
"npnbname 101x12$2ed0bb14-6710-42a8-931f-69d7ae3d0a8ex1atemail 384"nnb15-99-18"x0bna11-7-70x10x01"x0cnb18-64-85x10x02*x0cbxfcxf0xc9x85x06x10xc4xa7xa8x9ex01nqnbname 660x12$25a4b343-98da-42b8-85fa-be3e6ea0e0d2x1atemail 410"nnb20-50-93"x0cnb88-56-96x10x01"x0cnb50-78-43x10x02*x0cbxfcxf0xc9x85x06x10xb8xc7xaax9ex01"
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}}]}"
Буфер протокола в формате 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"}]}"
Результаты
Контрольные функции
- Сериализация
Формат | Операции | нс/оп |
---|---|---|
ProtocolBuffer | 42920 | 28149 |
JSON | 44228 | 27277 |
ProtocolBuffer как JSON | 30870 | 40263 |
- Десериализация
Формат | Операции | нс/оп |
---|---|---|
ProtocolBuffer | 110302 | 10970 |
JSON | 562112 | 2198 |
ProtocolBuffer как JSON | 69072 | 18385 |
Контрольные запросы
- ПОСТ
Формат | Рек/с |
---|---|
ProtocolBuffer | 2903.42 |
JSON | 2661.85 |
ProtocolBuffer как JSON | 2607.63 |
- ПОЛУЧИТЬ
Формат | Рек/с |
---|---|
ProtocolBuffer | 4067.33 |
JSON | 3927.37 |
ProtocolBuffer как JSON | 3813.99 |
Размер сериализованных данных в Redis
Формат | Размер в байтах |
---|---|
ProtocolBuffer | 312 |
JSON | 568 |
ProtocolBuffer как JSON | 576 |
Заключение
После тестирования легко сделать вывод, что использование Protocol Buffer в двоичном формате лучше во всех трех вопросах по сравнению с JSON: он выполняет больше операций в секунду при сериализации (~ +3%) и десериализации (~ +400%), API поддерживает больше запросов в секунду по POST
(~ +9%) и GET
(~ +3,5%) и размер сериализованных данных меньше (~ -55%). С другой стороны, Protocol Buffer as JSON работает хуже, чем JSON, и его не рекомендуется использовать. Недостатком является то, что поскольку это двоичный формат, сохраненные данные невозможно прочитать. Но я понимаю, что это небольшой недостаток, поскольку манипулировать данными будет программное обеспечение, а не человек.
Впечатляет разница в размере сериализованных данных (-55%). Если рассматривать крупные приложения, которые передают большой объем данных по сети и хранят их в кэшах и базах данных, то легко увидеть большое преимущество, которое дает использование Protocol Buffer: данные будут передаваться быстрее, будет использоваться меньшая пропускная способность сети и меньше места для хранения, что в конечном итоге означает экономию денег.
Кроме того, с инструментами для генерации кода на нескольких языках из определенной schema
, интеграция клиентов становится проще, хотя есть опасения по поводу эволюции schema
. Еще один инструмент, который интересен с этой точки зрения, — gRPC, но это уже для другого поста. 🙂
Ссылки
- https://developers.google.com/protocol-buffers
- https://developers.google.com/protocol-buffers/docs/reference/overview
- https://github.com/michelaquino/protobuffer-json-comparison
- [1] Клеппманн, Мартин. Проектирование приложений с интенсивным использованием данных. O’Reilly Media