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


import _ “net/http/pprof”

В HTTP-сервере по-умолчанию — net/http.DefaultServeMux — по пути /debug/pprof/ будут зарегистрированы обработчики, отдающие результаты профилирования.


curl -o cpu-profile.pb.gz http://<server-addr>/debug/pprof/profile

(подробнее см. https://godoc.org/net/http/pprof)


Но по опыту, не всегда все так просто и на практике с использованием pprof в бою, есть подводные камни.


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


Но что если приложение вообще не предполагает доступа по HTTP — например, это офлайн-обработчик очереди?


В зависимости от состояния инфраструктуры в компании, «внезапный» HTTP-сервер внутри процесса приложения может вызвать вопросы у отдела эксплуатации ;) Сервер дополнительно ограничивает возможности горизонтального масштабирования, т.к. не получится просто запустить несколько экземпляров приложения на одном хосте — процессы будут конфликтовать, пытаясь открыть один и тот же TCP-порт для pprof-сервера.


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


В зависимости от особенностей компании, само наличие возможности обращаться к чему-то, что не относится к основной продакшн-деятельности сервиса, может вызвать вопросы у отдела безопасности ;) Я работал в компании, где, по объективным причинам, доступ к чему угодно на стороне продакшна был исключительно у отдела эксплуатации. Единственный способ запустить профилировщик над работающим приложением было открыть задачу в баг-трекере эксплуатации, с описанием, какую curl-команду, в каком ДЦ, на каком сервере нужно запустить, какой ожидать результат и что с ним делать.


Или представьте ситуацию: рабочее утро. Вы открыли Slack и узнаете, что вечером, в одном из процессов продакшн-сервиса «что-то пошло не так», «где-то, что-то «задедлочилось», «начала течь память», «графики цпу поползли вверх» или приложение просто начало паниковать. Дежурные команды эксплуатации (или OOM Killer) не стали глубоко копать и просто перезапустили приложение или откатили последний релиз предыдущего дня.


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


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


Но если pprof это относительно дешево, почему бы не собирать данные профилирования автоматически, с некоторой периодичностью и хранить их где-то отдельно от продакшна, куда всем заинтересованным можно выдать доступы?


В 2010 г., Google опубликовали документ «Google-Wide Profiling: A Continuous Profiling Infrastructure for Data Centers» описывающий подход к непрерывному профилированию систем компании. А спустя сколько-то лет, компания запустила сервис непрерывного профилирования — Stackdriver Profiler — доступный всем желающим.


Принцип работы прост: вместо pprof-сервера, в приложение подключается stackdriver-агент, который, используя напрямую API runtime/pprof, периодически собирает с приложения разные типы профилирования и отправляет профили в облако. Все, что нужно разработчику, используя панель управления Stackdriver, выбрать нужный инстанс приложения, в нужной AZ и можно, постфактум, анализировать работу приложения в любой момент в прошлом.


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


Все описанные выше сложности и идеи далеко не новы и специфичны не только для Go. С ними, в том или ином виде, разработчики сталкивается практически во всех компаниях где я работал.


В какой-то момент, мне стало любопытно попробовать собрать аналог Stackdriver Profiler’а для произвольного Go-сервиса, который мог бы решить описанные проблемы. В качестве хобби-проекта, в свободное время, я работаю над profefe (https://github.com/profefe/profefe) — открытым сервисом непрерывного профилирования. Проект все еще находится в стадии экспериментов и периодических обсуждений, но уже пригоден для тестирования.


Задачи, которые я ставил перед проектом:


  1. Сервис будет разворачиваться на внутренней инфраструктуре компании.
  2. Сервис будет использоваться как внутренний инструмент компании. Поставщикам и потребителям данных можно доверять: на ранних этапах можно опустить вопросы авторизации запросов на запись/чтения и не пытаться заранее защищаться от злонамеренного использования.
  3. У сервиса не должно быть каких-то специальных ожиданий от инфраструктуры компании: все может жить в облаке или в собственных ДЦ; профилируемые приложения могут быть запущены внутри контейнеров («а все управляться кубернетесом») или могут работать на голом железе.
  4. Сервис должен быть прост в эксплуатации (кажется, до определенных масштабов, Prometheus — хороший пример).
  5. Стоит понимать, что выбранная архитектура может не удовлетворять условиям, в которых сервис будет использоваться. Скорее всего нужна будет возможность расширения /замены компонентов системы для масштабирования «на местах».
  6. В соответствии с (4), нужно постараться минимизировать обязательные внешние зависимости. Например, сервис должен как-то искать инстансы профилируемых приложений, но, по крайней мере на начальных этапах, хочется обойтись без явного сервис-дискавери.
  7. В сервисе будут хранить и каталогизировать профили Go-приложений. Ожидаем, что один pprof-файл занимает 100KB—2MB (heap-профили обычно сильно больше профилей CPU). От одного профилируемого инстанса, не имеет смысла слать больше N профилей в минуту (один агент Stackdriver’а шлет, в среднем, 2 профиля в минуту). Стоит сразу рассчитывать, что у одного приложения может быть от нескольких до нескольких сотен инстансов.
  8. Через сервис, пользователи будут искать разные типы профилей (cpu, heap, mutex, пр.) приложения или конкретного инстанса приложения, за некоторый промежуток времени.
  9. Из сервиса пользователь будет запрашивать отдельный pprof-профиль из результатов поиска.

Сейчас profefe состоит из двух компонентов:


profefe-collector — сервис-коллектор с простым RESTful API.
Задача коллектора получить pprof-файл и некоторые мета-данные и сохранить их в постоянное хранилище. API также позволяет клиентам искать профили по мета-данным в некотором временном окне или прочитать из хранилища конкретный профиль (или группу профилей одного типа).


agent — опциональная библиотека, которую следует подключить в приложение вместо pprof-сервера. Внутри приложения, в отдельной горутине, агент периодически запускает процесс профилирования (используя runtime/pprof), и отправляет полученные pprof-профили, вместе с мета-данными в коллектор.


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



Схема взаимодействия компонентов profefe


Выше я упоминал, что агент — компонент опциональный. Если нет возможности подключить его в существующие приложение, но в приложении уже подключен net/http/pprof-сервер, профили можно снимать любыми внешними инструментами и отправлять pprof-файлы в коллектор через HTTP API.


Например, на хостах можно настроить cron-задачу, которая будет периодически собирать профили с запущенных инстансов и отправлять их в profefe на хранение ;)



