Привет, Хабр! На связи Дмитрий Пшевский @pshevskiy и Семён Попов @samansay, технические лидеры юнита Data в Сбере.

Уже более 6 лет мы заботимся о клиентских данных Сбера — храним, дедублицируем, стандартизируем, маркируем. А сегодня хотим поговорить о производительности сервисов при работе с Ignite или другой подобной системой из облачной инфраструктуры. Мы не будем рассматривать аспекты развертывания и оптимизации работы самого кластера и обсудим производительность сервисов именно на прикладном уровне. Расскажем про сложности перехода на микросервисную архитектуру, работу с толстым и тонким клиентом и отказ от транзакций. Эта статья — обзор нашего доклада на JPoint 2023.

Что сейчас

Сегодня мы храним данные более 100 млн уникальных клиентов Сбера в Едином Профиле Клиента — централизованном хранилище данных со сквозным идентификатором клиента. К нам обращается едва ли не каждая система банка, мы выдерживаем нагрузку до 40 тыс. TPS в пике. Сейчас объём реляционного хранилища — около 50 Тб. О том, как у нас была устроена работа с данными раньше, можете прочитать ниже.

Как было раньше

7 лет назад в Сбере не было централизованной базы данных с клиентами. Каждая система хранила копию данных клиентов в своей БД. Количество таких систем исчислялось десятками. Нам нужно было объединить данные из всех систем: собрать всех клиентов банка в единое хранилище и создать «золотой» клиентский профиль. Предыдущие подходы к этой задаче не позволяли справляться с нагрузкой, так как общее количество клиентов из всех систем превышало 500 млн неуникальных профилей. Появлялись проблемы из-за отсутствия единого идентификатора: паспорт можно поменять, имена не уникальны, а риски объединения продуктов разных клиентов высоки. К тому же, новая система должна интегрироваться с 1200 уже существующими в Сбере, что означало колоссальную нагрузку для подобных систем.

У нас был кластер (тогда еще GridGain) на 200 узлов. Мы планировали положить в него данные всего банка и расширить до 1000 узлов. Все данные хотели расположить рядом с данными клиента: этакая абсолютная «клиентоцентричность». Быстрое масштабирование, обработка больших объемов данных, big data — семь лет назад это казалось прорывной идеей.

Но на старте даже с таким размером кластера начались проблемы с производительностью и надежностью. Поэтому мы решили не увеличивать размер кластера, а уменьшать. Расположили там только клиентские данные и подложили туда БД Oracle, чтобы обеспечить надежность.

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

Переход на микросервисную архитектуру

Долгое время мы работали на bare metal-окружении, и система показывала хорошую производительность. Но обслуживание монолита, хоть и распределённого, требовало особого подхода со стороны сопровождения. Установка зачастую занимала несколько часов. Требовался переход в резервный контур. К тому же монолитная архитектура не позволяла быстро и безопасно доставлять релизы в прод, и его сложно было сопровождать.

Посмотрите, сколько компонентов было развёрнуто на каждом сервере приложений!
Посмотрите, сколько компонентов было развёрнуто на каждом сервере приложений!

Есть и практический момент. Из-за размещения всех сервисов в одной JVM размер кучи подошёл слишком близко к критической отметке в 32 Гб. Если мы перешагнём этот предел, то потеряем сжатые ссылки и у нас какое-то время будет нелинейный рост потребления памяти, а нам этого не хотелось бы.

Кроме того, на таких объёмах кучи сложно настроить более или менее предсказуемое поведение сборщика мусора.

Серверные узлы кластера у нас находились на том же bare metal‑сервере и конкурировали за ресурсы процессора, диска и сети. Apache Ignite — это in‑memory хранилище, поэтому конкуренция была и за память. Даже с учётом того, что в нашем развёртывании используются жирные bare metal-серверы — 56 ядер, 768 Гб памяти.

Кроме очевидных проблем мы столкнулись ещё с одной: наши приложения иногда убивал OOM Killer. Серверные узлы Apache Ignite хранят данные в offheap и сами управляют их жизненным циклом. Память выделяется с помощью Malloc, который оптимизирован для работы в многопоточной среде. Чтобы эффективно работать с большим количеством потоков и снизить конкуренцию для выделения памяти, он разбивает её на арены. Количество арен прямо пропорционально количеству ядер в нашем процессоре. Арены в свою очередь разбиваются на кучи и чанки.

Получается деление памяти на регионы. Везде, где мы делим память на регионы, наверняка будет фрагментация. И это дало повод ООМ Killer считать, что мы покушаемся на надёжность своего же сервера.

Фрагментация памяти на наших серверах.
Фрагментация памяти на наших серверах.

Итак, мы столкнулись со следующими проблемами:

  1. Монолитная архитектура привела к росту приложения и размеру потребления памяти.

  2. Совместное размещение клиентского и серверного узла Ignite привело к гонке за ресурсы.

  3. Обслуживание монолита оказалось сложным.

И мы решили разбить монолит на микросервисы. А переход в облако позволил бы нам снизить стоимость сопровождения.

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

