Ir al contenido

Benchmarking

El comando de benchmark mide operaciones de la API de RouterOS contra un router real. Está pensado para desarrollo y planificación de capacidad, no para despliegues normales.

Compila y ejecuta el benchmark:

Ventana de terminal
go run ./cmd/benchmark

La ruta de configuración es opcional y por defecto es config/test.yaml:

Ventana de terminal
go run ./cmd/benchmark config/test.yaml

El benchmark lee la sección de MikroTik del archivo YAML seleccionado, se conecta a RouterOS y después ejecuta operaciones a nivel de API.

  • Adiciones/eliminaciones individuales de direcciones IPv4 e IPv6 en address-list.
  • Llamadas de búsqueda y listado de address-list.
  • Llamadas de creación, búsqueda y eliminación de reglas de firewall.
  • Tiempos de adición/listado/búsqueda/eliminación secuenciales de lotes IPv4.

El benchmark usa direcciones de prueba de prefijos de documentación como 198.51.100.0/24 y 2001:db8::/32, pero aun así escribe en las address lists y los menús de firewall de RouterOS configurados.

Connected to: lab-router
=== SINGLE OPERATION BENCHMARKS (RouterOS API) ===
Add single IPv4 35ms OK
Find IPv4 (1 entry) 12ms OK
Remove IPv4 by .id 18ms OK
=== BATCH ADD BENCHMARKS (sequential via API) ===
Add 100 IPv4 (sequential) 4s (40ms/ip, failures=0)

Usa los números para comparar versiones de RouterOS, modelos de hardware, API con TLS frente a texto plano y latencia de red.

Para el rendimiento real de arranque del bouncer, prefiere las pruebas funcionales de CAPI, porque la reconciliación usa adiciones masivas basadas en scripts y eliminaciones basadas en pool, no solo llamadas secuenciales a la API.

Estas cifras provienen de la suite de pruebas funcionales de CAPI (tests/functional/) ejecutada contra un MikroTik RB5009, no de la herramienta de operaciones individuales cmd/benchmark anterior. Consulta Arquitectura: Pool de conexiones y Adición masiva basada en scripts para conocer los mecanismos detrás de estos números.

EscenarioResultado
Adición optimista cache-first (la dirección aún no está en el router)~1–3 ms por operación
Adición check-first (listar entradas antes de añadir, evitado por la caché)~400 ms por IP
Adición masiva basada en scripts frente a llamadas secuenciales a la API~97× más rápida para lotes grandes
Bulk-add completo en RB5009, ~22.000 entradas de origen CAPI~29 s de bulk, ~34 s desde el arranque

La diferencia entre cache-first (~1-3 ms) y check-first (~400 ms) es la razón por la que existe la caché de direcciones en memoria descrita en Arquitectura: permite al bouncer saltarse el viaje de ida y vuelta listar-y-añadir de RouterOS para las direcciones que ya conoce.

Para despliegues a escala de CAPI (decenas de miles de entradas), consulta también Blocklists de CAPI y Ajuste de rendimiento para orientación sobre el dimensionado de pool_size.

El ciclo de vida completo, muestreado cada 100 ms

Sección titulada «El ciclo de vida completo, muestreado cada 100 ms»

La gráfica siguiente es una única captura continua de 70 segundos del RB5009 de producción, tomada con un muestreador a 10 Hz que lee /proc/stat en el propio router (la sección siguiente dice con qué instrumento, y qué lo ha sustituido). Cubre los tres estados con los que un operador se encuentra de verdad: el router ocioso con el bouncer parado y la lista de direcciones vacía, la primera reconciliación importando ~22.000 entradas, y el estado tranquilo posterior con la lista completa cargada. La CPU se lee en el eje izquierdo, la RAM en el derecho, y los marcadores discontinuos salen de los propios timestamps del log del bouncer. Cada punto es la media de cinco muestras de 100 ms; los picos sub-segundo alcanzan el 57% en rebanadas sueltas de 100 ms durante el import.

Gráfica de líneas de CPU en porcentaje y RAM en megabytes del router durante 70 segundos: CPU casi ociosa antes de arrancar el bouncer, una meseta sostenida en torno al 31% entre los marcadores de inicio de escrituras y reconciliación completa, y vuelta al estado ocioso; la RAM sube unos 19 MiB al conectar el bouncer y se mantiene plana durante todo el import.Gráfica de líneas de CPU en porcentaje y RAM en megabytes del router durante 70 segundos: CPU casi ociosa antes de arrancar el bouncer, una meseta sostenida en torno al 31% entre los marcadores de inicio de escrituras y reconciliación completa, y vuelta al estado ocioso; la RAM sube unos 19 MiB al conectar el bouncer y se mantiene plana durante todo el import.
Primera reconciliación en un RB5009 con 22.000 entradas, capturada el 2026-08-26. El instrumento que la dibujó ya no está en este repositorio; el dataset que hay detrás, sí.

