Ir al contenido

Límites de resolución

Cada número de CPU que produce mikroscope se cuenta en ticks del kernel, así que el cambio más pequeño que puede mostrar lo pone el kernel, no el agente. Una fuente lee por debajo del tick, los contadores de la propia CPU, y el anillo del agente fija hasta dónde llega cualquier lectura hacia atrás.

Un tick dura 10 ms. /proc/stat no cuenta tiempo, cuenta ticks de USER_HZ, 100 por segundo. Una muestra de 100 ms puede contener por tanto 10 ticks por núcleo, así que la fracción de ocupación de un núcleo se resuelve en escalones del 10 %, y la media de cuatro núcleos en escalones del 2,5 %. Sobre 1 s, la resolución de un núcleo es del 1 %.

Eso es aritmética del tick, no una propiedad del agente, y ninguna cadencia ni ajuste lo cambia. El agente envía los ticks en bruto y el intervalo real de cada muestra (dt_ns), nunca un porcentaje, de modo que la ventana sobre la que divides la eliges tú.

La contabilidad de ticks también está cuantizada en los bordes: un intervalo de 100,3 ms puede llevar 11 ticks, lo que daría una fracción de ocupación por encima de 1. La fracción de ocupación derivada está limitada a 1; los ticks en sí siguen en bruto.

Muestrear más rápido no afina esto. A 100 Hz una muestra contiene 0 o 1 tick ocupado, así que la fracción de ocupación por muestra tiene dos valores posibles; por encima de unos 20 Hz los contadores de ticks son un indicador de ocupación más que un porcentaje. Lo que sí compra una cadencia mayor está en Techo de muestreo.

Una fuente lee por debajo del tick: la unidad de monitorización del rendimiento de la CPU (PMU), a través de perf_event_open, que el agente recoge como la fuente perf cuando el contenedor es privilegiado. Es la única fuente de mikroscope que no viene de un fichero.

Una muestra en la que /proc/stat no marca ningún tick ocupado en ningún núcleo puede contener aun así millones de ciclos e instrucciones ejecutadas (medido). El tick redondea ese trabajo hasta hacerlo desaparecer; el contador no.

El número que merece la pena vigilar es el de instrucciones por ciclo (IPC), y el agente no lo calcula: envía las cuentas en bruto y la división la haces tú. El IPC distingue un núcleo que trabaja de un núcleo parado esperando a la memoria, algo que ningún contador de ticks puede expresar. Compáralo núcleo a núcleo: el IPC en reposo de dos núcleos de una misma placa puede diferir en más del doble (medido).

  • El agente pide siete contadores: cycles, instructions, cache-references, cache-misses, branch-instructions, branch-misses y bus-cycles.
  • Cuáles se abren depende de la CPU. Un contador que no se puede abrir está ausente, nunca a cero, así que lee la etiqueta counter de mikroscope_perf_events_total{counter,cpu} en vez de dar por hecho un conjunto (CPU sin probar).
  • Los eventos genéricos stalled-frontend y stalled-backend no se piden. Devuelven ENOENT donde se probaron y harían falta códigos de evento en bruto, propios de cada CPU.
  • Cada cuenta lleva enabled_ns y running_ns. En una CPU que multiplexa sus contadores, la cuenta en bruto cubre solo la parte del intervalo en que funcionó el contador, no el intervalo.
  • Sin privileged=yes falta la familia entera: los contadores se abren a nivel de sistema, y eso un contenedor sin privilegios no puede hacerlo. Consulta Modo privilegiado.

El colector usa esos mismos dos contadores para una de sus detecciones, ipc-collapse: por núcleo, una vez que hay al menos 20 s de historia, que las instrucciones por ciclo de un segundo caigan por debajo de la mitad de su mediana de los 60 s anteriores mientras la tasa de ciclos está por encima de su propia mediana. Se describe junto con las demás reglas en Reglas de detección.

No esperes un reloj más fino de PSI, cuyos totales de espera van en microsegundos, ni de schedstat, cuyos tiempos de ejecución y espera van en nanosegundos. El kernel de RouterOS medido hasta ahora no tiene ni /proc/pressure ni /proc/schedstat (medido). El agente lee ambos donde el kernel los tiene (aún no en un router).

En un kernel cuya columna irq de /proc/stat se queda en 0, como en el medido, el tiempo de IRQ hardware se cuenta dentro de system.

Tampoco hay un camino de trazado. El kernel medido no tiene ni eBPF, ni kprobes, ni ftrace, y el router no tiene directorio de módulos del kernel, ni cabeceras, ni compilador con que añadirlos (comprobado). Una función del kernel que falta no se puede añadir desde un contenedor.

El agente detecta al arrancar lo que tiene el kernel y lo publica en /capabilities: el mapa sources dice qué fuentes lee realmente este despliegue. Los campos de una fuente ausente faltan en cada muestra y en cada destino, nunca valen cero.

El otro límite duro es la profundidad, no la resolución. El agente guarda sus muestras en un anillo de --buffer segundos, 60 s por defecto y entre 10 y 3600 s permitidos, que contiene cadencia × búfer muestras. Nada más antiguo existe en ningún sitio del router.

Esa profundidad es también toda la ventana de la sección health de doctor, que lee el anillo una vez (el anillo entero cuando no guarda más de 10 000 muestras, y si no las 10 000 más recientes, así que lo más corto entre --buffer y 10 000 / cadencia segundos) e imprime cuántos segundos cubrió. Un doctor limpio dice que no hubo bucle, churn de STP, flap de enlace ni descartes de softnet en el último minuto con los valores por defecto, no en todo el día; un fallo que ocurre una vez al día es cosa de los paneles y de las reglas de alerta.

Un corte del colector o del grabador más corto que el anillo se rellena al reconectar: pide since=<seq> y recibe todas las muestras que se perdió. Un corte más largo que el anillo se informa como un hueco de longitud conocida, nunca se tapa. El agente responde con una línea {"gap":{"from":…,"to":…}} que nombra los números de secuencia perdidos, antes de las muestras que aún conserva; record lo escribe como un marcador que dice samples N..M lost, y forward lo cuenta en mikroscope_collector_gaps_total y se lo pasa a cada destino.

Un anillo más largo cuesta memoria en el agente, y el agente rechaza uno que no quepa. Al arrancar cobra cada línea a 3 456 B: la clase de tamaño del asignador de la que se sirve una línea con todas las fuentes activas (3 230 B), porque es lo que paga el heap. Una placa con más núcleos o líneas de interrupción cuesta más por línea, y una sin PMU, menos. El agente suma el presupuesto de la captura por disparo y sale con un error si el total supera el memory.max del contenedor. Si el total es más de la mitad del límite blando de memoria de Go, arranca pero registra un aviso, porque un heap tan justo mantiene al recolector de basura trabajando sin parar. Cómo dimensionar ambos límites está en Coste del agente.