Alertas con Node Exporter: ajuste de umbrales y canales de notificación

Métricas esenciales para construir reglas de alerta

Node Exporter expone una gran cantidad de indicadores del sistema operaitvo. Antes de crear reglas de alerta, conviene identificar las métricas que reflejan de forma fiable el estado de salud de un servidor.

El siguiente conjunto de expresiones de PromQL sirve como punto de partida para detectar problemas de recursos:

# Porcentaje de CPU no ociosa
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[1m]))) * 100

# Proporción de memoria consumida
(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes * 100

# Ocupación de filesystem
(node_filesystem_size_bytes - node_filesystem_avail_bytes) / node_filesystem_size_bytes * 100

# Saturación de E/S de disco
rate(node_disk_io_time_seconds_total[5m])

# Carga por núcleo
node_load1 / count without(cpu, mode) (node_cpu_seconds_total{mode="idle"})

Para redes, se pueden supervisar tanto el rendimiento como la calidad del tráfico:

# Tasa de errores en recepción
rate(node_network_receive_errs_total[2m]) / rate(node_network_receive_packets_total[2m])

# Ancho de banda entrante
rate(node_network_receive_bytes_total[5m])

Diseño de umbrales para alertas de infraestructura

Un umbral fijo no es válido para todos los entornos. Lo recomendable es combinar límites absolutos con proyecciones basadas en tendencias, especialmente para el almacenamiento.

Predición de agotamiento de disco

La siguiente regla activa una alerta cuando el espacio disponible desciende por debajo de un porcentaje crítico y la tendencia indica que se agotará en las próximas horas:

groups:
- name: filesystem-forecast
  rules:
  - alert: FilesystemExhaustionForecast
    expr: |
      (
        node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} /
        node_filesystem_size_bytes{fstype!~"tmpfs|overlay"} * 100 < 15
      and
        predict_linear(
          node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}[1h],
          2 * 60 * 60
        ) < 0
      and
        node_filesystem_readonly{fstype!~"tmpfs|overlay"} == 0
      )
    for: 30m
    labels:
      severity: warning
      domain: storage
    annotations:
      summary: "Espacio en filesystem con tendencia a agotarse"
      description: "El punto de montaje {{ $labels.mountpoint }} de {{ $labels.instance }} tiene menos del 15 % libre y podría llenarse en 2 horas."

Alerta de memoria baja

- alert: MemoryUnderPressure
  expr: |
    (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) /
    node_memory_MemTotal_bytes * 100 > 90
  for: 10m
  labels:
    severity: warning
    domain: memory
  annotations:
    summary: "Presión de memoria detectada"
    description: "La instancia {{ $labels.instance }} consume más del {{ printf \"%.2f\" $value }} % de su memoria desde hace 10 minutos."

Saturación de CPU

- alert: CpuSaturationDetected
  expr: |
    100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[2m]))) * 100 > 85
  for: 10m
  labels:
    severity: warning
    domain: cpu
  annotations:
    summary: "Uso elevado de CPU"
    description: "La instancia {{ $labels.instance }} mantiene un uso de CPU del {{ printf \"%.2f\" $value }} % durante más de 10 minutos."

Clasificación de alertas por impacto

Definir niveles de severidad claros facilita el enrutamiento y evita que el equipo de operaciones se sature. La siguiente tabla propone un esquema sencillo:

Severidad Tiempo de respuesta Impacto Ejemplo
Critical 15 min Servicio caído o riesgo inminente Filesystem con más del 95 % de uso, OOM
Warning 1 h Degradación perceptible CPU por encima del 85 %, latencia de disco alta
Info 4 h Riesgo potencial Tasa de errores de red levemente elevada

Configuración de Alertmanager para múltiples canales

Alertmanager agrupa, inhibe y envía las alertas. El siguiente archivo muestra cómo separar las notificaciones críticas del resto:

# alertmanager.yml
global:
  resolve_timeout: 10m
  smtp_smarthost: 'smtp.example.com:587'
  smtp_from: 'alertas@example.com'
  smtp_auth_username: 'alertas'
  smtp_auth_password: 'secreto'

route:
  group_by: ['alertname', 'cluster', 'service']
  group_wait: 20s
  group_interval: 4m
  repeat_interval: 6h
  receiver: 'default-receiver'

  routes:
  - match:
      severity: critical
    receiver: 'oncall-receiver'
    repeat_interval: 15m
    continue: false

  - match:
      severity: warning
    receiver: 'chat-receiver'

receivers:
- name: 'default-receiver'
  email_configs:
  - to: 'soporte@example.com'
    send_resolved: true

- name: 'oncall-receiver'
  webhook_configs:
  - url: 'https://pager.example.com/webhook'
    send_resolved: true
  sms_configs:
  - to: '+34600111222'
    text: '{{ template "sms.default.text" . }}'

- name: 'chat-receiver'
  slack_configs:
  - api_url: 'https://hooks.slack.com/services/YYY'
    channel: '#ops-alerts'
    send_resolved: true

Plantillas de notificación

Personalizar los mensajes mejora la legibilidad y acelera el diagnóstico. A continuación se muestran plentillas para SMS y correo:

templates:
- '/etc/alertmanager/templates/*.tmpl'

{{ define "sms.default.text" }}
[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .GroupLabels.alertname }}
Instancias: {{ range .Alerts }}{{ .Labels.instance }} {{ end }}
Resumen: {{ range .Alerts }}{{ .Annotations.summary }}{{ end }}
{{ end }}

