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

Иногда бывает сложно найти и устранить проблемы с производительностью в серверных системах, особенно когда в потоке обработки запросов участвует несколько служб и баз данных. Получение и отработка навыков исследования производительности может помочь вам сэкономить много времени, в том числе время во время крупных инцидентов, когда ваше производство сломалось, и вам нужно найти решение посреди ночи. Наличие таких навыков также может быть полезно при прохождении кейс-интервью — собеседования, на котором вас представляют и просят разрешить инцидент, произошедший в компании в прошлом. В этой статье я постараюсь написать полное руководство о том, как исследовать и устранять проблемы с производительностью в распределенных бэкенд-системах. Я буду использовать Java в качестве примера, но логика и техника должны быть применимы практически к любой серверной системе. Большое влияние на эту статью оказала книга Google Site Reliability Engineering, а также мой опыт устранения проблем с производительностью в Яндексе и Spotify.

Образ мышления: собрать, проанализировать, исправить

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

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

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

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

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

Сбор данных

Понимание системной архитектуры — многоуровневое приложение

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

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

Прежде всего разумно понять природу таких запросов и изолировать их:

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

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

Понимание компонентов и их метрик

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

Ключевые показатели всегда одни и те же, независимо от того, какой компонент вы проверяете: использование ЦП/памяти, количество запросов и задержка, размер пула обработки/очереди. Если у вас их нет на панели инструментов, всегда полезно добавить их. Вы также можете попробовать использовать инструменты Linux, такие как top или tcpdump. чтобы сразу получить некоторые данные. Проверка доступных метрик для каждого компонента, начиная с Load Balancer и заканчивая вашей базой данных, даст вам гораздо лучшее понимание проблемы.

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

Прикрепление профилировщика против ручных мертик

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

  • Возникла ли проблема при запуске профилировщика?
  • Возникла ли проблема при работающем профилировщике экземпляра?
  • В чем причина того, что некоторые методы работают медленно — из-за того, что код не оптимизирован или из-за количества запросов, которые вызвали вызов метода?
  • Является ли узкое место причиной проблемы или метод просто оказался самым медленным в выполнении?

За свою карьеру я видел, как много времени тратится впустую, потому что результаты профиля не понимаются в контексте:

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

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

Сформулировать теорию

Сужение поиска

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

Еще одна хорошая идея — просмотреть последние изменения в вашем репозитории контроля версий. Если вы видите, что что-то подозрительное было недавно объединено, сначала сосредоточьтесь на затронутом компоненте.

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

Наблюдение

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

Сосредоточьтесь на проверке

Хорошая теория тоже проста (вспомните бритву Оккама). А хорошая теория та, которую можно проверить. Старайтесь думать только о тех шагах, которые вам нужно проверить в первую очередь, и не думайте об изменениях, которые вам нужно сделать, чтобы решить проблему. Решение проблем с производительностью редко бывает подходящим временем для больших изменений — если вы всегда хотели представить новые компоненты или опробовать новую технологию, а затем внезапно возникла проблема с производительностью, которая требует именно тех изменений, которые вы всегда хотели, которые часто слишком хороши, чтобы быть истинный. Может быть, это так, или, может быть, вы просто выбрали доказательства, чтобы подтвердить свою предвзятость. Гораздо лучше сначала сосредоточиться только на объяснении, затем предпринять действия для его проверки и только потом думать о решениях.

Делая шаг назад

Очень легко сузить область поиска, сосредоточиться на одном компоненте, на который проблема больше всего влияет, и попытаться думать о проблеме только в контексте этого компонента. Вы видите, что загрузка ЦП на одном из сервисов высока — значит, проблема должна быть в сервисе. Это действительно хорошее начало, но не думайте о компоненте изолированно, а скорее о разделении причины и следствия: «Да, в службе высокая загрузка ЦП, но что вызывает это извне?». Если вы тратите слишком много времени на один компонент, ваши теории становятся все более и более сложными, а данные, которые вы получаете, все труднее и труднее интерпретировать — это знак сделать шаг назад и переоценить ситуацию с более высокого уровня.

Применение исправления

Смягчения

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

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

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

Исправления

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

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

Заключение

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

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