artículos / Dejé de Adivinar Qué Ollama Estaba Vivo:...

Dejé de Adivinar Qué Ollama Estaba Vivo: Así Nació ollamon

AWONG 9 minutos de lectura 7 vistas

La historia de por que construi ollamon: un monitor de flotas de Ollama en Go que autodescubre cada instancia leyendo el kernel, porque opero infra que cambia sin avisar. Lee la GPU con NVML nativo y junta todo en una vista unificada. Nacido de un incidente real en produccion.

Dejé de Adivinar Qué Ollama Estaba Vivo: Así Nació ollamon

Dejé de Adivinar Qué Ollama Estaba Vivo: Así Nació ollamon

El día que escribo esto se me cayeron dos servidores GPU en Las Vegas. No me enteré por una alerta. Me enteré porque un endpoint de producción —ollama01.enteracloud.mx— empezó a colgarse trescientos segundos por petición. nginx seguía mandando tráfico a un backend muerto, y yo, del otro lado del continente, no tenía forma rápida de saber cuál de mis instancias de Ollama seguía respondiendo y cuál ya era un fantasma.

Tenía ocho instancias de ollama serve repartidas en tres máquinas. Seis en un box con RTX 8000, dos en otro con RTX 5090, más una de respaldo en San Diego. Y ninguna de las máquinas GPU tenía Tailscale propio. Para saber qué estaba vivo, mi ritual era entrar por SSH —saltando por un subnet router— y hacer curl al /api/version de cada puerto, a mano, una por una. Cuando algo se caía, me enteraba tarde y a ciegas.

Pero el problema de fondo no era la caída de ese día. Era algo más estructural: opero sobre infraestructura que no es mía. El datacenter lo administra alguien más, y hace bien su trabajo —actualiza, mejora, reacomoda, mete tarjetas nuevas, mueve VMs entre VLANs—. El detalle es que esos cambios aterrizan sin un aviso, sin changelog. Y quien revisa, repara y escala encima de esa infra que se movió en silencio soy yo. La mejora es real; la sorpresa también.

El ejemplo que me terminó de convencer: un día una de las máquinas GPU simplemente amaneció en otra IP. Había cambiado de VLAN —de 10.8.4.98 a 10.8.4.162— por un reacomodo perfectamente razonable de quien administra el DC. Para mí significó que cualquier lista de hosts que yo mantuviera ya era mentira. Si hubiera tenido un archivo de configuración con las instancias hardcodeadas, habría estado persiguiendo IPs viejas sin saberlo.

Esa tarde decidí que no iba a volver a adivinar ni a perseguir cambios que no me avisan. Necesitaba ver mi flota completa de un vistazo, y que la herramienta se enterara sola de cómo había quedado la infra hoy. Así nació ollamon, un monitor de flotas de Ollama escrito en Go. Este post es la historia de por qué lo construí, qué decisiones tomé, y qué resuelve.

El Problema: Una Flota Sin Visibilidad, y Que Se Mueve

Ollama es maravilloso para correr un modelo local. Pero el día que pasas de una instancia a una flota —varias GPUs, varios hosts, varios ollama serve en puertos distintos— te quedas ciego. La herramienta no te dice nada del conjunto. Cada instancia es una isla con su propia API en localhost:<puerto>, y nadie las junta por ti.

Mi caso era peor por dos razones. La primera, la topología: los servidores GPU viven en una LAN aislada, sin IP pública y sin Tailscale propio. Se alcanzan por saltos. La segunda, y la que de verdad dolía: el terreno cambia debajo de mí. Una tarjeta que se reacomoda de VLAN, un drive que se intercambia, una VM que reaparece en otra IP, una instancia nueva que alguien levantó. Todo eso pasa, mejora la infra, y a mí me toca operar encima sin que nadie me pase el memo.

El costo real no era el downtime. Era la ceguera sobre un blanco móvil. Tomar decisiones de balanceo, de reparto de modelos, de capacidad, a punta de corazonadas y curl manual contra IPs que quizás ya no existen. Me había vuelto el sistema de monitoreo de un cluster que no paraba de cambiar de forma.

Por Qué Otro Más, y No un Dashboard de Estante

