84 Repositories verschwanden. Der Fix war mkdir.

git aws s3 gitlab mechanism

Drei Tage nachdem wir eine selbst gehostete GitLab-Instanz migriert hatten, fragte ein Kollege, ob zwei Projekte verschwunden seien. Das Web-UI zeigte die Projektseiten, die Issue-Listen, die Mitglieder. Die Datenbank verzeichnete für eines davon weiterhin 125 Commits und 584 KB. Fragte man git nach irgendetwas, kam: „A repository for this project does not exist yet.”

Es waren nicht zwei Projekte. Es waren 84 von 517 über die ganze Instanz, 34 davon aktiv, darunter Produktionsdienste. Drei Tage lang hatte es niemand bemerkt, weil die betroffenen Repositories fast per Definition diejenigen waren, die kürzlich niemand angefasst hatte.

Das Muster, das sagt: Niemand hat gelöscht

Der erste Reflex, wenn man hört, „Repositories verschwunden”, ist, nach einer Löschung zu suchen. Die Daten sagten etwas anderes, und die Aufteilung war sauber genug, um eine Regel zu sein.

Jeder Projektdatensatz war intakt: Issues, Merge Requests, Mitglieder, Events, eine korrekte Commit-Zahl, eine korrekte Größe in der Datenbank. Die git-Schicht darunter fehlte. Überleben die Metadaten, aber nicht die Daten, ist eine Löschung die falsche Hypothese; etwas in der Storage-Schicht ist falsch verschoben worden. Wären auch die Metadaten weg, spräche das für eine Löschung. Metadaten vorhanden, Daten fehlen, heißt Transfer oder Mount, und das verweist auf den Weg, den die Bytes genommen haben, nicht auf das Handeln irgendjemandes.

Diese Aufteilung bewahrte die Untersuchung davor, ihre erste Stunde damit zu verbringen, wer was getan hat, und zeigte stattdessen auf die Migration.

Was git tatsächlich verlangt

Ein git-Repository sind nicht „die Dateien”. Es ist ein Graph aus Objekten, plus ein Satz Referenzen, die in diesen Graphen zeigen. Die Referenzen liegen unter refs/, mit Branch-Spitzen in refs/heads und Tags in refs/tags, traditionell je eine kleine Datei pro Referenz.

Wachsen die Repositories, packt das Housekeeping diese Referenzen in eine einzige packed-refs-Datei, genauso wie lose Objekte gepackt werden. Das ist eine routinemäßige Performance-Optimierung, für jeden Nutzer des Repositories unsichtbar. Aber sobald das Packen abgeschlossen ist, enthalten refs/heads und refs/tags keine Dateien mehr. Sie sind leere Verzeichnisse.

Git verlangt weiterhin, dass refs/ existiert, bevor es ein Verzeichnis ein Repository nennt. Die am gründlichsten gepflegten Repositories, die gepackten, waren also genau diejenigen, in denen das Verzeichnis, das git verlangt, leer war.

S3 kennt keine leeren Verzeichnisse

Die Migration hatte den Gitaly-Storage-Baum über S3 zwischengelagert und ihn mit aws s3 sync zurückgespielt. Dieser Weg war aus guten Gründen gewählt worden: keine Routing-Änderungen, keine Security-Group-Änderungen, nichts auf dem Netzwerkpfad mutiert, und eine Sync, die in beide Richtungen inkrementell ist, sodass die Form „vorab seeden, dann Delta” ein kurzes Freeze-Fenster übersteht.

Aber S3-Keys sind flach. Es gibt im Objektmodell überhaupt keine Verzeichnisse; ein Verzeichnis ist eine Schlussfolgerung, die ein Client aus Key-Präfixen zieht, und ein Verzeichnis ohne Dateien darunter hat nichts, woraus sie zu ziehen wäre. Es kommt nicht an.

