Lesson  in  Kubernetes 101

Despliega un stack de observabilidad

Prometheus, Grafana y Loki en el clúster. Instrumenta una aplicación, deja que Prometheus la descubra sola con un ServiceMonitor, consulta sus métricas con PromQL, busca en los logs con LogQL y escribe tu primera alerta. Y luego cámbiale la base de datos por VictoriaMetrics sin tocar una sola consulta.

Los tres pilares, montados

Note

🕐 Este laboratorio tarda en arrancar más que el resto del curso, y es normal. Antes de que puedas escribir nada, el playground instala por Helm el stack completo de Prometheus (con su operador, Grafana, Alertmanager y kube-state-metrics) y además Loki con su promtail. Son bastantes imágenes que descargar: cuenta con dos a cuatro minutos desde que abres la lección hasta que la primera tarea se pone en verde.

Si ves la pestaña de la terminal disponible pero kubectl -n observabilidad get pods todavía devuelve Pods en ContainerCreating o Pending, no has hecho nada mal: sigue arrancando. Espera a que la primera tarea se marque sola antes de empezar.

En las dos lecciones anteriores has visto los dos huecos:

  • Los Events se borran a la hora.
  • kubectl top no tiene histórico, no conoce tu aplicación y no avisa de nada.

Los dos huecos se tapan con lo mismo, y no es un producto: es una arquitectura. Alguien recoge la señal, alguien la guarda, y alguien la consulta.

En este clúster ya están instalados Prometheus (métricas), Loki (logs) y Grafana (la ventana a ambos). Tu trabajo no es instalarlos: es conectar tu aplicación y entender por qué funciona.

kubectl -n observabilidad get pods
kubectl -n tienda get all

Paso 1: Prometheus no es un servicio, es un operador

Antes de tocar nada, mira lo que el chart ha instalado:

kubectl api-resources --api-group=monitoring.coreos.com

Ahí están: Prometheus, ServiceMonitor, PodMonitor, PrometheusRule, Alertmanager...

Reconocerás el patrón, porque lo construiste tú en el módulo de Extensibilidad. El Prometheus Operator es exactamente el mismo bucle que escribiste: observa objetos ServiceMonitor, y cuando aparece uno nuevo, regenera la configuración de Prometheus y la recarga. No hay nadie editando prometheus.yml a mano.

Esto tiene una consecuencia que cambia la forma de trabajar: añadir una aplicación a la monitorización deja de ser una tarea del equipo de plataforma. El equipo de desarrollo aplica un ServiceMonitor en su namespace, junto a su Deployment, en su repositorio. Prometheus se entera solo.

Paso 2: Conecta tu aplicación

Hacen falta dos objetos, y aquí está el 90 % de los fallos de la gente que empieza con Prometheus.

Crea monitorizacion.yaml:

cat << 'EOF' > monitorizacion.yaml
apiVersion: v1
kind: Service
metadata:
  name: web-metrics
  namespace: tienda
  labels:
    app: web
spec:
  selector:
    app: web
  ports:
  - name: metrics        # <- EL NOMBRE IMPORTA
    port: 80
    targetPort: 80
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: web
  namespace: tienda
  labels:
    app: web
spec:
  selector:
    matchLabels:
      app: web           # <- selecciona SERVICES, no Pods
  endpoints:
  - port: metrics        # <- por NOMBRE, no por número
    path: /metrics
    interval: 15s
EOF

El YAML, explicado con preguntas y respuestas

¿Por qué hacen falta dos objetos y no uno?

Porque un ServiceMonitor no selecciona Pods: selecciona Services. Prometheus llega a tus Pods a través de los EndpointSlice de un Service. El ServiceMonitor es una capa de configuración sobre un Service que ya existe.

Es la causa número uno de "he creado el ServiceMonitor y Prometheus no ve nada": no había Service, o el Service no tenía endpoints.

¿Por qué el puerto se referencia por nombre (port: metrics) y no por número?

