Понять, почему приложение использует больше ресурсов, чем ожидалось (будь то ЦП или память), бывает непросто. Журналы и агрегированные метрики дают лишь ограниченное представление. К счастью, есть способ лучше: профилирование ЦП или памяти может показать точную функцию, в которой приложение использует ЦП или выделяет память.
Сегодня мы рады объявить о поддержке профилирования ЦП и памяти для Workers и Durable Objects. На странице наблюдаемости Workers теперь вы можете запросить профиль ЦП или памяти активного Worker по требованию, изучить его в виде интерактивного графа пламени (flamegraph) и скачать файл профиля для дальнейшего анализа.
Этот метод профилирования дает полезное представление о том, что делает ваш код и как его можно улучшить в реальных условиях. Дело в том, что лучший способ понять свое приложение — это профилировать его в продакшене.
Чтобы опробовать это на одном из своих Workers, вы можете использовать интерфейс командной строки (CLI) или панель управления Cloudflare.
Чтобы использовать CLI, убедитесь, что установлен пакет cf, а затем просто выполните:
Чтобы использовать панель управления, перейдите в панель управления Cloudflare, а затем откройте список Workers в вашей учетной записи, перейдя по пунктам: Сборка (Build) → Вычисления (Compute) → Workers & Pages.
Выберите свой Worker и перейдите на его вкладку «Наблюдаемость» (Observability). Затем с помощью выпадающего списка выберите «Граф пламени» (Flamegraph):
После этого на этой странице вы можете запросить профили ЦП и памяти для своего Worker. Длительность определяет, как долго профайлер должен работать на вашем Worker. Вы также можете выбрать различные версии вашего Worker для профилирования. Вашему Worker требуется достаточный объем трафика для успешного профилирования, поэтому убедитесь, что вы выбрали версию с достаточным количеством запросов.
Как только вы нажмете кнопку «Захватить профиль» (Capture Profile), профайлер проработает в течение выбранного времени, после чего вы увидите отрисованный граф пламени, представляющий результаты профилирования. Каждый прямоугольник на графе пламени представляет вызов функции, причем ширина отражает объем времени ЦП или памяти, используемых этой функцией. Вы можете нажимать на различные функции, чтобы сосредоточиться на них, и наводить курсор, чтобы увидеть более подробную информацию о них:
Не бойтесь записать несколько профилей, а затем найти самые широкие функции, чтобы определить, что потребляет больше всего ресурсов в вашем Worker. Вы также можете обнаружить, что табличное представление полезно для быстрого понимания того, какие функции чаще всего встречаются в профиле.
Если ваш Worker написан на TypeScript, вам следует убедиться, что для вашего проекта включены исходные карты (source maps), иначе в вашем профиле могут отображаться обфусцированные имена функций, которые сложно понять.
Мы уже видели, как несколько команд в Cloudflare используют профилирование ЦП и памяти для поиска возможностей оптимизации использования памяти и ЦП, устранения ошибок нехватки памяти (OOM) и повышения производительности. Ниже приведены некоторые примеры.
Использование профилирования ЦП для поиска оптимизаций
Давайте рассмотрим реальный пример того, как можно использовать профилирование ЦП для оптимизации ваших Workers. Мы рассмотрим профиль ЦП Worker, реализующего привязку R2. Этот Worker получает много трафика, поэтому устранение даже малейшего количества потраченных впустую тактов ЦП может оказать огромное влияние на его производительность.
Мы настроили профайлер на захват трассировок ЦП длительностью 50 секунд. Получив отрисованный профиль, мы увидели следующее:
Как упоминалось ранее, самые широкие блоки — это те, которые потребляют больше всего времени ЦП. Некоторые из них неудивительны: например, decryptBlock — это расшифровка данных объекта в R2 на выходе, а fillResponse просто перемещает байты. Явных способов оптимизировать эти функции не было.
Поскольку некоторые блоки менее заметны, полезно заглянуть в табличное представление. Сортировка по выборкам (Samples) указывает на genericR2JsonReplacer:
Эта функция занимала более 5% времени ЦП, но наше внимание привлекла функция, вызывающая сама себя рекурсивно. Это выглядело как бесполезная работа, и так оно и было.
Несмотря на то, что замена вызывалась с помощью JSON.stringify для каждого узла в дереве JSON, сама функция замены также обходила дерево. Это означало, что значение, вложенное на пять уровней вглубь, обрабатывалось пять раз. Исправление этого позволило нам исключить дублирование работы и сделать функцию genericR2JsonReplacer в 2,7 раза быстрее.
Вторым кандидатом в профиле, на которого мы обратили внимание, был дублирующий вызов метрик.
Глядя на код, мы увидели что-то похожее на это:
Вызов метрик был настолько тяжелым, что всего один вызов составлял 1% времени ЦП в профиле. Поэтому сохранение результата первого вызова в переменную и отказ от его повторного вызова сэкономили нам изрядную долю времени ЦП.
Как мы использовали профилирование Worker внутри компании для решения проблем с памятью
У нас внутри компании был Worker с проблемами с памятью. Память P999 держалась на уровне около 133 МБ при лимите памяти Worker в 128 МБ. Из-за этого Worker часто завершался с ошибкой «Превышен объем памяти» (Exceeded Memory).
На изображении выше показан снимок экрана графика «Ошибки по статусу вызова» (Errors by invocation status), на котором количество ошибок «Превышен объем памяти» явно снижается в результате исправлений, выявленных с помощью профилирования.
Было сложно определить, что именно вызывает эти ошибки. Ошибки и метрики указывали на память как на источник проблемы, но это не объясняло, какая именно часть кода ее вызывает.
Команда сняла профиль кучи (heap profile) одного из Workers, работающих в продакшене, и открыла его с помощью pprof. Они увидели, что на код Prometheus приходилось примерно 66,7% выделений памяти в профиле. Этот код должен был быть отключен, но его часть все еще работала и создавала проблемы. Поскольку он инструментировал множество путей выполнения кода в Worker, в конечном итоге он использовал много памяти, хотя собранные данные фактически никогда не покидали Worker.
Этот код не подозревался в проблемах с памятью, так как считалось, что он отключен. Профайлер показал, что код отключен лишь частично, а плата за память взимается так, будто он включен полностью.
Команда полностью ударила путь кода Prometheus. Это быстро улучшило использование памяти Worker:
Процентиль
До (МБ)
После (МБ)
P50
P90
P99
113
P999
133
118
Это дало P999 около 10 МБ запасного пространства ниже лимита в 128 МБ.
Запрос профилирования на периферии (edge)
Workers уже некоторое время поддерживают локальное профилирование через Chrome DevTools, но запустить сеанс профилирования на Workers, работающих в продакшене, было невозможно. Хотя профилирование Worker локально полезно, Worker не получает те же типы и количество запросов, что и в продакшене. Так как же профилировать Worker или Durable Object, работающие там?
Одна из лучших особенностей Workers заключается в том, что их запросы направляются в тот дата-центр, который находится ближе всего к клиенту, отправившему запрос. Это снижает задержку, но означает, что Workers должны быть реплицированы между различными дата-центрами и физическими серверами (metal). Кроме того, когда Worker получает множество запросов в конкретном дата-центре, он может реплицироваться внутри конкретного физического сервера несколько раз для обработки всего трафика.
Durable Objects добавляют еще один уровень косвенности: они могут размещаться динамически и перемещаться.
Это и некоторые другие особенности Workers означают, что перед началом сеанса профилирования нам нужно сначала ответить на несколько вопросов:
- Какую версию разработчик хочет профилировать?
- В каком дата-центре эта версия запускалась в последнее время?
- Загружен ли изолят (isolate) Worker в этом дата-центре?
- Принадлежит ли изолят исключительно запрашивающему аккаунту?
- Для Durable Object: где находится точный активный первичный актер (primary actor)?
На некоторые из этих вопросов отвечает клиент, отправляющий запрос на профилирование. В большинстве случаев это будет дашборд, но вы также можете отправлять такие запросы самостоятельно (или поручить это своему агенту по кодированию). Пример запроса может выглядеть примерно так:
Примечательно, что в URL запроса указываются как имя скрипта, так и версия, которые необходимо профилировать.
В настоящее время новый изолят для создания профиля не запускается, поскольку цель состоит в том чтобы наблюдать за реальным выполнением в продакшене. Это означает, что если ваш Worker получает очень мало трафика, вам может быть сложно его профилировать. Поэтому важно выбрать версию вашего Worker, которая уже развернута.
Профилирование изолята без остановки Worker
Профиль полезен только в том случае, если приложение может продолжать работу во время сбора сэмплов. Поэтому среда выполнения Workers удерживает блокировку изолята только во время операций жизненного цикла профилировщика. Для профиля ЦП этот процесс выглядит следующим образом:
- Получить изолят и его блокировку.
- Создать профилировщик ЦП V8 и начать выборку с интервалом в одну миллисекунду.
- Освободить блокировку, чтобы могли выполняться обычные запросы.
- Ждать в течение запрошенной длительности.
- Снова захватить блокировку и остановить профилирование.
- Освободить блокировку и сериализовать результат за пределами критической секции.
Удержание блокировки в течение всего времени профилирования препятствовало бы продолжению выполнения JavaScript, что сделало бы профиль бесполезным.
Durable Objects
Durable Objects отличаются от Workers. Обычные Workers не имеют состояния: ни один Worker не является особенным или отличным от остальных, и мы выбираем запущенный изолят запрошенного Worker случайным образом. Durable Objects имеют состояние и имя. Это дает нам очень полезную возможность получить профиль конкретного изолята и конкретного объекта в любой точке мира. При профилировании Durable Object вы можете выбрать по имени, какой именно объект профилировать. Затем среда выполнения перенаправит запрос профилирования на нужный физический сервер, которому принадлежит конкретный актер, и предоставит профиль конкретного изолята, выполняющего этот объект.
Что дальше?
Профилирование по требованию — это только начало. Оно позволяет исследовать производительность ваших Workers способами, которые раньше были недоступны. Но у него есть и некоторые ограничения, о которых стоит упомянуть:
- Вы должны явно запустить сеанс профилирования. Это может означать, что вы пропустите важные периоды времени, в течение которых ваш Worker работает некорректно и когда вам хотелось бы увидеть профиль, чтобы понять, что пошло не так.
- Профилировщик памяти может показывать только те выделения памяти, которые произошли во время окна профилирования. Поэтому, если ваш Worker выделяет много памяти во время запуска, вы этого не увидите.
Чтобы решить эту проблему, мы уже работаем над кое-чем новым: непрерывным профилированием. Идея заключается в том, что мы будем автоматически собирать сэмплы профилирования для вашего Worker, чтобы вы могли просто изучать профили в дашборде. Это должно облегчить захват более редких событий.
Чтобы узнать больше о профилировании в Workers, включая функцию, представленную в этом посте в блоге, ознакомьтесь с нашей документацией.
Особая благодарность команде Workers Control Plane и команде Cloudflare Observability Platform, в особенности Уильяму Перрону (William Perron), который помог с реализацией дашборда для этого проекта.











