Как мы в клиринге переходили от REST к Kafka

8af5f6a6d31a61a9f422ae3f33f47a15.jpg

Введение

Всем привет! Меня зовут Владислав, я занимаюсь разработкой клиринговой системы в Мир Plat.Form. 

Клиринг — это сложная и высоконагруженная система, предназначенная для проведения взаиморасчетов по операциям, совершенным с использованием банковских карт. Моя коллега Наталья Азисова в другой статье на Хабре очень подробно рассказала, что такое клиринг, и как он работает.

Сегодня я хотел бы поделиться нашим опытом перехода от взаимодействия через REST к использованию Kafka между системой Фронт-Офиса, в которой хранятся данные об авторизованных транзакциях, и системой Бэк-Офиса, ответственной за выполнение клиринга. Выделить ключевые моменты, с которыми мы столкнулись, и поделиться практическими советами об успешной реализации этого перехода.

REST и Kafka — популярные подходы к обмену данными между системами. Оба метода имеют свои преимущества и недостатки, и выбор между ними зависит от конкретных потребностей проекта.

REST прост в использовании, понятен и широко применяется.  Однако,   с увеличением трафика возникла необходимость в ускорении взаимодействия систем. Поэтому, мы приняли решение перераспределить хранение данных и перейти на Kafka. Такой подход позволил Фронт-Офису непрерывно стримить данные, что привело к значительному сокращению времени получения данных на Клиринг.

Архитектура

Было. Авторизационные транзакции, поступающие в платежную систему, хранились в базе данных на стороне Фронт-Офиса. Данные передавались по запросу Клиринга через экспортеры, работающие по протоколу REST.

bf069bdc844d3aa51d11d4f633d8e655.jpg

Стало. Для интеграции мы приняли решение использовать подход с созданием слоя сервисных адаптеров на стороне потребителя.

На первом этапе сервисы адаптеры были настроены на прием сообщений из Kafka, их валидацию, а также сохранение информации в базу данных. Кроме того, в сервис-адаптер был реализован старый REST-контракт.

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

Более того, в долгосрочной перспективе такая стратегия может способствовать поэтапному перемещению функциональности в сервис-адаптер, что расширит его возможности и способствует развитию системы в сторону микросервисной архитектуры.

65a90874fdf6bc1d4967479e1243a849.jpg

Выбор формата передаваемых данных

При выборе формата для передачи данных важно учитывать потребности конкретного проекта в скорости и объеме передаче данных, поддержке различных платформ и инструментов, а также сложности в разработке и сопровождении.

Для нашего проекта мы рассмотрели два основных варианта: JSON и AVRO.

JSON популярен благодаря простоте чтения и записи для человека, а также его широкой поддержке в различных языках программирования. Однако, он может быть не очень эффективен по размеру файла и производительности при работе с большим объемом данных.

{
        "f1": "001",
        "f2": "002",
        "f3": "003",
        "f4": "004",
        "f5": "005",
        "f6": "006",
        "f7": "007",
        "f8": "008",
        "f9": "009",
        "f10": "010",
        "f11": "011"
}

AVRO обеспечивает компактное представление данных и эффективную сериализацию, что особенно важно при передаче больших объемов данных по сети или их хранении. 

Однако, для использования AVRO необходимо предварительно проработать схемы данных, что может потребовать дополнительного времени на разработку

{
  "type": "record",
  "namespace": "namespaceName",
  "doc": "https://linkToDoc",
  "name": "className",
  "fields": [
    {
      "name": "f1",
      "type": "string"
    },
    {
      "name": "f2",
      "type": "string"
    },
    {
      "name": "f3",
      "type": "string"
    },
    {
      "name": "f4",
      "type": "string"
    },
    {
      "name": "f5",
      "type": "string"
    },
    {
      "name": "f6",
      "type": "string"
    },
    {
      "name": "f7",
      "type": "string"
    },
    {
      "name": "f8",
      "type": "string"
    },
    {
      "name": "f9",
      "type": "string"
    },
    {
      "name": "f10",
      "type": "string"
    },
    {
      "name": "f11",
      "type": "string"
    }
  ]
}

Средний вес одного сообщения, передаваемого между Фронт-Офисом и Бэк-Офисом в AVRO составляет 340 байт, а в формате JSON 1900 байт.

e761621a702fbf8dc284485a30990f31.jpg

Ежедневно мы передаем по Kafka больше 8 миллионов сообщений, срок их хранения составляет 2 дня.

Мы остановились на формате AVRO из-за его более компактного представления, что экономит нам около 37,5 ГБ.

Реестр схем

Изменение структуры передаваемых сообщений — это важный аспект, который зачастую остается недооцененным. Не продумав управление данными впоследствии, можно столкнуться с проблемами при сериализации и десериализации.

При работе с JSON изменить состав полей достаточно просто, так этот формат имеет гибкую структуру. Но при работе с AVRO мы имеем жесткую структуру, привязанную к заранее заданной схеме. Если мы добавляем новое поле или меняем свойства, байтовое представление сообщения изменится. И сериализация его по старой схеме будет невозможно.

Для решения подобных проблем согласованности данных между отправителями и получателями в Confluent Avro используется механизм обеспечения эволюции структуры данных, основанный на Реестре схем.

Реестр схем представляет собой сервис, который включает в себя все варианты схем, что позволяет отправителю и получателю работать с различными форматами данных.