El import es la meseta: aproximadamente un 31% sostenido durante ~26 segundos, repartido entre los cuatro cores con el pool de conexiones rotando picos de un solo core del 50–100%. La RAM es la parte tranquila de la historia, con una sorpresa en el cuándo: el escalón de ~19 MiB cae al conectar, antes de importar una sola entrada — y 22.000 entradas después no se ha movido más. Mantener la lista le cuesta al router casi nada; la memoria no es donde muerde esta carga, la CPU durante las lecturas de la lista, sí. El tramo ocioso también enseña los propios blips de fondo del router — un dispositivo real nunca es una línea plana.

La serie la tomó cmd/perfmon, un instrumento de un solo uso que este repositorio llevó desde la 1.5.0 hasta que la 1.6.0 lo eliminó: compilaba en cruzado un muestreador de /proc/stat, lo cargaba en el router como contenedor, empujaba lotes a Loki, se traía una ventana en CSV y dibujaba este par de SVG. Ese código ya no está — mikroscope hace el mismo trabajo como herramienta mantenida, y la sección siguiente explica cómo usarla. Lo que el instrumento midió se queda: docs/src/data/perf-lifecycle.csv (141 filas, a 100 ms una de otra) y perf-lifecycle-markers.csv (tres marcadores, leídos del propio log del bouncer) siguen versionados junto a la figura, así que cualquier número de arriba se puede comprobar contra los datos de los que salió.

Lo que eso cuesta, dicho aquí y no descubierto más tarde: los dos SVG ya no se pueden regenerar desde este repositorio. Se dibujaron con la paleta del propio sitio leída de theme.css, así que un cambio de paleta ya no les llegará. Son un artefacto congelado del 2026-08-26, no una salida del build — toma una paleta nueva, o una pregunta nueva, como razón para volver a capturar y no para recolorear. El propio plot de mikroscope tampoco sustituye a ese generador: dibuja tres paneles sobre un mismo eje de tiempo a partir de una grabación, con su propia paleta y sin variante oscura, así que produce otra figura, no ésta.

El método, tal y como se midió: CPU de /proc/stat a 10 Hz en un contenedor del router, RAM de /proc/meminfo una vez por segundo, porque la memoria no se movía a 100 ms en esta carga. El cpu-load propio de RouterOS se actualiza solo una vez por segundo, así que los scripts no pueden ver por debajo de eso, y ésa es toda la razón por la que existía un muestreador. El protocolo de captura: parar el bouncer, borrar ambas listas de direcciones, dejar asentar el router, y arrancar el bouncer grabando a través de la primera reconciliación hasta el régimen estable.

Dos cosas que el dataset no lleva, y esta página tampoco llevaba antes: la versión de RouterOS de esa captura concreta — las demás cifras de referencia de aquí se tomaron en 7.22.1 — y cómo se derivó ram_used_mib. El propio verbo capture del instrumento emitía mem_avail_kib y mem_free_kib, así que la columna dibujada como RAM pasó por un paso de conversión que nunca vivió en el repositorio. La columna de CPU es la salida del instrumento; la de RAM está a una operación aritmética de distancia.

cmd/perfmon ya no está porque se convirtió en su propio proyecto. mikroscope es ese proyecto: un agente estático en Go que corre en el router, en un contenedor scratch, y lee el /proc del kernel compartido de 1 a 100 Hz, 10 por defecto, más una CLI en tu máquina que lo instala, graba una ventana con marcadores, dibuja la gráfica o funciona como colector hacia once destinos. Se lleva los pasos de despliegue, el constructor de imágenes sin Docker y el cliente de la API de RouterOS que nacieron aquí, con la atribución en cada paquete — así que lo que sigue es el mismo mecanismo, ahora probado, publicado y documentado por su cuenta.

Lo que el observador le cuesta al router está medido, no prometido: 2,69% de un core y 13,2 MiB residentes con el valor por defecto de 10 Hz del instalador, leídos del propio cgroup del agente en un RB5009UG+S+ (4 × 1,4 GHz Cortex-A72, RouterOS 7.24.2), en una ventana de 300 segundos en régimen estable con el colector enviando a InfluxDB 3, el 2026-09-18. Es la misma placa en la que se toman las cifras de este bouncer, que es lo que hace comparables las dos; no se traslada a ninguna otra placa. Lo que cuesta explica cómo se toma la cifra y cómo tomarla en tu propio dispositivo.