Porque el ServiceMonitor lo exige. spec.endpoints[].port es el nombre del puerto del Service, no el número. Si escribes port: 80 ahí, Prometheus busca un puerto que se llame "80", no lo encuentra, y no da ningún error: simplemente no genera el target.

Ese silencio es lo que convierte este fallo en una tarde perdida. Si tu ServiceMonitor no aparece en /targets, mira este campo primero.

(Existe spec.endpoints[].targetPort para casos raros, pero la forma correcta y robusta es nombrar el puerto en el Service y referenciarlo por nombre.)

¿Qué es selector.matchLabels? ¿Otro más?

Sí, y hay que tener el mapa claro, porque en Kubernetes hay selectores encadenados por todas partes:

ServiceMonitor --(selector)--> Service --(selector)--> Pods

El ServiceMonitor selecciona el Service por sus labels. El Service selecciona los Pods por las suyas. Si cualquiera de los dos eslabones falla, no hay métricas y no hay error.

¿Y cómo sabe Prometheus que debe leer este ServiceMonitor y no ignorarlo?

Buena pregunta, y es la segunda causa de fallo. El objeto Prometheus tiene un serviceMonitorSelector: por defecto, solo lee los ServiceMonitors que llevan ciertas labels. En este playground lo hemos abierto a propósito (serviceMonitorSelectorNilUsesHelmValues=false), para que Prometheus recoja cualquier ServiceMonitor del clúster.

En producción no querrás eso: querrás que un equipo no pueda tumbar el Prometheus central aplicando un ServiceMonitor que scrapea diez mil endpoints cada segundo.

¿Y si mi aplicación no expone /metrics?

Entonces no hay nada que recolectar, y ninguna herramienta del mundo lo arregla. Prometheus no adivina: tu aplicación tiene que estar instrumentada y exponer un endpoint en formato Prometheus. Es una librería (prometheus-client en Python, client_golang en Go...) y unas pocas líneas de código.

Lo que sí obtienes gratis, sin tocar la aplicación, son las métricas de infraestructura: kube-state-metrics te da el estado de todos los objetos (réplicas, reinicios, fases), y node-exporter el de los nodos. Ambos vienen con el chart. Lo que no obtienes gratis es "cuántos pedidos por segundo procesa mi tienda".

Note

💡 En este lab, nginx no está instrumentado de verdad: el /metrics que sirve es un fichero estático que el laboratorio le monta con un ConfigMap. Y tiene que ser un fichero en formato de exposición, no una página cualquiera: para que Prometheus marque un target como UP no basta con recibir un HTTP 200, tiene que parsear la respuesta. Si le apuntas a la página por defecto de nginx, el scrape falla con expected a valid start token y el target se queda DOWN aunque el servidor responda perfectamente. En un caso real, aquí iría una aplicación instrumentada o un exporter (nginx-prometheus-exporter) como sidecar.

Aplícalo:

kubectl apply -f monitorizacion.yaml
kubectl -n tienda get endpointslice -l kubernetes.io/service-name=web-metrics

Paso 3: Mira cómo Prometheus lo descubre solo

No has tocado Prometheus. No has reiniciado nada. Y sin embargo:

Abre la pestaña Prometheus y ve a Status → Targets. En menos de un minuto aparece tu target, en verde, UP.

Note

💡 Esa pestaña es el NodePort 30900 de cplane-01 servido en un iframe. Fuera de un playground harías lo de siempre, que además funciona con Services ClusterIP (la mayoría):

kubectl -n observabilidad port-forward svc/kps-kube-prometheus-stack-prometheus 9090:9090

Y abrirías http://localhost:9090. Ojo con el localhost: es el de la máquina donde corre el port-forward, no el de tu navegador, y el comando se queda ocupando la terminal hasta que lo cortas con Ctrl+C.

Eso lo ha hecho el operador: vio tu ServiceMonitor, regeneró la configuración, y le dijo a Prometheus que la recargara.

Prueba una consulta PromQL en /graph:

up{namespace="tienda"}

