Analizar el rendimiento de la producción con Stackdriver Profiler

Si bien los desarrolladores web de frontend y apps cliente por lo general usan herramientas como el Generador de perfiles de CPU de Android Studio o las herramientas de generación de perfiles incluidas en Chrome para mejorar el rendimiento de su código, las personas que trabajan en servicios de backend ya casi han adoptado las mismas técnicas o tienen un buen nivel de acceso. Stackdriver Profiler ofrece estas mismas funciones a los desarrolladores de servicios, sin importar si su código se ejecuta en Google Cloud Platform o en otro lugar.

La herramienta recopila información sobre el uso de CPU y la asignación de memoria de tus aplicaciones de producción. Stackdriver Profiler le asigna esa información al código fuente de la aplicación, lo que ayuda a identificar las partes de la aplicación que consumen más recursos y a entender mejor las características de rendimiento del código. La baja sobrecarga de las técnicas de recopilación que emplea la herramienta la hace adecuada para su uso continuo en entornos de producción.

En este codelab, aprenderás a configurar Stackdriver Profiler para un programa de Go y conocerás qué tipo de estadísticas sobre el rendimiento de aplicaciones puede presentar la herramienta.

Qué aprenderás

  • Cómo configurar un programa de Go para la generación de perfiles con Stackdriver Profiler
  • Cómo recopilar, ver y analizar los datos de rendimiento con Stackdriver Profiler

Requisitos

  • Un proyecto de Google Cloud Platform
  • Un navegador, como Chrome o Firefox
  • Se recomienda estar familiarizado con editores de texto estándares de Linux, como Vim, Emacs o Nano.

¿Cómo usarás este instructivo?

Ler Leer y completar los ejercicios

¿Cómo calificarías tu experiencia con Google Cloud Platform?

Principiante Intermedio Avanzado

Configuración del entorno a su propio ritmo

Si aún no tienes una Cuenta de Google (Gmail o Google Apps), debes crear una. Accede a Google Cloud Platform Console (console.cloud.google.com) y crea un proyecto nuevo:

Captura de pantalla de 2016-02-10 12:45:26.png

Recuerde el ID de proyecto, un nombre único en todos los proyectos de Google Cloud (el nombre anterior ya se encuentra en uso y no lo podrá usar). Se mencionará más adelante en este codelab como PROJECT_ID.

A continuación, debes habilitar la facturación en Cloud Console para usar los recursos de Google Cloud.

Ejecutar este codelab debería costar solo unos pocos dólares, pero su costo podría aumentar si decides usar más recursos o si los dejas en ejecución (consulta la sección “Limpiar” al final de este documento).

Los usuarios nuevos de Google Cloud Platform son aptos para obtener una prueba gratuita de USD 300.

Google Cloud Shell

Si bien Google Cloud se puede operar de forma remota desde su laptop, para simplificar la configuración en este codelab usaremos Google Cloud Shell, un entorno de línea de comandos que se ejecuta en la nube.

Activar Google Cloud Shell

En GCP Console, haga clic en el ícono de Cloud Shell en la barra de herramientas superior derecha:

Haga clic en "Start Cloud Shell":

El aprovisionamiento y la conexión al entorno debería llevar solo unos minutos:

Esta máquina virtual está cargada con todas las herramientas para desarrolladores que necesitará. Ofrece un directorio principal persistente de 5 GB y se ejecuta en Google Cloud, lo que permite mejorar considerablemente el rendimiento de la red y la autenticación. Gran parte de su trabajo, si no todo, se puede hacer simplemente con un navegador o su Google Chromebook.

Una vez conectado a Cloud Shell, debería ver que ya está autenticado y que el proyecto ya está configurado con su PROJECT_ID.

En Cloud Shell, ejecute el siguiente comando para confirmar que está autenticado:

gcloud auth list

Resultado del comando

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

Resultado del comando

[core]
project = <PROJECT_ID>

De lo contrario, puede configurarlo con este comando:

