Идем воевать с Java-боттлнеками

Меня зовут Игорь Колосов, я Automation/Performance Architect, Consultant в GlobalLogic, Харьков. В сфере IT уже около 10 лет. В 2011 году увлекся автоматизацией и всем, что связано с производительностью ПО, что и определило мой дальнейший карьерный вектор развития. Являюсь активным участником и спикером нескольких сообществ по тестированию программного обеспечения. В моем портфолио более 20 проектов, а повседневные задачи покрывают целый спектр различных компетенций — от архитектуры тестовых решений и старта новых проектов до создания тестового окружения и инфраструктуры.

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

Большинство приложений имеют аналогичную цель и создаются разработчиками, имеющими сходное мышление; существуют и распространенные ловушки, в которые могут попасть многие разработчики. К счастью, такие ловушки можно обнаружить и «вылечить», если знать, как искать и где искать. Сегодня я постараюсь немного помочь тем, кто раньше не боролся с проблемами производительности Java-приложений, и поделюсь материалом по мотивам своего доклада на GlobalLogic Java Conference 2019.

В каком направлении копать

На основании современной статистики можно выделить следующий топ групп проблем с производительностью Java-приложений:

  • базы данных;
  • облака и виртуализация;
  • программирование;
  • ОС, платформа, стороннее ПО;
  • кеширование;
  • CPU;
  • сеть;
  • память;
  • дисковая подсистема;
  • специфическое железо.

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

Базы данных. Одна из самых часто встречающихся — это группа проблем, связанных со слоями данных (с базами данных, с различными хранилищами и т. д.). Для осознания масштабов будет нелишним привести цитату из одной книги по производительности — «Look elsewhere. The database is always the bottleneck». Хорошим примером подобных боттлнеков могут служить конфликты при записи, неэффективные запросы, тяжеловесные Join.

Для наглядности вспомним об индексировании. Часто программистам рекомендуют иметь индекс для каждого foreign key в таблице, но всегда стоит помнить, какие именно запросы выполняются. Не все индексы будут использоваться. Неиспользуемые индексы займут место на диске, а база данных будет обновлять индексы каждый раз, когда происходит вставка/удаление записей. Это замедлит общую обработку запросов.

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

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

Рассмотрим пример с Java-сервлетом для наглядности. Смешивание вызовов между output stream сервлета и записью форматированного вывода через PrintWriter приведет к частой очистке сетевых буферов. Частая очистка буферов очень дорога с точки зрения производительности: дороже, чем перекодирование данных. Кодирование большого блока данных обычно ненамного дороже, чем кодирование небольшого блока данных (в этом случае вызов encoder будет наиболее ресурсоемкой операцией). Следовательно, частые вызовы encoder для небольших фрагментов динамических данных, чередующихся с вызовами для отправки предварительно шифрованных массивов байтов, могут замедлить работу приложения: выполнение частых вызовов encoder займет больше времени, чем один вызов для шифрования всего (включая статические данные). Неопытные программисты довольно часто совершают подобные ошибки.

ОС, платформа, стороннее ПО. Окружение с каждым годом становится все сложнее, зависимостей все больше, поэтому всегда существует риск столкнуться с проблемами, связанными с самой операционной системой, либо же с платформой, используемой нашим приложением или установленным на той же машине сторонним ПО. Часто с такими проблемами разработчики сталкиваются при тюнинге работы с транспортным протоколом (например, TCP). Из интересных случаев в моей практике попадались проблемы при работе с файлом подкачки Windows и замедления из-за iptables. Для десктопных приложений стоит обращать особое внимание на настройки различных фаерволлов и антивирусного ПО.

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

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

На этом скриншоте мы наглядно можем увидеть пример CPU bottleneck при визуализации средствами Amazon CloudWatch.

Специфическое железо. Не стоит забывать и о зоне риска, связанной с каким-то специфическим или специализированным hardware, особенно с учетом высокой популярности использования Java в сегменте IoT-решений.

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

А что же с самим приложением?

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

  • проблемы с архитектурой;
  • неправильный выбор технологий;
  • неэффективная garbage collection;
  • неэффективная конфигурация heap;
  • неправильный размер потокового стека;
  • проблемы многопоточности (deadlocks, starvation, live locks, race conditions и т. д.);
  • неэффективные алгоритмы;
  • некорректный выбор дизайн-паттернов;
  • неподходящие структуры данных.

Проблемы с архитектурой. Любая реализация системы начинается с архитектуры. Если на старте архитектор не задумывался о производительности решения, это может привести к плачевным и дорогим последствиям. Архитектура — очень важная отправная точка, для которой работает принцип «чем позже, тем дороже». К сожалению, в жизни мы часто сталкиваемся с ситуацией, когда за неделю перед релизом в production запускаются первые performance-тесты и как гром среди ясного неба звучит призыв аврально тюнить продукт. О производительности часто забывают и задумываются о ней в последний момент. Архитектор, имея нефункциональные требования к производительности на входе, может принять ряд ключевых решений, которые облегчат жизнедеятельность проекта и улучшат итоговый результат, по сути, понизив трудозатраты и стоимость выведения ПО на нужный уровень качества.

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