Но вот какие еще моменты нас ожидали:

  • Ограничение ресурсов. Облако — конкурентная среда. На каждой ноде может быть развёрнуто несколько сотен контейнеров. Если какой‑то из них будет чрезмерно потреблять ресурсы, это может привести к сбою работы других контейнеров на этой ноде. Ресурсы каждого микросервиса должны быть ограничены политиками cgroups для рационального использования и изоляции одних сервисов от других.

  • Stateless, easy recovery. Если мы изолируем сервисы, это может приводить к более частым сбоям. Например, если в контейнере не хватает памяти, его может «заботливо» убить ООМ Killer. Но Kubernetes или любой другой оркестровщик предоставляет такую возможность как автоматическое восстановление. Сервер восстановит такой контейнер на другой ноде, но состояние потеряется. Поэтому желательно делать сервисы stateless и оптимизировать время подъёма контейнера, чтобы они быстрее восстанавливались.

  • Переход к горизонтальному масштабированию. Если у нас увеличится количество потребителей, мы легко сможем масштабировать нагрузку увеличением количества реплик сервиса. Но как в таком разнообразии сервисов найти нужный?

  • Service mesh. Service mesh помогает перенести в sidecar все фичи, которые реализовывались на прикладном уровне — шифрование, мониторинг и прочие. За использование Service mesh мы платим взаимодействием не напрямую, а через прокси. Важно помнить, что sidecar потребляет ресурсы и увеличивает время отклика.

Нужно учитывать, что не всё, что хорошо работает на bare metal, будет хорошо работать в облаке. Давайте посмотрим, будет ли толстый клиент, которого мы используем в нашем кластере, таким же надёжным и быстрым в облачном окружении.

Толстый клиент в облаке

По сути толстый клиент — это тот же серверный узел. Только он не хранит данные, но при этом знает всё о топологии кластера и может работать как координатор транзакции. На самом деле толстый клиент был создан как временное решение. Что‑то вроде бесплатного клиента на время разработки Ignite, чтобы быстро получить функциональное средство для взаимодействия с кластером. Но нет ничего более постоянного, чем временное: мы до сих пор его используем????

Пример запуска толстого клиента.
Пример запуска толстого клиента.

Запускаем и получаем ошибку:

Интересно: делаем то же самое, что и раньше, а подключиться не можем. Давайте разбираться. Посмотрим на конфигурацию серверного узла, которую мы использовали для подключения: поиск нод кластеров и взаимодействия между ними. Наиболее интересующие нас части этой XML — это localhost и порты. Особенного внимания заслуживает настройка usePairedConnections, которая отвечает за отдельные каналы для входящих и исходящих сообщений и позволяет снизить latency.

Скажем пару слов о том, как работает сеть в облаке. Каждый контейнер живёт в своем network namespace, использует свои внутренние интерфейсы и полностью изолирован от других контейнеров. При этом исходящие соединения с помощью бриджей и iptable мы можем пробросить наружу и установить соединение с серверной нодой Ignite.

Но когда серверный узел захочет соединиться с клиентским узлом, мы получим ошибку, потому что используем внутренний IP-адрес, который не работает во внешней сети. Мы можем попробовать установить соединение по клиентскому IP, но тут не будет открытых портов.

Для решения этой проблемы нам необходимо пробросить порты. Это легко можно сделать с помощью Kubernetes. У Apache Ignite есть свой DiscoverySpi, который позволяет работать с Kubernetes. Но есть один нюанс: все серверные узлы должны быть установлены в Kubernetes. То есть по сути мы должны полностью мигрировать в облако.

Вспомним, что мы должны использовать persistence storage, которое даёт нам быстрый разогрев в данных в Ignite и дополнительную надёжность. Получается, что нам нужно будет хранить данные, а это уже не stateless, а stateful.

Но у Kubernetes есть persistence volume. И для того, чтобы это заработало, нам нужен внешний block storage — например, NFS-сервер. Мы монтируем сетевой диск к нашей ноде, наше приложение использует его как persistence volume и сохраняет туда данные. Если нода падает, мы смонтируем сетевой диск на другую ноду и поднимем приложение на ней.

Тут не всё так просто. Ignite требуются быстрые диски, чтобы он мог хранить на них write ahead log (WAL) и данные. Сначала мы сохраняем данные в WAL, а потом асинхронно добавляем в хранилище. Чтобы в случае сбоя мы их не потеряли, результат клиенту мы отдаём только после записи в WAL, а это приносит дополнительную задержку.

Запись данных в хранилище Ignite.
Запись данных в хранилище Ignite.

В эксплуатации из-за замедления дисков могут случаться проблемы, например, нода выпадает из кластера. Особенно это заметно при снятии снимка данных для восстановления или другой большой нагрузки на диски. Поэтому, чтобы оставить кластер стабильным, можно пожертвовать latency со стороны клиента. Проблему можно решить включением опции forceClientToServerConnections, которая говорит, что серверный узел может игнорировать ошибки подключения серверного к клиентскому и отключить usePairedConnections. Это повлечёт за собой небольшие проблемы с peer class loading для continuous queries. Но решить это можно копированием используемых вами классов на серверные узлы. 