Ventana de terminal
curl -fsSL https://raw.githubusercontent.com/jmrplens/mikroscope/main/install.sh | bash
export MIKROSCOPE_ROUTER=admin@192.168.88.1
mikroscope doctor # comprobación previa, de solo lectura
mikroscope install --ephemeral # lista cada comando que va a ejecutar, pregunta y escribe
mikroscope status # qué hay instalado, y si responde

--ephemeral conserva la única costumbre que merece la pena heredar del instrumento antiguo: fuerza la raíz del contenedor a la tmpfs y lo registra con start-on-boot=no, así que un instrumento no deja rastro en un dispositivo que solo tomaba prestado. Déjalo fuera para un despliegue que deba sobrevivir a un reinicio. Sin toolchain de Go en tu máquina, añade --remote-image jmrplens/mikroscope-agent:latest y el propio router se descarga la imagen — no se compila nada y no se sube nada.

Las dos trampas del firewall por defecto que se comían el tráfico del muestreador de este repositorio son las mismas dos, y el instalador de mikroscope se encarga de ambas: la regla raw drop the rest (in-interface-list=!LAN) descarta cada paquete de un veth que no esté en la lista de interfaces LAN, y drop local if not from default IP range coincide con un origen fuera de la address-list LANs. El síntoma conviene reconocerlo porque nada lo registra: el ICMP al contenedor responde mientras su TCP saliente no genera entrada de conntrack ni contador de masquerade, y el sniffer no muestra nada. Las dos trampas del cortafuegos es la versión larga.

Ventana de terminal
mikroscope record --for 3m --out first-reconcile # una línea escrita aquí se convierte en marcador
# en otra terminal: systemctl start cs-routeros-bouncer
mikroscope plot --in first-reconcile # first-reconcile.svg, determinista

record escribe first-reconcile.jsonl, .csv y .markers.csv, y mikroscope mark --out first-reconcile "reconciliación completa" añade un marcador a una grabación que ya existe. Arranca en la muestra más reciente del agente, así que ponlo en marcha antes que el bouncer — o pasa --from-start para traerte primero lo que el anillo del agente aún guarde, 60 segundos con el valor por defecto del instalador.

El protocolo de captura de la tanda de 2026-08 es la parte que decide si los números significan algo, y no ha cambiado nada de él: parar el bouncer, borrar ambas listas de direcciones, dejar asentar el router, empezar a grabar y solo entonces arrancar el bouncer. Una cosa sí ha mejorado. La memoria ahora se lee y se envía en cada tick y no una vez por segundo — en el dispositivo de referencia los niveles de memoria del kernel se movían unas 24 veces por segundo, así que un suelo ahí habría tirado movimiento real — de modo que la serie de RAM vuelve al ritmo del muestreador en lugar de a 1 Hz.

La gracia de un colector es que la ventana que quieres ya está guardada. mikroscope forward --influx http://influx:8181 --influx-db mikroscope escribe el nivel de kernel en InfluxDB 3 de forma continua — el token sale de MIKROSCOPE_INFLUX_TOKEN, nunca de un flag — y a partir de ahí una pregunta sobre CPU es una consulta, no un despliegue. El token le llega a curl por la entrada estándar, como fichero --config, porque un argumento lo ve cualquier usuario local con ps y -H "Authorization: …" es un argumento como otro cualquiera:

Ventana de terminal
printf 'header = "Authorization: Bearer %s"\n' "$MIKROSCOPE_INFLUX_TOKEN" |
curl -s --config - --get \
http://influx:8181/api/v3/query_sql \
--data-urlencode "db=mikroscope" \
--data-urlencode "format=json" \
--data-urlencode "q=SELECT time, avg(busy_ratio) * 100 AS cpu_pct
FROM mikroscope_cpu
WHERE host = 'router'
AND time >= '2026-09-24T18:00:00Z'
AND time < '2026-09-24T18:02:00Z'
GROUP BY time ORDER BY time"

host es lo que fije el --host-tag del colector — router por defecto, y rb5009 en el colector que está detrás de las cifras de este proyecto. busy_ratio es una fracción por core entre 0 y 1, calculada por el sink y recortada a 1, así que promediarla sobre la etiqueta cpu en un mismo instante da el porcentaje de todos los cores que el dataset retirado llamaba cpu_pct. Los deltas de ticks en crudo están en la misma fila, con el intervalo real de la muestra dt_ns, si prefieres hacer tú esa aritmética — y una tasa calculada sobre el periodo nominal en lugar del real es exactamente el tipo de error del que habla la sección siguiente.

