Анализируйте производительность производства с помощью Stackdriver Profiler

Хотя разработчики клиентских приложений и веб-интерфейса часто используют такие инструменты, как Android Studio CPU Profiler или инструменты профилирования, встроенные в Chrome, для повышения производительности своего кода, аналогичные методы не так доступны и не так широко используются разработчиками бэкенд-сервисов. Stackdriver Profiler предоставляет разработчикам сервисов те же возможности, независимо от того, где работает их код: на Google Cloud Platform или где-то ещё.

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

В этой лабораторной работе вы узнаете, как настроить Stackdriver Profiler для программы на Go, а также познакомитесь с тем, какую информацию о производительности приложения может предоставить этот инструмент.

Чему вы научитесь

  • Как настроить программу Go для профилирования с помощью Stackdriver Profiler.
  • Как собирать, просматривать и анализировать данные о производительности с помощью Stackdriver Profiler.

Что вам понадобится

  • Проект Google Cloud Platform
  • Браузер, например Chrome или Firefox
  • Знакомство со стандартными текстовыми редакторами Linux, такими как Vim, EMAC или Nano

Как вы будете использовать это руководство?

Прочитайте это только до конца Прочитайте и выполните упражнения.

Как бы вы оценили свой опыт работы с Google Cloud Platform?

Новичок Средний Опытный

Настройка среды для самостоятельного обучения

Если у вас ещё нет учётной записи Google (Gmail или Google Apps), необходимо её создать . Войдите в консоль Google Cloud Platform ( console.cloud.google.com ) и создайте новый проект:

Скриншот от 2016-02-10 12:45:26.png

Запомните идентификатор проекта — уникальное имя для всех проектов Google Cloud (имя, указанное выше, уже занято и не будет вам работать, извините!). Далее в этой практической работе он будет обозначаться как PROJECT_ID .

Далее вам необходимо включить биллинг в Cloud Console, чтобы использовать ресурсы Google Cloud.

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

Новые пользователи Google Cloud Platform имеют право на бесплатную пробную версию стоимостью 300 долларов США .

Google Cloud Shell

Хотя Google Cloud можно управлять удаленно с вашего ноутбука, для упрощения настройки в этой лабораторной работе мы будем использовать Google Cloud Shell — среду командной строки, работающую в облаке.

Активировать Google Cloud Shell

В консоли GCP щелкните значок Cloud Shell на верхней правой панели инструментов:

Затем нажмите «Запустить Cloud Shell»:

Подготовка и подключение к среде займет всего несколько минут:

Эта виртуальная машина оснащена всеми необходимыми инструментами разработки. Она предлагает постоянный домашний каталог объёмом 5 ГБ и работает в облаке Google Cloud, что значительно повышает производительность сети и аутентификацию. Значительную часть работы в этой лаборатории, если не всю, можно выполнить, просто используя браузер или Chromebook от Google.

После подключения к Cloud Shell вы должны увидеть, что вы уже аутентифицированы и что проекту уже присвоен ваш PROJECT_ID .

Выполните следующую команду в Cloud Shell, чтобы подтвердить, что вы прошли аутентификацию:

gcloud auth list

Вывод команды

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

Вывод команды

[core]
project = <PROJECT_ID>

Если это не так, вы можете установить его с помощью этой команды:

gcloud config set project <PROJECT_ID>

Вывод команды

Updated property [core/project].

В Cloud Console перейдите в пользовательский интерфейс Profiler, нажав «Profiler» на левой панели навигации:

Вы также можете использовать строку поиска Cloud Console для перехода к интерфейсу Profiler: просто введите «Stackdriver Profiler» и выберите найденный элемент. В любом случае вы увидите интерфейс Profiler с сообщением «Нет данных для отображения», как показано ниже. Проект новый, поэтому данные профилирования для него пока не собраны.

Теперь пришло время что-то прорекламировать!

Мы будем использовать простое синтетическое приложение Go, доступное на Github . В терминале Cloud Shell, который вы всё ещё держите открытым (и пока в интерфейсе Profiler всё ещё отображается сообщение «Нет данных для отображения»), выполните следующую команду:

$ go get -u github.com/GoogleCloudPlatform/golang-samples/profiler/...

Затем переключитесь в каталог приложения:

$ cd ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/profiler/hotapp

Каталог содержит файл «main.go», представляющий собой синтетическое приложение, в котором включен агент профилирования:

main.go

...
import (
        ...
        "cloud.google.com/go/profiler"
)
...
func main() {
        err := profiler.Start(profiler.Config{
                Service:        "hotapp-service",
                DebugLogging:   true,
                MutexProfiling: true,
        })
        if err != nil {
                log.Fatalf("failed to start the profiler: %v", err)
        }
        ...
}

Агент профилирования по умолчанию собирает профили ЦП, кучи и потоков. Приведённый здесь код позволяет собирать профили мьютексов (также известных как профили «конкурентов»).

Теперь запустите программу:

$ go run main.go

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

$ go run main.go
2018/03/28 15:10:24 profiler has started
2018/03/28 15:10:57 successfully created profile THREADS
2018/03/28 15:10:57 start uploading profile
2018/03/28 15:11:19 successfully created profile CONTENTION
2018/03/28 15:11:30 start uploading profile
2018/03/28 15:11:40 successfully created profile CPU
2018/03/28 15:11:51 start uploading profile
2018/03/28 15:11:53 successfully created profile CONTENTION
2018/03/28 15:12:03 start uploading profile
2018/03/28 15:12:04 successfully created profile HEAP
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:04 successfully created profile THREADS
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:25 successfully created profile HEAP
2018/03/28 15:12:25 start uploading profile
2018/03/28 15:12:37 successfully created profile CPU
...

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