a8c4182ad1879ac6b8de9dae5810ad62.jpg

Более подробную информацию о Реестре схем и эволюции данных можно найти по ссылке: https://docs.confluent.io/platform/current/schema-registry/fundamentals/schema-evolution.html

Тем не менее, Реестр схем имеет свои недостатки. Для его функционирования требуется запуск отдельного сервиса, что может сказаться на отказоустойчивости. В случае недоступности данного сервиса могут возникнуть проблемы с передачей и приемом данных.

При использовании сериализатора, предоставляемого Confluent io.confluent: kafka-avro-serializer, отказаться от Реестра схем не получится.

Однако можно воспользоваться библиотекой org.apache.avro и создать собственный сериализатор, используя org.apache.avro.io.DatumWriter и org.apache.avro.io.DatumReader. И хранить схемы на Producer и Consumer.

c78a152bc34d21fa62f70c6eac77ee8d.jpg

Так как мы работали с уже устоявшимся форматом передаваемых сообщений, то предпочли не использовать сервис Реестра схем. Вместо этого мы выбрали подход с самописным сериализатором и при необходимости изменения формата сообщения внедрили подход с версионированием топиков (каждый новый формат сообщения записывается в собственный топик).

38f0c5df7c515548a2c02904779a5ad2.jpg

Такой подход позволил нам избежать лишнего сервиса в системе.

Если сравнивать самописный сериализатор и реализацию от confluent, то для 100000 сообщений формата:

{
        "f1": "001",
        "f2": "002",
        "f3": "003",
        "f4": "004",
        "f5": "005",
        "f6": "006",
        "f7": "007",
        "f8": "008",
        "f9": "009",
        "f10": "010",
        "f11": "011"
}

мы получили следующие результаты.

Время записи в Kafka confluent: 1176 миллисекунд.
Время записи в Kafka самописного сериализатора: 783 миллисекунд.

Время чтения из Kafka confluent: 1925 миллисекунд.
Время чтения из Kafka самописного десериализатора: 1943 миллисекунд.

У нашего самописного варианта скорость записи в Kafka выше, но скорость чтения незначительно ниже. Однако наш вариант сериализатора имеет существенный недостаток, на данный момент он может работать только с единственным вариантом схемы.

Можно расширить функционал собственного сериализатора для работы с эволюцией схемы. Однако, если вам необходим эволюционный механизм, то лучше воспользоваться готовым решением от Confluent.

Нужно отметить, что не явным преимуществом использования Реестра схем является необходимость в нем при работе с внешними утилитами.

Взаимодействие команд

При совместной работе команд разработчиков требуется предварительное согласование интеграционных требований. Это касается формата передаваемых данных, использования ключей и заголовков. Также важно выбрать заранее сериализаторы, которые будут использоваться в проекте. Например, если в качестве ключа используется UUID, можно выбрать между StringSerializer и UUIDSerializer.

Также необходимо обсудить варианты передачи схем существует два пути.
Первый передавать только схемы и генерировать классы будет каждый участник системы. Второй заранее сгенерировать jar-файлы и работать уже с ними.

Преимущества генерации общего jar-файла включают снижение риска несоответствия классов. Однако при частых изменениях придется пересобирать и перезагружать общий jar файл. Выбор между этими подходами зависит от конкретной ситуации и предпочтений.

Минимизация рисков

Для минимизации возможных рисков в нашей системе мы реализовали поэтапный переход. Каждый сервис-адаптер на начальном этапе имел встроенную возможность сбора данных по старому пути через REST API. Это решение оказалось весьма полезным в критических ситуациях, когда требовалось обработать сообщения, не соответствующие новому контракту между системами.

Мы также реализовали механизм повторного сбора сообщений. Однако крайне важно не забывать вести логирование и сохранять оффсеты, иначе ручной поиск сообщений в Kafka может быть затруднительным.

При работе с пакетами данных, важно помнить, что даже одно поврежденное сообщение в пакете может привести к сбою при вставке в базу данных, что повлечет за собой необходимость переотправки всего пакета. Поэтому для вставки батча можно рассмотреть использование оператора INSERT IGNORE с последующей проверкой количества ожидаемых и фактически добавленных записей. Такой подход поможет избежать потери ценных данных и обеспечит более стабильную работу системы.

Заключение

Важное значение имеет взаимодействие между командами разработки, где предварительное согласование формата и способа передачи данных играют решающую роль.

В ходе реализации перехода с REST на Kafka в качестве передаваемого формата данных мы выбрали AVRO, за счет его более компактного представления.

Выбор между использованием Реестра схем и созданием кастомного сериализатора зависит от особенностей проекта и требуемого уровня гибкости. Так как мы работали с уже устоявшимся набором полей, а механизм эволюции данных для нас не играл решающей роли, то мы отказались от использования сервиса реестра схем.

Хотелось бы подчеркнуть важность мер по минимизации рисков при переходе на новую архитектуру. Снизить риски нам помог поэтапный переход и встроенная поддержка сбора данных по старому REST пути, а также механизм повторной обработки сообщений. Такой подход позволил обеспечить более стабильную работу системы.

Мы надеемся, что наш опыт и практические советы оказались полезными для вас и помогут вам успешно реализовать переход на Kafka в вашем проекте.

© Habrahabr.ru