Автор статьи: Сергей Прощаев (@sproshchaev), Руководитель направления Java-разработки в FinTech.
В современных распределенных системах, где компоненты могут быть реализованы на разных языках программирования, возникает необходимость в эффективной коммуникации между ними. Брокеры сообщений играют ключевую роль в этом процессе, обеспечивая асинхронную передачу данных и снижая зависимость между модулями. Однако для бесперебойного взаимодействия критически важно выбрать наиболее универсальный формат обмена информацией. Ведь важно понять что и как мы будем передавать и тут есть несколько вариантов
Его величество, JSON!
JSON (JavaScript Object Notation) стал фактическим стандартом в таких сценариях, и не случайно.
Во-первых, его “человеко-читаемая” структура, например:
{
"id": 12345,
"name": "Alice Johnson",
"age": 28,
"email": "alice.johnson@example.com",
"is_student": false,
"address": {
"street": "Main St, 45",
"city": "New York",
"country": "USA",
"postal_code": "10001"
},
"phone_numbers": [
"+1-555-123-4567",
"+1-555-987-6543"
],
"hobbies": ["reading", "hiking", "photography"],
"birth_date": "1995-08-15",
"metadata": {
"created_at": "2023-09-20T14:30:00Z",
"updated_at": "2023-09-21T09:15:00Z"
}
}Из этого набора букв и цифр интуитивно становится понятно, что это за персона и это дает возможность разработчикам быстро анализировать данные без дополнительной документации и инструментов.
Во-вторых, практически все языки программирования поддерживают работу с JSON «из коробки», что устраняет барьеры для интеграции.
Наконец, отсутствие необходимости в сложных инструментах, таких как компиляторы схем, упрощает внедрение даже в гетерогенных средах.
Все эти качества делают JSON незаменимым инструментом в арсенале современного разработчика распределенных систем.
Но, несмотря на обозначенные преимущества, JSON имеет и ряд недостатков:
1. Избыточность данных
JSON требует повторения ключей для каждого объекта в массиве, что увеличивает размер передаваемых данных. Пример:
[
{
"id": 1,
"name": "Alice",
"department": "HR"
},
{
"id": 2,
"name": "Bob",
"department": "IT"
}
]Здесь ключи id, name, department дублируются для каждого сотрудника. В больших массивах это приводит к лишнему расходу трафика и памяти.
2. Отсутствие строгой типизации
JSON не поддерживает явную типизацию полей. Это может вызвать ошибки, если данные не соответствуют ожидаемому типу, например если система ожидает числовое значение age, строка "25" приведет к сбоям в работе.
{
"age": "25" // Ожидается число, но пришла строка?
}Аналогично, даты в формате строки ("2023-09-20") не имеют встроенной валидации, и некорректный формат (например, "20-09-2023") может нарушить логику приложения.
3. Нет встроенной поддержки версионирования
Изменение структуры JSON часто ломает обратную совместимость. Добавление, удаление или переименование полей требует ручного управления, что усложняет поддержку систем.
На��ример, если мы сформируем Версию №1
// Версия 1:
{
"user": {
"name": "Alice",
"phone": "555-1234"
}
}И далее внесем изменения в структуру и получим Версию №2:
// Версия 2:
{
"user": {
"full_name": "Alice", // Поле переименовано
"phone_number": "555-1234", // Поле переименовано
"email": "alice@example.com" // Новое поле
}
}То «старые клиенты», которые ожидают поля phone, не смогут обработать новую структуру и для решения этой проблемы придется либо поддерживать устаревшие поля параллельно с новыми, либо внедрять middleware для преобразования изменившихся данных.
Альтернатива JSON — бинарные форматы
Современные высоконагруженные системы, такие как потоковые платформы на базе Apache Kafka, всё чаще отказываются от JSON в пользу бинарных форматов. Этот сдвиг обусловлен ключевыми преимуществами бинарных данных, которые критически важны для масштабируемых и отказоустойчивых решений.
Наиболее часто встречающиеся в проектах бинарные форматы это Apache Avro, Protocol Buffers, Apache Thrift.
Во всех бинарных форматах есть схемы, которые описывают то, как организованы данные: типы полей, их порядок, вложенные структуры, кодеки сжатия и т.д. Без схемы бинарные данные — это просто набор байтов. Схема позволяет преобразовать их в осмысленные объекты. Схема проверяет, соответствуют ли данные ожидаемым типам и формату и позволяет компактно кодировать данные.
Данные — это сериализованные бинарные объекты, соответствующие описанной схеме.
Давайте рассмотрим кратко особенности каждого из бинарных форматов:
Apache Avro
Apache Avro — это бинарный формат сериализации данных, созданный для работы в распределенных системах, таких как Hadoop и Kafka. Его ключевая идея — разделение схемы и данных, что делает его незаменимым в сценариях, где важна компактность, скорость и обратная совместимость.
Схема в Avro описывается отдельно от самих данных. Например, для объекта User схема выглядит так:
{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "long"},
{"name": "name", "type": "string"},
{"name": "email", "type": ["string", "null"], "default": null}
]
}В этом примере type: "record" определяет структуру данных, и далее поля схемы содержат информацию об имени и типе данных, причем можно задать описание типа поля в виде возможных значений, так например email может быть строкой или null.
Данные в Avro хранятся в бинарном виде без дублирования ключей. Бинарные данные занимают на 20–80% меньше места, чем JSON, что является достаточно критичным для оптимизации трафика в стриминговых платформах, таких к которым относится Kafka.
Avro также позволяет и безопасно обновлять схемы. Например, если мы добавим новое поле is_active:
{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "long"},
{"name": "name", "type": "string"},
{"name": "email", "type": ["string", "null"], "default": null},
{"name": "is_active", "type": "boolean", "default": true} // Нов.поле
]
}Старые клиенты, не знающие о is_active, будут использовать значение по умолчанию (true), а новые — корректно обработают данные.
Protocol Buffers (Protobuf)
Protocol Buffers или Protobuf — это бинарный формат сериализации данных, разработанный компанией Google для эффективного обмена информацией между сервисами. Protobuf требует описания структуры данных в .proto-файлах, что гарантирует типобезопасность и высокую производительность. Protobuf является основой для gRPC — высокопроизводительного фреймворка RPC.
Схема данных определяется в специальном файле, который компилируется в код на нужном языке программирования. Protobuf поддерживает более 10 языков программирования (C++, Java, Python, Go и др.) Пример схемы для объекта User:
syntax = "proto3";
message User {
int64 id = 1;
string name = 2;
string email = 3;
bool is_active = 4; // Новое поле в версии 2
}Здесь: proto3 — версия синтаксиса. Каждое поле имеет уникальный номер (например, id = 1), который используется для идентификации данных в бинарном формате.
Protobuf поддерживает как обратную, так и прямую совместимость. Добавление новых полей (например, is_active в примере выше) не ломает работу старых клиентов — они просто игнорируют неизвестные поля. Удаление устаревших полей тоже допустимо, если их номера не используются повторно. Пример обновленной схемы (версия 2):
message User {
int64 id = 1;
string name = 2;
string email = 3;
bool is_active = 4; // Новое поле
string phone = 5; // Еще одно новое поле
}Apache Thrift
Apache Thrift — это гибридный инструмент, сочетающий бинарную сериализацию данных и удаленные вызовы процедур (RPC). Thrift был создан разработчиками Facebook и теперь поддерживается Apache. Thrift позволяет создавать высокопроизводительные распределенные системы, используя единую систему типов и протоколов.
В Thrift структуры данных и API сервисов определяются в спецификации с расширением .thrift. Пример для объекта User и простого сервиса:
namespace java com.example.thrift // Пространство имен для Java
struct User {
1: required i64 id,
2: required string name,
3: optional string email,
4: bool is_active = true // Поле с дефолтным значением
}
service UserService {
User get_user(1: i64 id) // Метод для получения пользователя
}В этом примере зарезервированное слово struct определяет структуру данных, а service задает интерфейс RPC-сервиса. Каждое поле имеет уникальный номер.
Thrift поддерживает безопасное обновление схем. Если мы добавляем новые поля с модификатором optional, то старые клиенты смогут продолжить работу. Удаление устаревших полей также допустимо, если они не были помечены как required. Пример обновленной схемы (добавление поля phone):
struct User {
1: required i64 id,
2: required string name,
3: optional string email,
4: bool is_active = true,
5: optional string phone // Новое поле
}Thrift также генерирует код для клиентов и серверов на основе .thrift-файла.
Преимущества очевидны, но как всем этим управлять?
Для контроля версий схем и обеспечения совместимости в распределенных системах используется специальный регистр Schema Registry — это централизованный репозиторий, где хранятся и управляются схемы данных.
В экосистеме Kafka сервис, предоставляемый со стороны Schema Registry позволяет валидировать данные при записи в топики, управлять версионированием, проверяя, что новые схемы не сломают старых клиентов и избегать дублирования схем, сокращая ошибки при обновлениях. Это решает ключевую проблему эволюции данных — разработчики могут безопасно изменять структуру, не опасаясь, что изменения нарушат работу существующих сервисов.
Заключение
Мы рассмотрели основные и наиболее популярные форматы обмена информацией в распределенных системах. Безусловно то, что JSON остается удобным для человека форматом благодаря своей читаемости и простоте отладки. Однако в распределенных системах, где критичны скорость, компактность и надежность, бинарные форматы вроде Apache Avro, Protocol Buffers и Apache Thrift демонстрируют неоспоримое превосходство. Они минимизируют размер данных, ускоряют обработку, обеспечивают строгую типизацию и поддерживают эволюцию схем, что особенно важно для высоконагруженных систем, таких как Kafka. Выбор между JSON и бинарными форматами зависит от задачи: там, где важна человеко-ориентированность — JSON, а где требуется масштабируемость и эффективность — бинарные решения становятся оптимальным выбором.
Недавно прошел открытый урок на тему «Kafka Connect: Лёгкая интеграция с внешними системами». На нём мы разобрали архитектуру Kafka Connect, принципы работы коннекторов, реализовали настройку и запуск коннекторов для работы с базами данных и файловыми системами. Кроме того, участники получили советы по эффективной отладке и масштабированию Kafka Connect. Если интересно — посмотрите в записи.
Чтобы открыть доступ ко всем открытым урокам, а заодно проверить своей уровень знаний Apache Kafka, пройдите вступительное тестирование.
