Lo que cuesta de verdad un feed completo
Medí una tabla completa dentro del esquema de producción en vez de estimarla, y la medición mató uno de mis propios valores por defecto
Michel Wijnberg
“¿Aguanta una tabla completa?” es la pregunta que le hacen a todo producto BGP y a la que casi ninguno responde con un número. La razón honesta es que la respuesta es incómoda de producir: hay que cargar una tabla real en el esquema real y mirar.
Así que eso es este artículo. Una tabla IPv4 completa, 1.071.538 mejores caminos de 78.538 AS de origen, cargada en el esquema de producción sobre la misma versión mayor de PostgreSQL que corre en el laboratorio. Cada cifra de abajo es una medición de eso, no un modelo de eso.
También contiene lo más útil que hace medir, que es contradecirle. Uno de los números de abajo ya estaba enviado como valor por defecto en Osprey, y la medición lo mostró por debajo del tráfico ordinario. Desde entonces se ha subido por un factor de cuatro.
La unidad son las sesiones, no los routers
El error de dimensionamiento más común, y conviene quitarlo de en medio primero.
Un router con cuatro upstreams de tabla completa reporta cuatro peers por una sola conexión BMP. Cuesta cuatro veces lo que cuesta un router con un solo upstream. Dimensionar por número de equipos se queda corto exactamente por su factor de multihoming, es decir, por aquello para lo que construyó la red.
Lo que cuesta una tabla completa, por unidad
Los tamaños son los que reporta el propio PostgreSQL para sus relaciones, que son base 1024 pese a las etiquetas MB y GB, y las cifras por fila se derivan de ellos. Así que 412 “MB” son 412 MiB: multiplique 1.071.538 filas por 403 bytes y obtiene 411,8 MiB, no los 432 MB que sugeriría una lectura decimal.
| Unidad | Coste | Alcance |
|---|---|---|
bgp_rib_entry con sus 9 índices | 412 MB (403 B/fila) | por sesión de peer monitorizada |
bgp_best_path con 4 índices | 327 MB (319 B/fila) | por AS y dominio de enrutamiento |
bgp_rib_entry_history con 3 índices | 376 B por intervalo | por transición registrada |
| Conjunto de prefijos BMP por peer, heap de Go | 53 MiB (52 B/prefijo) | por sesión de peer monitorizada |
| Búfer de debounce del RIB del engine, tope por defecto | 267 MiB (560 B/entrada) | por engine, con independencia de los feeds |
Dos cosas de esa tabla merecen destacarse.
Los índices son algo más de la mitad de la huella. En bgp_rib_entry, por sesión:
clave primaria 82 MB, peer_prefix 42 MB, prefix_gist 32 MB, prefix 23 MB,
origin_as 8,9 MB, staleness 7,5 MB, next_hop 7,3 MB. Esos son los siete que merece
nombrar; los dos restantes son menores y no cambian el argumento. Los siete solos suman
203 MB frente a un total de 412 MB, y la clave primaria por sí sola es una quinta parte de
todo. Si alguna vez se preguntó si un índice más sale gratis en una tabla de este tamaño,
el más barato aquí cuesta 7,3 MB por sesión.
Medir eso también encontró un defecto que nadie había advertido.
idx_bgp_rib_peer_lastseen resultó ser idéntico byte a byte al índice de staleness:
7,5 MB y una escritura btree extra en cada fila del RIB, sin comprar nada. Se ha
eliminado. Ese tipo de cosa es invisible hasta que se ponen los tamaños uno al lado del
otro.
403 bytes por fila es con communities. Se incluye una cadena realista de seis communities; las rutas sin communities cuestan unos 55 bytes menos. Cite la cifra más barata sin decir cuál es y su guía de dimensionamiento es un 14 % optimista antes de que nadie haya desplegado nada.
Despliegues calculados
| Despliegue | Base de datos en vivo | Heap BMP |
|---|---|---|
| 1 router de borde, 2 upstreams | 1,15 GB | 107 MiB |
| 2 routers de borde, 2 upstreams cada uno | 1,98 GB | 213 MiB |
| 4 routers × 2 upstreams | 3,6 GB | 426 MiB |
| Feed de route reflector, 20 sesiones de tabla completa | 8,6 GB | 1,04 GiB |
Son cifras de estado vivo. La historia va aparte, y es en la historia donde la cosa se pone interesante.
El churn no se lee de una instantánea
Una tabla le dice lo que cuesta sostenerla. No le dice nada de lo que cuesta registrarla, porque eso depende de con qué frecuencia cambia, y una instantánea no tiene eje temporal.
Así que el churn se midió a partir de archivos MRT públicos: colector RIPE RIS rrc00, 2026-08-10, cuatro ventanas de una hora repartidas por el día, 6,8 millones de mensajes UPDATE en 98 sesiones de peer distintas. Se aplica a la muestra el debounce de 30 segundos de Osprey. La supresión por hash de atributos deliberadamente no se modela, así que son una cota superior de lo que se registraría en realidad.
Aquí un intervalo es una transición de estado persistida tras el debounce. No es un UPDATE BGP en crudo, que puede llevar muchos prefijos a la vez, y no es un evento de prefijo, que el debounce puede colapsar dentro de un intervalo existente.
| Por sesión | Intervalos registrados al día | Almacenamiento de historia |
|---|---|---|
| Mediana | 413.000 | 155 MB/día → 14 GB por 90 días |
| p90 | 840.000 | 316 MB/día → 28 GB por 90 días |
| La más pesada observada | 8.300.000 | 3,1 GB/día → 281 GB por 90 días |
El debounce hace trabajo real: colapsa cada ventana de 30 segundos a un registro por (sesión, prefijo), lo que elimina el 45,6 % de los eventos de prefijo en crudo. Apagarlo en un objetivo de tabla completa multiplica la factura de historia por 1,8 aproximadamente, y lo que retiene a cambio es visibilidad más fina de las transiciones que ocurren dentro de la ventana. En un feed DFZ eso es casi por completo el tipo de detalle que esperaba no tener que almacenar fila a fila.
Dos advertencias que yo querría si estuviera leyendo esto. rrc00 es el colector multihop de RIPE y sus peers son más ruidosos que una sesión de tránsito típica local a un IXP, así que trátelas como pesimistas. Y la dispersión importa mucho más que la mediana: la sesión más pesada es 20 veces la mediana. Dimensione por p90, alarme por la cola, y no deje que nadie le venda la mediana como plan de capacidad.
El número que estaba mal
Osprey tiene un cortacircuitos sobre el registro de historia. Si un objetivo produce más de
storm_ceiling_per_hour intervalos, el registro se suprime durante el resto de esa hora,
para que una sesión que hace flapping no llene un disco sin vigilancia.
El valor por defecto enviado era 250.000 por hora.
Ahora póngalo junto a la medición. La sesión real más pesada de la muestra de rrc00 corre a unos 346.000 intervalos por hora, que son los 8,3 millones al día de la tabla de arriba. No es un descontrol. No es un bucle de flapping. Es un peer real, en un colector real, un lunes cualquiera.
El valor por defecto lo disparaba la operación normal, por sí sola. Y el modo de fallo de un cortacircuitos de supresión es del tipo callado: nada se cae, nada alarma con fuerza, la historia simplemente deja de ser completa y el hueco solo es visible si va a buscarlo.
El valor por defecto ahora es 1.000.000 por hora, derivado de la misma medición y no de la intuición. El churn p90 es de unos 35.000 intervalos por hora y sesión, así que el perfil calculado más grande de arriba, un feed de route reflector con 20 sesiones de tabla completa, ronda los 700.000 por hora en p90. El nuevo techo queda por encima de eso con margen y por encima de la sesión real más pesada, y sigue estando muy por debajo de lo que produce un descontrol de verdad: una sesión que repite la tabla en un bucle de flapping genera múltiplos de esto, así que el cortacircuitos sigue cortando.
Algo que la misma medición deja claro, y que prefiero decir a dejar implícito: un objetivo que agrega muchas sesiones ruidosas puede superar el techo legítimamente. Las 98 sesiones de rrc00 juntas producen unos 50 millones de intervalos al día, alrededor de 2 millones por hora, cómodamente por encima del nuevo techo. Si apunta Osprey a algo así, el cortacircuitos saltará, y hace bien.
Lo cual conviene decir sin rodeos, porque los dos números parecen contradecirse: el techo es una guarda contra descontroles, no un plan de capacidad. Está puesto por objetivo, por encima de lo que produce la operación normal en el perfil más grande que dimensionamos, y por debajo de lo que produce una sesión que repite su tabla en bucle. Un objetivo agregador que lo supere legítimamente ha crecido más allá de un objetivo, no más allá del producto.
Por eso dispararlo se registra en vez de solo escribirse al log. Cuando el techo entra en acción, la historia queda incompleta durante el resto de esa hora, y ese hueco se escribe en un libro de puntos ciegos que puede consultar. Un sistema de monitorización que deja de monitorizar en silencio es peor que uno que deja de hacerlo en voz alta, y lo único que los distingue es si el hueco es un registro de primera clase.
Lo que esto no le dice
Todo lo de arriba son bytes: lo que ocupa una tabla completa en PostgreSQL, lo que el servidor BMP retiene en heap de Go, y a qué velocidad se acumula la historia. Eso es un eje, y una pregunta de dimensionamiento tiene varios.
No medido aquí y por tanto no afirmado:
- CPU, ni en ingesta ni en consulta.
- Tiempo hasta la primera respuesta útil después de que una sesión levante, que es el número que un operador de verdad nota.
- La ruta de mensajes entre el servidor BMP y la base de datos: lo que cuestan el bus y el estado del websocket en vivo mientras una tabla converge.
- Latencia de consulta contra una tabla de este tamaño, cosa distinta de su huella.
Son mediciones aparte y no las he hecho. Una guía de dimensionamiento que insinúe calladamente lo contrario, dándole una cifra de almacenamiento con aplomo y dejándole suponer que el resto escala igual, está haciendo justo aquello contra lo que existe este artículo.
Por qué publicar un valor por defecto que uno tenía mal
Porque la alternativa es peor de una forma concreta.
Una guía de dimensionamiento construida a partir de estimaciones es infalsable. Nadie puede comprobarla, así que nadie encuentra el error, así que el error se envía. La razón por la que este se encontró es que los números se produjeron cargando una tabla real y reproduciendo actualizaciones reales, y a un flujo de actualizaciones real le da igual lo que el fichero de configuración considere razonable.
La medición costó cerca de un día. Encontró un índice duplicado, un valor por defecto equivocado por un factor de cuatro, y produjo la primera respuesta honesta que este producto tiene a “¿aguanta una tabla completa?”. Que es: sí, a 412 MB y 53 MiB de heap por sesión para el estado vivo, y en algún punto entre 14 GB y 281 GB por 90 días para la historia, dependiendo enteramente de lo ruidosos que sean sus peers. Ese rango final es la respuesta real, no su punto medio.
La idea
Cada número de este artículo es una medición del esquema enviado, y uno de ellos contradijo el código enviado.
Ese es el argumento para medir su propio producto en vez de razonar sobre él. No porque razonar sea descuidado, sino porque un valor por defecto elegido por razonamiento no tiene manera de decirle que estaba mal. Una tabla real lo hace en un día.
Este es el último artículo de esta serie. El siguiente empieza por debajo de todo esto: cómo un grabador se une al protocolo de otro sin llegar a formar parte de él, una puerta principal cada vez.