gcloud config set project <PROJECT_ID>

Resultado del comando

Updated property [core/project].

En Cloud Console, navegue hasta la IU de Profiler. Para ello, haga clic en “Profiler” en la barra de navegación izquierda:

Como alternativa, puede usar la barra de búsqueda de Cloud Console para navegar a la IU de Profiler. Para ello, escriba & Stackdriver Profiler y seleccione el elemento que encuentre. En cualquier caso, deberías ver la IU de Profiler con el mensaje "No data to display" (No hay datos para mostrar) como el que se muestra a continuación. El proyecto es nuevo, por lo que aún no se recopilaron datos de la generación de perfiles.

Es hora de crear un perfil.

Usaremos una aplicación sintética simple de Go disponible en GitHub. En la terminal de Cloud Shell que aún tiene abierta (y mientras aún se muestra el mensaje “No hay datos para mostrar” en la IU de Profiler), ejecute el siguiente comando:

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

Luego, cambie al directorio de la aplicación:

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

El directorio contiene el archivo "main.go" que es una app sintética que tiene habilitado el agente de generación de perfiles:

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)
        }
        ...
}

El agente de generación de perfiles recopila perfiles de CPU, montón y subprocesos de forma predeterminada. El código que se muestra aquí permite que se recopilen los perfiles de exclusiones mutuas (también conocidos como "contention&quot).

Ahora, ejecuta el programa:

$ go run main.go

A medida que se ejecuta el programa, el agente de generación de perfiles recopilará perfiles de los cinco tipos configurados de forma periódica. La colección se aleatoriza con el tiempo (con una tasa promedio de un perfil por minuto para cada uno de los tipos), por lo que la recopilación de cada tipo puede tardar hasta tres minutos. El programa te avisa cuando crea un perfil. Los mensajes están habilitados por la marca DebugLogging en la configuración anterior; de lo contrario, el agente se ejecuta de forma silenciosa:

$ 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
...

La IU se actualizará poco después de que se recopile el primer perfil. No se actualizará automáticamente después de ese período, por lo que, para ver los datos nuevos, deberás actualizar la IU del generador de perfiles de forma manual. Para ello, haz clic en el botón Ahora (Now) del selector del intervalo de tiempo dos veces:

Después de que se actualice la IU, verá algo similar a lo siguiente:

El selector de tipo de perfil muestra los cinco tipos disponibles:

Ahora, revisaremos cada uno de los tipos de perfiles y algunas funciones importantes de la IU y, luego, realizaremos algunos experimentos. En esta etapa, ya no necesitará la terminal de Cloud Shell, por lo que puede salir de ella presionando CTRL-C y escribiendo "exit".

Ahora que recopilamos algunos datos, veamos con más detalle los datos. Utilizamos una aplicación sintética (la fuente está disponible en GitHub) que simula los comportamientos típicos de los diferentes tipos de problemas de rendimiento en producción.

Código de uso intensivo de CPU

Selecciona el tipo de perfil de CPU. Después de que la IU la cargue, verás en el gráfico tipo llama los cuatro bloques de hoja de la función load, que juntos representan el consumo de la CPU:

Esta función está escrita específicamente para consumir muchos ciclos de CPU ejecutando un bucle cerrado:

main.go

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

La función se llama de forma indirecta desde busyloop() a través de cuatro rutas de acceso de la llamada: busyloop → {foo1, foo2} → {bar, baz} → load. El ancho de un cuadro de función representa el costo relativo de la ruta de llamada específica. En este caso, las cuatro rutas tienen aproximadamente el mismo costo. En un programa real, la intención es centrarse en la optimización de las rutas de llamada más importantes en términos de rendimiento. El gráfico tipo llama, que destaca visualmente las rutas más costosas con cuadros más grandes, facilita su identificación.

Puedes usar el filtro de datos de perfil para definir mejor la pantalla. Por ejemplo, intenta agregar un filtro "Show stacks" especificando la string de filtro "baz&quot. Deberías ver una captura de pantalla como la siguiente, en la que solo se muestran dos de las cuatro rutas de acceso de llamada a load(). Estas dos rutas de acceso son las únicas que atraviesan una función que tiene la string "baz" en su nombre. Este tipo de filtrado resulta útil cuando desea enfocarse en una parte de un programa más grande (por ejemplo, porque solo es el propietario de parte de él).

Código que requiere mucha memoria

Ahora cambia al tipo de perfil &mont. Asegúrese de quitar todos los filtros que creó en los experimentos anteriores. Ahora deberías ver un gráfico tipo llama en el que allocImpl, llamado por alloc, se muestra como el consumidor principal de memoria en la app:

La tabla de resumen sobre el gráfico tipo llama indica que la cantidad total de memoria utilizada en la app es de aproximadamente 57.4 MiB, la mayor parte se asigna por función allocImpl. Esto no es sorprendente debido a la implementación de esta función:

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))
        }
}

