Instrumentación de Servicios con Prometheus
Este artículo es un resumen de la charla presentada en el primer meetup de Golang en Santiago.
La instrumentación es el acto de agregar instrumentos o elementos que nos permiten observar y examinar el estado de una máquina. Si entendemos el concepto de máquina como algo universal, nos podríamos estar refiriendo a tuberías, autos, cocinas o bien, sistemas de software.
Hace un par de meses comenzamos a instrumentar nuestros servicios de producción (aplicaciones web, etc.) y de operación interna (routers, DNS, etc.) para observar su estado interno y poder reaccionar rápidamente apenas un sistema no reporte los indicadores que esperamos ver.
Independiente de la escala del sistema o producto, es importantísimo conocer el estado interno, ya sea de un sistema desarrollado por nosotros o por terceros. Es fundamental, para poder adquirir intuición de cómo nuestros sistemas están respondiendo hacia nuestros usuarios finales.
Para algunos esto podría ser algo nuevo, porque representa un cambio de paradigma de cómo solíamos hacer monitoreo: muy enfocado en el servidor. En Prey utilizamos Nagios para monitorear los servidores (Nagios es bueno), pero no hablaremos de eso en este artículo. Esto se trata de observar el estado interno de un sistema en función de la experiencia de usuario.
Decidimos usar Prometheus, que es un sistema de monitoreo completo, desarrollado por los amigos de Soundcloud. Nos gustó, por sobre otras opciones, porque no necesita dependencias y por su similitud con borgmon, el sistema de monitoreo de Google, que está ampliamente documentado en el libro recientemente publicado por el equipo de Ingeniería de Google, Site Reliability Engineering, muy recomendado por cierto.
Prometheus nos permite agregar instrumentos a nuestros servicios. Las métricas que reporten estos instrumentos, a través de un endpoint HTTP, son coleccionadas por el servidor de Prometheus de forma periódica, generalmente cada 10 segundos.
El endpoint HTTP /metrics muestra el estado interno del sistema a partir de todos los instrumentos que hayan sido configurados en la base de código.
Por ejemplo, si hubiéramos instrumentado un servidor HTTP, podríamos ver las métricas así:
$ curl server:8000/metrics
http_request_total{server=1, code=200, method=GET} 20
http_request_total{server=1, code=502, method=GET} 1
Prometheus a su vez cuenta con un poderoso lenguaje de cálculo que ofrece una serie de funciones que nos permiten explorar la serie de datos una vez almacenados, por ejemplo:
- http_request_total{server=1} para obtener todas las solicitudes del servidor 1.
- http_request_total{code=~”2.+”} para obtener todas solicitudes 2xx.
- sum(http_request_total) by (code) para obtener la suma de solicitudes agrupada por códigos.
Pero la gracia está en poder examinar métricas en función de la experiencia del usuario, por ejemplo, si un servidor reportara:
http_request_total{server=1, code=202} 20
http_request_total{server=1, code=502} 20000
No nos daríamos cuenta de que el servidor 1 está generando miles de 502, sino, que nuestros usuarios estarían recibiendo 99.9% de timeouts. ¡Una catástrofe!
Qué y cómo instrumentar
En el libro Site Reliability Engineering, Rob Ewaschuck nos entrega lo que él llama los cuatro indicadores de oro, en el fondo, cuatro cosas que sí o sí deberíamos estar observando.
Qué
Latencia: El tiempo que le toma a una solicitud ser servida. Pero desde el punto de vista del usuario, en términos de experiencia y frustración. En la medida que las solicitudes sean servidas más rápido el índice de frustración del usuario debería ser menor. Esto no solo debería aplicar para solicitudes correctas, sino que también para aquellas que generen errores, porque es mucho mejor que un error 500, por ejemplo, se devuelva inmediatamente que hacer esperar al usuario más de 10 segundos para recién entregarle un error.
Tráfico: Una medida de cuánta demanda tiene un servicio. Para los servicios basados en Web, esto corresponde generalmente a las SPS (Solicitudes Por Segundo). Para otros servicios, como bases de datos, podría ser CPS (Consultas Por Segundo).
Errores: Es la cantidad de solicitudes que fallan, pero también, desde el punto de vista del usuario. ¿Por qué? Porque un error podría ser natural, como un error 500, por ejemplo, pero también deberíamos observar aquellos errores no naturales, como examinar la cantidad de veces que una respuesta 200 no entrego un resultado correcto. ¿Se imaginan si Google Images devolviese perros en vez de gatos, si la consulta fuese gatos?.
Saturación: Cuán capaz es un sistema en función de su uso, por ejemplo, cuántas conexiones concurrentes puede soportar. Cuánta memoria RAM necesita para satisfacer su demanda, etc. En el caso de un sistema de gestión de archivos, la unidad de saturación podría estar dada por la cantidad de descriptores de archivos utilizados.
Cómo
Prometheus nos ofrece cuatro alternativas de instrumentación, esto es, funciones que podremos usar en nuestra base de código para posteriormente obtener métricas.
Contadores: Los contadores responden a una función monotónica, o dicho de otra forma, es un número que siempre sube.
Gauge: No encontré la traducción para un Gauge, pero la regla de oro es, si es que el número puede bajar, es un gauge.
Histograma y Sumarios: Tanto los histogramas como los sumarios nos permiten observar muestras de datos agrupadas, por ejemplo, para obtener cuánto tiempo se demora un servicio en servir el 99% de las solicitudes y ese tipo de cosas. La principal diferencia entre ambos, es que los sumarios definen grupos estándar a priori, 50%, 80%, 90%, 99% 99.9% por ejemplo (menos flexible pero más rápido), mientras que los histogramas se calculan “en vivo” en el servidor de Prometheus (más flexible pero más lento).
Alertas
Prometheus además nos permite configurar alertas en función de la serie de datos, pero hablaremos de esto en otro artículo.
Demostración
Supongamos que nuestro “Departamento de Requerimientos” nos pide un servicio web que muestre una imagen de un gato de forma aleatoria, y que una vez puesto en producción podamos observar el estado interno del sistema.
Este servicio se podría implementar de la siguiente forma en Go, en el archivo gatos.go:
package mainimport (
"fmt"
"math/rand"
"net/http"
)var gatos = map[string]string{
"negro": "http://fotosdegatos.com/negro.jpg",
"albino": "http:/fotosdegatos.com/algino.jpg",
"grumpy": "http://fotosdegatos.com/grumpy.jpg",
}var nombres = []string{"negro", "albino", "grumpy"}func handleGatos(w http.ResponseWriter, r *http.Request) {
ganador := int(rand.Float64() * 3)
gato := nombres[ganador]
gatito := gatos[gato]
fmt.Fprintf(w, gatosHTML, gato, gatito)
}func main() {
http.HandleFunc("/", handleGatos)
http.ListenAndServe(":8080", nil)
}const gatosHTML = `<!DOCTYPE html>
<html>
<head></head>
<body>
<div>
<h1>%s</h1>
<img src="%s">
</div>
</body>
</html>`
Voila. Si corremos este servicio con go run gatos.go y abrimos un navegador en http://localhost:8080 nos deberíamos encontrar con un gato.
Instrumentando el servicio
Digamos que estamos interesados en observar:
- ¿Cuántas fotos se han mostrado?
- ¿Qué fotos de gatos se han mostrado?
- ¿Se muestran de forma aleatoria?
Creemos un nuevo archivo, llámese instrumentos.go y utilicemos la librería Go de prometheus para crear los instrumentos:
package mainimport (
"net/http" "github.com/prometheus/client_golang/prometheus"
)var (
totalImpresionesDeGatitos = prometheus.NewCounter(prometheus.CounterOpts{
Name: "total_gatitos_impresiones",
Help: "Total de impresiones de gatitos",
})
totalImpresionesDeGatitosVec = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "total_gatitos_impresiones_vec",
Help: "Total de impresiones de gatitos vector",
}, []string{"gato"})
)func init() {
prometheus.MustRegister(totalImpresionesDeGatitos)
prometheus.MustRegister(totalImpresioensDeGatitosVec)
http.Handle("/metrics", prometheus.Handler())
}
Una vez que hemos creado nuestros instrumentos, podemos agregarlos al archivo gatos.go
func handleGatos(w http.ResponseWriter, r *http.Request) {
totalImpresionesDeGatitos.Add(1)
ganador := int(rand.Float64() * 3)
gato := nombres[ganador]
gatito := gatos[gato]
totalImpresionesDeGatitosVec.WithLabelValues(gato).Add(1)
fmt.Fprintf(w, gatosHTML, gato, gatito)
}
Si corremos el servicio con el archivo de instrumentos, go run gatos.go instrumentos.go, y probamos unas cuentas impresiones e ingresamos a http://localhost:8080/metrics, deberíamos encontrarnos con:
# HELP total_gatitos_impresiones Total de impresiones de gatitos
# TYPE total_gatitos_impresiones counter
total_gatitos_impresiones 9
# HELP total_gatitos_impresiones_vec Total de impresiones de gatitos vector
# TYPE total_gatitos_impresiones_vec counter
total_gatitos_impresiones_vec{gato="albino"} 4
total_gatitos_impresiones_vec{gato="grumpy"} 2
total_gatitos_impresiones_vec{gato="negro"} 3
Instalando Prometheus
Instalar el servidor de Prometheus es muy fácil, basta con descargar el binario pre-compilado y ejecutarlo. Para que el servidor coleccione las métricas de nuestro servicio, tenemos que agregar el “job” al archivo de configuración prometheus.yml.
scrape_configs:
- job_name: "servicio-gatitos"
scrape_interval: 10s
target_groups:
- targets: ["localhost:8080"]
Una vez que hemos realizado la instalación y configuración, podremos ingresar a la interfaz de Prometheus en http://localhost:9090 y comenzar a responder las preguntas.
Podemos también ver un gráfico con la línea de tiempo de las observaciones.
Al parecer, a lo largo de este artículo, nuestro servicio mostró 9 fotos de gatitos, 4 del gato albino, 2 del grumpy y 3 del gato negro. Si seguimos la Ley de los Grande Números, si se estarían mostrando de forma aleatoria hasta ahora.
Y ahí lo tienen, hemos instrumentado un simple servicio, utilizando pocas líneas de código y una semántica apropiada a la lógica que introducimos al comienzo del artículo.
Gracias a Jairo Luiz y Thiago Arroadie por la invitación al meetup. A Miguel Michelson, Javier Acuña, Patricio Jofré y Javier Cala por sus comentarios en el borrador de la presentación. ¡Y gracias a todos los asistentes del meetup por su feedback in-situ!