Cron-задача собирает и отправляет профили приложения в profefe-коллектор


Подробнее про API profefe можно прочитать в документации на GitHub.


Планы


Пока что, единственный способ взаимодействовать с profefe-коллектором, это HTTP API. Одна из задач на будущее — собрать отдельный UI-сервис, через который можно будет визуально показать хранимые данные: результаты поиска, общий обзор производительности кластера, пр.


Собирать и хранить данные профилирования это неплохо, но «без применения, данные бесполезны». В команде, где я работаю, есть набор экспериментальных инструментов для сбора базовой статистики по нескольким pprof-профилям с сервиса. Это очень помогает в анализе последствий обновления ключевых зависимостей приложения или результатов большого рефакторинга (к сожалению, не всегда производительность в продакшне соответствует ожиданиям на основе запусков изолированных бенчмарков и профилирования в тестовом окружении). Я хочу добавить похожую функциональность для сравнения и анализа хранимых профилей в API profefe.


Несмотря на то, что главный фокус profefe, непрерывное профилирование Go-сервисов, формат профилей pprof совсем не привязан к Go. Для Java, JavaScript, Python и пр. есть библиотеки позволяющие получить данные профилирования в этом формате. Возможно, profefe может стать полезным сервисов для приложений написанных на других языках.


Кроме прочего, в репозитории есть некоторое количество открытых вопросов описанных в трекере проекта на GitHub.


Заключение


В последние несколько лет в среде разработчиков закрепилась популярная идея, что для достижения «видимости» (observability) сервиса нужны три компонента: метрики, логи и трейсинг (“three pillars of observability”). Мне кажется, что видимость, это возможность эффективно отвечать на вопросы о работоспособности системы и ее составляющих. Метрики и трейсинг позволяют понять систему в целом. Логи охватывают заведомо-описанные части системы. Профилирование это еще один сигнал для достижения видимости, позволяющий понять систему на микроуровне. Непрерывное профилирования на протяжении периода времени, еще и помогает понять как отдельные составляющие и окружение влияли и влияют на работоспособность и производительность всей системы.

Комментарии (2)


  1. Berserkr
    08.10.2019 09:28

    почему решили писать с нуля, а не помочь доработать одно из существующих решений?
    https://github.com/conprof/conprof тут идея интересная
    https://github.com/segmentio/pprof-server отличный, взрослый проект


    1. Vii Автор
      08.10.2019 12:43

      Про pprof-server я не слышал. Но, если не ошибаюсь, это сервис-дискавери для pprof-серверов. Это не решает проблему анализа постфактум, которую я описывал в статье.


      conprof — больше похож на profefe. Но про него я узнал только летом (через полтора года после начала работы над profefe). Я успел немного пообщаться с разработчиками на летнем GopherCon. Не знаю как сейчас, но тогда там была очень «сильная любовь» к форку TSDB из prometheus. Были сомнения, сможет ли, на практике, TSDB эффективно индексировать и отдавать pprof-файлы (db заточена под временные ряды, где значение — просто число, метрика, а не блоб в несколько мегабайт; примерно это же обсуждают в https://github.com/conprof/conprof/issues/26).