Was ein voller Feed wirklich kostet
Ich habe eine volle Tabelle in das Produktionsschema gemessen statt sie zu schätzen, und die Messung hat einen meiner eigenen Defaults erledigt
Michel Wijnberg
“Verkraftet es eine Full Table?” ist die Frage, die jedem BGP-Produkt gestellt wird, und fast keines antwortet mit einer Zahl. Der ehrliche Grund: Die Antwort ist unangenehm zu erzeugen, denn man muss eine echte Tabelle in sein echtes Schema laden und nachsehen.
Genau das ist dieser Beitrag. Eine volle IPv4-Tabelle, 1.071.538 Best Paths von 78.538 Origin-ASen, geladen in das Produktionsschema auf derselben PostgreSQL-Hauptversion, die im Labor läuft. Jede Zahl unten ist eine Messung davon, kein Modell davon.
Er enthält außerdem das Nützlichste, was Messen leistet, nämlich Ihnen zu widersprechen. Eine der Zahlen unten war in Osprey bereits als Default ausgeliefert, und die Messung zeigte sie unterhalb des normalen Verkehrs. Sie wurde inzwischen um den Faktor vier angehoben.
Die Einheit sind Sessions, nicht Router
Der mit Abstand häufigste Dimensionierungsfehler, und er gehört zuerst aus dem Weg geräumt.
Ein Router mit vier Full-Table-Upstreams meldet vier Peers über eine BMP-Verbindung. Er kostet das Vierfache eines Routers mit einem Upstream. Nach Gerätezahl zu dimensionieren zählt genau um Ihren Multihoming-Faktor zu niedrig, also um genau das, wofür Sie das Netz gebaut haben.
Was eine volle Tabelle pro Einheit kostet
Die Größen sind PostgreSQLs eigene gemeldete Relationsgrößen, die trotz der Kürzel MB und GB auf 1024 basieren, und die Pro-Zeile-Werte sind daraus abgeleitet. 412 “MB” sind also 412 MiB: 1.071.538 Zeilen mal 403 Byte ergeben 411,8 MiB, nicht die 432 MB, die eine dezimale Lesart nahelegen würde.
| Einheit | Kosten | Geltungsbereich |
|---|---|---|
bgp_rib_entry inkl. aller 9 Indexe | 412 MB (403 B/Zeile) | pro überwachter Peer-Session |
bgp_best_path inkl. 4 Indexe | 327 MB (319 B/Zeile) | pro AS und Routing-Domain |
bgp_rib_entry_history inkl. 3 Indexe | 376 B pro Intervall | pro aufgezeichneter Transition |
| BMP-Präfixmenge pro Peer, Go-Heap | 53 MiB (52 B/Präfix) | pro überwachter Peer-Session |
| Engine-RIB-Debounce-Puffer beim Default-Cap | 267 MiB (560 B/Eintrag) | pro Engine, unabhängig von den Feeds |
Zwei Dinge aus dieser Tabelle lohnen das Hervorheben.
Indexe sind etwas mehr als die Hälfte des Fußabdrucks. Auf bgp_rib_entry, pro
Session: Primärschlüssel 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. Das sind die sieben, die zu
nennen sich lohnt; die beiden übrigen sind kleiner und ändern nichts am Punkt. Die sieben
allein kommen auf 203 MB von insgesamt 412 MB, und der Primärschlüssel allein ist ein
Fünftel von allem. Falls Sie sich je gefragt haben, ob ein weiterer Index auf einer
Tabelle dieser Größe umsonst ist: Der billigste hier kostet immer noch 7,3 MB pro Session.
Beim Messen fiel auch ein Defekt auf, den niemand bemerkt hatte.
idx_bgp_rib_peer_lastseen erwies sich als byte-identisch mit dem
staleness-Index: 7,5 MB und ein zusätzlicher Btree-Schreibvorgang auf jeder RIB-Zeile,
für nichts. Er wurde entfernt. So etwas ist unsichtbar, bis man die Größen nebeneinander
legt.
403 Byte pro Zeile ist mit Communities. Ein realistischer String mit sechs Communities ist enthalten; Routen ohne Communities kosten etwa 55 Byte weniger. Nennen Sie die günstigere Zahl, ohne zu sagen welche es ist, und Ihr Dimensionierungsleitfaden ist 14 % zu optimistisch, bevor überhaupt jemand etwas ausgerollt hat.
Durchgerechnete Deployments
| Deployment | Live-Datenbank | BMP-Heap |
|---|---|---|
| 1 Border-Router, 2 Upstreams | 1,15 GB | 107 MiB |
| 2 Border-Router, je 2 Upstreams | 1,98 GB | 213 MiB |
| 4 Router × 2 Upstreams | 3,6 GB | 426 MiB |
| Route-Reflector-Feed, 20 Full-Table-Sessions | 8,6 GB | 1,04 GiB |
Das sind Live-Zustandszahlen. Historie ist davon getrennt, und bei der Historie wird es interessant.
Churn lässt sich nicht aus einem Snapshot ablesen
Eine Tabelle sagt Ihnen, was sie zu halten kostet. Über die Kosten des Aufzeichnens sagt sie nichts, denn die hängen davon ab, wie oft sich die Tabelle ändert, und ein Snapshot hat keine Zeitachse.
Churn wurde daher aus öffentlichen MRT-Archiven gemessen: RIPE-RIS-Collector rrc00, 2026-08-10, vier über den Tag verteilte Ein-Stunden-Fenster, 6,8 Millionen UPDATE-Nachrichten über 98 verschiedene Peer-Sessions. Ospreys 30-Sekunden-Debounce wird auf die Stichprobe angewandt. Attribut-Hash-Unterdrückung ist bewusst nicht modelliert, also sind das Obergrenzen dessen, was tatsächlich aufgezeichnet würde.
Ein Intervall ist hier eine persistierte Zustandsänderung nach dem Debounce. Es ist kein rohes BGP-UPDATE, das viele Präfixe auf einmal tragen kann, und kein Präfix-Event, das der Debounce in ein bestehendes Intervall einklappen kann.
| Pro Session | Aufgezeichnete Intervalle pro Tag | Historie-Speicher |
|---|---|---|
| Median | 413.000 | 155 MB/Tag → 14 GB pro 90 Tage |
| p90 | 840.000 | 316 MB/Tag → 28 GB pro 90 Tage |
| Schwerste beobachtete | 8.300.000 | 3,1 GB/Tag → 281 GB pro 90 Tage |
Der Debounce leistet echte Arbeit: Er faltet jedes 30-Sekunden-Fenster auf einen Datensatz pro (Session, Präfix), was 45,6 % der rohen Präfix-Events entfernt. Ihn auf einem Full-Table-Ziel abzuschalten multipliziert die Historie-Rechnung um etwa 1,8, und was Sie dafür bekommen, ist feinere Sicht auf Transitionen innerhalb des Fensters. Auf einem DFZ-Feed ist das fast ausschließlich die Art von Detail, die Sie hofften nicht Zeile für Zeile speichern zu müssen.
Zwei Einschränkungen, die ich als Leser haben wollte. rrc00 ist RIPEs Multihop-Collector und seine Peers sind lauter als eine typische IXP-lokale Transit-Session, behandeln Sie diese Zahlen also als pessimistisch. Und die Streuung zählt weit mehr als der Median: Die schwerste Session ist das 20-Fache des Medians. Dimensionieren Sie auf p90, alarmieren Sie auf dem Rand, und lassen Sie sich von niemandem den Median als Kapazitäts- plan verkaufen.
Die Zahl, die falsch war
Osprey hat einen Schutzschalter auf der Historienaufzeichnung. Erzeugt ein Ziel mehr als
storm_ceiling_per_hour Intervalle, wird die Aufzeichnung für den Rest dieser Stunde
unterdrückt, damit eine flappende Session nicht unbeaufsichtigt eine Platte füllt.
Der ausgelieferte Default war 250.000 pro Stunde.
Und nun daneben die Messung. Die schwerste einzelne echte Session in der rrc00-Stichprobe läuft mit etwa 346.000 Intervallen pro Stunde, also den 8,3 Millionen pro Tag aus der Tabelle oben. Kein Amoklauf. Keine Flap-Schleife. Ein echter Peer, auf einem echten Collector, an einem gewöhnlichen Montag.
Der Default wurde vom Normalbetrieb ausgelöst, von ganz allein. Und der Fehlermodus eines Unterdrückungsschalters ist der leise: Nichts stürzt ab, nichts alarmiert laut, die Historie hört einfach auf vollständig zu sein, und die Lücke ist nur sichtbar, wenn man danach sucht.
Der Default liegt jetzt bei 1.000.000 pro Stunde, abgeleitet aus derselben Messung statt aus Intuition. p90-Churn liegt bei ungefähr 35.000 Intervallen pro Stunde und Session, also läuft das größte durchgerechnete Profil oben, ein Route-Reflector-Feed mit 20 Full-Table-Sessions, bei p90 um die 700.000 pro Stunde. Die neue Obergrenze liegt mit Luft darüber und über der schwersten einzelnen echten Session, und dabei immer noch weit unter dem, was ein echter Amoklauf produziert: Eine Session, die ihre Tabelle in einer Flap-Schleife wiederholt, erzeugt ein Vielfaches davon, der Schalter schaltet also weiterhin ab.
Etwas, das dieselbe Messung klarmacht und das ich lieber ausspreche als impliziere: Ein Ziel, das viele laute Sessions aggregiert, kann die Obergrenze legitim auslösen. Die 98 rrc00-Sessions zusammen erzeugen rund 50 Millionen Intervalle pro Tag, etwa 2 Millionen pro Stunde, deutlich über der neuen Grenze. Wenn Sie Osprey auf so etwas richten, wird der Schalter auslösen, und das zu Recht.
Das gehört ausdrücklich gesagt, denn die beiden Zahlen sehen aus, als widersprächen sie sich: Die Obergrenze ist ein Amoklaufschutz, kein Kapazitätsplan. Sie ist pro Ziel gesetzt, über dem, was Normalbetrieb auf dem größten von uns dimensionierten Profil erzeugt, und unter dem, was eine Session produziert, die ihre Tabelle in einer Schleife wiederholt. Ein aggregierendes Ziel, das sie legitim überschreitet, ist einem Ziel entwachsen, nicht dem Produkt.
Deshalb wird das Auslösen aufgezeichnet und nicht bloß geloggt. Wenn die Obergrenze greift, ist die Historie für den Rest dieser Stunde unvollständig, und diese Lücke wird in ein abfragbares Blind-Spot-Register geschrieben. Ein Monitoring-System, das stillschweigend aufhört zu überwachen, ist schlimmer als eines, das laut aufhört, und der einzige Unterschied ist, ob die Lücke ein erstklassiger Datensatz ist.
Was das nicht sagt
Alles oben sind Bytes: was eine volle Tabelle in PostgreSQL belegt, was der BMP-Server im Go-Heap hält, und wie schnell Historie anwächst. Das ist eine Achse, und eine Dimensionierungsfrage hat mehrere.
Hier nicht gemessen und daher nicht behauptet:
- CPU, beim Ingest oder bei der Abfrage.
- Zeit bis zur ersten brauchbaren Antwort, nachdem eine Session hochkommt, also die Zahl, die eine Betreiberin tatsächlich spürt.
- Der Nachrichtenweg zwischen BMP-Server und Datenbank: was Bus und Live-Websocket-Zustand kosten, während eine Tabelle konvergiert.
- Query-Latenz gegen eine Tabelle dieser Größe, im Unterschied zu ihrem Fußabdruck.
Das sind eigene Messungen, und ich habe sie nicht gemacht. Ein Dimensionierungsleitfaden, der stillschweigend etwas anderes andeutet, indem er Ihnen eine selbstsichere Speicherzahl gibt und Sie annehmen lässt, der Rest skaliere genauso, tut genau das, wogegen dieser Beitrag argumentiert.
Warum man einen Default veröffentlicht, den man falsch hatte
Weil die Alternative auf eine bestimmte Weise schlechter ist.
Ein aus Schätzungen gebauter Dimensionierungsleitfaden ist unfalsifizierbar. Niemand kann ihn prüfen, also findet niemand den Fehler, also geht der Fehler in den Versand. Dass dieser gefunden wurde, liegt daran, dass die Zahlen durch Laden einer echten Tabelle und Wiedergabe echter Updates entstanden sind, und einen echten Update-Strom kümmert nicht, was die Konfigurationsdatei für vernünftig hält.
Die Messung kostete etwa einen Tag. Sie fand einen doppelten Index, einen um den Faktor vier danebenliegenden Default, und sie erzeugte die erste ehrliche Antwort dieses Produkts auf “verkraftet es eine Full Table”. Nämlich: ja, bei 412 MB und 53 MiB Heap pro Session für den Live-Zustand, und irgendwo zwischen 14 GB und 281 GB pro 90 Tage für Historie, ganz abhängig davon, wie laut Ihre Peers sind. Diese Spanne ist die echte Antwort, nicht ihr Mittelwert.
Der Punkt
Jede Zahl in diesem Beitrag ist eine Messung des ausgelieferten Schemas, und eine davon hat dem ausgelieferten Code widersprochen.
Das ist das Argument dafür, das eigene Produkt zu messen statt über es nachzudenken. Nicht weil Nachdenken nachlässig wäre, sondern weil ein durch Nachdenken gewählter Default keine Möglichkeit hat, Ihnen zu sagen, dass er falsch war. Eine echte Tabelle tut es an einem Tag.
Das ist der letzte Beitrag dieser Reihe. Der nächste beginnt darunter: wie ein Recorder dem Protokoll eines anderen beitritt, ohne Teil davon zu werden, eine Vordertür nach der anderen.