{{ define "email.default.html" }}
<html>
  <head>
    <title>Notificación de alertas</title>
    <style>
      .critical { background-color: #f8d7da; }
      .warning { background-color: #fff3cd; }
      .info { background-color: #d1e7dd; }
    </style>
  </head>
  <body>
    <h2>Estado: {{ .Status | toUpper }}</h2>
    {{ range .Alerts }}
    <div class="{{ .Labels.severity }}">
      <h3>{{ .Labels.alertname }} - {{ .Labels.severity | toUpper }}</h3>
      <p><strong>Instancia:</strong> {{ .Labels.instance }}</p>
      <p><strong>Resumen:</strong> {{ .Annotations.summary }}</p>
      <p><strong>Descripción:</strong> {{ .Annotations.description }}</p>
      <p><strong>Inicio:</strong> {{ .StartsAt.Format "2006-01-02 15:04:05" }}</p>
    </div>
    {{ end }}
  </body>
</html>
{{ end }}

Ejemplo completo de reglas y enrutamiento

El siguiente archivo reúne varias alertas de infraestructura y las organiza por tipo de recurso:

# infra_alerts.yml
groups:
- name: node-resource
  interval: 30s
  rules:
  - alert: DiskAlmostFull
    expr: |
      (node_filesystem_size_bytes{fstype!="tmpfs"} -
       node_filesystem_avail_bytes{fstype!="tmpfs"}) /
      node_filesystem_size_bytes{fstype!="tmpfs"} * 100 > 95
    for: 5m
    labels:
      severity: critical
      team: infrastructure
    annotations:
      summary: "Filesystem casi lleno"
      description: "{{ $labels.instance }}: {{ $labels.mountpoint }} al {{ printf \"%.2f\" $value }} % de capacidad."

  - alert: MemoryExhaustionRisk
    expr: |
      (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) /
      node_memory_MemTotal_bytes * 100 > 95
    for: 5m
    labels:
      severity: critical
      team: infrastructure
    annotations:
      summary: "Riesgo de agotamiento de memoria"
      description: "{{ $labels.instance }} utiliza el {{ printf \"%.2f\" $value }} % de memoria."

  - alert: LoadAboveCores
    expr: |
      node_load1 / count without(cpu, mode) (node_cpu_seconds_total{mode="idle"}) > 2.5
    for: 15m
    labels:
      severity: warning
      team: infrastructure
    annotations:
      summary: "Carga por núcleo elevada"
      description: "{{ $labels.instance }} mantiene una carga de {{ printf \"%.2f\" $value }} por núcleo."

- name: node-network
  interval: 60s
  rules:
  - alert: NetworkReceiveErrors
    expr: |
      rate(node_network_receive_errs_total[2m]) /
      rate(node_network_receive_packets_total[2m]) > 0.02
    for: 20m
    labels:
      severity: warning
      team: network
    annotations:
      summary: "Tasa de errores de red elevada"
      description: "La interfaz {{ $labels.device }} de {{ $labels.instance }} presenta una tasa de error de {{ printf \"%.4f\" $value }}."

  - alert: DiskIOSaturation
    expr: |
      rate(node_disk_io_time_seconds_total[5m]) > 0.75
    for: 25m
    labels:
      severity: warning
      team: storage
    annotations:
      summary: " saturación de E/S en disco"
      description: "El disco {{ $labels.device }} de {{ $labels.instance }} está ocupado más del {{ printf \"%.2f\" ($value * 100) }} % del tiempo."

Enrutamiento con inhibición

Para evitar ruido, se puede inhibir una alerta de advertencia cuando ya existe una crítica para el mismo recurso:

# route.yml
route:
  receiver: 'default-receiver'
  group_by: ['alertname', 'cluster', 'instance']
  routes:
  - receiver: 'critical-oncall'
    match:
      severity: critical
      team: infrastructure
    continue: false
  - receiver: 'warning-ops-chat'
    match:
      severity: warning
      team: infrastructure
    continue: false
  - receiver: 'network-team'
    match:
      team: network

inhibit_rules:
- source_match:
    severity: 'critical'
  target_match:
    severity: 'warning'
  equal: ['alertname', 'cluster', 'instance']

Estrategias avanzadas de alertado

Umbrales dinámicos con percentiles históricos

En lugar de valores fijos, se pueden comparar las métricas actuales contra percentiles de datos históricos. La siguiente expresión dispara cuando el espacio libre cae por debajo del 80 % del percentil 10 de la última semana:

(
  node_filesystem_avail_bytes / node_filesystem_size_bytes * 100
) < (
  quantile_over_time(0.1,
    (node_filesystem_avail_bytes / node_filesystem_size_bytes * 100)[7d]
  ) * 0.8
)

Agregación y reducción de ruido

Agrupar por instancia y servicio evita recibir una notificación por cada etiqueta. También es útil añadir una cláusula for suficientemente larga para filtrar picos momentáneos.

Optimización de la evaluación y las consultas

Separar los grupos por frecuencia de evaluación mejora el rendimiento de Prometheus:

# prometheus.yml
global:
  evaluation_interval: 1m

rule_files:
  - "infra_alerts.yml"

groups:
- name: critical-fast
  interval: 15s
  rules: []

- name: normal
  interval: 1m
  rules: []

Para reducir la cardinalidad de las consultas, agregar por instancia y modo antes de aplicar la función de tasa:

# Antes: alta cardinalidad
rate(node_cpu_seconds_total[5m])

# Después: agregación previa
avg by(instance, mode) (rate(node_cpu_seconds_total[5m]))

Etiquetas: Node Exporter prometheus alertmanager PromQL

Publicado el 6-19 16:38