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

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

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

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

Что вы узнаете

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

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

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

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

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

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

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

Самостоятельная настройка среды

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

Скриншот от 10 февраля 2016 г., 12:45:26.png

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

Затем вам нужно включить выставление счетов в облачной консоли, чтобы использовать ресурсы Google Cloud.

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

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

Облачная оболочка Google

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

Активировать облачную оболочку Google

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

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

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

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

После подключения к 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
...

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

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

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

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

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

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

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

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

main.go

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

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

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

Код с интенсивным использованием памяти

Теперь переключитесь на тип профиля «Куча». Обязательно удалите все фильтры, созданные в предыдущих экспериментах. Теперь вы должны увидеть график пламени, где 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%.

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

Если вы выберете «Threads» в селекторе типа профиля, дисплей переключится на график пламени, где большую часть ширины занимают функции 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, ожидающую разблокировки блокировки. Он также записывает (в метрике «Задержка») время, в течение которого заблокированная горутина ожидала блокировки. В этом примере имеется один конкурирующий стек, а общее время ожидания блокировки составило 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.