Y ahora, las que de verdad usarás. Estas vienen de kube-state-metrics y funcionan sin instrumentar nada:

# Reinicios de contenedores en la última hora: el detector de CrashLoop
rate(kube_pod_container_status_restarts_total[1h]) > 0

# CPU real por Pod
sum(rate(container_cpu_usage_seconds_total{namespace="tienda"}[5m])) by (pod)

# Lo que kubectl top no te podía decir: uso REAL frente a lo RESERVADO
sum(rate(container_cpu_usage_seconds_total{namespace="tienda"}[5m])) by (pod)
  / sum(kube_pod_container_resource_requests{namespace="tienda", resource="cpu"}) by (pod)

Esa última consulta es el rightsizing de la lección anterior, pero con histórico y sobre todo el clúster a la vez. Aquí es donde se ve lo que aporta el stack.

Paso 4: Los logs, con Loki

Loki está desplegado, y Promtail (su agent) corre como un DaemonSet: un Pod por nodo, leyendo los ficheros de log de todos los contenedores del nodo.

kubectl -n observabilidad get daemonset

Ese DaemonSet es exactamente el patrón que estudiaste en el módulo de Workloads, y es el patrón de recogida de logs: el agent va al nodo, no a la aplicación. Tu aplicación sigue escribiendo a stdout y no sabe que Loki existe. Ese desacoplamiento es lo que hace que el sistema escale.

Abre la pestaña Grafana (admin / admin), ve a Explore, elige la fuente de datos Loki y consulta:

{namespace="tienda"}
{namespace="tienda"} |= "GET"
{namespace="observabilidad"} |= "error"

La idea que hay que llevarse: Loki no indexa el texto de los logs, solo las etiquetas (namespace, pod, container). Esas etiquetas son las mismas de Kubernetes. Por eso puedes saltar de una métrica a los logs del mismo Pod sin cambiar de lenguaje mental: la correlación no es magia, es que las dos herramientas usan el modelo de objetos de Kubernetes como sistema de coordenadas.

Note

⚠️ Promtail está descontinuado. Grafana lo declaró en fin de vida y su sucesor es Grafana Alloy (basado en OpenTelemetry Collector). El chart loki-stack que usa este lab sigue funcionando, pero para un despliegue nuevo hoy usarías Alloy o el propio OpenTelemetry Collector, que recoge las tres señales (métricas, logs y trazas) con un solo agent. El patrón (DaemonSet en cada nodo) no cambia; cambia el binario.

Paso 5: Tu primera alerta

Aquí está el tercer hueco que dejaba kubectl top: nadie te avisa.

Una alerta en Prometheus es, otra vez, un CRD. Crea alertas.yaml:

cat << 'EOF' > alertas.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: web-alertas
  namespace: tienda
  labels:
    app: web
spec:
  groups:
  - name: web.reglas
    rules:
    - alert: WebCaida
      expr: up{namespace="tienda"} == 0
      for: 1m
      labels:
        severity: critical
      annotations:
        summary: "La web de la tienda no responde"
        description: "El endpoint {{ $labels.instance }} lleva 1 minuto sin responder."

    - alert: WebDesaparecida
      expr: absent(up{namespace="tienda"})
      for: 1m
      labels:
        severity: critical
      annotations:
        summary: "La web de la tienda ha desaparecido del radar"
        description: "Prometheus ya no tiene ningun target en el namespace tienda."

    - alert: PodReiniciandoEnBucle
      expr: rate(kube_pod_container_status_restarts_total{namespace="tienda"}[10m]) > 0
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "El Pod {{ $labels.pod }} se está reiniciando en bucle"
EOF

¿Qué hace el for: 1m? Es la diferencia entre un sistema de alertas y un generador de ruido. La condición tiene que ser cierta de forma sostenida durante ese tiempo antes de que la alerta se dispare. Sin él, un pico de dos segundos te despierta a las cuatro de la mañana. Con él, un reinicio normal durante un despliegue no molesta a nadie.

