Si bien los desarrolladores de apps para clientes y de sitios web de frontend suelen usar 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 técnicas equivalentes no han sido tan accesibles ni adoptadas por quienes trabajan en servicios de backend. Stackdriver Profiler ofrece estas mismas capacidades a los desarrolladores de servicios, independientemente de si su código se ejecuta en Google Cloud Platform o en otro lugar.

La herramienta recopila información de las aplicaciones de producción sobre la asignación de memoria y el uso de la CPU. Este 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 el uso continuo en entornos de producción.
En este codelab, aprenderás a configurar Stackdriver Profiler para un programa Go y te familiarizarás con los tipos de estadísticas sobre el rendimiento de la aplicación que puede presentar la herramienta.
Qué aprenderás
- Cómo configurar un programa en 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
- 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?
¿Cómo calificarías tu experiencia con Google Cloud Platform?
Configuración del entorno de autoaprendizaje
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:
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, deberás habilitar la facturación en la consola de Cloud 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 manera remota desde tu 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.
Activa 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 que te conectes a Cloud Shell, deberías ver que ya te autenticaste y que el proyecto ya se configuró con tu PROJECT_ID.
En Cloud Shell, ejecuta el siguiente comando para confirmar que tienes la autenticación:
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, puedes configurarlo con el siguiente comando:
gcloud config set project <PROJECT_ID>
Resultado del comando
Updated property [core/project].
En la consola de Cloud, haz clic en "Profiler" en la barra de navegación de la izquierda para acceder a la IU de Profiler:

Como alternativa, puedes usar la barra de búsqueda de Cloud Console para navegar a la IU de Profiler. Solo escribe "Stackdriver Profiler" y selecciona el elemento encontrado. De cualquier manera, deberías ver la IU de Profiler con el mensaje "No hay datos para mostrar", como se muestra a continuación. El proyecto es nuevo, por lo que aún no tiene datos de generación de perfiles recopilados.

Ahora es el momento de obtener un perfil de algo.
Usaremos una aplicación sintética simple en Go disponible en GitHub. En la terminal de Cloud Shell que aún tienes abierta (y mientras el mensaje "No hay datos para mostrar" sigue apareciendo en la IU del Profiler), ejecuta el siguiente comando:
$ go get -u github.com/GoogleCloudPlatform/golang-samples/profiler/...
Luego, cambia 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)
}
...
}De forma predeterminada, el agente de creación de perfiles recopila perfiles de CPU, montón y subprocesos. El código aquí permite la recopilación de perfiles de mutex (también conocidos como "contención").
Ahora, ejecuta el programa:
$ go run main.go
A medida que se ejecuta el programa, el agente de creación de perfiles recopilará periódicamente perfiles de los cinco tipos configurados. La recopilación se aleatoriza con el tiempo (con una tasa promedio de un perfil por minuto para cada uno de los tipos), por lo que puede tardar hasta tres minutos en recopilar cada uno de los tipos. El programa te indica cuándo crea un perfil. Los mensajes se habilitan con 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. Después de eso, no se actualizará automáticamente, por lo que, para ver los datos nuevos, deberás actualizar la IU del generador de perfiles de forma manual. Para ello, haz clic dos veces en el botón Ahora del selector de intervalos:

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

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

Ahora, revisemos cada uno de los tipos de perfil y algunas capacidades importantes de la IU, y luego realicemos algunos experimentos. En esta etapa, ya no necesitas la terminal de Cloud Shell, por lo que puedes salir de ella presionando Ctrl + C y escribiendo "exit".
Ahora que recopilamos algunos datos, analicémoslos con más detalle. Usamos una app sintética (la fuente está disponible en GitHub) que simula comportamientos típicos de diferentes tipos de problemas de rendimiento en producción.
Código con uso intensivo de la CPU
Selecciona el tipo de perfil de CPU. Después de que se cargue la IU, verás en el gráfico de llamas los cuatro bloques hoja de la función load, que representan de forma colectiva todo el consumo de CPU:

Esta función se escribió específicamente para consumir muchos ciclos de CPU ejecutando un bucle ajustado:
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 llamada: busyloop → {foo1, foo2} → {bar, baz} → load. El ancho de un cuadro de función representa el costo relativo de la ruta de acceso de llamada específica. En este caso, las cuatro rutas tienen aproximadamente el mismo costo. En un programa real, debes enfocarte en optimizar las rutas de llamadas que son más importantes en términos de rendimiento. El gráfico de llamas, que enfatiza visualmente las rutas más costosas con cuadros más grandes, facilita la identificación de estas rutas.
Puedes usar el filtro de datos del perfil para definir mejor la pantalla. Por ejemplo, intenta agregar un filtro "Mostrar pilas" que especifique "baz" como la cadena de filtro. Deberías ver algo como la siguiente captura de pantalla, en la que solo se muestran dos de las cuatro rutas de llamadas a load(). Estas dos rutas son las únicas que pasan por una función con la cadena "baz" en su nombre. Este filtrado es útil cuando deseas enfocarte en una subparte de un programa más grande (por ejemplo, porque solo posees una parte de él).

Código con uso intensivo de memoria
Ahora cambia al tipo de perfil "Heap". Asegúrate de quitar los filtros que creaste en experimentos anteriores. Ahora deberías ver un gráfico de llamas en el que allocImpl, llamado por alloc, se muestra como el principal consumidor de memoria en la app:

La tabla de resumen que se encuentra sobre el gráfico de llamas indica que la cantidad total de memoria usada en la app es de ~57.4 MiB en promedio, y la mayor parte la asigna la función allocImpl. Esto no es sorprendente, dada 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, asignando 64 MiB en fragmentos más pequeños y, luego, almacenando punteros a esos fragmentos en una variable global para protegerlos de la recolección de basura. Ten en cuenta que la cantidad de memoria que muestra el generador de perfiles como utilizada es ligeramente diferente de 64 MiB: el generador de perfiles de montón de Go es una herramienta estadística, por lo que las mediciones tienen una sobrecarga baja, pero no son precisas en bytes. No te sorprendas si ves una diferencia de alrededor del 10% como esta.
Código con uso intensivo de E/S
Si eliges "Hilos" 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á ocupada por las funciones wait y waitImpl:

En el resumen sobre el gráfico de llamas, puedes ver que hay 100 goroutines que aumentan su pila de llamadas desde la función wait. Esto es exactamente correcto, dado que el código que inicia estas esperas se ve así:
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 dedica tiempo inesperado a esperas (como E/S). El analizador de CPU no suele tomar muestras de estas pilas de llamadas, ya que no consumen una parte significativa del tiempo de CPU. A menudo, querrás usar filtros de "Ocultar pilas" con los perfiles de subprocesos, por ejemplo, para ocultar todas las pilas que terminan con una llamada a gopark,, ya que suelen ser goroutines inactivas y menos interesantes que las que esperan E/S.
El tipo de perfil de subprocesos también puede ayudar a identificar puntos en el programa en los que los subprocesos esperan un mutex que posee otra parte del programa durante un período prolongado, pero el siguiente tipo de perfil es más útil para eso.
Código con uso intensivo de la contención
El tipo de perfil Contention identifica los bloqueos más "deseados" en el programa. Este tipo de perfil está disponible para los programas en Go, pero se debe habilitar de forma explícita especificando "MutexProfiling: true" en el código de configuración del agente. La recopilación funciona registrando (con la métrica "Contention") la cantidad de veces que un bloqueo específico, cuando se desbloquea con una goroutine A, tuvo otra goroutine B esperando que se desbloqueara. También registra (en la métrica "Delay") el tiempo que la goroutine bloqueada esperó el bloqueo. En este ejemplo, hay una sola pila de contención y el tiempo total de espera del bloqueo fue de 11.03 segundos:

El código que genera este perfil consta de 4 rutinas que compiten por un mutex:
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, aprendiste cómo se puede configurar un programa de Go para usarlo con Stackdriver Profiler. También aprendiste a recopilar, ver y analizar los datos de rendimiento con esta herramienta. Ahora puedes aplicar tu nueva habilidad a los servicios reales que ejecutas en Google Cloud Platform.
Aprendiste a configurar y usar Stackdriver Profiler.
Más información
- Stackdriver Profiler: https://cloud.google.com/profiler/
- Paquete runtime/pprof de Go que usa Stackdriver Profiler: https://golang.org/pkg/runtime/pprof/
Licencia
Este trabajo cuenta con una licencia Atribución 2.0 Genérica de Creative Commons.