La función se ejecuta una vez y asigna 64 MiB en fragmentos más pequeños y, luego, almacena punteros en esas partes en una variable global para evitar que se recolecten los elementos no utilizados. Ten en cuenta que la cantidad de memoria que se muestra en el generador de perfiles es ligeramente diferente de 64 MiB: el generador de perfiles del montón de Go es una herramienta estadística, de modo que las mediciones tienen una sobrecarga baja, pero no son precisas. No te sorprendas si ves una diferencia de un 10% similar.

Código intensivo de E/S

Si eliges “Threads” en el selector de tipo de perfil, la pantalla cambiará a un gráfico tipo llama en el que la mayor parte del ancho esté asignado a las funciones wait y waitImpl:

En el resumen anterior del gráfico tipo llama, puedes ver que hay 100 goroutines que hacen crecer su pila de llamadas desde la función wait. Esto es exactamente así, dado que el código que inicia estas esperas tiene el siguiente aspecto:

main.go

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

Este tipo de perfil es útil para comprender si el programa pasa algún tiempo inesperado en espera (como E/S). Por lo general, el generador de perfiles de CPU no realiza el muestreo de esas pilas de llamadas, ya que no consumen una parte significativa del tiempo de la CPU. A menudo, querrás usar filtros "Ocultar pilas" con perfiles de subprocesos, por ejemplo, para ocultar todas las pilas que terminan en una llamada a gopark,, ya que, a menudo, son rutinas de inactividad inactivas y menos interesantes que las que esperan en E/S.

El tipo de perfil de subprocesos también puede ayudar a identificar puntos en el programa en los que los subprocesos esperan una exclusión mutua de otra parte del programa durante un período prolongado, pero el siguiente tipo de perfil es más útil para eso.

Código de contención

El tipo de perfil de contención identifica los bloqueos más "quizás" en el programa. Este tipo de perfil está disponible para los programas de Go, pero debe habilitarse de manera explícita. Para ello, especifica MutexProfiling: true en el código de configuración del agente. La colección funciona grabando (debajo de la métrica "Contenciones") la cantidad de veces que un bloqueo específico, al desbloquearse con una goroutine A, tenía otra goroutine B que debía desbloquearse. También registra (en la métrica &Demora) el tiempo que la goroutine bloqueada bloqueó el bloqueo. En este ejemplo, hay una sola pila de contención y el tiempo de espera total del bloqueo fue de 11.03 segundos:

El código que genera este perfil consta de 4 rutinas que compiten por una exclusión mutua:

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)
        }
}

En este lab, aprendió cómo se puede configurar un programa de Go para usarlo con Stackdriver Profiler. También aprendió a recopilar, ver y analizar los datos de rendimiento con esta herramienta. Ahora puede aplicar su nueva habilidad a los servicios reales que ejecuta en Google Cloud Platform.

Aprendiste a configurar y usar Stackdriver Profiler.

Más información

Licencia

Este trabajo cuenta con una licencia Atribución 2.0 Genérica de Creative Commons.