Das Journal bricht mitten im Eintrag ab. Kein Kernel-Panic, kein OOM-Dump, keine Shutdown-Meldung. Die Kiste ist weg, und kurz danach setzt ein Hardware-Watchdog sie zurück. Zwischen dem 15. Juni und dem 31. August ist das meinem Desktop sechzehn Mal passiert: 15. Juni, 26. Juni, ein Cluster aus drei um den 21.–22. Juli, 1., 3., 4. August, der 9. (zweimal), der 11. (zweimal), der 22. (zweimal), der 24. und der 31. Die meisten endeten mit dem Watchdog; der Rest mit meinem Finger auf dem Power-Button.
Die Maschine ist ein GMKtec EVO-X2 — Ryzen AI Max+ 395, 128 GB Unified Memory, und eine 2-TB-Lexar NQ790: QLC, das billigste NAND pro Gigabyte im Regal. SMART hat nichts davon gemeldet. Die SSD läuft heute noch in der Kiste. Das hier ist das Post-Mortem des Sommers — und von dem, was danach kam: Die QLC hat ihren Job behalten, eine PCIe-3.0-SSD ist aus einer Schublade gekommen, um die Arbeit zu machen, die zählt, und die Migration hat sich an einem Boot-Menü beinahe zweimal selbst rückgängig gemacht.
Dieser Post hat zwei Hälften, weil die Geschichte zwei Hälften hat. Die Hardware-Hälfte erklärt, warum zwei SSDs im selben M.2-Slot sich wie verschiedene Spezies verhalten: Vier Specs entscheiden über deine echte Geschwindigkeit, und keine davon steht auf dem Karton. Die Software-Hälfte ist alles, was ich gelernt und geprüft habe, während ich auf der langsamen SSD gelebt habe: eine Mount-Option, die den Kernel eingefroren hat, TRIM-Kadenz, Btrfs-Hygiene, zram-Größe, Kernel-Regler und eine Scheduler-Entscheidung, die die meisten Guides für diese Klasse von SSD falsch treffen.
Teil 1 — die Hardware: gleicher Slot, andere Physik
M.2 ist eine Form, keine Fähigkeit. Beide SSDs passen in denselben Slot, auf beiden Klebern stehen Tausende Megabyte pro Sekunde, und unter echter Last ist eine siebenundfünfzigmal schneller als die andere. Vier Faktoren haben das gemacht.
Vier Bit pro Zelle
Flash speichert Bits als Spannungslevel in einer Zelle. SLC legt eines ab, TLC drei, QLC vier. Vier Bit heißen sechzehn Spannungslevel, und sie auseinanderzuhalten ist langsam: QLC-Zellen vertragen rund 1.000 Löschzyklen gegenüber 3.000 bei TLC, und sie lassen sich nicht schnell in kleinen Einheiten überschreiben. Niemand verkauft dir langsames Flash offen. Der Controller versteckt es: Writes landen in einem Teil des NAND, der im schnellen Ein-Bit-Modus läuft — ein pseudo-SLC-Write-Cache —, und werden später auf die billigen Zellen umgezogen.
Eine QLC-SSD sind zwei SSDs. Warmer Cache, ein Write dauert ~20 µs. Leerer Cache, jeder Write landet direkt auf QLC bei ~253 ms. Dieselbe SSD, am selben Tag, die sich bei SMART für völlig gesund erklärt.
Der Cache vor den Zellen
Der pseudo-SLC-Cache ist die echte Performance der SSD, und er fällt aus zwei Richtungen ab. Er schrumpft, je voller die SSD wird — die schnelle Region ist teilweise dynamisch, umgewandelt aus Kapazität, die du bereits benutzt. Und er verstopft: Der Controller weiß, dass ein Block frei ist, erst, wenn ihm jemand Bescheid gibt. Das ist TRIMs Job.
Btrfs schreibt Copy-on-Write: Jeder Write allokiert einen frischen Block und gibt den alten frei. Mein fstrim-Timer stand auf dem Distro-Default: wöchentlich. CoW hat also freigegebene Blöcke schneller produziert, als TRIM sie gemeldet hat, die veralteten Einträge haben sich im Cache gestapelt, und unter einer Desktop-plus-Services-Last war der Cache in 22 bis 47 Stunden leer. Dann kostet jeder Write 253 ms, die I/O-Queue wächst schneller, als sie abfließt — ich habe zugesehen, wie sie in drei Stunden von 450 auf 6.192 Anfragen tief ging —, journald verhungert, Timer feuern nicht mehr, und der Watchdog holt die Kiste ab. Ein btrfs-Snapshot, der eine Millisekunde dauern sollte, dauert 20 Sekunden. Die I/O-Pressure-Baseline auf dieser Kiste lag bei 42 % — chronisch, und die ganze Zeit in den Metriken sichtbar.
Davon sehen die Etiketten nichts. Auf dem Karton der QLC stehen 7 GB/s. Unter Live-Last am selben Tag hat die SSD 13 MB/s bewegt; in dem Sturm, der die Kiste eingefroren hat, 0,3 MB/s. Eine Festplatte schafft an einem schlechten Tag 150 MB/s. Eine ruhige QLC liest sequenziell weiter mit 1–3 GB/s — und genau das ist der Job, den ich ihr am Ende gelassen habe.
Eine SSD ohne DRAM hält ihre Map in deinem RAM
Flash lässt sich nicht an Ort und Stelle überschreiben, also pflegt der Controller eine Map von logischen Blöcken auf physische Zellen. SSDs mit DRAM halten diese Map im eigenen Speicher: Die Samsung 970 EVO Plus von 2019 trägt 1 GB davon mit. Die Lexar hat keinen — Maxio MAP1602, DRAM-los —, also leiht sie sich 10 MB Host-RAM über HMB, den Host Memory Buffer.
Auf dieser Maschine heißt das: Die Übersetzungsmap der SSD konkurriert mit allem anderen um Unified Memory, inklusive einer GPU, die 51 GiB davon beanspruchen kann. Das ist der Budget-Stapel: QLC-Zellen, kein DRAM, eine geliehene Map. Der Premium-Stapel von 2019 schlägt ihn bei allem außer Preis pro Gigabyte und den sequenziellen Reads, aus denen kein Arbeitstag besteht. Sieben Jahre Fortschritt sind in Kapazität pro Dollar geflossen, und der Rest des Stapels wurde stillschweigend billiger.
Der Bus ist die unwichtigste Spec
PCIe 3.0 x4 endet bei knapp 4 GB/s, PCIe 4.0 x4 bei knapp 8. Die Samsung ist Gen3-nativ und läuft mit Gen3-Tempo im Gen4-Slot des EVO-X2 — und das fällt kein einziges Mal ins Gewicht, weil der Chip, nicht der Bus, die Decke ist. Was der Aufkleber noch besser versteckt als den Bus, ist die Queue-Tiefe: Die Durchsätze auf den Spec-Sheets werden mit 32 Kommandos in der Pipeline gemessen, streamend. Der Tag eines Entwicklers ist Queue-Tiefe eins — ein Tastendruck, ein Shell-Prompt, ein git status, eine Datenbank, die wissen will, ob ihr Write angekommen ist.
Gleiche Kiste, gleicher Nachmittag, Live-Last, fio:
| Metrik (4K) | Samsung 970 EVO Plus (TLC, PCIe 3.0) | Lexar NQ790 (QLC, PCIe 4.0) |
|---|---|---|
| Random Read, QD1 | 35.300 IOPS @ 28 µs | 620 IOPS @ 1,6 ms |
| Random Read, QD32 | 304.657 IOPS @ 1,19 GB/s | 804 IOPS |
| fsync pro Write, QD1 | 0,78 ms | ~200 ms |
Siebenundfünfzigmal so viele synchrone Random Reads, 256× beim fsync. Ein Fünftel einer Sekunde schrumpft auf unter eine Millisekunde, wenn eine Datenbank wissen will, ob ihr Write angekommen ist. Die neuere SSD am neueren Bus verliert um zwei Größenordnungen gegen eine SSD, die alt genug ist, um eine Schublade als Zuhause zu haben.
Das war die Hardware-Hälfte. Das hier ist, was sie mit meinem Sommer gemacht hat.
Die Freezes, sortiert
Sechzehn Freezes. Im Nachhinein sortieren sie sich in vier Klassen und einen Haufen Selbstschüsse: Neun haben mir gezeigt, wie die Teile zusammenspielen, sieben tragen meine Fingerabdrücke.
Der Write-Cache ist eine Batterie
Das Post-Mortem vom 4. August hat drei Crashes an drei Tagen mit einer SSD verbunden, die nie einen Fehler gemacht hatte. Der wöchentliche TRIM zwischen dem ersten und dem zweiten Crash hat 446 GiB veraltete Blöcke bearbeitet und dauerte 74 Minuten. In einem gesunden Zeitplan bildet sich dieser Rückstau nie — meine täglichen Trims schaffen jetzt 50–100 GiB in 10–15 Minuten.
Der 11. August war dieselbe Klasse in Verkleidung: Ein Browser-History-Service war 40 Stunden lang in einer Crash-Loop — 3.677 Server-Restarts und 1.335 Agent-Restarts pro Boot — und jeder Restart hat eine zu 90 % volle SSD bearbeitet. Zwei Freezes an einem Tag. Der Bug darunter war schön: Der Service hat mattn/go-sqlite3-Parameter wie _journal_mode=WAL an modernc.org/sqlite übergeben, der sie stillschweigend ignoriert. Kein WAL, kein Busy-Timeout, SQLITE_BUSY-Stürme, Restart-Loop, toter SLC-Cache, eingefrorener Kernel. Eine Crash-Loop ist ein Storage-Workload.
Der Fix-Stapel seitdem: täglicher fstrim mit Idle-I/O-Priorität, commit=300 auf den QLC-Mounts — Metadata-Commits alle fünf Minuten statt alle 30 Sekunden, grob 10× weniger Metadata-Write-Amplification — und ein gedeckeltes Journal, weil dessen Writes um dieselben Cache-Blöcke konkurrieren.
Page Cache ist Speicher
Der 9. August, zweimal in einer Nacht. Mein Auto-Commit-Daemon liest bei der Discovery 260+ Git-Repos, und der Kernel bucht ~16 GB Page Cache auf sein Cgroup. Das Cgroup hat ein 16-GB-Ceiling, also fängt der Kernel an zurückzuholen — Page Cache, höflich, ohne OOM-Kill; der Kill-Zähler blieb die ganze Zeit auf null. Der Daemon liest sofort wieder, was gerade verdrängt wurde. Und wieder: 27.312 Boundary-Hits, 91 % CPU, und die systemweite Memory-Pressure bei 95 %, bis der Watchdog gefeuert hat.
Dabei ist nie Speicher ausgegangen. Der Speicher war voll mit Cache für eine Schleife, die nicht aufhört zu lesen. Der Fix war langweilig: ein weiches Limit unter dem harten, damit der Kernel vor der Klippe drosselt, und weniger Discovery-Worker.
Ein mmap ist ein Swap-Vertrag
Der 22. August, zweimal in einer Nacht. Ich betreibe einen lokalen LLM-Server, der 21,6 GB Model-Weights mmap-t — Shared Memory, und Shmem kann der Kernel auf genau einem Weg verdrängen: Swap. Mein Swap ist zram. Um 22:25 zeigte der OOM-Dump des Kernels zram bereits bei 100 % seiner 29,5 GB, womit das Modell permanent unvertreibbar war. Monitoring hatte um 23:44 und 23:50 alarmiert. An Discord. Wo niemand saß. Das Journal bricht um 00:27 ab.
Die Forensik aus der oomd-Ära erklärt, warum das immer wieder passierte: Unter anhaltendem Druck hat ein flacher 60-Sekunden-Restart die 22 GB Weights alle 2–3 Minuten neu gelesen. Acht Kills, rund 180 GB NAND-Reads in 20 Minuten, die SSD macht Strafarbeit für eine Config-Entscheidung. Das Modell startet jetzt mit exponentiellem Backoff, statt alle paar Minuten 21,6 GB neu zu lesen.
Die Durchschnitte haben es versteckt
Der 31. August, der demütigende. Die Kiste ist eingefroren mit leerem zram, freiem Speicher und null OOM-Kills — jeder Schwellwert, den ich überwacht habe, war komfortabel. Was tatsächlich passiert ist: Nach einem neuntägigen Backup-Ausfall (dieselbe Freeze-Nacht hat auch mein externes Disk-Shelf gekillt) haben alle persistenten Timer beim Boot gleichzeitig nachgeholt. Eine Backup-Neusendung im Terabyte-Bereich, zwei wöchentliche Scrubs, und ein LLM, der bei jedem Startversuch gecoredumpt ist — drei Crashes, jeder mit einem 21,6-GB-Cold-Read von je 27–43 Minuten, der vierte lief noch, als das Journal abriss. Vier Full-Disk-Reader auf einer QLC-SSD.
Die Memory-Pressure kam in Schüben: PSI avg10 über 50 %, wiederholt, zwei Stunden lang. Mein neuester Guard-Schwellwert hat den Ein-Minuten-Durchschnitt beobachtet, der im ganzen Boot nie über 3,93 % kam; die letzte Messung vor dem Freeze las 0,49 %. Der Teil, auf den ich am wenigsten stolz bin: Diesen Schwellwert habe ich nach genau dieser Incident-Klasse gebaut, gegen einen synthetischen 55-%-Durchschnitt getestet, und nie gegen die echte Telemetrie geprüft — die nie annähernd so etwas produziert hat. Phantom-Schutz, kalibriert gegen ein Szenario, das ich mir ausgedacht habe. Ersetzt habe ich ihn durch einen Leaky Bucket über die episodischen Spitzen — denn die Schübe waren das Signal, und die Schübe waren die ganze Zeit da.
Die mit meinen Fingerabdrücken
- Zwei Metadata-Exhaustion-Freezes im Juni. Einmal, weil ich
btrfs balanceundnix-collect-garbagegleichzeitig auf einem zu 97 % vollen Dateisystem laufen ließ. Einmal, weil mein nächtlicher GC nicht wusste, dass BTRFS-Chunks existieren: Dateien löschen gibt Extents innerhalb bereits allokierter Chunks frei, und als das Device bei 100 % Allokation mit 91 % Metadata ankam, hatten die Transaktionen für die Löschungen selbst keinen Platz mehr. - Drei Freezes im Juli waren meine eigenen Prozesse, die RAM gefressen haben: Eine neunstündige Crush-Coding-Session mit einem Peak von 40,2 GB und 118,6 GB Writes, und ein durchgehangener
bun testbei 61 GB. Der Kernel-I/O-Stack ist beide Male unter meinem eigenen User-Slice eingefroren. - Mein Emergency-Guard ist am 22. August achtzig Minuten lang sieben Mal angesprungen, weil er ständig die Last neu angefahren hat, gegen die er verteidigen sollte. Er hat den LLM-Service gestoppt, aber seinen Socket offengelassen, und systemd spawnt ihn beim nächsten Verbindungsaufbau gerne neu — samt komplettem 21,6-GB-Cold-Load. Der Guard stoppt jetzt beides. Der Schwellwert eines Guards muss von dem Ding wieder erfüllbar sein, das er bewacht; meiner war es nicht.
- Und an einem Morgen, als das Device bei 0 % unallocated und zram bei 85 % stand, habe ich
btrfs balancevon Hand gestartet. Die geplanten Balances hatten sich an dem Morgen korrekt selbst übersprungen. Der Kernel ist in zweieinhalb Minuten gestorben.
Teil 2 — die Software: alles über der SSD
Die Hardware setzt die Decke; die Software entscheidet, wo zwischen 7 GB/s und 0,3 MB/s du tatsächlich lebst. Mein Git-Log aus diesem Sommer liest sich wie eine Fieberkurve: „disable NVMe discards at block layer to prevent data corruption“, „mitigate QLC NAND SLC cache exhaustion causing WDT crashes“, „force fstrim daily schedule to prevent SLC cache exhaustion regression“. Dieser Abschnitt ist dieses Log, erklärt.
Die Mount-Option, die den Kernel eingefroren hat
Meine Mounts trugen discard-Varianten — kontinuierliches TRIM, die Lehrbuch-Empfehlung. Auf dieser SSD war das Gift. Die iostat-Aufzeichnung vom 8. Juli zeigt warum: 86 Discards pro Sekunde, jeder 253 ms am Device, die Queue 71 tief, und Btrfs-Commit-Zeiten von 17,8 Sekunden, während das Dateisystem Metadata durch einen Controller schieben wollte, der mit NAND-Rechnen beschäftigt war. Der Corruptions-Scan danach zählte 91.561 Checksummen-Fehler. Die Discards waren der I/O-Workload, der alles andere verhungern ließ.
Der Fix klingt nach Ketzerei: nodiscard auf jedem Mount, TRIM verschoben auf einen täglichen fstrim-Timer mit Idle-I/O-Priorität. Die Arbeit bündeln, terminieren, wenn sonst niemand die SSD will, und klein genug halten, dass kein Rückstau entsteht: Der Wochen-Default hatte 446 GiB veraltete Blöcke anwachsen lassen, und das Ausräumen dauerte 74 Minuten. Tägliche Läufe trimmen den Umsatz eines Tages — 50–100 GiB in 10–15 Minuten. Nach dem Remount allein, ohne eine geänderte Datei, sprang derselbe sequenzielle Read von 14,4 auf 1.276 MiB/s. Neunundachtzigfach, durch eine Mount-Option.
Drei weitere Optionen verdienen ihren Platz auf den QLC-Mounts. commit=300: Metadata-Commits alle fünf Minuten statt alle 30 Sekunden, grob 10× weniger Metadata-Write-Amplification, und ein Fünf-Minuten-Datenverlustfenster, das ich mit täglichen Snapshots akzeptiere. noatime, weil ein Zeitstempel pro Lesezugriff reine Reibung ist. Und space_cache=v2, der moderne Free-Space-Cache. Zwei Dinge bleiben absichtlich draußen: compress-force — die Upstream-Doku sagt, die Heuristiken sollen entscheiden, und das normale compress=zstd halbiert meinen nix-Store ohnehin — und qgroups, die jede Transaktion für eine Abrechnung besteuern, die ich nicht konsumiere.
Dateisysteme sind Policies, keine Geschwindigkeiten
Bevor ich /nix verschoben habe, habe ich ext4, XFS und Btrfs auf der echten Samsung gebenchmarkt, unter Live-Last. Das Ergebnis war ein Unentschieden: 4K-Random-Reads zwischen 17,0k und 18,1k IOPS über alle drei, fsync innerhalb von 3 ms voneinander, die Streuung zwischen den Läufen größer als jeder Dateisystem-Unterschied. Wer bei einer Last wie meiner eine Dateisystemwahl als Geschwindigkeitssieg verkauft, verkauft Rauschen.
Also habe ich nach Policy gewählt. /nix ist Btrfs wegen der Kompression: zstd speichert eine 629-Pfade-Stichprobe mit 1,89×, das nix-Projekt hat die Store-Preallocation vor Jahren genau deshalb abgeschaltet, und der Store ist zum halben Platz umgezogen. Die ClickHouse-Telemetrie — 180 GB am Tag an asynchronen Writes, auf die niemand wartet — bekam eine eigene XFS-Partition: append-lastig, kein Copy-on-Write, keine Snapshots, die man bezahlt. Die heißen Datenbanken ziehen auf ein nodatacow-Subvol auf der Samsung: In-place Writes, keine Prüfsummen pro Block, fsync im 1–2-ms-Bereich. Das trägt eine dokumentierte Landmine — No-CoW gilt nur für Extents, die sich kein Snapshot teilt, also macht ein einziger geplanter Snapshot das Subvol still wieder zu CoW. Die Policy sagt schlicht: „niemals snapshotten“. Ich schreibe Policies für die Version von mir, die nachts um 2 Uhr auf Dinge klickt.
Ein Btrfs-Hygiene-Set rundet das ab, weil zwei Juni-Freezes vom Allocator kamen, nicht von der SSD: df lügt auf Btrfs — freier Platz liegt in Chunks, nach Typ getrennt, und Dateien löschen gibt Extents innerhalb allokierter Chunks frei, ohne dem Device etwas zurückzugeben. Bei 100 % Allokation mit 91 % Metadata hatten die Transaktionen für die Löschungen selbst keinen Platz mehr. Die Zähler beobachten jetzt die Chunk-Allokation direkt; der Garbage Collector weigert sich, unter 5 GiB unallocated oder über 90 % Metadata zu laufen; ein wöchentlicher, begrenzter Balance gibt leere Chunks in Happen zu zehn zurück; und eine 10-GiB-Notfallreserve liegt auf dem Dateisystem, damit eine volle Platte nie den Weg zur Reparatur blockiert.
zram ist eine Speicher-Strategie, und ich hatte sie verkehrt herum
Es gibt keinen Disk-Swap auf dieser Maschine, mit Absicht: Swappen auf die QLC hieße, RAM mit den langsamsten Writes der Kiste zu bestrafen. Swap ist zram — komprimierte Pages im RAM — und alles an seinem Tuning ist kontraintuitiv.
Ich habe ihn dreimal vergrößert, bevor ich die Anzeige verstanden habe. Bei 30 % des RAM saß das Device im Dauerzustand bei 97 % Füllstand: 27,4 GiB swapped Pages, komprimiert auf 10,7 GiB physisch bei 2,6×, während 53 % des Speichers verfügbar waren und die Memory-Pressure bei 0,26 % lag. Eine vollkommen gesunde Maschine, deren Swap-Anzeige „kritisch“ sagte. Der Füllstand ist nur mit Headroom ein Klippensignal. Bei 50 % (~47 GiB) liest dieselbe Last 58 %, die Shmem-Klippe rückt ~19 GiB weiter raus, und der Leerlauf kostet null — zram allokiert physische Pages nur für tatsächlich gespeicherte Daten.
Die Kompressionsstufe ist ein Benchmark, kein Glaube: zstd Level 1 liefert 2,85× bei 373 MiB/s, der Kernel-Default Level 3 liefert 2,90× bei 334 MiB/s, Level 5 liefert 2,96× bei 172 MiB/s — 48 % langsamer für 2 % mehr Ratio. Auf 4-KiB-Pages finden die höheren Stufen keine Muster, die ihre CPU bezahlen. Also Level 1.
vm.swappiness habe ich zweimal spektakulär falsch gemacht, bevor ich die Kernel-Doku gelesen habe. Bei 1 („nie swappen“) ist die Kiste im Mai OOM-gestorben, weil der Kernel Page Cache in Disk-I/O recycelt hat, statt RAM zu komprimieren. Bei 10 hat er weiter File-Pages gegenüber zram bevorzugt, und der August brachte BTRFS-Writeback-Stürme. Bei 150 bevorzugt der Kernel zram — ~370 MiB/s Kompression im RAM — statt Page Cache zu verdrängen, der bei ~253 ms pro QLC-Write zurückkommt. Die Doku ist knapp: für zram, nimm 100+. Diese eine Zahl war die größte einzelne I/O-Reduktion auf der Kiste.
Dann das Geständnis. Vor ein paar Tagen habe ich alle aktuellen NixOS-, Arch- und Distro-Guides gegen diese Config geprüft: 20 von 23 Domains gleichauf oder besser, die meisten Abweichungen absichtlich und dokumentiert — und zwei zram-Sysctls waren die ganze Zeit still falsch. page-cluster steht default auf 3 und liest acht Pages pro Swap-Fault vor: eine Seek-Optimierung für Platten — und verbrennt CPU, um Pages zu dekomprimieren, die niemand bestellt hat. watermark_boost_factor steht default auf 15000 und erzwingt Reclaim bei Fragmentierungs-Hinweisen, eine bekannte Stutter-Quelle. Fedora, ChromeOS und Pop!_OS liefern für beide Null. Meine Config war beim Storage besser als die Wikis und ist trotzdem noch mit einem Readahead aus den Neunzigern über komprimierten RAM gelaufen.
Die Kernel-Regler, die gezählt haben
Der Rest des vm.*-Blocks, jede Zeile mit einer Freeze-Klasse dahinter: dirty_ratio=5 mit dirty_background_ratio=1 lassen Writeback bei ~940 MB beginnen und bei ~4,7 GB fertig werden — Flushes in kleinen, gleichmäßigen Happen statt in Bursts, die den SLC-Cache monopolisieren. vfs_cache_pressure=150 bevorzugt Dentry- und Inode-Cache zurückzuholen — billig, ohne Disk-I/O — statt Page Cache. watermark_scale_factor=100, weil der alte Wert 10 Panic-Reclaim verursachte: nichts, nichts, dann ein plötzlicher I/O-Sturm. min_free_kbytes=2 GB hält Puffer für Kernel- und GPU-Allokationen. Und MGLRU läuft mit 1.000 ms Mindest-TTL, der dokumentierte Sweet Spot für heiße Pages unter Druck.
Der Scheduler und die Messgeräte
Der konventionelle Rat ist Scheduler none für NVMe — die SSD soll das selbst sortieren. Der Rat nimmt an, dass eine SSD genau eine Persönlichkeit hat. Eine QLC, die je nach Cache-Zustand mit 20 µs oder 253 ms antwortet, bricht die Annahme so gründlich, dass sich das blk-cgroup-Kostenmodell gar nicht gegen sie kalibrieren lässt. Diese Kiste läuft absichtlich mit BFQ auf der NVMe: Er priorisiert interaktive I/O über Bulk-Writer, und jeder Service ist in Tier getaggt — sshd im Interactive-Tier, danach Desktop, Services, Datenbanken, Hintergrund, Builds, und Maintenance-Jobs wie fstrim auf Idle-Priorität mit Nice 10. Wenn die SSD würgt, ist der Shell-Prompt trotzdem erster in der Schlange.
Dann die Messgeräte, weil keine der Reparaturen ohne sie auffindbar gewesen wäre. /proc/pressure (PSI) meldet, wie viel der letzten 10 und 60 Sekunden einige Tasks — oder alle Tasks — auf I/O oder Speicher gestanden haben. Die chronische 42-%-I/O-Baseline stand den ganzen Sommer in diesen Dateien. Zwei Leseregeln haben die Post-Mortems überlebt: some avg10 auf Schübe beobachten, weil meine 60-Sekunden-Durchschnitte im Boot, der einfriert, nie über 3,93 % kamen — die letzte Messung lag bei 0,49 %, vier Minuten vor dem Tod; und auf Episoden reagieren, nicht auf Pegel — der Ersatz-Guard zählt avg10-Spitzen in einem Leaky Bucket und löst auf dem Zählerstand aus. Das Deploy-Skript weigert sich jetzt auszuliefern, wenn Memory-Pressure 20 % überschreitet oder zram über 95 % sitzt, während die Pressure steigt. Und ein SMART-Zähler ist mehr wert als jedes Dashboard: Unsafe Shutdowns. Meiner erreichte Anfang August 58 — 46 % von 126 Power Cycles. Jeder Freeze, den der Watchdog eingesammelt hat, hat einen dazugelegt. Die SSD hat protokolliert, was die Dashboards verpasst haben.
Reboot-ready ist eine Boot-Menü-Aussage
Mit 4.414 von 4.414 Store-Paths nachgewiesen auf der Samsung habe ich „reboot-ready“ in meine Notizen geschrieben. Dann habe ich ins Boot-Menü geschaut. Der Deploy davor hatte das neue System aktiviert, ohne einen Boot-Eintrag zu schreiben: Das Build-Tool war bei der Test-Aktivierung über zwei fehlgeschlagene Services abgebrochen, und der Rescue-Pfad meines Deploy-Skripts liest genau diesen Fehler als „aktiviert trotzdem, Fehler kosmetisch“. Ein Reboot in dem Moment hätte still die Generation vom Vortag gebootet, die komplette Migration rückgängig gemacht — und jeder Health-Check wäre die ganze Zeit grün geblieben. Zwei Kommandos haben es repariert — das System-Profil auf das aktivierte System zeigen, den Boot-Eintrag schreiben —, danach hat der Init-Pfad auf der neuen SSD verifiziert.
Dieselbe Fehlerklasse kam zwei Tage später wieder, ärgerlicher. Eine parallele Session hat vierzehn Generationen im Fenster vor dem Reboot deployt, der Default des Boot-Menüs ist auf Store-Paths gewandert, die nur auf der QLC existierten, und der Reboot um 2 Uhr nachts hing, bevor journald überhaupt starten konnte: keine Logs, kein Panic, eine Maschine, die auf einen Kernel wartet, der nicht mehr gemountet war. Zwölf Stunden später habe ich von Hand einen „älteren“ Eintrag gewählt — und es war die Flip-Generation selbst. Die Maschine war die ganze Zeit schon migriert. Generierungsnummern lügen über einen Store-Wechsel. Alle vierzehn toten Einträge sind inzwischen weggeräumt, und jeder künftige Build landet direkt auf der Samsung — die Drift-Klasse ist strukturell weg.
Jeder Fix in dieser Hälfte ist eine Zeile in einer NixOS-Config. Der Diff ist die Dokumentation, der Rollback eine Generation zurück, und die Mount-Optionen werden zur Eval-Zeit geprüft — eine ungültige Kombination stirbt im Build, nicht in einer Emergency Shell um 2 Uhr nachts.
Der Fix lag in einer Schublade
Ein alter Windows-Laptop ist vor ein paar Jahren gestorben. Seine Samsung 970 EVO Plus — 1 TB, TLC, PCIe 3.0 — lag seitdem in einem USB-C-Gehäuse, BitLocker inklusive. Im August habe ich das endlich erledigt: dislocker hat sie read-only entriegelt, rsync hat 178 GiB — 550.573 Einträge — mit 148 MB/s auf den RAID1-Pool gebracht, ein zweiter Dry Run hat null Unterschiede verifiziert, und die SSD wurde gewiped und in den freien M.2-Slot des EVO-X2 eingebaut — mit 98 % ihrer Lebenserwartung. Gesamte Hardware-Kosten: ein Schraubendreher. Die Quittung für diesen Kauf ist die Tabelle oben.
Bevor ich irgendwas verschoben habe, habe ich jeder Workload eine Frage gestellt: Wenn diese I/O passiert, wer wartet synchron darauf? Ein Tastendruck, ein Shell-Prompt, ein Git-Push, ein Datenbank-Fsync — da wartet jemand. Ein Modell, das lädt, ein Backup, das streamt, ein Telemetrie-Write — da wartet niemand. Das teilt die Maschine in drei Tiers: Der RAM nimmt die heißen Working Sets, die Samsung jeden synchronen Cold Path, die QLC und der HDD-Pool das Bulk-Streaming. Eine zweite Frage zählt genauso: Drückt die Workload das Zeug über ihr aus dem RAM? Ein 21-GB-Modellload, der den Page Cache deiner Shell verdrängt, gehört ins untere Tier — neben alles andere, worauf niemand wartet.
Der Store zieht um
/nix zuerst: 129 GB Store, hinter denen jeder Process Spawn ansteht — heute bedient mit 2,4 GB/s und 27 µs pro Cache-Miss, statt auf der QLC in der Queue zu stehen. Die Dateisystem-Policy stand wegen der Benchmarks oben fest; der Store ist auf Btrfs zum halben Platz umgezogen, und der neue Pool wurde mit block-group-tree formatiert — ein Problem, das du einmal bei mkfs löst und nie wieder.
In der Migrationsreihenfolge gibt es einen Schritt, der gebissen hat: Nach dem Deploy nochmal synchronisieren. Der Deploy schreibt neue Store-Paths auf die alte SSD, nachdem dein erstes rsync fertig ist; wer den zweiten Sync überspringt, bootet in eine unvollständige Closure. Das Stage-2-Init liegt in /nix/store, was den Mount neededForBoot macht — ein Flag, das ich mir inzwischen auswendig buchstabieren kann.
Was die QLC behält
Alles, worauf niemand wartet. 589 GB KI-Modelle — Cold Loads sind Bandbreite, und der Retry-Storm, der sie früher neu gelesen hat, ist mit Backoff und Socket Activation gefixt, nicht mit einer schnelleren SSD. Steam, 106 GB. Und die ClickHouse-Partition: 180 GB asynchrone Telemetrie-Writes pro Tag, mit Absicht in die billigsten Zellen der Kiste.
Und eine Lücke, schriftlich festgehalten: Alles oben sichert auf einen RAID1-Pool im selben Gehäuse. Das überlebt eine tote NVMe, aber kein Hausfeuer.
Was jetzt läuft
| Einstellung | QLC-Root und /data |
Samsung (tlc) |
|---|---|---|
| Mount-Optionen | noatime,nodiscard,space_cache=v2,commit=300, compress=zstd |
Dito, ohne commit=300 — Default-Commits alle 30 s; commit=300 ist eine QLC-Erhaltungsmaßnahme, keine TLC-Maßnahme |
| TRIM | Täglicher fstrim mit Idle-Priorität |
Füllt das SLC-Write-Budget nachts statt wöchentlich auf |
| Scrub | Wöchentlich mit Deferral-Guard | Skippt bei I/O-Druck oder während ein Backup streamt; die Verlegung selbst wird überwacht |
| Balance | Wöchentlich, begrenzt, gegated | Gibt leere Chunks zurück, bevor GC sie braucht, in begrenzten Happen |
| qgroups, bees | Aus | Eine Metadata-Steuer pro Write und Random-4K-Hashing kaufen auf diesem NAND nichts |
| zram | 50 % des RAM (~47 GiB), zstd(level=1), swappiness=150, page-cluster=0, watermark_boost_factor=0 |
Maschinenweit — siehe das Geständnis oben |
| Scheduler | bfq auf jeder NVMe/SATA-SSD, Services in Prioritäts-Tiers |
Interaktive I/O steht vorne in der Schlange, wenn die SSD würgt |
Prüf deine Kiste
Sechs Kommandos, fünf Minuten:
sudo smartctl -a /dev/nvme0 | grep -i unsafe
btrfs filesystem df /
compsize /nix
cat /proc/pressure/io
cat /sys/block/nvme0n1/queue/scheduler
zramctl
Das erste druckt deinen Unsafe-Shutdown-Zähler — meiner hatte Anfang August 58 erreicht, und jeder harte Reset seitdem hat einen dazugelegt. Das zweite zeigt die Chunk-Allokation; Metadata über 90 % ist der Vorbote, der die Juni-Freezes gekillt hat. Das dritte druckt, was deine Kompressions-Mount-Option wirklich spart; nahe 1,0 ist die Option Dekoration. Das vierte: Lies some avg10, nicht nur die Minuten-Durchschnitte — die Schübe sind das Signal. Das fünfte sagt dir, wer schlichtet, wenn die SSD würgt; wenn dort none steht und deine SSD zwei Persönlichkeiten hat, kennst du jetzt den Regler. Das sechste zeigt zram-Füllstand und Kompressionsratio — ein volles zram mit Headroom ist eine Anzeige, ein volles zram ohne Headroom ein Countdown. Und nach jedem Deploy, der /nix anfasst: ins Boot-Menü schauen — die Aktivierung und der Boot-Eintrag sind zwei verschiedene Dinge.
Das komplette Archiv — ein Post-Mortem für jeden der sechzehn, plus jeden Guard — liegt in SystemNix. Deine SSD ist wahrscheinlich in Ordnung. Prüf, was du sie tun lässt.
Und wenn deine Maschinen sich danebenbenehmen, während jedes Dashboard grün bleibt — genau diese Sorte Chaos werde ich bezahlt, um sie zu entwirren. Erzähl mir von deinem.