Неправильный выбор технологий. Вторая по дороговизне группа проблем с производительностью Java-приложений — неправильный выбор технологий. Мы не пишем приложение в вакууме. Мы используем сторонние библиотеки, различные 3rd-party-компоненты, связываем наш продукт с какими-то сторонними сервисами, добавляем различные технологии для увеличения удобства написания и хорошей поддержки кода и т. д. На выходе может получиться огромная сборная солянка с проблемами во внутренних взаимодействиях, причем какие-то из внедренных технологий сами по себе могут обладать низкой производительностью. Неправильный анализ и выбор инструментария может проявить себя уже в момент performance-тестов либо, еще хуже, уже в live-режиме, когда с системой будут работать реальные люди. В такой ситуации «лепить подорожник» и тюнить систему или выкорчевывать плотно проросшие технические решения будет очень дорого.

Особенно болезненным это становится уже на стадии legacy. Как-то мне встретился legacy-проект еще на апплетах с дичайшей мешаниной из морально устаревших медленных библиотек. В качестве вишенки на торте выступали куски сервера на Perl. Слово «производительность» было, в принципе, неприменимо к этому монстру Франкенштейна, одна только сборка занимала часов 8.

Конфигурация JVM. Думаю, все прекрасно помнят, что Java-код запускается на такой штуке, как JVM (Java Virtual Machine). Поэтому когда мы говорим о проблемах с производительностью Java, нельзя не упомянуть целую группу проблем с производительностью, связанных с самой платформой, с Java-машиной.

Чаще всего это будут некие проблемы с конфигурацией JVM, например конфигурация сборки мусора. Java позволяет достаточно гибко конфигурировать garbage collection. Всегда стоит помнить о грамотном планировании capacity вашего окружения и не забывать настроить размер кучи (параметры -Xms и -Xmx) с учетом ожидаемого железа в production. Довольно важная настройка — размер стека потоков (-Xss). Используя эти ключевые настройки бездумно, вы рискуете столкнуться с общим замедлением приложения.

Также стоит упомянуть такое явление как баги в самой JVM, исправляемые в свежих апдейтах. Иногда включение новых фич, ориентированных на поднятие производительности, ведет к обратной ситуации из-за «сырости» и недоработок.

Некорректное программирование. Практически каждый программист проходит через стадию «руки-клешни» и набивает свои шишки. Очень много боттлнеков проявляются в связи с самим кодом и решениями во время программирования. Часто можно встретить проблемы с потоками, такие как deadlocks, starvation, live locks, race conditions. Если программист неаккуратно использует потоки и прерывания, очень высока вероятность того, что у вас возникнут проблемы при мультипоточной работе в асинхронных приложениях.

Также неэффективные алгоритмы и неправильно подобранные структуры данных в вашей программе могут быть источником боттлнеков.

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

Примеры боттлнеков в трехслойной архитектуре

На сегодняшний день трехслойная архитектура является одной из самых популярных. Трехуровневая архитектура — это тип программной архитектуры, которая состоит из трех «уровней» или «слоев» логических вычислений. Они часто используются в приложениях как особый тип клиент-серверной системы. 3-уровневые архитектуры предоставляют множество преимуществ для сред производства и разработки за счет модульности пользовательского интерфейса, бизнес-логики и уровней хранения данных. Это дает большую гибкость командам разработчиков, позволяя им обновлять определенную часть приложения независимо от других частей. Эта дополнительная гибкость может улучшить общее время выхода на рынок и сократить время цикла разработки, предоставляя командам разработчиков возможность заменять или обновлять независимые уровни, не затрагивая другие части системы.

Например, пользовательский интерфейс веб-приложения может быть переработан или модернизирован без затрагивания основной функциональной бизнес-логики и логики доступа к данным. Эта архитектурная система часто идеально подходит для встраивания и интеграции стороннего программного обеспечения в существующее приложение. Подобная гибкость интеграции также делает ее идеальной для встраивания аналитического программного обеспечения в уже существующие приложения и по этой причине часто используется поставщиками встроенных аналитических средств. Трехуровневые архитектуры часто применяются в облачных либо локальных приложениях, а также в приложениях «программное обеспечение как услуга» (SaaS).

Пример трехслойной архитектуры:

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

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

Коммуникация

  • Неэффективная балансировка нагрузки.
  • Недостаточная/плохая конфигурация сетевых интерфейсов.
  • Чрезмерно тяжеловесная безопасность.
  • Неадекватная общая пропускная способность.
  • Непродуманная сетевая архитектура.

Слой Web-server

  • Битые ссылки.
  • Некорректный дизайн транзакций.
  • Перегруженная безопасность.
  • Недостаточная аппаратная емкость.
  • Web-server плохо сконфигурирован.
  • Неэффективная балансировка нагрузки.
  • Неэффективное использование ресурсов ОС.
  • Недостаточная пропускная способность.

