O que um feed completo realmente custa
Medi uma tabela cheia dentro do esquema de produção em vez de estimá-la, e a medição matou um dos meus próprios padrões
Michel Wijnberg
“Ele aguenta uma tabela cheia?” é a pergunta que todo produto BGP recebe e que quase nenhum responde com um número. A razão honesta é que a resposta é constrangedora de produzir: você precisa carregar uma tabela real no seu esquema real e olhar.
Então é isso que este artigo é. Uma tabela IPv4 cheia, 1.071.538 melhores caminhos de 78.538 AS de origem, carregada no esquema de produção sobre a mesma versão maior de PostgreSQL que o laboratório roda. Toda cifra abaixo é uma medição disso, não um modelo disso.
Ele também contém a coisa mais útil que a medição faz, que é contradizer você. Um dos números abaixo já estava entregue como padrão no Osprey, e a medição o mostrou abaixo do tráfego ordinário. Desde então foi elevado por um fator de quatro.
A unidade são sessões, não roteadores
O erro de dimensionamento mais comum, e vale tirar da frente primeiro.
Um roteador com quatro upstreams de tabela cheia reporta quatro peers por uma conexão BMP. Ele custa quatro vezes o que custa um roteador de um upstream só. Dimensionar por contagem de dispositivos subestima exatamente pelo seu fator de multihoming, isto é, pela coisa para a qual você construiu a rede.
O que uma tabela cheia custa, por unidade
Os tamanhos são os que o próprio PostgreSQL reporta para suas relações, que são base 1024 apesar dos rótulos MB e GB, e as cifras por linha derivam deles. Então 412 “MB” são 412 MiB: multiplique 1.071.538 linhas por 403 bytes e você tem 411,8 MiB, não os 432 MB que uma leitura decimal sugeriria.
| Unidade | Custo | Escopo |
|---|---|---|
bgp_rib_entry com todos os 9 índices | 412 MB (403 B/linha) | por sessão de peer monitorada |
bgp_best_path com 4 índices | 327 MB (319 B/linha) | por AS e domínio de roteamento |
bgp_rib_entry_history com 3 índices | 376 B por intervalo | por transição registrada |
| Conjunto de prefixos BMP por peer, heap Go | 53 MiB (52 B/prefixo) | por sessão de peer monitorada |
| Buffer de debounce do RIB do engine, teto padrão | 267 MiB (560 B/entrada) | por engine, independentemente dos feeds |
Duas coisas nessa tabela merecem destaque.
Os índices são pouco mais da metade da pegada. Em bgp_rib_entry, por sessão: chave
primária 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. Esses são os sete que valem nomear; os dois
restantes são menores e não mudam o argumento. Os sete sozinhos somam 203 MB contra um
total de 412 MB, e a chave primária por si só é um quinto de tudo. Se você já se perguntou
se mais um índice sai de graça numa tabela desse tamanho, o mais barato aqui ainda custa
7,3 MB por sessão.
Medir isso também encontrou um defeito que ninguém tinha notado.
idx_bgp_rib_peer_lastseen acabou sendo idêntico byte a byte ao índice de
staleness: 7,5 MB e uma escrita btree extra em toda linha do RIB, comprando nada. Foi
removido. Esse tipo de coisa é invisível até você pôr os tamanhos lado a lado.
403 bytes por linha é com communities. Uma string realista de seis communities está incluída; rotas sem communities custam cerca de 55 bytes a menos. Cite o número mais barato sem dizer qual é e o seu guia de dimensionamento está 14% otimista antes de alguém ter implantado qualquer coisa.
Implantações calculadas
| Implantação | Banco vivo | Heap BMP |
|---|---|---|
| 1 roteador de borda, 2 upstreams | 1,15 GB | 107 MiB |
| 2 roteadores de borda, 2 upstreams cada | 1,98 GB | 213 MiB |
| 4 roteadores × 2 upstreams | 3,6 GB | 426 MiB |
| Feed de route reflector, 20 sessões de tabela cheia | 8,6 GB | 1,04 GiB |
Esses são números de estado vivo. Histórico é à parte, e é no histórico que fica interessante.
Churn não se lê num instantâneo
Uma tabela diz o que custa segurá-la. Não diz nada sobre o que custa registrá-la, porque isso depende de com que frequência ela muda, e um instantâneo não tem eixo temporal.
Então o churn foi medido a partir de arquivos MRT públicos: coletor RIPE RIS rrc00, 2026-08-10, quatro janelas de uma hora espalhadas pelo dia, 6,8 milhões de mensagens UPDATE em 98 sessões de peer distintas. O debounce de 30 segundos do Osprey é aplicado à amostra. A supressão por hash de atributos deliberadamente não é modelada, então esses são um limite superior do que seria de fato registrado.
Um intervalo aqui é uma transição de estado persistida após o debounce. Não é um UPDATE BGP cru, que pode carregar muitos prefixos de uma vez, e não é um evento de prefixo, que o debounce pode colapsar dentro de um intervalo existente.
| Por sessão | Intervalos registrados por dia | Armazenamento de histórico |
|---|---|---|
| Mediana | 413.000 | 155 MB/dia → 14 GB por 90 dias |
| p90 | 840.000 | 316 MB/dia → 28 GB por 90 dias |
| Mais pesada observada | 8.300.000 | 3,1 GB/dia → 281 GB por 90 dias |
O debounce faz trabalho de verdade: colapsa cada janela de 30 segundos em um registro por (sessão, prefixo), o que remove 45,6% dos eventos de prefixo crus. Desligá-lo num alvo de tabela cheia multiplica a conta de histórico por cerca de 1,8, e o que você retém em troca é visibilidade mais fina das transições que ocorrem dentro da janela. Num feed DFZ isso é quase inteiramente o tipo de detalhe que você esperava não ter de guardar linha a linha.
Duas ressalvas que eu ia querer se estivesse lendo isto. rrc00 é o coletor multihop da RIPE e seus peers são mais barulhentos que uma sessão de trânsito típica local a um IXP, então trate esses valores como pessimistas. E a dispersão importa muito mais que a mediana: a sessão mais pesada é 20 vezes a mediana. Dimensione no p90, alarme na cauda, e não deixe ninguém lhe vender a mediana como plano de capacidade.
O número que estava errado
O Osprey tem um disjuntor no registro de histórico. Se um alvo produz mais de
storm_ceiling_per_hour intervalos, o registro é suprimido pelo resto daquela hora, para
que uma sessão em flapping não encha um disco sem ninguém olhando.
O padrão entregue era 250.000 por hora.
Agora ponha isso ao lado da medição. A sessão real mais pesada da amostra rrc00 roda a cerca de 346.000 intervalos por hora, que são os 8,3 milhões por dia da tabela acima. Não é um descontrole. Não é um laço de flapping. É um peer real, num coletor real, numa segunda-feira comum.
O padrão era disparado pela operação normal, sozinho. E o modo de falha de um disjuntor de supressão é do tipo quieto: nada quebra, nada alarma alto, o histórico simplesmente deixa de ser completo e a lacuna só é visível se você for procurá-la.
O padrão agora é 1.000.000 por hora, derivado da mesma medição e não da intuição. O churn p90 é de aproximadamente 35.000 intervalos por hora por sessão, então o maior perfil calculado acima, um feed de route reflector com 20 sessões de tabela cheia, gira em torno de 700.000 por hora no p90. O novo teto fica acima disso com folga e acima da sessão real mais pesada, e ainda assim bem abaixo do que um descontrole genuíno produz: uma sessão reprisando a tabela num laço de flapping gera múltiplos disso, então o disjuntor ainda disjunta.
Algo que a mesma medição deixa claro, e que quero afirmar em vez de deixar implícito: um alvo agregando muitas sessões barulhentas pode ainda assim estourar o teto legitimamente. As 98 sessões do rrc00 juntas produzem cerca de 50 milhões de intervalos por dia, uns 2 milhões por hora, confortavelmente além do novo teto. Se você apontar o Osprey para algo assim, o disjuntor vai disparar, e está certo em disparar.
O que vale dizer sem rodeios, porque os dois números parecem discordar: o teto é uma guarda contra descontrole, não um plano de capacidade. Ele é definido por alvo, acima do que a operação normal produz no maior perfil que dimensionamos, e abaixo do que uma sessão reprisando sua tabela em laço produz. Um alvo agregador que legitimamente o exceda superou um alvo, não o produto.
É por isso que dispará-lo é registrado, e não apenas logado. Quando o teto entra, o histórico fica incompleto pelo restante daquela hora, e essa lacuna é escrita num livro de pontos cegos que você pode consultar. Um sistema de monitoramento que silenciosamente para de monitorar é pior que um que para ruidosamente, e a única coisa que os distingue é se a lacuna é um registro de primeira classe.
O que isto não lhe diz
Tudo acima são bytes: o que uma tabela cheia ocupa no PostgreSQL, o que o servidor BMP retém no heap Go, e com que rapidez o histórico se acumula. Isso é um eixo, e uma pergunta de dimensionamento tem vários.
Não medido aqui e, portanto, não afirmado:
- CPU, na ingestão ou na consulta.
- Tempo até a primeira resposta útil depois que uma sessão sobe, que é o número que um operador de fato sente.
- O caminho de mensagens entre o servidor BMP e o banco: o que o barramento e o estado do websocket ao vivo custam enquanto uma tabela converge.
- Latência de consulta contra uma tabela deste tamanho, coisa distinta da sua pegada.
Essas são medições separadas e eu não as fiz. Um guia de dimensionamento que sugira discretamente o contrário, dando um número de armazenamento confiante e deixando você supor que o resto escala igual, está fazendo exatamente aquilo contra o que este artigo existe.
Por que publicar um padrão que você errou
Porque a alternativa é pior de um jeito específico.
Um guia de dimensionamento construído a partir de estimativas é infalsificável. Ninguém consegue conferir, então ninguém acha o erro, então o erro é entregue. A razão de este ter sido achado é que os números foram produzidos carregando uma tabela real e reprisando atualizações reais, e um fluxo real de atualizações não liga para o que o arquivo de configuração acha razoável.
A medição custou cerca de um dia. Encontrou um índice duplicado, um padrão errado por um fator de quatro, e produziu a primeira resposta honesta que este produto tem para “ele aguenta uma tabela cheia”. Que é: sim, a 412 MB e 53 MiB de heap por sessão para estado vivo, e em algum ponto entre 14 GB e 281 GB por 90 dias para histórico, dependendo inteiramente de quão barulhentos são seus peers. Essa última faixa é a resposta real, não o seu ponto médio.
O ponto
Todo número deste artigo é uma medição do esquema entregue, e um deles contradisse o código entregue.
Esse é o argumento para medir o próprio produto em vez de raciocinar sobre ele. Não porque raciocinar seja descuidado, mas porque um padrão escolhido por raciocínio não tem como lhe dizer que estava errado. Uma tabela real faz isso em um dia.
Este é o último artigo desta série. O próximo começa por baixo de tudo isso: como um gravador entra no protocolo de outra pessoa sem se tornar parte dele, uma porta da frente de cada vez.