La pregunta obligada: ¿por qué no usar algo que ya existe? Probé la idea mentalmente y choqué con lo de siempre. Las soluciones de monitoreo asumen que tú les das la lista de targets, o que metes un exporter por cada cosa, o que todo vive en una red plana y bonita. Pero darle una lista de targets a un sistema cuando el terreno se mueve sin avisar es firmar para mantener esa lista a mano para siempre. Ninguna entendía que "la instancia 11443 tiene gemma3:27b cargado en la GPU 3 a 71 grados" es la unidad de información que me importa, y mucho menos que esa instancia podía estar en otra IP mañana.

Y está mi sesgo, que ya conocen si leen este blog: prefiero construir la herramienta exacta antes que doblar una genérica. Quería algo sin configuración —que no hubiera una lista que mantener—, que se autodescubriera cada vez, que corriera en cualquier glibc sin arrastrar dependencias, y que hablara tanto JSON como Prometheus. Algo que pudiera tirar en un box y olvidarme, aunque el DC cambiara debajo. Así que lo escribí.

La Decisión Clave: Descubrir Leyendo el Kernel

El corazón de ollamon —y la parte de la que estoy más orgulloso— es que no tiene archivo de configuración con la lista de instancias. No le dices qué puertos mirar. Los encuentra solo, leyendo el kernel, cada vez. Esto no es un lujo: es la respuesta directa a operar sobre infra que muta sin avisar. Si el terreno se mueve, la herramienta no puede depender de un mapa viejo; tiene que volver a derivar la verdad desde cero en cada ciclo.

La cadena es esta: leo /proc/net/tcp (y su gemelo IPv6) buscando sockets en estado LISTEN; de cada uno saco el puerto local y el inode del socket. Ese inode es la llave: recorro /proc/<pid>/fd/ de cada proceso buscando un descriptor que apunte a socket:[inode], y así amarro puerto con PID. Confirmo que ese PID es realmente Ollama mirando su /proc/<pid>/cmdline en busca de ollama serve. Y recién entonces lo pruebo por HTTP.

// Sin config: el agente encuentra cada `ollama serve` solo, cada ciclo.
// 1) sockets en LISTEN -> (puerto, inode)
listeners := parseProcNetTCP("/proc/net/tcp")   // st == 0x0A es LISTEN
// 2) inode -> pid, recorriendo /proc//fd/* -> "socket:[]"
pid := pidForInode(listeners[i].inode)
// 3) confirmar que el pid es Ollama
if !strings.Contains(cmdline(pid), "ollama serve") { continue }
// 4) probar la API directa, no el CLI
ver := httpGet("http://127.0.0.1:" + port + "/api/version")

Como el agente corre en el propio host GPU, el descubrimiento es local: no importa en qué IP haya quedado la máquina hoy, el agente la lee desde adentro. El cambio de VLAN que me habría roto una lista hardcodeada, a ollamon ni le hace cosquillas. Y un detalle que me importaba: ollamon nunca shellea el CLI de Ollama. No corre ollama ps ni parsea su salida. Habla directo con la API HTTP de cada instancia: /api/version, /api/ps para lo que está cargado en memoria, /api/tags para lo que hay en disco. Es más rápido, más estable, y no depende del formato de impresión de una versión a otra.

Dos Binarios: Agente y Hub

La forma del problema pedía dos piezas, y aquí tomé prestada la disciplina de arquitectura de otra de mis herramientas, TunAPI: entrypoints delgados en cmd/, lógica en internal/, tipos en pkg/, inyección de dependencias sin globals, timeouts HTTP explícitos en todo.

ollamon-agent corre uno por cada box con GPU. Es el que ensucia las manos: lee /proc (por eso necesita root) y habla con NVML (por eso se compila con CGO). Expone en el puerto 9909 los endpoints /snapshot, /instances, /gpu, /health, /schema y /metrics.

ollamon-hub es el agregador. Pollea el /snapshot de cada agente cada N segundos y arma una vista unificada en el puerto 9910. Aquí la decisión importante fue compilarlo CGO=0, estático y puro Go: el hub no toca hardware, así que no necesita NVML, y al ser estático corre en cualquier distro sin pelear con la versión de glibc. Lo tiro en cualquier lado y funciona.