Columna del dataset retiradoDe dónde sale ahora la misma magnitud
cpu_pctavg(busy_ratio) * 100 sobre la etiqueta cpu de mikroscope_cpu en un time
ram_used_mib(total_kb - available_kb) / 1024.0 de mikroscope_mem, una fila por tick
perf-lifecycle-markers.csvuna línea escrita en record, o mikroscope mark después

scripts/router-perf-query.sh, en este repositorio, envuelve ese curl para el puñado de preguntas que este proyecto se hace de verdad — CPU por core, memoria, descartes de softnet, el coste del propio agente y la continuidad de la ventana — y toma MIKROSCOPE_INFLUX_URL y MIKROSCOPE_INFLUX_TOKEN del entorno, los mismos nombres de variable que usa el colector. Su cabecera documenta las dos trampas que conviene conocer: los contadores de softnet y del observador son deltas por muestra, así que hay que sumarlos y no restar extremos, y un bin parcial de date_bin en el borde de una ventana promedia sobre menos de un segundo.

Medidas en InfluxDB y SQL es la lista de columnas de cada tabla, y el sink de InfluxDB es lo que las escribe. Que Grafana lea esa misma base de datos es el camino más corto a una gráfica; el grafana/dashboard.json de este repositorio es solo de Prometheus y cubre las métricas del bouncer, no el kernel del router.

Dos costumbres separan un número accionable de uno que engaña, y ambas se aprendieron por las malas afinando este bouncer.

Intercala las variantes. Un router no es una máquina en silencio: enruta tráfico, ejecuta su propio trabajo de fondo y deriva a lo largo de los minutos. Medir la variante A un rato y luego la B otro rato atribuye esa deriva al cambio. Altérnalas — A, B, A, B — dentro de la misma conexión y el mismo minuto, y compara muestras emparejadas. La primera comparación in situ de la reducción de proplist se hizo en dos ventanas separadas y dio −2%; la versión intercalada dio −11,2%, con las 48 parejas favoreciendo el mismo lado. El mismo código y el mismo router; solo cambió el calendario.

Desconfía de una curva que no es monótona. Barrer el tamaño de bloque una vez por valor, en secuencia, produjo esto:

bloque 50 6,926s
bloque 100 4,679s
bloque 250 9,760s <- el doble que sus vecinos por ambos lados
bloque 500 5,395s
bloque 1000 6,194s

Ningún modelo de coste produce esa forma.

Reporta una dispersión, no un solo número. El arreglo evidente — intercalar los tamaños entre rondas y quedarse con el mejor tiempo de cada uno — sigue siendo incorrecto, solo que de forma menos evidente. El mínimo es un estimador defendible cuando el ruido es unilateral, pero tira justo la información que decide si la comparación significa algo. Repetido guardando todas las muestras, el mismo barrido dice:

bloquemedianamediaIQR
505,78 s6,86 s1,76 s
1005,06 s5,31 s0,22 s
2505,31 s6,39 s2,47 s
5005,50 s5,77 s0,84 s
10004,36 s5,75 s2,55 s

La dispersión dentro de un mismo tamaño llega a 2,55 s, mientras que la distancia entre el mejor y el peor tamaño es de 1,43 s. El ruido de una variante es mayor que la diferencia entre variantes, así que el barrido no resuelve nada — y, en efecto, qué tamaño “gana” depende del estimador: mediana y mínimo eligen 1.000, la media elige 100. Reportar solo el mejor de tres rondas habría escondido todo eso tras una tabla de aspecto impecable.

La conclusión sobrevivió, porque nunca dependió del ranking: insertar una fila domina, así que ningún tamaño de bloque en este rango mueve el import de forma medible. Pero la primera redacción enunciaba un modelo de coste de dos términos con dos cifras significativas que los datos no sostienen.

Un reflejo útil: antes de creerte un resultado, pregunta qué más cambió entre las dos mediciones. Casi siempre es el tiempo.

  • Confirma que el destino es un router de laboratorio.
  • Usa address lists de prueba dedicadas cuando sea posible.
  • Mantén command_timeout lo bastante alto para dispositivos lentos.
  • Revisa el router tras una interrupción y elimina cualquier comentario benchmark-* si una ejecución se detiene a medias.