После обновления пользовательского интерфейса вы увидите что-то вроде этого:

Селектор типа профиля показывает пять доступных типов профилей:

Давайте рассмотрим каждый тип профиля и некоторые важные возможности пользовательского интерфейса, а затем проведём несколько экспериментов. На этом этапе вам больше не нужен терминал Cloud Shell, поэтому вы можете выйти из него, нажав CTRL+C и введя «exit».

Теперь, когда мы собрали данные, давайте рассмотрим их более подробно. Мы используем синтетическое приложение (исходник доступен на Github ), которое имитирует поведение, типичное для различных проблем производительности в продакшене.

Код, интенсивно использующий процессор

Выберите тип профиля ЦП. После того, как пользовательский интерфейс загрузит его, на графике Flame вы увидите четыре блока-листа для функции load , которые в совокупности учитывают всё потребление ЦП:

Эта функция специально написана для потребления большого количества циклов ЦП путем запуска короткого цикла:

main.go

func load() {
        for i := 0; i < (1 << 20); i++ {
        }
}

Функция вызывается косвенно из busyloop () через четыре пути вызова: busyloop → { foo1 , foo2 } → { bar , baz } → load . Ширина прямоугольника функции представляет относительную стоимость конкретного пути вызова. В данном случае все четыре пути имеют примерно одинаковую стоимость. В реальной программе необходимо сосредоточиться на оптимизации путей вызова, наиболее важных с точки зрения производительности. Диаграмма пламени, визуально выделяющая более затратные пути более крупными прямоугольниками, упрощает их идентификацию.

Вы можете использовать фильтр данных профиля для дальнейшего уточнения отображения. Например, попробуйте добавить фильтр «Показать стеки», указав «baz» в качестве строки фильтра. Вы должны увидеть что-то похожее на скриншот ниже, где отображаются только два из четырёх путей вызова функции load() . Эти два пути — единственные, которые проходят через функцию со строкой «baz» в имени. Такая фильтрация полезна, когда вы хотите сосредоточиться на части более крупной программы (например, если вы владеете только её частью).

Код, интенсивно использующий память

Теперь переключитесь на тип профиля «Куча». Убедитесь, что удалены все фильтры, созданные в предыдущих экспериментах. Теперь вы должны увидеть график, на котором allocImpl , вызываемый функцией alloc , отображается как основной потребитель памяти в приложении:

Сводная таблица над графиком пламени показывает, что общий объём используемой памяти в приложении в среднем составляет около 57,4 МБ, большая часть которой выделяется функцией allocImpl . Это неудивительно, учитывая реализацию этой функции:

main.go

func allocImpl() {
        // Allocate 64 MiB in 64 KiB chunks
        for i := 0; i < 64*16; i++ {
                mem = append(mem, make([]byte, 64*1024))
        }
}

Функция выполняется один раз, выделяя 64 МиБ памяти небольшими порциями, а затем сохраняя указатели на эти порции в глобальной переменной, чтобы защитить их от удаления сборщиком мусора. Обратите внимание, что объём памяти, отображаемый профилировщиком, немного отличается от 64 МиБ: профилировщик кучи Go — это статистический инструмент, поэтому измерения не требуют больших накладных расходов, но не имеют точности до байта. Не удивляйтесь, если увидите такую разницу примерно в 10%.

Код с интенсивным вводом-выводом

Если в селекторе типа профиля выбрать «Потоки», отображение переключится на график пламени, большую часть ширины которого занимают функции wait и waitImpl :

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

main.go

func main() {
        ...
        // Simulate some waiting goroutines.
        for i := 0; i < 100; i++ {
                go wait()
        }

Этот тип профиля полезен для понимания того, тратит ли программа непредвиденное время на ожидание (например, ввода-вывода). Такие стеки вызовов обычно не анализируются профилировщиком ЦП, поскольку они не потребляют значительной доли процессорного времени. Фильтры «Скрыть стеки» часто используются с профилями потоков, например, чтобы скрыть все стеки, завершающиеся вызовом gopark, поскольку они часто представляют собой бездействующие горутины и менее интересны, чем те, которые ожидают ввода-вывода.

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

Код с высокой конкуренцией

Тип профиля «Contention» определяет наиболее «желательные» блокировки в программе. Этот тип профиля доступен для программ на Go, но должен быть явно включён путём указания параметра « MutexProfiling: true » в коде конфигурации агента. Сбор данных основан на регистрации (в метрике «Contentions») количества раз, когда определённая блокировка, разблокированная горутиной A, ожидала разблокировки другой горутиной B. Также регистрируется (в метрике «Delay») время, в течение которого заблокированная горутина ожидала разблокировки. В этом примере имеется один стек конкуренции, а общее время ожидания блокировки составило 11,03 секунды:

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

main.go

func contention(d time.Duration) {
        contentionImpl(d)
}

func contentionImpl(d time.Duration) {
        for {
                mu.Lock()
                time.Sleep(d)
                mu.Unlock()
        }
}
...
func main() {
        ...
        for i := 0; i < 4; i++ {
                go contention(time.Duration(i) * 50 * time.Millisecond)
        }
}

В этой лабораторной работе вы узнали, как настроить программу на Go для использования со Stackdriver Profiler. Вы также узнали, как собирать, просматривать и анализировать данные о производительности с помощью этого инструмента. Теперь вы можете применить свои новые навыки к реальным сервисам, работающим на Google Cloud Platform.

Вы узнали, как настраивать и использовать Stackdriver Profiler!

Узнать больше

Лицензия

Данная работа распространяется по лицензии Creative Commons Attribution 2.0 Generic License.