¿Y por qué dos alertas para lo mismo? No son lo mismo, y esta es probablemente la trampa que más alertas mudas ha causado en la historia de Prometheus. Guárdate la respuesta un momento: la vas a comprobar tú.

kubectl apply -f alertas.yaml
kubectl -n tienda get prometheusrule

Compruébalo en Prometheus, pestaña Alerts. Tus tres reglas están ahí, en verde. Y ahora tira la web:

kubectl -n tienda scale deployment web --replicas=0

Espera un minuto y mira la pestaña Alerts otra vez.

Lo que acaba de pasar: == 0 no es lo mismo que "no hay dato"

Se ha disparado WebDesaparecida. WebCaida sigue en verde, aunque la web esté tan caída como puede estarlo. Si esa fuera tu única alerta, nadie te habría avisado.

El motivo está en cómo Prometheus construye su lista de targets. Los targets salen de los EndpointSlice del Service, y esos endpoints son las IPs de los Pods. Al escalar a 0 no queda ningún Pod, así que no queda ningún endpoint, así que no queda ningún target.

Y sin target no hay métrica up. La serie no vale 0: deja de existir.

web con 2 Pods    →  up{...} = 1, up{...} = 1     →  `up == 0` no casa: evalúa a vacío
web sin respuesta →  up{...} = 0                  →  `up == 0` casa: ALERTA
web escalada a 0  →  (no hay serie `up`)          →  `up == 0` evalúa a vacío: SILENCIO

up == 0 es un filtro sobre las series que existen. Si no existe ninguna, el filtro devuelve vacío, y una regla cuya expresión devuelve vacío no está disparada: está apagada. Por eso la ves evaluarse una y otra vez sin cambiar de estado.

absent() es la función que existe precisamente para esto: devuelve 1 cuando su argumento no devuelve nada, y nada cuando sí devuelve algo. Es la única forma de alertar sobre la ausencia de una señal.

La pareja up == 0 + absent(up) cubre las dos averías, que son distintas y se arreglan distinto: "está ahí y no responde" (el proceso ha muerto, el puerto está cerrado) y "ya no está" (alguien borró el Deployment, el Service se quedó sin selector, el ServiceMonitor dejó de casar). La segunda es la más peligrosa de las dos, porque una monitorización que se cae a sí misma no se queja.

Devuélvela a la vida y mira cómo las dos vuelven a verde:

kubectl -n tienda scale deployment web --replicas=2
Note

⚠️ absent() tiene un límite que conviene conocer antes de llenar el clúster de reglas: no puede avisarte de algo que nunca existió. Si el ServiceMonitor está mal desde el primer día y ese target no llegó a aparecer jamás, absent() se dispara y no sabes si es una avería o un error de configuración. Para inventarios grandes se usan absent_over_time() o reglas generadas a partir de la lista de servicios que deberían estar.

Y el tercer pilar: las trazas

Faltan las trazas, y no las vas a montar aquí porque necesitan algo que un laboratorio no puede darte: una aplicación de verdad, con varios servicios llamándose entre sí.

La idea, en una frase: una traza sigue una petición concreta a través de todos los servicios que atraviesa, y te dice dónde se fue el tiempo. Las métricas te dicen que el p99 se ha ido a 3 segundos; la traza te dice que 2,8 de esos segundos se los comió una consulta a la base de datos, en el tercer servicio de la cadena.

Hoy el estándar es OpenTelemetry, y su promesa es que la instrumentación deja de ser propietaria: instrumentas una vez, y luego decides a dónde mandas los datos (Jaeger, Tempo, o el SaaS que sea). El backend lo cambias con un fichero de configuración; el código de la aplicación no se toca.

Y aquí sí hay que ser honesto sobre el coste: las métricas y los logs los puedes obtener sin tocar tu aplicación (métricas de infraestructura con kube-state-metrics, logs con un DaemonSet). Las trazas no. Requieren instrumentar el código, propagar cabeceras entre servicios, y que todos los servicios de la cadena colaboren. Un solo servicio sin instrumentar rompe la traza. Por eso es el pilar que más gente aplaza, y el que más echa de menos el día del incidente raro.