// El hub preserva el último snapshot bueno si un agente cae.
// Así una GPU inalcanzable no borra la última verdad conocida.
snap, err := pollAgent(ctx, agentURL)
if err != nil {
    return cache.last(agentURL)   // degradar, no mentir con vacío
}
cache.store(agentURL, snap)

Ese cache.last() resuelve justo lo que me dolió el día del incidente: cuando un agente no responde, el hub no me muestra un hueco vacío ni explota; me muestra lo último que supo, marcado, mientras reintenta. La caída de un nodo no me deja sin tablero.

GPU de Verdad: NVML Nativo, No Parsear nvidia-smi

Para las métricas de GPU pude haber parseado la salida de nvidia-smi, como hace medio mundo. No lo hice. ollamon usa NVML nativo vía go-nvml, haciendo dlopen de libnvidia-ml.so en tiempo de ejecución. Eso me da, sin scrapear texto: utilización, memoria usada y total, consumo en watts, temperatura, UUID de cada tarjeta, y —el dato que más quería— qué PIDs están computando en cada GPU.

Ese último cruce es el que cierra el círculo. Como ya tengo el mapa puerto a PID del descubrimiento, y NVML me da PID por GPU, puedo decir con precisión: la instancia que escucha en 11443 es este PID, que está corriendo en la GPU 3, que ahora mismo va al 98% con gemma3:27b cargado. De adivinar a saber.

// /snapshot del hub: dos hosts, ocho instancias, ocho GPUs
{
  "host": "las-gpu0",
  "instances": [
    { "port": 11443, "version": "0.23.2", "loaded": ["gemma3:27b"], "pid": 41201 }
  ],
  "gpus": [
    { "index": 3, "name": "Quadro RTX 8000", "util": 98,
      "mem_used_mb": 24010, "mem_total_mb": 49152,
      "temp_c": 71, "power_w": 232, "compute_pids": [41201] }
  ]
}

Cómo Quedó Desplegado

ollamon corre 24/7 por systemd, habilitado, sobreviviendo reboots. Un agente en cada box GPU y el hub en mi nodo de San Diego:

[Unit]
Description=ollamon agent
After=network-online.target

[Service]
User=root                       # lee /proc//fd de los ollama
EnvironmentFile=/etc/ollamon/agent.env
ExecStart=/usr/local/bin/ollamon-agent
Restart=always

[Install]
WantedBy=multi-user.target

La configuración es toda por entorno —OLLAMON_LISTEN, OLLAMON_HOST, OLLAMON_AGENTS (la lista CSV que pollea el hub), OLLAMON_POLL_INTERVAL— y la autenticación es opcional: un OLLAMON_SECRET que viaja en header X-Secret. Vacío significa sin auth, que es lo correcto cuando todo vive detrás de una red Tailscale de confianza. Ojo con el matiz: la lista de agentes sí la conoce el hub, porque son hosts estables en Tailscale; lo que se autodescubre son las instancias y GPUs dentro de cada host, que es justo lo que cambia sin avisar. El hub levantó la vista unificada a la primera: dos hosts, ocho instancias, ocho GPUs, todas reportando.

Qué Resuelve, en Una Frase

ollamon convirtió mi flota invisible y movediza en algo que veo de un vistazo. Ya no entro por SSH a hacer curl puerto por puerto para saber qué vive, ni persigo IPs que cambiaron sin que me avisaran. Cuando reparto modelos entre GPUs, cuando decido pesos de balanceo, cuando una tarjeta se calienta o un ollama serve se cae, lo veo en un solo JSON —o lo scrapeo con Prometheus desde /metrics.

Encaja en el mismo lugar que el resto de lo que construyo: herramientas pequeñas, self-hosted, sin configuración ceremoniosa, que resuelven un dolor concreto que tuve en producción. Como OpenClaw, como TunAPI, como ProjectHub. ollamon es open source y está en github.com/andyeswong/ollamon.

La lección de fondo, otra vez la misma: no se opera lo que no se ve. Y cuando operas sobre infra que mejora en silencio, la herramienta para verla no puede depender de que alguien te avise: tiene que descubrir el mundo tal como quedó hoy, sola. Son unas tardes de Go bien invertidas. La próxima vez que el terreno se mueva, lo quiero ver en el tablero, no descubrirlo por un timeout de trescientos segundos.

compartir_artículo

LinkedIn Facebook X

artículos_relacionados