Автор статьи: Сергей Прощаев (@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, пройдите вступительное тестирование.