Почему серверы начинают тормозить сами по себе? Инженер, работавший с системами на сотни миллионов пользователей, о скрытых причинах сбоев

Илья Детинкин
Илья Детинкин
Замедление цифрового сервиса далеко не всегда связано с новым релизом, ошибкой разработчика или нехваткой вычислительных ресурсов. Иногда система продолжает работать по прежним правилам, но постепенно начинает выполнять одну и ту же задачу во много раз дольше. Причина может скрываться в росте объема данных, неудачной структуре запроса или тысячах небольших операций, каждая из которых сама по себе кажется несущественной. В результате компания узнает о проблеме не из системы мониторинга, а от пользователя, который первым замечает, что привычная операция стала занимать слишком много времени. О том, как обнаружить такую деградацию до того, как она превратится в серьезную проблему, «Экспресс газета» поговорила с инженером Ильей Детинкиным, специализирующимся на архитектуре и устранении структурных ограничений серверных систем. В его профессиональном опыте – работа с крупными международными цифровыми сервисами, в том числе над серверной архитектурой системы виртуальных аватаров и платформой локализации одного из крупнейших приложений в своей категории.

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

Илья Детинкин: Потому что меняется сама среда, в которой система работает. Прежде всего растет объем данных и количество операций. Представим запрос к базе данных, который обрабатывает десять тысяч записей. Он может выполняться очень быстро. Но если через несколько лет в базе уже несколько миллионов строк, тот же самый запрос начинает работать по совершенно другим правилам. Ему приходится просматривать больше данных, строить более крупные промежуточные результаты, выполнять больше соединений и сортировок.

При этом в истории изменений может не быть никакого события, которое непосредственно объясняет ухудшение производительности. Не было нового релиза, не менялась логика приложения – просто данные выросли до такого объема, при котором прежняя архитектура перестала быть эффективной.

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

— Но в реальности компании часто поступают проще: если система стала медленной, ей увеличивают вычислительные ресурсы. Насколько это рабочий подход?

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

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

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

— Как инженер находит эту лишнюю работу?

Илья Детинкин: Я обычно рассматриваю диагностику как последовательность из нескольких этапов. Сначала нужно получить измеримый показатель. Не «страница медленная», а, например, «операция занимает десять секунд при таких-то условиях». Без исходного значения невозможно доказать, что последующее изменение действительно что-то улучшило. Затем нужно определить, где именно возникает задержка.

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

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

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

— Можете привести пример, когда такой подход позволил существенно ускорить реальную систему?

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

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

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

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

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

— Получается, достаточно найти один неудачный запрос?

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

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

Мы изменили логику: во время обработки партии система стала накапливать необходимые пересчеты, а выполнять их – один раз после завершения обработки. Само исправление было относительно простым. Сложнее было понять, где искать причину. Это хороший пример того, почему нельзя диагностировать производительность только по принципу «найдем самый медленный участок». Иногда гораздо важнее вопрос, сколько раз выполняется относительно небольшая операция.

— Какие ошибки при оптимизации систем встречаются чаще всего?

Илья Детинкин: Одна из самых распространенных – попытка исправить симптом вместо причины. Если пользователь говорит, что страница загружается медленно, естественно первым делом искать самый тяжелый запрос. Но причина может находиться совсем в другом месте.

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

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

— А насколько часто компании вообще начинают искать причину только после жалоб пользователей?

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

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

— Можно ли заранее понять, что система скоро столкнется с ограничением?

Илья Детинкин: Во многих случаях да, если регулярно наблюдать за динамикой. Проблемы производительности редко возникают из ниоткуда. Обычно система постепенно приближается к некоторому ограничению.

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

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

— В вашей работе приходилось сталкиваться именно с такими структурными ограничениями?

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

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

— Вы работали над системами, которыми пользовались сотни миллионов человек. Чем отличается поиск проблем в таких масштабах?

Илья Детинкин: Масштаб очень сильно меняет цену инженерного решения. Если небольшая система выполняет какую-то лишнюю операцию один раз, это может быть практически незаметно. Но если она выполняется миллионы раз, даже небольшая неэффективность превращается в существенную нагрузку.

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

— А что происходит, если проблема обнаружена, но быстро переписать систему невозможно?

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

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

— Вы также занимались созданием внутренних инструментов. Насколько важна возможность сделать решение универсальным, а не исправить только одну конкретную проблему?

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

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

— Как, на ваш взгляд, изменится подход к производительности систем в ближайшие годы?

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

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

— То есть искусственный интеллект не отменяет работу инженера?

Илья Детинкин: Нет. Хороший инженер по-прежнему должен понимать, что именно измеряется, почему система ведет себя таким образом и какое изменение действительно решит проблему.

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

Василий Черный