Ко всем новостям

Platform V Generators: как мы сделали турбонаддув для «атмосферной» Java

26.06.2023

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

О том, как наращивать мощности без инвестиций в «железо» рассказывает команда Platform V Generators. На примере продукта разберемся, как раскачать сервис автонумерации с 2 до 10 тыс. запросов и обеспечить высокую производительность за счет оптимизации программного кода и без смены СУБД.

Что такое сервис автонумерации и где используется

Platform V Generators — сервис, который обеспечивает уникальную идентификацию бизнес- и прикладных сущностей (договоров, счетов, квитанций) в распределенных вычислительных средах, позволяет создавать счетчики и получать из них уникальные целочисленные значения по заданному пользователем шаблону.

Platform V Generators появился в Сбере как решение задач по организации документооборота. В экосистеме банка выпускается огромное количество документов: распоряжения, накладные, счета-фактуры, — каждый из которых должен маркироваться собственным идентификатором.

Чтобы унифицировать этот процесс и сделать его эффективным, банк пошел по пути реализации собственного сервиса.

Сначала решение работало на стеке Java+SpringBoot+Kubernetes/Openshift. Для работы со счетчиками был реализован метод next, доступный по HTTP протоколу по адресу /counters/<name>/next. При обработке запроса сервис обновлял счетчик через блокировку в базе данных и возвращал новое значение.

Согласно данным нагрузочного тестирования такая версия решения позволяла обрабатывать ~2000 запросов в секунду. Но после стабилизации сервиса в промышленной эксплуатации стало очевидно, что нужно повышать производительность.

Мы сформулировали задачу для пилотного проекта — достичь 10 000 rps, не масштабируя СУБД. На тот момент ситуация была следующая:

  • Старт одного экземпляра приложения занимал ~10c. В совокупности со значительным потреблением памяти >200Мб это приводило к чрезмерному потреблению вычислительных ресурсов без возможности быстрого реагирования на изменения нагрузки.
  • Профилирование показывало, что потоки большую часть времени заблокированы и ждут операций ввода-вывода. Как следствие — низкая эффективность и утилизация.
Без заголовка.png
  • В случае большого количества запросов к одному счетчику транзакции образовывали «очередь» и обрабатывались последовательно, так как обновление счетчиков происходило через блокировку. Все это также ограничивало пропускную способность.

Переезд на Quarqus

Самым простым вариантом было шардирование. Но любое увеличение инстансов баз данных вынудило бы пересмотреть расходы на «железо», поэтому решили зайти с другой стороны и повысить возможности сервиса за счет оптимизации кода.

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

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

Выбрали Quarqus — он оптимально отвечал всем условиям задачи по раскачке сервиса. Это фреймворк нового поколения, который, на наш взгляд, можно назвать одним из самых перспективных решений для высоконагруженных сервисов. Quarqus упрощает разработку backend, обеспечивает поддержку Docker и Kubernetes, сокращает время старта программ. И вообще позволяет вести разработку в парадигме serverless архитектуры, что очень важно, если вы хотите шагать в ногу со временем и успешно справляться с новыми вызовами.

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

После замены Spring на Quarkus и компиляции Java-кода в исполняемый файл плагином Native Image к GraalVM получили время запуска за <1с и потребление памяти <100Mб на 1 экземпляр приложения (для п1);

Императивный подход заменили реактивным с библиотекой Mutiny интегрированной в Quarkus, что позволит обрабатывать запросы без блокировок на меньшем пуле потоков исполнения. Следствие — плотная утилизация доступных вычислительных ресурсов (для п2)

Без заголовка2.png
3.png

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

4.png

Слой Front:

  • Принимает входящие запросы от пользователей;
  • Формирует окна из входящих запросов в пределах заданного промежутка времени и вычисляет общую дельту изменения по каждому счетчику (для п4);
  • Для каждого окна выполняет запросы в слой Back;
  • Масштабируется пропорционально входящей нагрузке независимо от производительности и настроек БД (для п3).

Слой Back:

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

Тюнинг

Благодаря новой архитектуре появились новые параметры, доступные для настройки:

  • front_instances и back_instances - количество экземпляров для компонент слоя Front и Back;
  • front_frame_delay и back_frame_delay - период времени, в течении которого формируются окна на слоях Back и Front (в миллисекундах).

Пояснения:

  • Мы можем увеличить front_instances, чтобы принять возросшее количество запросов пользователей.
  • Мы можем увеличить back_instances, чтобы иметь возможность утилизировать ресурсы базы данных на нужном уровне.
  • Мы можем увеличить front_frame_delay, чтобы уменьшить нагрузку на слой Back.
  • Мы можем увеличить back_frame_delay, чтобы уменьшить нагрузку на БД.

Тестирование

В процессе тестирования запросы выполнялись с выделенной виртуальной машины к 10 различным счетчикам. Компоненты сервиса и экземпляр Platform V Pangolin были запущены в контейнерах в кластере Openshift со следующими лимитами:

  • Front/Back: 1000m/512Mi;
  • PostgreSQL: 4000m/1024Mi.Тест 1 (2k rps)

Конфигурация:

  • Front (replicas: 2), Back (replicas: 2);
  • Tank (instances: 2000, schedule: const(2000, 1m).

Результаты:

  • Среднее время ответа (95-й процентиль): 28 мс
  • Суммарная загрузка ядер процессора: ~400m
  • Общее потребление памяти: ~500Mi

Тест 2 (10k rps)

Конфигурация:

  • Front (replicas: 3), Back (replicas: 2);
  • Tank (instances: 10000, schedule: const(10000, 1m).

Результаты:

  • Среднее время ответа (95-й процентиль): 29 мс
  • Суммарная загрузка ядер процессора: ~700m
  • Общее потребление памяти: <1000Mi

Тест 3 (40k rps)

Конфигурация:

  • Front (replicas: 12), Back (replicas: 3);
  • Tank (instances: 20000, schedule: const(40000, 1m).

Результаты:

  • Среднее время ответа (95-й процентиль): 31мс
  • Суммарная загрузка ядер процессора: ~2000m
  • Общее потребление памяти: <2000Mi

Итог

В результате редизайна сервиса мы достигли новой производительности на уровне 10000 запросов в секунду и подтвердили возможность обрабатывать до 40000 запросов в секунду.

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

  • Сократить время старта контейнера и как следствие — обеспечить быструю реакцию на входящую нагрузку и эластичность масштабируемости.
  • Облегчить поды и сократить потребление памяти и сократить больше чем в 2 раза, в нашем случае — с более чем 200 Мб <100Mб на 1 экземпляр приложения.
  • Обеспечить неблокируемую обработку запросов, сократив количество потоков и реализовав принципы реактивного программирования, при которых компоненты обращаются друг к другу без ожидания ответов.

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