Resumen

  • Un stack de observabilidad son tres funciones: recoger, almacenar, consultar. Prometheus, Loki y Grafana son una implementación posible.
  • El Prometheus Operator es el patrón operador que construiste tú: observa ServiceMonitor y PrometheusRule, y regenera la configuración de Prometheus. Nadie edita ficheros a mano.
  • Un ServiceMonitor selecciona Services, no Pods, y referencia el puerto por nombre. Los dos fallos más comunes, y ninguno de los dos da error: solo silencio.
  • kube-state-metrics y node-exporter te dan la observabilidad de la infraestructura gratis. Las métricas de negocio exigen instrumentar tu aplicación.
  • Los logs se recogen con un DaemonSet por nodo. La aplicación escribe a stdout y no se entera de nada. (Promtail está en fin de vida: hoy se usa Alloy o el OpenTelemetry Collector.)
  • Una alerta es un CRD, y su campo más importante es for: sin él tienes un generador de ruido, no un sistema de avisos.
  • Las trazas son el único pilar que exige tocar el código, y el estándar para hacerlo es OpenTelemetry.

El mismo PromQL, otra base de datos

Acabas de montar un stack que funciona. Antes de darlo por bueno, mira lo que cuesta:

kubectl top pods -n observabilidad --sort-by=memory

Ese Prometheus está guardando dos horas de métricas de un clúster con tres Pods, y ya se le nota en la memoria. Con retention=15d y un clúster de verdad, ese número crece rápido, y crece en RAM: Prometheus mantiene en memoria los bloques recientes y un índice invertido de todas las series activas.

Eso no es un defecto de Prometheus. Es el resultado de una decisión de diseño deliberada: Prometheus está pensado para ser efímero, local y no distribuido. Su propia documentación lo dice sin rodeos: no es un almacenamiento a largo plazo duradero.

Y ahí es donde entra otra pieza.

Paso 1: Instala VictoriaMetrics

VictoriaMetrics es una base de datos de series temporales compatible con Prometheus. No es un sustituto de Prometheus a secas: es un sustituto de su almacenamiento, con una implementación distinta pensada para durar y para ocupar menos.

helm upgrade --install vm vm/victoria-metrics-single \
  --namespace observabilidad \
  --set server.fullnameOverride=vmsingle \
  --set server.retentionPeriod=1 \
  --set server.service.type=NodePort \
  --set server.service.nodePort=30428 \
  --wait
kubectl -n observabilidad get pods -l app.kubernetes.io/name=victoria-metrics-single
kubectl -n observabilidad get svc vmsingle

Un solo Pod. Un solo binario, sin dependencias externas: eso es el modo single-node, y aguanta bastante más de lo que la gente supone (millones de series activas en una sola máquina). El modo cluster (vminsert / vmstorage / vmselect, tres componentes que se escalan por separado) existe para cuando esto se queda pequeño, y esa separación es justo lo que Prometheus no te da.

Paso 2: Conecta Prometheus con remote_write

Aquí está la arquitectura que se usa de verdad en producción, y merece la pena entenderla antes de teclearla:

  ServiceMonitor  ──▶  Prometheus  ──remote_write──▶  VictoriaMetrics
                       (recolecta,                       (guarda,
                        retención corta)                retención larga)

Prometheus sigue haciendo lo que mejor hace: descubrir targets y recogerlos. Pero además, cada pocos segundos, reenvía todo lo que recoge a otro sitio por el protocolo remote_write. Prometheus se queda con unas horas para las alertas; VictoriaMetrics se queda con los meses.

Y esto no se configura editando ningún fichero. El objeto Prometheus es un CRD, y remoteWrite es un campo de su spec:

kubectl -n observabilidad patch prometheus kps-kube-prometheus-stack-prometheus \
  --type=merge \
  -p '{"spec":{"remoteWrite":[{"url":"http://vmsingle.observabilidad.svc:8428/api/v1/write"}]}}'

