Сокращение потребления памяти в микросервисной архитектуре без изменения кода и перезапуска контейнеров

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

Архитектура тестового стенда

Стараясь пойти по самому простому и быстрому пути я развернул микросервисный комплекс Online Boutique.

Комплекс представляет собой небольшой интернет магазин из 12 независимых микросервисов, написанных на разных языках программирования и работающих в единой сети Docker.

  • demo-frontend (Golang) — шлюз API и веб-интерфейс
  • demo-cartservice (C#) — управление корзиной покупателей
  • demo-adservice (Java) — сервис контекстной рекламы
  • demo-currencyservice (Node.js) — конвертация валют
  • demo-paymentservice (Node.js) — процессинг платежей
  • demo-emailservice (Python) — рассылка уведомлений
  • demo-recommendationservice (Python) — сервис товарных рекомендаций
  • demo-productcatalogservice (Golang) — каталог товаров
  • demo-checkoutservice (Golang) — оформление заказов
  • demo-shippingservice (Golang) — служба доставки
  • demo-redis-cart (C / Redis) — in-memory хранилище данных
  • demo-loadgenerator (Python / Locust) — эмулятор реального поведения пользователей

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

Градуальный стресс-тест: от 10 до 500 пользователей

Чтобы понять базовый аппетит системы, я провёл ступенчатый прогрев и сбор метрик

10 пользователей --> RAM: ~340 MiB, CPU: ~14%

100 пользователей --> RAM: ~387 MiB, CPU: ~49%

500 пользователей --> RAM: 565.9 MiB, CPU: ~322% (91.3 RPS)

Динамика потребления ресурсов до поднятия контейнера оптимизатора

  • На 10 пользователях: Система находится в покое. Суммарный объем RAM сервисов — ~340 MiB, нагрузка CPU — ~14%.
  • На 100 пользователях: Нагрузка выросла до 18 запросов в секунду (RPS). Обработано 1 982 запроса (0.00% ошибок). Память выросла до ~387 MiB, CPU — ~49%.
  • На 500 пользователях (Стабильное плато): Суммарный поток запросов достиг 91.3 RPS. Успешно обработано 30 416 сквозных сценариев без единого сбоя.Нагрузка на процессор суммарно превысила 322%.Память всех микросервисов достигла пикового значения 565.9 MiB.

Поднятие контейнера оптимизатора

Когда система находилась под нагрузкой в 500 пользователей, я запустил свою утилитку.

  • Утилита автоматически обнаружила все 12 микросервисов
  • Просканировал структуру рабочего пространства в оперативной памяти
  • Применил свои магические механизмы оптимизации
  • Ни один сервис не бул перезапущен, соединение с пользователями не прерывалось ни на миллисекунду

Результаты замеров

Сокращение потребления памяти в микросервисной архитектуре без изменения кода и перезапуска контейнеров

Освобождено чистой оперативной памяти: 80.4 Mb (14.2% по всему кластеру).

P.S. После включения утилитки потребление ресурсов CPU не изменилось.

Дополнительная информация

Я проводил ещё эксперимент и поднимал архитектуру в 40 микросервисов и повышал потребление оперативной памяти до 20 гб. Самое интересное, что процент оптимизации вырастал до 40-55%. К сожалению я не вёл записей по тому эксперименту, а повторять его было максимально лень. На основании того что получилось, могу сказать, что моя утилитка будет интересна разве что средним и более проектам. Возможно я проведу эксперимент более подробный и с учётом всех нюансов, но только если это будет кому-то интересно.