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

Этот инструмент собирает информацию об использовании ЦП и распределении памяти из ваших производственных приложений. Он связывает эту информацию с исходным кодом приложения, помогая определить части приложения, потребляющие больше всего ресурсов, и иным образом освещая характеристики производительности кода. Низкие накладные расходы на используемые инструментом методы сбора данных делают его подходящим для непрерывного использования в производственных средах.
В этом практическом занятии вы узнаете, как настроить Cloud Profiler для программы на Go, и познакомитесь с тем, какие аналитические данные о производительности приложения может предоставить этот инструмент.
Что вы узнаете
- Как настроить программу на Go для профилирования с помощью Cloud Profiler.
- Как собирать, просматривать и анализировать данные о производительности с помощью Cloud Profiler.
Что вам понадобится
- Проект Google Cloud Platform
- Браузер, например Chrome или Firefox.
- Знание стандартных текстовых редакторов Linux, таких как Vim, EMACs или Nano.
Как вы будете использовать этот учебник?
Как бы вы оценили свой опыт работы с платформой Google Cloud Platform?
2. Настройка и требования
Настройка среды для самостоятельного обучения
- Войдите в Cloud Console и создайте новый проект или используйте существующий. Если у вас еще нет учетной записи Gmail или Google Workspace, вам необходимо ее создать .



Запомните идентификатор проекта (Project ID) — уникальное имя для всех проектов Google Cloud (указанное выше имя уже занято и вам не подойдёт, извините!). В дальнейшем в этом практическом занятии оно будет обозначаться как PROJECT_ID .
- Далее вам потребуется включить оплату в Cloud Console, чтобы использовать ресурсы Google Cloud.
Выполнение этого практического задания не должно стоить дорого, если вообще что-либо. Обязательно следуйте инструкциям в разделе «Очистка», где указано, как отключить ресурсы, чтобы избежать дополнительных расходов после завершения этого урока. Новые пользователи Google Cloud имеют право на бесплатную пробную версию стоимостью 300 долларов США .
Google Cloud Shell
Хотя Google Cloud можно управлять удаленно с ноутбука, для упрощения настройки в этом практическом задании мы будем использовать Google Cloud Shell — среду командной строки, работающую в облаке.
Активировать Cloud Shell
- В консоли Cloud нажмите «Активировать Cloud Shell» .
.

Если вы никогда раньше не запускали Cloud Shell, вам будет показан промежуточный экран (внизу), описывающий его назначение. В этом случае нажмите «Продолжить» (и вы больше никогда его не увидите). Вот как выглядит этот одноразовый экран:

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

Эта виртуальная машина оснащена всеми необходимыми инструментами разработки. Она предоставляет постоянный домашний каталог размером 5 ГБ и работает в облаке Google, что значительно повышает производительность сети и аутентификацию. Большая часть, если не вся, работа в этом практическом задании может быть выполнена с помощью обычного браузера или вашего Chromebook.
После подключения к Cloud Shell вы увидите, что ваша аутентификация пройдена и что проект уже настроен на ваш идентификатор проекта.
- Выполните следующую команду в Cloud Shell, чтобы подтвердить свою аутентификацию:
gcloud auth list
вывод команды
Credentialed Accounts
ACTIVE ACCOUNT
* <my_account>@<my_domain.com>
To set the active account, run:
$ gcloud config set account `ACCOUNT`
- Выполните следующую команду в Cloud Shell, чтобы убедиться, что команда gcloud знает о вашем проекте:
gcloud config list project
вывод команды
[core] project = <PROJECT_ID>
Если это не так, вы можете установить это с помощью следующей команды:
gcloud config set project <PROJECT_ID>
вывод команды
Updated property [core/project].
3. Перейдите в Cloud Profiler.
В консоли Cloud Console перейдите в пользовательский интерфейс Profiler, нажав на кнопку «Profiler» в левой панели навигации:

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

Теперь пришло время составить профиль!
4. Проанализируйте эталонный показатель.
Мы воспользуемся простым синтетическим приложением на Go, доступным на Github . В открытом терминале Cloud Shell (пока в пользовательском интерфейсе профилировщика отображается сообщение «Нет данных для отображения») выполните следующую команду:
$ 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».
5. Анализ данных профилировщика
Теперь, когда мы собрали некоторые данные, давайте рассмотрим их подробнее. Мы используем синтетическое приложение (исходный код доступен на Github ), которое имитирует поведение, типичное для различных типов проблем с производительностью в производственной среде.
Код, интенсивно использующий процессор
Выберите тип профиля ЦП. После загрузки пользовательского интерфейса на графике пламени вы увидите четыре блока-листа функции 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, поскольку они часто представляют собой простаивающие горутины и менее интересны, чем те, которые ожидают операций ввода-вывода.
Тип профилирования потоков также может помочь выявить точки в программе, где потоки длительное время ожидают мьютекса, принадлежащего другой части программы, но для этого более полезен следующий тип профилирования.
Код, вызывающий острую конкуренцию
Тип профиля «Конфликт» определяет наиболее «желаемые» блокировки в программе. Этот тип профиля доступен для программ на Go, но его необходимо явно включить, указав « MutexProfiling: true » в коде конфигурации агента. Сбор данных работает путем записи (в метрике «Конфликты») количества раз, когда конкретная блокировка, разблокируемая горутиной A, вызывала ожидание разблокировки другой горутиной B. Также записывается (в метрике «Задержка») время ожидания блокировки заблокированной горутиной. В этом примере имеется один стек конфликтов, и общее время ожидания блокировки составило 10,5 секунд:

Код, генерирующий этот профиль, состоит из 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)
}
}
6. Резюме
В этой лабораторной работе вы узнали, как настроить программу на Go для использования с Cloud Profiler. Вы также научились собирать, просматривать и анализировать данные о производительности с помощью этого инструмента. Теперь вы можете применить свои новые навыки к реальным сервисам, которые вы запускаете на платформе Google Cloud Platform.
7. Поздравляем!
Вы научились настраивать и использовать Cloud Profiler!
Узнать больше
- Cloud Profiler: https://cloud.google.com/profiler/
- Пакет среды выполнения Go runtime/pprof, используемый Cloud Profiler: https://golang.org/pkg/runtime/pprof/
Лицензия
Данная работа распространяется под лицензией Creative Commons Attribution 2.0 Generic.