В поисках перформанса, часть 2: Профилирование Java под Linux

в 14:24, , рубрики: java, linux, perf, performance, performance optimization, profiling, Блог компании JUG.ru Group, высокая производительность, Разработка под Linux

Бытует мнение, что бесконечно можно смотреть на огонь, воду и то, как другие работают, но есть и ещё кое-что! Мы уверены, что можно бесконечно говорить с Сашей goldshtn Гольдштейном о перформансе. Мы уже брали у Саши интервью перед JPoint 2017, но тогда разговор касался конкретно BPF, которому был посвящен доклад Саши.

На этот раз мы решили копнуть глубже и узнать фундаментальные проблемы мониторинга производительности и варианты их решения.
В поисках перформанса, часть 2: Профилирование Java под Linux - 1

С чего стоит начинать

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

Саша Гольдштейн: Если начинать думать о производительности только в момент, когда ваши пользователи жалуются, с вами они будут недолго. Для многих инженерия производительности — это траблшутинг и crisis mode. Телефоны звонят, свет мигает, система упала, клавиатура горит — обычные трудовый будни перформанс-инженера. В реальности же они проводят большую часть своего времени, планируя, проектируя, мониторя и предотвращая кризисы.

Для начала, capacity planning — оценка ожидаемой нагрузки системы и использования ресурсов; проектирование масштабируемости поможет избежать узких мест и получить значительные увеличения в нагрузке; инструментирование и мониторинг жизненно необходимы для понимания того, что происходит внутри системы, чтобы не копаться вслепую; благодаря установке автоматического оповещения вы точно будете знать о любых возникающих проблемах, как правило ещё до того, как пользователи начнут жаловаться; ну и конечно же будут единичные кризисы, решать которые придётся в стрессовых условиях.

Стоит отметить, что тулы постоянно меняются, но сам процесс остаётся неизменным. Приведу пару конкретных примеров: capacity planning вы можете сделать и на бумажке на коленке; можно использовать APM-решения (как New Relic или Plumbr) для end-to-end инструментирования и мониторинга, AB и JMeter для быстрого нагрузочного тестирования и так далее. Чтобы узнать больше, можно почитать книгу Брендана Грегга «Systems Performance» — это отличный источник по теме жизненного цикла и методологии перформанса, а «Site Reliability Engineering» от Google освещает тему установки характеристик производительности (Service Level Objectives) и их мониторинга.

— Допустим, мы поняли, что проблема есть: с чего начинать? Мне часто кажется, что многие (особенно не профессиональные перформанс-инженеры) сразу готовы расчехлять JMH, переписывать всё на unsafe и «хакать компиляторы». Потом смотреть, что получилось. Но ведь в реальности лучше не с этого начинать?

Саша Гольдштейн: Это довольно распространённая практика, когда при написании кода и проведении базовых тестов профайлера возникают проблемы с перформансом, которые можно легко исправить, поменяв код или «хакнув компиляторы». Однако на проде, исходя из моего опыта, это делается не так часто. Многие проблемы присущи только какой-то одной среде, вызваны меняющимися паттернами рабочей нагрузки или связаны с узкими местами вне кода вашего приложения, и лишь малую часть можно замикробенчмаркать и улучшить на уровне исходного кода «умными» хаками.

Вот пара примеров для иллюстрации:

  • Пару лет назад Datadog столкнулись с проблемой, когда вставки и обновления базы данных в PostgreSQL скакали от 50 мс до 800 мс. Они использовали AWS EBS с SSD. Что это дало? Вместо тюнинга базы данных или изменения кода приложения они обнаружили, что виноват во всём троттлинг EBS: у него есть квота по IOPS, в случае превышения которой вы попадёте под ограничение производительности.
  • Недавно у меня был пользователь с проблемой огромных скачков времени отклика на сервере, которые были связаны с задержками сбора мусора. Некоторые запросы занимали более 5 секунд (и появлялись они абсолютно бессистемно), так как сборка мусора выходила из-под контроля. Внимательно изучив систему, мы обнаружили, что с распределением памяти приложения или же с тюнингом сборки мусора всё было в порядке; из-за скачка в размере рабочей нагрузки фактическое использование памяти повысилось и вызвало свопинг, что абсолютно губительно для реализации любой сборки мусора (если сборщику нужно подкачивать и откачивать память, чтобы отмечать активные объекты, — это конец).
  • Пару месяцев назад Sysdig столкнулся с проблемой изоляции контейнера: находясь рядом с контейнером Х, операции с файловой системой, выполняемые контейнером Y, были гораздо медленнее, в то время как использование памяти и загрузка процессора для обоих контейнеров были очень низкими. После небольшого исследования они обнаружили, что кэш каталогов ядра перегружался контейнером Х, что в дальнейшем вызывало коллизию хэш-таблицы и, как следствие, значительное замедление. Опять-таки, изменение кода приложения или распределения ресурсов контейнера эту проблему бы не решили.

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