Mira los logs del operador si quieres verlo trabajar. Ha detectado el cambio en el CR, ha regenerado el prometheus.yml y le ha dicho a Prometheus que lo recargue. Es exactamente el bucle que escribiste tú en el módulo de Extensibilidad, gobernando ahora una pieza crítica de tu plataforma.

Paso 3: Pregúntale a VictoriaMetrics

Ahora la parte que hace que todo esto merezca la pena.

Abre la pestaña VictoriaMetrics y entra en /vmui. Es la interfaz propia de VictoriaMetrics. Escribe la misma consulta que escribiste en Prometheus:

up{namespace="tienda"}
sum(rate(container_cpu_usage_seconds_total{namespace="tienda"}[5m])) by (pod)

Funcionan. Sin tocar una coma.

¿Por qué funciona?

Porque VictoriaMetrics habla el protocolo de Prometheus, en los dos extremos:

  • Escritura: acepta remote_write, que es el mecanismo estándar por el que Prometheus exporta sus muestras.
  • Lectura: expone /api/v1/query y /api/v1/query_range, exactamente los mismos endpoints que Prometheus. Cualquier cliente que sepa hablar con Prometheus sabe hablar con VictoriaMetrics sin saber que existe.

Esto es una lección que va mucho más allá de esta herramienta: en observabilidad, la interfaz de Prometheus se ha convertido en el estándar de facto. Thanos, Mimir, Cortex, VictoriaMetrics... todos son piezas distintas por dentro y todos se comportan como un Prometheus por fuera. Es lo que te permite cambiar el motor sin cambiar ni una alerta, ni un dashboard, ni una línea de tu aplicación.

Paso 4: Añádelo a Grafana

Compruébalo donde importa. Grafana descubre sus fuentes de datos leyendo ConfigMaps etiquetados (lo hace un sidecar que el chart despliega; míralo con kubectl -n observabilidad get pods -l app.kubernetes.io/name=grafana -o jsonpath='{.items[0].spec.containers[*].name}').

Crea vm-datasource.yaml:

cat << 'EOF' > vm-datasource.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: vm-datasource
  namespace: observabilidad
  labels:
    grafana_datasource: "1"      # <- sin esto, el sidecar lo ignora
data:
  vm.yaml: |
    apiVersion: 1
    datasources:
    - name: VictoriaMetrics
      type: prometheus           # <- lee esto dos veces
      url: http://vmsingle.observabilidad.svc:8428
      access: proxy
      isDefault: false
EOF
kubectl apply -f vm-datasource.yaml

Espera unos segundos (el sidecar recarga solo), abre la pestaña Grafana, ve a Explore, y elige la fuente VictoriaMetrics. Lanza cualquiera de las consultas de antes.

Ese type: prometheus es la moraleja entera de esta unidad en una línea. No has integrado VictoriaMetrics con Grafana. Le has dicho a Grafana que hay un Prometheus en esa URL, y VictoriaMetrics se ha comportado como tal.

Paso 5: MetricsQL, y dónde está la letra pequeña

VictoriaMetrics no ejecuta PromQL: ejecuta MetricsQL, que es un superconjunto. Todo tu PromQL funciona, y además hay atajos que PromQL no tiene:

# En PromQL, esto es un error: rate() exige una ventana.
rate(container_cpu_usage_seconds_total{namespace="tienda"})

# En MetricsQL, la ventana se deduce del intervalo del gráfico.
# Y funciones que en PromQL requieren malabares:
rollup_rate(container_cpu_usage_seconds_total[5m])   # min, max y avg de un golpe

Aquí conviene ser honesto, y es el tipo de cosa que un libro debe decirte:

Note

⚠️ MetricsQL es un superconjunto, y eso corta por los dos lados. Tus consultas de Prometheus funcionan en VictoriaMetrics. Pero una consulta que uses funciones de MetricsQL no funcionará en Prometheus. Es una puerta de entrada fácil y una puerta de salida más cara: si escribes tus alertas y dashboards en MetricsQL puro, vuelve a Prometheus deja de ser un helm uninstall.

