Ein Ring Buffer im Flash: wie ein Off-Grid-Gateway im Schlaf keine Daten verliert
Auf einem Bienenstand gibt es keinen Strom, also muss das Gateway fast die ganze Nacht ausgeschaltet bleiben. Wie ein nichtflüchtiger Ring Buffer im Flash die Telemetrie der Bienenstöcke aufbewahrt, Neustarts übersteht und nichts löscht, bevor die Cloud es bestätigt hat.

Auf einem Bienenstand gibt es keine Steckdose. Das ist ein banaler Satz, und er entscheidet einen Großteil der Architektur von bjtOS. Manchmal zwingt er sogar dazu, Entscheidungen wieder aufzumachen, die abgeschlossen schienen: Genau das ist passiert, als ich mich mit dem Buffering des Gateways befasst habe, also jener Box, die die Daten aller Bienenstöcke eines Standes einsammelt und in die Cloud schickt.
Das Ergebnis ist ein nichtflüchtiger Ring Buffer im Flash: ein Ringpuffer, der in permanenten Speicher geschrieben wird und Abschaltungen wie Neustarts übersteht. In diesem Beitrag erzähle ich, warum er nötig ist, wie er aufgebaut ist, und was mich beim Entwerfen am meisten überrascht hat: dass der weit verbreitete Instinkt “schreib nicht zu oft in den Flash, du nutzt ihn ab” nur zur Hälfte stimmt, und dass ausgerechnet die fehlende Hälfte die ist, die man zum Entscheiden braucht.
Das Problem: das leistungsfähigste Teil ist auch das hungrigste
Im Gateway stecken zwei sehr unterschiedliche Gehirne.
Das eine ist ein kleiner Linux-Rechner. Er kann mit dem Mobilfunknetz sprechen, verschlüsseln, Warteschlangen verwalten, es erneut versuchen, wenn etwas schiefgeht. Er ist bequem, und er ist auch hungrig: rund um die Uhr eingeschaltet verlangt er in einer Winternacht fast zehn Wattstunden.
Das andere ist ein Mikrocontroller mit Funk, jener, der auf die Nodes der Bienenstöcke horcht. Er verbraucht rund vierzig Milliwatt, eine Größenordnung weniger, und kann dauerhaft eingeschaltet bleiben, ohne die Energiebilanz zu belasten.
Die Rechnung ist schnell gemacht. Wenn ich ein Gateway will, das von einem Solarpanel vernünftiger Größe versorgt wird, so wie ich es beim Thema wie lange ein batteriebetriebener Bienenstock durchhält erzählt habe, dann kann der Rechner nicht die ganze Nacht laufen. Er muss fast immer aus sein und zwei- oder dreimal zwischen Sonnenuntergang und Sonnenaufgang hochfahren, um alles im Block zu verschicken.
Gut. Aber die Sensoren der Bienenstöcke wissen nicht, dass der Rechner schläft. Die Funknachrichten treffen die ganze Nacht weiter ein. Jemand muss sie notieren.
Warum flüchtiger Speicher nicht reicht
Die erste naheliegende Antwort ist, die Daten im RAM des Mikrocontrollers zu halten, der ja ohnehin immer läuft. Es ist zugleich die falsche Antwort, und man kommt nur darauf, wenn man sich vor Augen führt, wie eine Off-Grid-Nacht wirklich aussieht.
Nachts produziert das Panel nichts. Die Batterie gibt nach, besonders in der Kälte, und besonders dann, wenn das Mobilfunkmodem Strom zieht, um sich ins Netz einzubuchen. Ein Brownout, also ein Spannungseinbruch, der den Mikrocontroller neu starten lässt, ist kein exotischer Zwischenfall für die Rubrik der Restrisiken: Er ist der Normalfall, den das System aushalten muss.
Und ein Neustart löscht das RAM. Eine ganze Nacht voller Daten verschwunden, lautlos, ohne dass es jemandem bis zum nächsten Morgen auffällt.
Der Puffer muss also in nichtflüchtigem Speicher liegen, sprich im Flash: einem Speicher, der das Geschriebene auch ohne Strom behält.
Program und Erase sind nicht dieselbe Operation
Jetzt kommt der Teil, der meine Art, die Sache zu entwerfen, verändert hat.
Wer im Embedded-Bereich arbeitet, trägt einen konditionierten Reflex mit sich herum: nicht zu viel in den Flash schreiben, du nutzt ihn ab. Das ist ein gesunder Hinweis, aber wenn man dort stehen bleibt, entwirft man am Ende um das Problem herum statt hindurch.
Im NOR-Flash, jenem, der in den von uns verwendeten Mikrocontrollern integriert ist, gibt es zwei klar getrennte Operationen:
- Program, das eigentliche Schreiben. Es setzt die benötigten Bits auf null, geschieht in kleinen Häppchen, auch viele Male innerhalb desselben Sektors, sofern man auf noch jungfräulichen Platz schreibt. Es verschleißt nicht nennenswert.
- Erase, das Löschen. Es setzt einen ganzen Sektor, typischerweise 4 KB, wieder auf lauter Einsen. Das ist die Operation mit der begrenzten Endurance, in der Größenordnung von hunderttausend Zyklen pro Sektor.
Die praktische Konsequenz kehrt den Instinkt um. Jede einzelne Nachricht zu schreiben, sobald sie eintrifft, ist keine Verschwendung: Es ist ein Program, es kostet wenig, und es macht den Puffer widerstandsfähig gegen Stromausfälle, denn wenn der Mikrocontroller eine Millisekunde später stirbt, liegt das Datum bereits dort. Was zu zählen ist, ist nicht die Zahl der Schreibvorgänge, sondern die Zahl der Umläufe, die der Puffer über den Speicher macht, denn bei jedem Umlauf bezahlt man ein Löschen auf den wiederverwendeten Sektoren.
Es ist eine Unterscheidung, die es erlaubt, das vorsichtigere Verhalten zu wählen, nämlich sofort und immer zu schreiben, ohne es mit der Lebensdauer der Hardware zu bezahlen.
Ein Ring, zwei Indizes, und die Fähigkeit, sich selbst wiederherzustellen
Die Struktur des Puffers ist die älteste des Fachs: ein Ringpuffer, ein Ring Buffer. Ein Kopf, der schreibend vorrückt, ein Ende, das lesend vorrückt, ein Ring von Plätzen, auf dem beide kreisen. Wenn der Kopf am Ende ankommt, beginnt er wieder von vorn und nutzt den bereits gelesenen Platz erneut. Es ist eine FIFO fester Größe: Sie wächst nie, fragmentiert nie, und hat von Anfang an eine bekannte Obergrenze der Belegung. Auf einem Gerät, das jahrelang allein funktionieren muss, ist Vorhersagbarkeit mehr wert als Eleganz.
Jeder geschriebene Record trägt einen minimalen Header: einen Marker, um ihn zu erkennen, die Länge, und einen CRC, also eine Prüfsumme, die sagt, ob der Inhalt intakt ist. Sie sind für den schlimmsten Moment da, und der ist der Neustart.
Beim Einschalten vertraut der Mikrocontroller niemandem: Er scannt die Partition neu und rekonstruiert Kopf und Ende von null, indem er die Records einen nach dem anderen liest. Ist der Strom mitten in einem Schreibvorgang ausgefallen, fällt dieser Record durch die CRC-Prüfung, wird verworfen, und das Ende bleibt konsistent. Der Puffer repariert sich selbst, ohne dass ihm jemand hinterlassen haben müsste, was er einen Augenblick vor dem Sterben gerade tat.
Über den rohen Flash lässt sich ein Dateisystem legen, das für Mikrocontroller gedacht ist und Wear Leveling mitbringt, also die Verteilung des Verschleißes über alle Sektoren, sowie eine bereits gelöste Robustheit gegen Stromverlust. Man bezahlt ein wenig zusätzliches Schreiben für die Metadaten, was bei unseren Volumina irrelevant ist, und vermeidet im Gegenzug, eine Klasse von Bugs von Hand nachzubauen, die andere bereits bezahlt haben. Die manuelle Verwaltung der rohen Partition bleibt die Rückfalloption, falls eines Tages eine deterministischere Effizienz nötig wäre.
Die Zahlen, die der beruhigende Teil sind
Nehmen wir einen Bienenstand mit zwanzig Bienenstöcken, unseren typischen Fall.
Ein Record passt in rund vierzig Byte, also passen in einen 4-KB-Sektor etwa hundert davon. Eine vierzehn Stunden lange Winternacht produziert bei dichtem Sendetakt etwa fünfhundertsechzig Nachrichten: etwas mehr als zwanzig Kilobyte.
Zwanzig Kilobyte. Auf einer Partition, der wir fünfhundert bis tausend widmen.
Und warum dann so großzügig, wenn die Spitze so klein ist? Nicht wegen der normalen Nacht. Wegen der Nacht, in der das Modem sich nicht ins Netz einbucht. In dem Fall wacht der Rechner auf, versucht es, scheitert, und der Puffer wird nicht geleert: Die Daten bleiben liegen und gehen im nächsten Fenster wieder los. Der Spielraum dient nicht dazu, den Verkehr zu fassen, sondern dazu, Tage an Autonomie zu kaufen, wenn die Mobilfunkabdeckung fehlt.
Zum Verschleiß: rund eintausenddreihundert Records pro Tag ergeben dreizehn Sektoren pro Tag. Auf einer Partition von einem Megabyte, also zweihundertsechsundfünfzig Sektoren, macht der Ring knapp alle zwanzig Tage einen Umlauf. Das sind weniger als zwanzig Löschvorgänge pro Sektor und Jahr, gegenüber einer Endurance von hunderttausend Zyklen. Der Engpass ist schlicht nicht dieser: Wir bewegen uns auf einer Skala, die die Hardware nie zu Gesicht bekommen wird. Man prüft es einmal mit einer Tabellenkalkulation nach und schaut dann nicht mehr hin.
Es lohnt sich, das zu sagen, weil es die Art von Rechnung ist, die sich immer vorher lohnt: Sie hat mir erspart, gegen ein Problem zu optimieren, das nicht existiert, und hat mich das sicherere Verhalten ohne Schuldgefühle wählen lassen.
Die Regel, die alles ehrlich macht: gelöscht wird erst nach der Bestätigung
Das letzte Stück ist das, was aus einem Puffer eine Garantie macht.
Wenn der Rechner aufwacht, übergibt der Mikrocontroller ihm die Daten nicht, um sie danach loszuwerden. Er reicht sie ihm, indem er sie vom Ende her liest, und das Ende rückt erst dann vor, wenn die Bestätigung eintrifft, dass diese Daten auf der anderen Seite, in der Cloud, angenommen wurden. Keine Löschung vor der Bestätigung. Nie.
Das Ergebnis ist eine At-least-once-Zustellung, mindestens einmal: Im schlimmsten Fall kommt dieselbe Nachricht zweimal an und wird weiter unten als Duplikat erkannt und entfernt, denn jede Nachricht trägt einen stabilen Identifikator mit sich. Der akzeptable Worst Case ist das Duplikat. Der Verlust ist es nicht.
Die beiden Warteschlangen arbeiten also in Reihe, und jede hat ihr eigenes Handwerk. Die im Flash auf dem Mikrocontroller sammelt über den langen Zeitraum und überlebt den ausgeschalteten Rechner. Die auf dem Rechner, die es in unserem System bereits gibt und die erprobt ist, verwaltet die einzelne Sitzung Richtung Cloud mit ihren Wiederverbindungen und ihren Versuchen. Es ist dieselbe Philosophie, mit der die gesamte Transportkette von bjtOS entstanden ist, wovon ich in ich habe die Straße vor den Sensoren gebaut erzählt habe: Das Datum geht nie verloren, es sammelt sich an, bis es einen Weg gibt, es zuzustellen.
Was offen bleibt
Ich wäre unehrlich, würde ich es als erledigte Sache erzählen. Das hier ist ein Entwurf, der geschrieben, dimensioniert und begründet ist, noch kein Code, der auf einem Bienenstand läuft. Was fehlt, weiß ich genau, und es ist der lehrreichste Teil:
- das Auslese-Protokoll, also wie die beiden Gehirne während der Übergabe miteinander sprechen, mit den Markern für Sitzungsbeginn und -ende und dem an die Bestätigung gekoppelten Vorrücken des Endes;
- die Uhrzeit, die der Mikrocontroller heute vom Rechner bekommt, der sie seinerseits aus dem Netz nimmt. Bei ausgeschaltetem Rechner muss er sie zwischen zwei Aufwachvorgängen selbst halten und sich neu ausrichten, wenn der andere wieder hochfährt;
- der Eilweg: Ein Diebstahlalarm kann nicht auf das Nachtfenster warten, er muss den Rechner sofort wecken. Und damit der Empfänger ihn erkennen kann, ohne den Inhalt der Nachricht zu öffnen, braucht es ein im Header lesbares Prioritäts-Bit, das mit der Firmware der Nodes und damit mit dem versionierten Wire-Vertrag abgestimmt werden muss;
- der Watchdog: was passiert, wenn der Rechner hochfährt und nie bestätigt, weil er beim Booten oder beim Einbuchen ins Netz hängen geblieben ist.
Das Kriterium: was passiert im schlimmsten Moment
Die Grundentscheidung ist aber gefallen, und sie hält einem einfachen Kriterium stand, das ich beim Entwerfen oft benutze: Was passiert im schlimmsten Moment?
Der schlimmste Moment ist hier eine Februarnacht, mit schwacher Batterie, einem Modem ohne Empfang und einer Spannung, die tanzt. In diesem Moment muss es irgendwo ein Heft geben, in dem bereits alles steht und das sich nicht löscht, wenn das Licht ausgeht.
Es ist dasselbe Prinzip, das den ganzen Rest des Systems leitet: Einen digital nativen Bienenstock entwirft man ausgehend von den Randbedingungen, nicht von den Sensoren. Wenn dich das Gesamtbild interessiert, gibt es wie ein Smart-Bienenstock funktioniert, und was wir gerade aufbauen, findest du auf den Seiten zur Technologie und zum Produkt.