Слой App-server

  • Утечки памяти.
  • Бесполезная/неэффективная сборка мусора.
  • Плохая конфигурация соединений с БД.
  • Бесполезные/неэффективные транзакции.
  • Сложности масштабирования из-за неоптимальных stateful-сессий.
  • App-server плохо сконфигурирован.
  • Неэффективное использование аппаратных ресурсов.
  • Неэффективная модель доступа к объектам.
  • Проблемная модель безопасности.
  • Неэффективное использование ресурсов ОС.

Слой данных

  • Неэффективные SQL statements.
  • Маленький query plan cache.
  • Неэффективная модель SQA queryl.
  • Некорректная конфигурация БД.
  • Недостаточное кеширование данных.
  • Избыточные соединения с БД.
  • Избыточный процессинг строк в единицу времени.
  • Отсутствующее/неэффективное индексирование.
  • Проблемы с распараллеливанием.
  • Неэффективный подход к масштабированию.
  • Переусложнение архитектуры БД.
  • Deadlocks.

Интеграция сторонних сервисов (3rd-party)

  • Увеличение трафика.
  • Сторонние сервисы потребляют пропускную способность.
  • Неэффективный код.
  • Недостаточное время отклика стороннего сервиса.

Идем на войну с bottlenecks

Дам ряд советов стратегического характера со стороны подходов:

  1. Задумывайтесь на этапе планирования архитектуры о своей производительности. Если вы делаете highload, делайте highload со старта и донесите эту мысль до всех участников процесса. И заложите соответствующие архитектурные решения.
  2. Аккуратно и взвешенно проводите решения по техническому стеку, допустим по 3rd-party-компонентам или библиотекам.
  3. Проводите код-ревью с учетом производительности. Такие код-ревью хороши всегда, но полезнее их проводить ближе к продакшен-стадии.
  4. Performance-тестировщики — полезные ребята. Не забывайте о них. Если у вас нет в команде специально выделенных людей, почитайте о перформанс-тестировании и хотя бы в минимальном объеме тестируйте производительность сами. Нагрузите свой сервер, посмотрите, как он работает под нагрузкой, проведите трейсинг производительности клиента, посмотрите общий user experience.
  5. Профайлеры рулят. Это очень важный элемент, который позволяет нам отлаживать и оптимизировать свой код, бороться с проблемами производительности. Хорошими представителями этого сегмента инструментария будут такие профайлеры, как JProfiler (наверное, топ-1 сейчас), VisualVM, YourKit.
  6. Не забывайте о прозрачности. В этом нам поможет мониторинг утилизации ресурсов и, если финансы позволяют, Application Performance Monitoring (APM). Такие инструменты мониторинга, как Zabbix, Prometheus, Nagios, не потребуют больших инвестиций. Поэтому мониторинг утилизации ресурсов практически всегда можно получить в относительно короткие сроки. Если у вас есть DevOps в команде, он обычно сам предложит варианты и сам проведет имплементацию. Если же нет, то поднимите этот вопрос со своей стороны. Есть бюджет на APM — вы уже на передовой борьбы с боттлнеками. Идеальным вариантом будут мощные APM-системы Dynatrace, AppDynamics, New Relic или Stackify Retrace как недорогая альтернатива.

Советы для самых юных

  1. Разберитесь с тем, как Java-машина работает с памятью. Я был удивлен, но многие девелоперы, даже достаточно опытные, не знают, что такое heap, eden space, generations и т. д. Разберитесь, как работает сборка мусора, в каком случае и что будет более эффективным решением.
  2. Разберитесь с работой JIT-компиляции. Да, она тоже может тормозить.
  3. Знайте свой environment. Разберитесь с тем, в каком окружении будет работать ваша система. Здесь имеет смысл говорить об окружении как software, так и hardware.
  4. Помните о нефункциональных требованиях. Если вы их держите в голове, то будете задумываться о том, как их достичь.
  5. Не забывайте о ваших объектах сессий.
  6. Не забывайте о пулах соединений.
  7. «Потом доконфигурируем» часто забывается.
  8. Используйте кеширование.
  9. Не оптимизируйте то, что нет смысла сейчас оптимизировать. Premature-оптимизация вредна.
  10. Читайте хорошие книги. Прочесть 1–2 книги по производительности будет полезно практически каждому Java-разработчику.

Что почитать

Удачи в борьбе!

Похожие статьи:
[Дмитрий Овчаренко — CEO & Founder at Alcor, 15+ лет в оперативном управлении ІТ-бизнесом, помог десяткам иностранных технологических...
Департамент цифровізації Міноборони хоче трансформувати «Резерв+» в онлайн-ТЦК, додавши низку нових послуг. Про це повідомила...
Хотите окунуться в мир разработки игр на Unity? Это увлекательное и творческое занятие! Вы научитесь создавать свои игровые...
238-й выпуск подкаста «Откровенно про IT карьеризм». В подкасте пойдет речь о науке, котиках...
Привіт, я Володимир, Java-розробник в Perfectial, Java Lead в LITS і ментор на Cursor Education. Готуючись...
Яндекс.Метрика