Nichts davon ist in einer Byte-Zählung sichtbar. Wir hatten die Größen auf beiden Seiten verglichen, und beide Seiten stimmten überein, denn leere Verzeichnisse halten keine Bytes. Jedes Objekt kam intakt an. Die zwei leeren Verzeichnisse, die git verlangt, existierten am Ziel schlicht nicht, und git weigerte sich, den Baum als Repository anzuerkennen, obwohl jedes Objekt jedes Commits dort saß.

Das ist der Teil, den man verinnerlichen sollte. Die Kopie war treu. Sie hat exakt das transportiert, was ihr Vertrag zusagt: Objekte. Der Vertrag ist nicht das Dateisystem, und das Dateisystem trug die Bedeutung.

Die Rechnung, die eine Vermutung zur Root Cause machte

Eine Weile sah die Auswahl zufällig aus, und das ist die Form, die einen an der Hypothese zweifeln lässt. Warum gerade diese 84? Manche lagen seit Jahren brach, aber auf eines wurde vor sechs Wochen gepusht. Zufall ist, wie eine falsche Theorie von innen aussieht.

Dann haben wir gezählt. 517 Repositories mit Inhalt. 433 davon hatten lose Referenz-Dateien, ihre refs/-Verzeichnisse enthielten also Dateien und überstanden daher die Sync. 84 nicht. 517 ist 433 plus 84, ohne Rest.

Der Schaden wurde vollkommen davon bestimmt, ob das Housekeeping das Repository gepackt hatte. Deshalb ging er zu alten und brachliegenden Repositories und erfasste trotzdem etwas frisches: Packen ist eine Funktion der Historie, nicht der aktuellen Aktivität. Eine Korrelation, die exaktes Zählen übersteht, ist eine Root Cause; der Rest ist der Beweis.

Die Checks, die grün blieben

Jetzt der unangenehme Teil. Die Exit-Kriterien der Migration waren abgenommen, und die darin benannten Checks hatten gepasst. Drei Tage lang war jedes Health-Signal grün.

CheckWas er tatsächlich tut
gitlab:gitaly:checkEine Zeile: testet den Service, öffnet nie ein Repository
gitlab:check~560 Zeilen Konfiguration, Konnektivität, Namespaces

Der irreführende Eintrag ist gitlab:check. Er gibt eine Zeile pro Projekt aus, was für alle Welt wie ein Check pro Repository aussieht. Er ist unter „Projects have namespace” einsortiert. Er fasst nie ein Repository an. Beide Checks liefen sauber, während 16 Prozent der Repositories der Instanz unlesbar waren, und beide würden morgen unter demselben Fehler wieder sauber laufen.

Die Tasks, die tatsächlich prüfen, sind git:fsck, das die Integrität über alle Repositories prüft, und git:checksum_projects, das die Referenzen jedes Projekts per Checksumme vergleicht. Keines von beiden ist Teil von gitlab:check, und genau deshalb lief keines. Die Exit-Kriterien benannten die zwei Tasks, die nachweislich nicht den Fehler erkennen konnten, gegen den sie als Gate gedacht waren.

checksum_projects ist das richtige Werkzeug um jede Migration, und es braucht kein Wissen darüber, was schiefging, um es zu erwischen: vor dem Umzug auf der Quelle laufen lassen, danach auf dem Ziel, und diffen. Ein verloren gegangenes refs/ zeigt sich sofort. Eine Verifikation beweist nur, was sie tatsächlich testet, und der günstigste Zeitpunkt, um zu lernen, was ein Check testet, ist, bevor Sie von ihm abhängen.

Wo dies verallgemeinert, und der blinde Fleck

Die Wiederherstellung waren vier mkdir-Aufrufe pro Repository. Kein Restore, keine Restarts, zu keinem Zeitpunkt Datenverlust. Die Repositories kamen in dem Moment zurück, in dem die Verzeichnisse existierten.

Die Regeln, in der Reihenfolge dessen, was sie uns gekostet haben:

Leere Verzeichnisse sind Daten. Jeder Baum, dessen Struktur Bedeutung trägt, ein git-Storage-Root, ein Mail-Spool, ein Anwendungsverzeichnis mit Lock-Verzeichnis, muss mit einem Werkzeug bewegt werden, dessen Vertrag das Dateisystem einschließt. Den Baum mit tar nach S3 bringen, statt Datei für Datei zu syncen, und das ganze Problem verschwindet, denn tar trägt die Verzeichnisse als Einträge, statt sie aus Keys zu erschließen.

Ein grüner Check ist nur eine Aussage über die Frage, die dieser Check stellt. Migrations-Exit-Kriterien sind eine Liste von Fragen, und wenn niemand geprüft hat, was die Checks tatsächlich testen, kann die Liste eine Liste der falschen Fragen sein.

Und der ehrliche. Vor dieser Migration war der Sync-Weg untersucht und aufgeschrieben worden, was er stillschweigend verwirft: Hardlinks, Symlinks, Ownership. Jeder Punkt damals sorgfältig vermessen. Leere Verzeichnisse standen nicht auf der Liste. Die Notiz entstand während der Migration, die später exakt diesen Weg nutzte, und der eine Punkt, der in einer sonst sorgfältigen Aufzählung fehlte, ist der, der 84 Repositories brach.

Eine Aufzählung dessen, was ein Werkzeug verwirft, ist nur so gut wie ihre blinden Flecken, und die Ränder sind von innen unsichtbar, vorher und nachher auf exakt dieselbe Weise. Der Check, der es erwischt hätte, ist ein einziger Befehl, und er gehört vor jeden Umzug auf Objektspeicher: find <tree> -type d -empty | wc -l. Ist diese Zahl null, kann die Sync den Baum tragen. Ist sie es nicht, ist die Zahl die Liste dessen, was gleich verloren geht, aufgeschrieben, bevor es verloren geht, und nicht danach.

$ cat CLICKHOUSE .md
· 8 Min. Lesezeit

136 Millionen PUTs für 17 GiB Daten

Objektspeicher rechnet pro Operation ab, und ein ClickHouse-Part auf einer S3-Disk ist nicht ein Objekt, sondern eines pro Spalte. Die Kosten eines Cold Tier sind also eine Funktion davon, wie viele Parts existieren, nicht wie viele Bytes sie halten, und jede Einstellung, die Merges aushungert, wird zu einer Zeile auf der Rechnung. Zwei Chart-Defaults haben genau das getan, und der Fix, der es beendet hat, war nie committet worden.

clickhouse s3 finops observability mechanism
$ cat GITLAB .md
· 6 Min. Lesezeit

Die Registry antwortete in 45 Millisekunden. Der Build brauchte zwei Minuten länger.

Nach einer Server-Migration wurden Builds gegen eine selbst gehostete Paket-Registry zwei Minuten langsamer, und alle Berichte zeigten auf den Umzug. Jede fehlschlagende Anfrage war ein 500 nach 45 Millisekunden; die gesamte Regression war npms Retry-Backoff. Die Ursache war eine Zeile von 63.000, die noch auf einen Objektspeicher zeigte, den es nicht mehr gab, und der genau dafür geschriebene Check iterierte eine handgeschriebene Tabellenliste und übersah sie.

gitlab debugging migration reliability observability
$ cat KUBERNETES .md
· 7 Min. Lesezeit

Die Constraint war erfüllt. Die Zone war trotzdem leer.

Eine topologySpreadConstraint ist immer nur eine Aussage über die Population, die ihr Selector matcht, und ein vom Operator gesetztes Label kann verwandte Deployments unbemerkt in eine gemeinsame Zählung zusammenfassen. Drei Collector erfüllten jeder für sich ihre harte Zonen-Spread, während ihre Vereinigung eine Zone leer ließ, und die Standard-Reparatur konvergierte jedes Mal auf dieselbe falsche Antwort.

kubernetes scheduling opentelemetry finops mechanism