Un consejo práctico: quédate en PromQL estándar salvo que tengas una razón concreta. La compatibilidad es el activo que estás comprando; no la gastes por ahorrar tres caracteres.

El resto de la familia

Lo que has montado es la pieza central, pero el proyecto tiene más:

  • vmagent: un scraper que puede sustituir a Prometheus por completo en la recogida. Entiende los mismos ServiceMonitor, gasta bastante menos memoria, y puede hacer buffer en disco si el destino no responde. En el montaje que has hecho, Prometheus sigue siendo el que recoge; con vmagent, Prometheus desaparece del diagrama.
  • vmalert: ejecuta reglas de alerta y de recording contra VictoriaMetrics, con la misma sintaxis de los PrometheusRule. Es lo que necesitas si eliminas Prometheus, porque las alertas se iban con él.
  • VictoriaMetrics Operator: sus propios CRDs (VMServiceScrape, VMRule, VMAlert...) y un conversor que lee los ServiceMonitor y PrometheusRule que ya tienes. Otra vez lo mismo: el ecosistema se ha puesto de acuerdo en que el ServiceMonitor es el contrato.
  • VictoriaLogs: el equivalente para logs, en el mismo espacio que Loki.

Es decir: puedes sustituir el stack entero, o solo la pieza que te duela. Esa gradualidad es la propuesta.

Entonces, ¿Prometheus o VictoriaMetrics?

La pregunta está mal planteada, y esa es la lección.

Prometheus define el estándar. Es el proyecto graduado de la CNCF, es lo que todo el mundo sabe leer, y su modelo de datos y su API son el lenguaje común de la observabilidad. No lo estás abandonando: lo estás implementando con otro motor.

VictoriaMetrics es una implementación de ese estándar optimizada para durar más y ocupar menos. Y no es la única: Thanos y Mimir resuelven lo mismo con arquitecturas distintas.

La pregunta útil no es cuál es mejor, sino cuándo el Prometheus de serie deja de bastarte:

NecesitasCon Prometheus soloCon un backend de largo plazo
Alertas sobre lo que pasa ahora
Retención de 15 días
Retención de un año❌ (la RAM y el disco te frenan)
Ver 30 clústeres en un dashboard❌ (no federa bien)
Alta disponibilidad de las métricas❌ (dos Prometheus = dos verdades)
Sobrevivir a la caída del nodo❌ (los datos son locales)

Si las tres últimas filas no te hacen falta, no montes esto. Añadir una base de datos distribuida a un clúster que no la necesita es una forma cara de tener más cosas que se rompen.

Y cuando sí te haga falta, ya sabes lo mejor de todo: tus alertas, tus dashboards y tus aplicaciones no se enteran.

Resumen

  • Prometheus está diseñado para ser efímero y local. Su almacenamiento no es el sitio donde guardar un año de métricas, y no pretende serlo.
  • remote_write es el mecanismo estándar para enviar las muestras a otro sitio: Prometheus recoge, otro guarda. En el operador es un campo del CRD, no un fichero.
  • VictoriaMetrics implementa la API de Prometheus en los dos extremos (escritura y lectura). Por eso Grafana lo declara con type: prometheus y funciona sin tocar una consulta.
  • La interfaz de Prometheus es el estándar de facto de la observabilidad. VictoriaMetrics, Thanos y Mimir son motores distintos detrás de la misma fachada. Cambias el motor sin cambiar nada más.
  • MetricsQL es un superconjunto de PromQL: te da funciones extra, y te ata si las usas. Quédate en PromQL estándar salvo que tengas un motivo.
  • La familia completa (vmagent, vmalert, el operador con su conversor de ServiceMonitor) permite sustituir el stack por partes, no de golpe.
  • Y la respuesta más importante: si no tienes un problema de retención, de escala o de alta disponibilidad, no cambies nada.