Есть и более важная проблема. Community-версия Ignite не поддерживает rolling update. То есть, если вы обновите серверные узлы, но не обновите клиентские, получите такую ошибку:

Что это значит для развёртывания в облаке? Вы не сможете нормально обновиться, сохранить обратную совместимость, использовать канареечные релизы — все лучшие практики, которые мы ожидаем от облака. Получается, при работе с толстым клиентом есть проблемы, поэтому нужно подумать над альтернативами. В Ignite они есть:

  • REST — HTTP‑запросы. Правда, обычно их используют для тестовых задач и целей разработки.

  • JDBC. В целом, всем хорош, но по функциональности проигрывает тонкому клиенту.

Попробуем поработать с тонким клиентом.

Переходим на тонкий клиент

Запустить тонкий клиент не сложнее, чем толстый: потребуется та же зависимость и список IP.

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

В версии Apache Ignite 2.12 был баг: выбирался и всегда использовался в качестве координатора транзакций первый адрес в списке. И это создавало проблемы.

Но можно перемешать (shuffle) этот список и на каждом поде получить свой список IP-адресов и свой серверный узел. Это поможет равномерно распределить нагрузку, что мы и видим на мониторинге:

Если раньше нагрузку выдерживал только один узел, сейчас она равномерно распределяется между 8 узлами.

Но простор для оптимизаций ещё есть. По сути, сейчас координатор выбирается рандомно, и, если ему нужны какие-то данные, он, вероятно, обратится за ними к другому узлу, на котором эти данные хранятся. Хотелось бы избежать этого сетевого скачка и сразу выбирать координатор, у которого эти данные есть.

Для тонкого клиента есть настройка Partition Awareness. Её включение позволяет клиенту получить информацию о карте распределения и смаршрутизировать запрос на нужный серверный узел кластера. Делается это с помощью включения настройки setPartitionAwarenessEnabled.

Давайте посмотрим, помогла ли оптимизация.

Очень странно, но производительность не увеличилась. И мы нашли виновника: это транзакции, при использовании которых взаимодействие происходит через выбранный при старте транзакции координатор. Как бы ни был Apache Ignite хорош, предвидеть, какие данные собирается читать клиент в этой транзакции, он ещё не научился.

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

Первичную проверку тонкий клиент прошёл. Попробуем пользоваться им и дальше.

Подводим промежуточные итоги

Мы построили отказоустойчивую распределенную высоконагруженную систему для хранения данных более 100 млн клиентов и решили проблему горизонтального масштабирования. А помог нам в этом продукт Platform V DataGrid — распределённая резидентная СУБД для высокопроизводительных вычислений от СберТеха на базе Apache Ignite.

Команда СберТеха доработала Apache Ignite в области производительности, безопасности и надёжности. Ключевым отличием от OSS стала такая функциональность, как cache dump, real time replication, cache data compression. Подробнее про это можно почитать здесь.

Ниже — сравнение функций обеспечения надёжности, которые были разработаны командой СберТеха как в Apache Ignite, так и в Platform V DataGrid:

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

А пока — спасибо за внимание, и до встречи!

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


  1. RedTea
    29.11.2023 10:27

    Сижу, читаю, задаюсь вопросом, вроде все указанные проблемы решаются в redis из коробки, зачем столько сложностей? Какие бенифиты дает ignite которых нет в redis?

    Вроде по тестам ignite сильно позади по производительности.


    1. pshevskiy
      29.11.2023 10:27

      Абсолютно согласен что Redis быстрая и надежная db, но все же сравнивать его и Apache Ignite не совсем корректно. Все что между ними общего - это то что они хранят данные in-memory и относятся к nosql db. А вот реализованы они совсем по разному. Apache Ignite в отличии от Redis поддерживает синхронную репликацию и ACID транзакции, а в нашем примере - работа с клиентскими данными - строгая консистентность важнее производительности, по крайней мере для наших сценариев использования. Вообще первоначальной задачей Apache Ignite в Сбере была заменить Oracle, так как объемы данных не позволяли его использовать в ближайшей перспективе, а не использовать как кеш БД.

      По поводу бенчмарков, это сложная история, и напрямую сравнить Redis и Ignite врятли возможно, если только если притянуть какой-то узкий сценарий, например crud операции с Atomic кешами. Но возможно коллеги от Ignite смогут меня поправить:)


    1. Igor_Gordeev
      29.11.2023 10:27

      у редис асинхронная репликация и персист с aof и rdb снапшотами, не гарантирует сохранность данных и rpo 0, как тут игнайт с wal


  1. SergeyMax
    29.11.2023 10:27
    +1

    Привет, Хабр! На связи Дмитрий Пшевский @pshevskiy и Семён Попов @samansay

    Привет, Дмитрий и Семëн! Пользуясь тем, что вы на связи, хотелось бы задать вопрос по поводу импортозамещения. Планирует ли сбер переименовать Ignite в что-то типа СберРозжиг?