— Наверняка следует сначала посмотреть, как приложение/сервис работает в продакшне. Какие инструменты ты для этого рекомендуешь, а какие — не очень?

Саша Гольдштейн: Мониторинг и профайлинг на проде — это набор инструментов и техник.

Начинаем с метрик высокоуровневого перформанса, фокусируясь на использовании ресурсов (процессор, память, диск, сеть) и характеристике нагрузки (# запросов, ошибок, типов запросов, # запросы к базе данных). Есть стандартные тулы для получения этих данных для каждой операции и времени выполнения. К примеру, на Linux обычно используют инструменты вроде vmstat, iostat, sar, ifconfig, pidstat; для JVM используют JMX-based тулы или jstat. Это метрики, которые можно непрерывно собирать в базу данных, возможно с 5-ти или 30-ти секундным интервалом, чтобы можно было проанализировать скачки и при необходимости вернуться в прошлое, чтобы скоррелировать предыдущие операции по развёртыванию, релизы, мировые события или изменения рабочей нагрузки. Важно, что многие фокусируются на собирании только средних показателей; они хоть и хороши, но, по определению, не представляют полное распределение того, что вы измеряете. Гораздо лучше собирать процентили, а по возможности даже и гистограммы.

Следующий уровень — это операционные метрики, которые обычно нельзя непрерывно собирать или хранить долгое время. Они включают в себя: лог сбора мусора, запросы сети, запросы к базе данных, классовые нагрузки и так далее. Разобраться в этих данных после того, как их где-то хранили, иногда гораздо труднее, чем, собственно, их собирать. Это позволяет, однако, задавать вопросы, вроде «какие запросы работали, пока нагрузка ЦП базы данных повышалась до 100%» или «какими были IOPS дисков и время отклика во время выполнения этого запроса». Одни лишь числа, особенно в виде средних показателей, не позволят вам провести подобного рода исследование.

И наконец, «хардкорный» уровень: SSH в сервере (или удаленный запуск тулов) для сбора большего количества внутренних метрик, которые нельзя хранить во время штатной работы сервиса. Это инструменты, которые обычно называют «профайлерами».

Для профайлинга Java-продакшна существует множество жутких тулов, которые не только дают большой оверхэд и задержки, но могут ещё и лгать вам. Несмотря на то, что экосистеме уже около 20 лет, есть лишь несколько надёжных профайлинговых техник с низким оверхэдом для JVM-приложений. Я могу порекомендовать Honest Profiler Ричарда Ворбертона, async-profiler Андрея Паньгина и, конечно, моего любимчика — perf.

Кстати, много тулов фокусируются на профайлинге процессора, разбираясь в том, какой путь выполнения кода вызывает высокую загрузку ЦП. Это здорово, но часто проблема не в этом; нужны инструменты, которые могут показывать пути выполнения кода, ответственные за распределение памяти (async-profiler теперь может делать и это), ошибки отсутствия страницы, непопадание в кэш, доступы к диску, запросы сети, запросы к базе данных и другие события. Меня в этой области привлекла именно проблема поиска правильных перформанс-тулов для исследования работающей среды.

Профилирование Java под Linux

— Я слышал, что под Java/Linux стек есть куча проблем с достоверностью замеров. Наверняка с этим можно как-то бороться. Как ты это делаешь?

Саша Гольдштейн: Да, это печально. Вот как выглядит текущая ситуация: у вас есть быстрая конвейерная линия с огромным количеством разных частей, которые нужно протестировать, чтобы найти дефекты и понять скорость приложения/сервиса. Вы не можете проверить абсолютно каждую часть, поэтому ваша основная стратегия — это проверять 1 часть в секунду и смотреть, всё ли в порядке, и вам нужно это делать через «крохотное окно» над этой «лентой», потому что подходить ближе уже опасно. Вроде неплохо, не так ли? Но потом оказывается, что когда вы пытаетесь в него посмотреть, оно показывает вам не то, что происходит на конвейере прямо сейчас; оно ждёт, пока конвейер перейдёт в волшебный «безопасный» режим, и только после этого даёт вам всё увидеть. Также оказывается, что многих частей вы не увидите никогда, потому что конвейер не может войти в свой «безопасный» режим, пока они рядом; и ещё оказывается, что процесс поиска дефекта в окошке занимает целых 5 секунд, так что делать это каждую секунду невозможно.

Примерно в таком состоянии сейчас находится множество профайлеров в мире JVM. YourKit, jstack, JProfiler, VisualVM — у всех у них одинаковый подход к профайлингу ЦП: они используют семплинг потоков в безопасном состоянии. Это значит, что они используют документированный API, чтобы приостановить все JVM-треды и взять их стек-трейсы, которые затем собирают для отчёта с самыми горячими методами и стеками.

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

Есть исследование, показывающее, насколько это плохо, когда у каждого профайлера своя точка зрения по поводу самого горячего метода в одной и той же рабочей нагрузке (Миткович и соавт., «Evaluating the Accuracy of Java Profilers»). Более того, если у вас есть 1000 тредов в сложном стеке вызовов Spring, часто собирать стек-трейсы не получится. Возможно, не чаще 10 раз в секунду. В результате ваши данные по стекам будут отличаться от фактической рабочей нагрузки ещё больше!

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

Существует два раздельных подхода и один гибридный:

  • Honest Profiler Ричарда Ворбертона использует международный недокументированный API, AsyncGetCallTrace, который возвращает стек-трейс одного треда, не требует перехода в безопасное состояние и вызывается с помощью обработчика сигнала. Изначально он был спроектирован Oracle Developer Studio. Основной подход заключается в установке обработчика сигнала и его регистрации на сигнал с установленным временем (например, 100 Гц). Затем необходимо взять стек-трейс любого треда, который в данный момент работает внутри обработчика сигнала. Очевидно, что есть непростые задачи, когда дело доходит до эффективного объединения стек-трейсов, особенно в контексте обработчика сигнала, но этот подход отлично работает. (Этот подход использует JFR, требующий коммерческую лицензию)
  • Linux perf может предоставлять богатый сэмплинг стеков (не только для инструкций ЦП, но и для других событий, таких как доступ диска и запросы сети). Проблема заключается в преобразовании адреса Java-метода в имя метода, что требует наличие JVMTI-агента, достающего текстовый файл (perf map), который perf может читать и использовать. Есть также проблемы с реконструкцией стека, если JIT использует подавление указателей фрейма. Этот подход вполне может работать, но требует небольшой подготовки. В результате, однако, вы получите стек-трейсы не только для JVM-тредов и Java-методов, но и для всех имеющихся у вас тредов, включая стек ядра и стеки C++.
  • async-profiler Андрея Паньгина сочетает в себе два подхода. Он устанавливает набор образцов perf, но также использует обработчик сигнала для вызова AsyncGetStackTrace и получения Java-стека. Объединение двух стеков даёт полную картину того, что происходит в потоке, позволяя избегать проблем с преобразованием имен Java-методов и подавлением указателей фрейма.

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

Профилирование в контейнерах

— К слову об окружениях, сейчас модно всё паковать в контейнеры, здесь есть какие-то особенности? О чём следует помнить, работая с контейнеризованными приложениями?

Саша Гольдштейн: С контейнерами возникают интересные проблемы, которые многие тулы полностью игнорируют и в результате вообще перестают работать.

Вкратце напомню, что контейнеры Linux построены вокруг двух ключевых технологий: группы контроля и пространства имён. Группы контроля позволяют увеличить квоту ресурсов для процесса или для группы процессов: CPU time caps, лимит памяти, IOPS хранилища и так далее. Пространства имён делают возможной изоляцию контейнера: mount namespace предоставляет каждому контейнеру свою собственную точку монтирования (фактически, отдельную файловую систему), PID namespace — собственные идентификаторы процессов, network namespace даёт каждому контейнеру собственный сетевой интерфейс и так далее. Из-за пространства имён множеству тулов сложно правильно обмениваться данными с контейнезированными JVM-приложениями (хотя некоторые из этих проблем свойственны не только JVM).

Прежде чем обсудить конкретные вопросы, будет лучше, если мы вкратце расскажем о разных видах observability тулов для JVM. Если вы не слышали о некоторых из них, самое время освежить ваши знания:

  • базовые тулы вроде jps и jinfo предоставляют информацию о действующих JVM-процессах и их конфигурации;
  • jstack можно использовать, чтобы достать thread dump (стек-трейс) из действующих JVM-процессов;
  • jmap — для получения head dump действующих JVM-процессов или более простых гистограмм классов;
  • jcmd используется, чтобы заменить все предыдущие тулы и отправить команды действующим JVM-процессам через JVM attach interface; он основан на сокете домена UNIX, который используется JVM-процессами и jcmd для обмена данными;
  • jstat — для мониторинга базовой информации JVM-перформанса, как загрузка класса, JIT-компиляция и статистика сборки мусора; основан на JVM, генерирующей файлы /tmp/hsperfdata_$UID/$PID с этими данными в бинарном формате;
  • Serviceability Agent предоставляет интерфейс для проверки памяти JVM процессов, тредов, стеков и так далее и может быть использован с дампом памяти и не только живых процессов; он работает, читая память процесса и структуры внутренних данных;
  • JMX (управляемые бины) может использоваться для получения информации о перформансе от действующего процесса, а также для отправления команд, чтобы контролировать его поведение;
  • JVMTI-агенты могут присоединяться к разным интересным JVM-событиям, таким как загрузка классов, компиляция методов, запуск/остановка треда, monitor contention и так далее.

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

  • большинство инструментов требуют доступ к бинарникам процесса для поиска объектов и значений. Всё это находится в mount namespace контейнера и недоступно из хоста. Частично этот доступ можно обеспечить при помощи bind-mounting из контейнера или к вводу mount namespace контейнера в профайлере во время выполнения symbol resolving (это то, что сейчас делают perf и BCC тулы, о которых я рассказывал в предыдущем интервью).
  • если у вас есть JVMTI-агент, который генерирует perf map (например, perf-map-agent), она будет написана в хранилище контейнера /tmp, используя ID процесса контейнера (например, /tmp/perf-1.map). Map-файл должен быть доступен хосту, а хост должен ждать верный ID процесса в имени файла. (Опять же, perf и BCC теперь могут делать это автоматически).
  • JVM attach interface (на который полагаются jcmd, jinfo, jstack и некоторые другие тулы) требует верных PID и mount namespace attach файла, а также сокет UNIX домена, использовавшегося для обмена данными с JVM. Эту информацию можно прокинуть при помощи jattach utility и создания attach файла, входя в пространство имён контейнера или при помощи bind-mounting соответствующих директорий на хосте.
  • использование файлов данных о производительности JVM (в /tmp/hsperfdata_$UID/$PID), которыми пользуется jstat, требует доступ к монтированию пространства имён контейнера. Это легко адресуется bind-монтированием /tmp контейнера на хосте;
  • самый простой подход к использованию JMX-based инструментов — это, пожалуй, доступ к JVM, как если бы она была удалённой — конфигурируя RMI endpoint, как вы бы делали для удалённой диагностики;
  • тулы Serviceability Agent требуют точного соответствия версий между JVM-процессом и хостом. Думаю, вы понимаете, что не стоит запускать их на хосте, особенно если он использует разное распределение и на нём установлены разные версии JVM.

Тут можно подумать: а если мне просто положить перформанс-тулы в контейнер, чтобы все эти проблемы с изоляцией возникали не из-за меня? Хоть идея неплохая, множество тулов не будут работать и с такой конфигурацией из-за seccomp. Docker, к примеру, отклоняет системный вызов perf_event_open, необходимый для профайлинга с perf и async-profiler; он также отклоняет системный вызов ptrace, который используется большим количеством тулов для чтения объёма памяти JVM-процесса. Изменение политики seccomp для принятия этих системных вызовов ставит хост под угрозу. Также, помещая тулы профайлинга в контейнер, вы увеличиваете его поверхность атаки.


Мы хотели продолжить разговор и обсудить влияние железа на профилирование…

В поисках перформанса, часть 2: Профилирование Java под Linux - 2

Совсем скоро Саша приедет в Санкт-Петербург, чтобы провести тренинг по профилированию JVM-приложений в продакшне и выступить на конференции Joker 2017 с докладом про BPF, поэтому, если вы хотите погрузиться в тему глубже – у вас есть все шансы встретиться с Сашей лично.

Автор: ValeriaKhokha

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js