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

Комментарии (18)


  1. alteest2
    20.05.2025 11:59

    Так то да, но ... попробуйте сравнить, хранение в Кафе, например джисона, который сжимается gzip и например протобаф. Посмотрите кто больше занимает места.

    А ещё возможность поиска и пр.


    1. lastrix
      20.05.2025 11:59

      В общем случае бинарный формат хранения с сжатием gzip даст больше сохраненного места, чем json с сжатием.

      Что мешает сжимать protobuf?


      1. BugM
        20.05.2025 11:59

        На практике разница зачастую несущественна. Зато удобство эксплуатации отличается на порядки.


      1. alteest2
        20.05.2025 11:59

        Да ради бога, вопрос в использовании CPU utilization. Если можете себе позволить, то вперёд. Вопрос от объемов и вот когда начнёте упираться в производительность, то удачи это все размотать


        1. lastrix
          20.05.2025 11:59

          Я лишь указал, что сравнивать размер данных сырого protobuf и сжатого json - не очень актуально.
          А жать данные - да, это всегда вопрос утилизации проца. Но иногда бывает так, что потери в проце становятся ничтожными, в сравнении с потерями от передачи сырых данных.


    1. ptr128
      20.05.2025 11:59

      Сжатый protobuf явно будет занимать меньше места, чем такой же сжатый json. Но не забывайте, что даже на 10 Гбит сжатие наверняка увеличит задержки и, скорее всего, снизит скорость. А уж на 40/100 Гбит, обычных для обмена данными внутри k8s, сжатие вообще бессмысленно. Иными словами, если достаточно 100 Мбит сети, то преимущества двоичных неочевидны. А уже на 10 Гбитах сразу увидите разницу.

      Не забывайте ещё, что нагрузка CPU при преобразовании чисел из символьного представления в двоичное или обратно будет существенно различаться от нагрузки при декодировании или кодировании в двоичном формате.


      1. BugM
        20.05.2025 11:59

        Вы сколько топиков Кафки на 10гбит сделали или поддерживаете? И есть не секрет, то что в них едет?


        1. ptr128
          20.05.2025 11:59

          Вы сколько топиков Кафки на 10гбит сделали или поддерживаете?

          На проде сейчас около пятиста. Но у нас 40/100 гигабитка.

          И есть не секрет, то что в них едет?

          Самый большой объем - трекинг транспорта (только полувагонов сотня тысяч), обмен с АСУ ТП двух портов и десятка комбинатов/заводов/фабрик.

          Само собой, данные обогащаются из нескольких источников.


          1. BugM
            20.05.2025 11:59

            10гбит это примерно гигабайт данных в секунду. Или 60 гигабайт в минуту. Делим на 100 тысяч вагонов и получаем 600 килобайт на вагон в минуту или 36 мегабайт в час. На каждый! вагон.

            Вы точно на много порядков не ошиблись? Не видно чего там столько писать можно.

            Ну или у вас странная архитектура когда вы тащите полный паспорт вагона (или что-то подобное) весом в мегабайты в каждом сообщении. Я такое видел и это не проблема Кафки, это проблема архитектуры системы.


            1. ptr128
              20.05.2025 11:59

              Во-первых, данные приходят неравномерно, а низкая латентность необходима. Ничего об этом не слышали?

              Во-вторых, прочитайте полностью список, чтобы опять не выглядеть глупо.


              1. BugM
                20.05.2025 11:59

                У меня ваши цифры прямо на порядки не сходятся. Неравномерностью объяснимо х2-х3. То есть днем должен идти такой потом, а ночью ниже.


                1. ptr128
                  20.05.2025 11:59

                  Неравномерность, это когда за минуту мимо 20 тысяч дефектоскопов по всей стране проехало 10 тысяч вагонов, а за последующие 59 минут - сотня.


                  1. BugM
                    20.05.2025 11:59

                    Отлично, проехало. Отчеты положили в табличку или s3 в зависимости от объема и записали в Кафку 20 тысяч сообщений вида: id + таймстемп + статус + еще мелочь какая. Итого максимум байт 100 на запись или 2 мегабайта всего.

                    Итого поток выходит 2 мегабайта в минуту в прыжке. Мегабита хватит. Ладно 5 мегабит пусть будет чтобы с хорошим запасом.

                    Да 10 гигабит все еще не хватает много порядков.


  1. xomiakba
    20.05.2025 11:59

    Для JSON изменили название ключевых полей и сказали - обратно не совместим. Для других форматов добавили новое поле и сказали - обратно совместим.

    Выглядит как подгонка под мнение - если в JSON только добавить поле, то старые клиенты тоже не сломаются.

    Бинарные форматы не панацея и далеко не всегда экономят трафик или процессор.

    Они так же требуют валидацию, и также страдают от проблем со совместимости при значимых изменений в структуре данных.

    Касательно избыточности json - ничего не мешает отдавать заголовки отдельно, а данные отдельно массивом. Так же схема + данные. Как это делает click house. И вот тут преимущество бинарных форматов уже не так разительно… точнее его практически нет. Разве что в пустыне через слабый 3g интернет загрузится на несколько миллисекунд быстрее.

    Так почему Кафка выбирает скорость и причем здесь бинарные потоки данных? Не понятно.


  1. muturgan
    20.05.2025 11:59

    Protobuf требует описания структуры данных в .proto-файлах, что гарантирует высокую производительность. Л - логика!