<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Onkar Raghunath Shelke</title>
    <description>The latest articles on DEV Community by Onkar Raghunath Shelke (@onkar_s).</description>
    <link>https://dev.to/onkar_s</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1226673%2F3b0c6447-024c-4cc8-b04e-ec18d9339d4a.jpeg</url>
      <title>DEV Community: Onkar Raghunath Shelke</title>
      <link>https://dev.to/onkar_s</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/onkar_s"/>
    <language>en</language>
    <item>
      <title>Deleting the corrupted chunk file made it worse</title>
      <dc:creator>Onkar Raghunath Shelke</dc:creator>
      <pubDate>Sat, 26 Sep 2026 02:26:35 +0000</pubDate>
      <link>https://dev.to/onkar_s/deleting-the-corrupted-chunk-file-made-it-worse-2oah</link>
      <guid>https://dev.to/onkar_s/deleting-the-corrupted-chunk-file-made-it-worse-2oah</guid>
      <description>&lt;p&gt;A Prometheus pod went into &lt;code&gt;CrashLoopBackOff&lt;/code&gt; and stayed there. Nine restarts, the same fatal error every time, the first one logged at 10:06 UTC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;level=ERROR source=main.go:1409 msg="Fatal error"
  err="opening storage failed:
       /prometheus/chunks_head/000225: invalid magic number 0"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pod ran with a PVC-backed TSDB. Something had killed the process while it was mid-write, and one file in &lt;code&gt;chunks_head/&lt;/code&gt; had been created but never got its header written. Zero bytes where the magic number should be.&lt;/p&gt;

&lt;p&gt;The fix looks obvious. One file is bad, so delete the bad file. That's what makes this incident worth writing up, because deleting it makes things worse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;level=ERROR source=main.go:1409 msg="Fatal error"
  err="opening storage failed: found unsequential head chunk files
       /prometheus/chunks_head/000224 (index: 224) and
       /prometheus/chunks_head/000226 (index: 226)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the sequence has a hole in it and Prometheus won't open the directory at all. You've traded one broken file for a broken directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in those files
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;chunks_head/&lt;/code&gt; holds memory-mapped chunk files, numbered in sequence. They're the on-disk side of Prometheus's in-memory head block: the couple of hours of samples that haven't been compacted into a persistent block yet. Each file grows to at most 128 MiB, and when the next write won't fit, Prometheus cuts a new file with the next number in the sequence. That's why the numbers climb to &lt;code&gt;000225&lt;/code&gt; on a busy instance rather than sitting at single digits.&lt;/p&gt;

&lt;p&gt;Each file opens with an 8-byte header: a 4-byte magic number, a 1-byte version, then 3 bytes of padding. From &lt;code&gt;tsdb/chunks/head_chunks.go&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// MagicHeadChunks is 4 bytes at the beginning of a head chunk file.&lt;/span&gt;
&lt;span class="n"&gt;MagicHeadChunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0x0130BC91&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prometheus preallocates the file before it writes into it, and writes pass through a 4 MiB buffer on the way to disk. So there's a real window in which the file exists and its header doesn't. Kill the process inside that window and you get a file full of zeros. Zero isn't &lt;code&gt;0x0130BC91&lt;/code&gt;, so validation rejects it, and the error prints the value it found in hex. That's the &lt;code&gt;invalid magic number 0&lt;/code&gt;. Unless the zeroed file happens to be the newest one, which turns out to matter a lot. More on that below.&lt;/p&gt;

&lt;p&gt;Two checks run over that directory at startup, and they are exactly the two failures above. One function walks the files, rejects a sequence with a gap in it, then rejects any file whose magic doesn't match. Both are fatal. The gap check runs first, which is why deleting the bad file swaps one error for the other instead of getting you halfway.&lt;/p&gt;

&lt;p&gt;You don't need Prometheus to apply those rules yourself. Forty lines of Python will do it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;MAGIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x0130BC91&lt;/span&gt;  &lt;span class="c1"&gt;# MagicHeadChunks
&lt;/span&gt;&lt;span class="n"&gt;HEADER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;          &lt;span class="c1"&gt;# 4 magic + 1 version + 3 padding
&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks_head&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getsize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;HEADER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;short file (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;magic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;I&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;magic&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;MAGIC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid magic number &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;magic&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; files, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;..&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;why&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  CORRUPT  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;why&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  GAP      &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;06&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;06&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: Prometheus will refuse to open this&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point it at a directory with a zeroed file in the middle and it reports what Prometheus reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 files, 000224..000226
  CORRUPT  000225: invalid magic number 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete the offending file, run it again, and the second failure is waiting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2 files, 000224..000226
  GAP      000224 -&amp;gt; 000226: Prometheus will refuse to open this
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this against a copy of the directory before you touch the real one. It tells you whether you're dealing with one bad file or several, and that decides how much you're about to throw away.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the process died
&lt;/h2&gt;

&lt;p&gt;The incident record doesn't say what killed it. The usual candidates all leave the same file on disk: an OOM kill, which shows up as exit code 137 and is easy to confirm from the pod's last state; a SIGKILL after the termination grace period expired during a rollout or a node drain; a node that lost power or was force-deleted; or kubelet evicting the pod under disk or memory pressure. Any of those can land inside the window between a chunk file being created and its header reaching disk.&lt;/p&gt;

&lt;p&gt;Worth checking anyway, because the class of death tells you whether it'll happen again next week. If it was an OOM kill, the memory limit is the real bug and the corrupt chunk file is a symptom. If it was a grace-period timeout, Prometheus needed longer to shut down cleanly than it was given, and raising &lt;code&gt;terminationGracePeriodSeconds&lt;/code&gt; is cheaper than another one of these.&lt;/p&gt;

&lt;p&gt;Except the obvious story doesn't quite survive contact with the source, and this is the part of the incident I still can't close. When a process is killed abruptly, the file it was in the middle of cutting is by definition the newest file in the directory. Prometheus handles that case on its own, as the next section explains, and nobody ever sees an error. For this to reach a human at all, &lt;code&gt;000225&lt;/code&gt; needed at least one file after it, and the second error confirms there was one: it names &lt;code&gt;000226&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So something zeroed a file that wasn't the newest, and a process dying mid-write doesn't explain that. The candidate I'd look at first is the storage layer underneath the PVC losing a writeback that Prometheus believed had landed. That's a hypothesis, not a finding, and I don't have the evidence to settle it. But it does change what you check after clearing the directory. If a not-newest file was zeroed, the interesting question isn't how the pod died. It's whether that volume acknowledges writes it hasn't actually committed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Prometheus didn't fix this itself
&lt;/h2&gt;

&lt;p&gt;Prometheus repairs a lot of its own storage damage, which is why this crashloop is surprising once you know that. It's worth laying out what it does and doesn't handle, because the pattern is not obvious from the outside.&lt;/p&gt;

&lt;p&gt;Start with the one that's most relevant here, because it's the reason this incident is odd rather than routine. Before any validation runs, &lt;code&gt;openMMapFiles&lt;/code&gt; calls &lt;code&gt;repairLastChunkFile&lt;/code&gt;. That function looks at the highest-numbered file only. If it's too short to hold a magic number, or its magic reads as zero, it deletes the file and carries on without logging anything alarming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// We either don't have enough bytes for the magic number or the magic number is 0.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MagicChunksSize&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Corrupt file, hence remove it.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoveAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lastFile&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"delete corrupted, empty head chunk file during last file repair: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the exact failure this article is about, self-healed, as long as the zeroed file is the newest one. Which is the common case, and which is why most people never see this error at all.&lt;/p&gt;

&lt;p&gt;Write-ahead log corruption is repaired too. When the head hits a corrupt WAL record during init, Prometheus increments &lt;code&gt;prometheus_tsdb_wal_corruptions_total&lt;/code&gt; and calls &lt;code&gt;Repair&lt;/code&gt;, which truncates the log at the last good record and carries on.&lt;/p&gt;

&lt;p&gt;Corruption found while &lt;em&gt;reading&lt;/em&gt; memory-mapped chunk contents is also repaired. &lt;code&gt;Head.Init()&lt;/code&gt; loads those chunks, and if the load fails it calls &lt;code&gt;removeCorruptedMmappedChunks&lt;/code&gt;, deletes the bad files, and replays the WAL to rebuild what they held. There's a metric for that too, &lt;code&gt;prometheus_tsdb_mmap_chunk_corruptions_total&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Neither path ran here, and the reason is a call-ordering detail. The magic-number and sequence checks don't live in &lt;code&gt;Head.Init()&lt;/code&gt;. They run earlier, in &lt;code&gt;openMMapFiles&lt;/code&gt;, which is called from &lt;code&gt;NewChunkDiskMapper&lt;/code&gt; while storage is still being opened. When they fail, &lt;code&gt;tsdb.Open&lt;/code&gt; returns an error and the process exits. That's why the log says &lt;code&gt;opening storage failed&lt;/code&gt; rather than anything about chunks. The repair logic sits behind a door that never got opened.&lt;/p&gt;

&lt;p&gt;There's a wrinkle worth knowing if you go reading that code. The comment above the magic check says the error is deliberately passed up so the Head's repair mechanism can deal with it. In practice the failure here happened while storage was being opened, which is upstream of that mechanism, so the process died instead. Whatever the intent, the observed behaviour is a crashloop.&lt;/p&gt;

&lt;p&gt;So the rule of thumb is four-tiered. A zeroed newest chunk file: deleted quietly before you notice. Bad WAL records: self-healed. Bad chunk contents: self-healed. A chunk file set that fails validation at open, which means a gap, a bad magic number anywhere but the last file, or an unrecognised format version: fatal, and yours to fix by hand. A missing header on a file that isn't last is the fourth kind, and that's this incident.&lt;/p&gt;

&lt;p&gt;One thing not to reach for: there is no &lt;code&gt;promtool tsdb repair&lt;/code&gt;. The &lt;code&gt;tsdb&lt;/code&gt; subcommands are &lt;code&gt;analyze&lt;/code&gt;, &lt;code&gt;bench&lt;/code&gt;, &lt;code&gt;create-blocks-from&lt;/code&gt;, &lt;code&gt;dump&lt;/code&gt;, &lt;code&gt;dump-openmetrics&lt;/code&gt; and &lt;code&gt;list&lt;/code&gt;, and that's true both in current Prometheus and in 3.4.0. &lt;code&gt;promtool tsdb list&lt;/code&gt; and &lt;code&gt;analyze&lt;/code&gt; are genuinely useful here for confirming your persistent blocks are fine, but nothing in promtool repairs a head chunk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovering it
&lt;/h2&gt;

&lt;p&gt;The order matters, mostly because the operator will fight you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pause the Prometheus CR first.&lt;/strong&gt; Skip this and the operator recreates the pod while you're working on the volume, and you get to do the whole thing twice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; monitoring patch prometheus prometheus &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; merge &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"spec":{"paused":true}}'&lt;/span&gt;
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; monitoring scale statefulset prometheus-prometheus &lt;span class="nt"&gt;--replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; monitoring delete pod prometheus-prometheus-0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--grace-period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nt"&gt;--ignore-not-found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pausing the individual CR beats scaling the prometheus-operator deployment to zero. It isolates the change to one instance instead of freezing reconciliation for every Prometheus in the cluster, which matters if you're running per-tenant instances and only one of them is broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mount the PVC somewhere else.&lt;/strong&gt; Use a Prometheus image rather than something like busybox, so &lt;code&gt;promtool&lt;/code&gt; is there when you want to check the blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prom-fix&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monitoring&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fix&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;quay.io/prometheus/prometheus:v3.4.0&lt;/span&gt;
      &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sleep"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3600"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/prometheus&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data&lt;/span&gt;
      &lt;span class="na"&gt;persistentVolumeClaim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;claimName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prometheus-prometheus-db-prometheus-prometheus-0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Find out where the data actually is.&lt;/strong&gt; This one costs people ten minutes. If the StatefulSet mounts the volume with a &lt;code&gt;subPath&lt;/code&gt;, the TSDB isn't at the root of the PVC. Here the pod used &lt;code&gt;subPath: prometheus-db&lt;/code&gt;, so inside the debug pod the data sat at &lt;code&gt;/prometheus/prometheus-db/&lt;/code&gt;, with &lt;code&gt;chunks_head/&lt;/code&gt; under that. Check before deleting anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; monitoring &lt;span class="nb"&gt;exec &lt;/span&gt;prom-fix &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /prometheus /prometheus/prometheus-db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Clear the whole directory, not the one file.&lt;/strong&gt; This is the part the first attempt got wrong. Removing &lt;code&gt;000225&lt;/code&gt; on its own is what produced the sequence gap. Remove all of them and Prometheus rebuilds the head from the WAL at startup. My rule now: clear the directory, never a single file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; monitoring &lt;span class="nb"&gt;exec &lt;/span&gt;prom-fix &lt;span class="nt"&gt;--&lt;/span&gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'rm /prometheus/prometheus-db/chunks_head/*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Put it back:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; monitoring delete pod prom-fix
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; monitoring scale statefulset prometheus-prometheus &lt;span class="nt"&gt;--replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; monitoring patch prometheus prometheus &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; merge &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"spec":{"paused":false}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Less than it looks like, which is the good news buried in this.&lt;/p&gt;

&lt;p&gt;The memory-mapped chunk files are not the system of record. The WAL is. Anything in &lt;code&gt;chunks_head/&lt;/code&gt; that hasn't been compacted yet is also in the write-ahead log, so clearing the directory and letting the WAL replay reconstructs the head. Your persistent blocks, the ULID-named directories holding the bulk of the history, are never touched by any of this. They were intact here and replayed cleanly.&lt;/p&gt;

&lt;p&gt;What you're risking is the window the WAL itself covers, and only if the WAL is also damaged. The head becomes compactable once it spans one and a half times the chunk range, and the chunk range defaults to two hours, so somewhere between two and three hours of samples can be sitting in the head at any moment. That's the rough size of the exposure. In this incident nothing was lost from the head at all. The visible cost was about 30 minutes with no scraping while the pod was down and being fixed, which shows up as a gap in the graphs rather than as missing history.&lt;/p&gt;

&lt;p&gt;That trade is worth stating plainly, because it changes how fast you can move. Clearing &lt;code&gt;chunks_head/&lt;/code&gt; is close to free when the WAL is healthy. Confirm the &lt;code&gt;wal/&lt;/code&gt; directory exists and isn't empty, then delete and stop agonising.&lt;/p&gt;

&lt;p&gt;I wouldn't delete a PVC for this, however tempting it looks at 3am. It's faster to type and it throws away every block on that volume, which is the one part of the storage that was never in danger. If you find yourself reaching for that, it's usually because the &lt;code&gt;subPath&lt;/code&gt; confusion above has convinced you the data is gone. It isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually needed fixing
&lt;/h2&gt;

&lt;p&gt;Nobody was paged for this. The pod sat in &lt;code&gt;CrashLoopBackOff&lt;/code&gt; and somebody noticed during a routine check.&lt;/p&gt;

&lt;p&gt;That's the real finding. A Prometheus that is down cannot alert on itself being down, and any rule evaluated by the broken instance is worth nothing while it's broken. The check has to come from outside: a second Prometheus scraping this one, a blackbox probe against its &lt;code&gt;/-/healthy&lt;/code&gt; endpoint, or an Alertmanager watchdog wired to a dead man's switch that pages when the heartbeat stops rather than when it arrives.&lt;/p&gt;

&lt;p&gt;The cheapest version, if you run kube-state-metrics and have another Prometheus that can see the namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kube_pod_container_status_waiting_reason{
  namespace="monitoring", container="prometheus", reason="CrashLoopBackOff"
} == 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give it &lt;code&gt;for: 5m&lt;/code&gt;. That metric is a gauge, so compare it against a value and leave it at that. If you'd rather alert on restarts, &lt;code&gt;kube_pod_container_status_restarts_total&lt;/code&gt; is a counter and needs &lt;code&gt;increase()&lt;/code&gt; over a window, not a bare comparison.&lt;/p&gt;

&lt;p&gt;Two more things worth doing while it's fresh. Write the recovery down, because the sequence-gap trap is exactly what gets rediscovered at 3am by someone who reads the first error and reaches straight for &lt;code&gt;rm&lt;/code&gt;. And check how the process died, since a corrupt chunk file caused by a memory limit will be back on its own schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;invalid magic number 0&lt;/code&gt; means a chunk file was created but its header never reached disk.&lt;/li&gt;
&lt;li&gt;Deleting that one file gets you &lt;code&gt;found unsequential head chunk files&lt;/code&gt; instead. Clear the whole &lt;code&gt;chunks_head/&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;Prometheus already deletes a zeroed &lt;em&gt;newest&lt;/em&gt; chunk file by itself, in &lt;code&gt;repairLastChunkFile&lt;/code&gt;. It self-repairs bad WAL records and bad chunk contents too. A chunk file set that fails validation at open is fatal, and that's this one.&lt;/li&gt;
&lt;li&gt;Which raises the question I couldn't answer: a not-newest file was zeroed, so suspect the volume, not the process.&lt;/li&gt;
&lt;li&gt;There is no &lt;code&gt;promtool tsdb repair&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Pause the Prometheus CR before touching the volume, and check for a &lt;code&gt;subPath&lt;/code&gt; before you delete.&lt;/li&gt;
&lt;li&gt;The WAL is what saves you. Persistent blocks are never involved, so don't delete the PVC.&lt;/li&gt;
&lt;li&gt;Alert on Prometheus's own crashloop from outside Prometheus, or you'll find it by accident.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>prometheus</category>
      <category>observability</category>
      <category>sre</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Kubelet watches inodes. Just not until it's an emergency.</title>
      <dc:creator>Onkar Raghunath Shelke</dc:creator>
      <pubDate>Thu, 17 Sep 2026 05:34:56 +0000</pubDate>
      <link>https://dev.to/onkar_s/kubelet-watches-inodes-just-not-until-its-an-emergency-351c</link>
      <guid>https://dev.to/onkar_s/kubelet-watches-inodes-just-not-until-its-an-emergency-351c</guid>
      <description>&lt;p&gt;A worker node pages with &lt;code&gt;NodeFilesystemFilesFillingUp&lt;/code&gt;. It's on course to run out of inodes. You check the disk first, because that's what everyone checks first, and it reads 83% used: 103G used, 22G available. Then you check inodes: 67% used, 11M used, 5.3M free.&lt;/p&gt;

&lt;p&gt;The disk is the fuller of the two, and the inode table is the one that paged. That's not a contradiction. &lt;code&gt;NodeFilesystemFilesFillingUp&lt;/code&gt; is a trajectory alert built on &lt;code&gt;predict_linear&lt;/code&gt;, so it fires on the slope, not on the level. Nothing was wrong with 67%. What was wrong was how fast it was getting there, and the fact that the only mechanism in kubelet that would have intervened is the one that evicts pods.&lt;/p&gt;

&lt;p&gt;Two notes on what follows. I no longer have access to that cluster, so I can't paste the original &lt;code&gt;df&lt;/code&gt;, &lt;code&gt;du&lt;/code&gt; or &lt;code&gt;tune2fs&lt;/code&gt; captures. Where a number comes from the incident record I say so, and where I've reconstructed something from arithmetic I show the arithmetic. The ext4 demonstrations further down you can run yourself in about two seconds.&lt;/p&gt;

&lt;p&gt;If you want to compare your own node before reading on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; / &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What kubelet actually watches
&lt;/h2&gt;

&lt;p&gt;Kubelet's defence against images filling a node is image garbage collection. Once the image filesystem passes &lt;code&gt;imageGCHighThresholdPercent&lt;/code&gt; (85 by default), it deletes unused images, oldest first, until usage drops to &lt;code&gt;imageGCLowThresholdPercent&lt;/code&gt; (80).&lt;/p&gt;

&lt;p&gt;Both of those are percentages of bytes. Image GC never looks at file counts.&lt;/p&gt;

&lt;p&gt;Kubelet isn't blind to inodes, though, and it's worth being exact here, because the detail changes the conclusion. On Linux the default hard eviction set is five signals (&lt;code&gt;pkg/kubelet/eviction/defaults_linux.go&lt;/code&gt;, quoted here at tag &lt;code&gt;v1.37.0&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;DefaultEvictionHard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="s"&gt;"memory.available"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;"100Mi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s"&gt;"nodefs.available"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;"10%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s"&gt;"nodefs.inodesFree"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;"5%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s"&gt;"imagefs.available"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;"15%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s"&gt;"imagefs.inodesFree"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"5%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both filesystems get an inode threshold, and both sit at 5% free. That's a Linux-only file. The two next to it are shorter: &lt;code&gt;defaults_windows.go&lt;/code&gt; and &lt;code&gt;defaults_others.go&lt;/code&gt; each carry three signals, &lt;code&gt;memory.available&lt;/code&gt;, &lt;code&gt;nodefs.available&lt;/code&gt; and &lt;code&gt;imagefs.available&lt;/code&gt;, with no inode thresholds at all. On Linux the coverage is there. Check it against whatever version you're running, because these files do change.&lt;/p&gt;

&lt;p&gt;So kubelet will act on inodes. The question is when. 5% free means 95% used, and that's the hard eviction floor: the point where kubelet reclaims what it can and starts evicting pods to save the node. Image garbage collection, the mechanism that runs quietly in the background before anything is on fire, triggers at 85% and stops at 80%, and both of those are bytes.&lt;/p&gt;

&lt;p&gt;Count the mechanisms watching each resource. Bytes get two: background image GC at 85%, and hard eviction at 10% free on &lt;code&gt;nodefs&lt;/code&gt; or 15% free on &lt;code&gt;imagefs&lt;/code&gt;. Inodes get one, and it's the eviction floor. There is no early trigger on inodes, so the first thing that happens when you run low is also the most disruptive thing that can happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why small files hit a wall the disk doesn't
&lt;/h2&gt;

&lt;p&gt;Quick question before the numbers. Fill a filesystem with nothing but one-byte files. What percentage of the disk is used by the time you run out of inodes? Take a guess.&lt;/p&gt;

&lt;p&gt;ext4 decides how many inodes a filesystem gets at creation time, and that number never changes afterwards. It comes from &lt;code&gt;inode_ratio&lt;/code&gt;: one inode per N bytes of capacity. The stock &lt;code&gt;/etc/mke2fs.conf&lt;/code&gt; uses 16384. And any file that isn't empty takes at least one 4 KiB block, no matter how small it is.&lt;/p&gt;

&lt;p&gt;You can see the ratio on a throwaway image file. No root needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ truncate -s 1G img &amp;amp;&amp;amp; mkfs.ext4 -q -F -i 16384 img
$ tune2fs -l img | grep -E '^(Inode count|Block count|Block size):'
Inode count:              65536
Block count:              262144
Block size:               4096
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;262,144 blocks × 4,096 bytes ÷ 16,384 is 65,536 inodes. If every file uses one block, all 65,536 inodes are gone after 256 MiB. That's 25% of the disk. If you guessed higher, most people do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch it fail
&lt;/h3&gt;

&lt;p&gt;You don't have to take the arithmetic on trust. &lt;code&gt;mkfs.ext4 -d&lt;/code&gt; builds a filesystem straight from a directory, so you can pack small files into an image without mounting anything and without root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;files
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 4000&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;printf &lt;/span&gt;x &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; files/f&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 64M img
mkfs.ext4 &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; 16384 &lt;span class="nt"&gt;-d&lt;/span&gt; files img
dumpe2fs &lt;span class="nt"&gt;-h&lt;/span&gt; img 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(Inode count|Free inodes|Block count|Free blocks):'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inode count:              4096
Block count:              16384
Free blocks:              11040
Free inodes:              84
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;97.9% of inodes used, 32.6% of blocks. The files themselves account for only 24.4 of those 32.6 points: 4,000 files at one 4 KiB block each is 15.6 MiB, and 15.6 MiB of 64 MiB is 24.4%. The remaining 8.2 points are the journal and filesystem metadata.&lt;/p&gt;

&lt;p&gt;Add 200 more files and it stops being theoretical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ for i in $(seq 4001 4200); do printf x &amp;gt; files/f$i; done
$ truncate -s 64M img2 &amp;amp;&amp;amp; mkfs.ext4 -q -F -i 16384 -d files img2
mkfs.ext4: Could not allocate inode in ext2 filesystem while populating file system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, it says ext2. That's the underlying library's error string, not a mistake. The image that worked had 84 inodes left and two thirds of its blocks free. Somewhere around file 4,085 the inodes run out, and from there the free blocks stop mattering. On e2fsprogs 1.47.2 the whole sequence runs in under two seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reverse-engineering the node's mkfs parameter
&lt;/h2&gt;

&lt;p&gt;Back to the real node, and this part is arithmetic rather than a capture, because I can't re-run anything on that cluster.&lt;/p&gt;

&lt;p&gt;11M inodes used plus 5.3M free is about 16.3 million inodes in total. The filesystem is roughly 124-133 GiB, working from 103G used plus 22G available and allowing for ext4's reserved blocks. At the stock 16,384 bytes per inode, a filesystem that size only gets 8.1-8.7 million inodes. This one already had 11 million in use, which is more than the stock ratio can even allocate. So it wasn't formatted at 16384.&lt;/p&gt;

&lt;p&gt;Divide the size by the inode count and you get roughly 7,500-9,000 bytes per inode, which lines up with 8192. At that ratio the all-small-files case exhausts inodes at 50% of the disk instead of 25%. I can't confirm it directly, but the arithmetic constrains it to about 8192, and on your own nodes &lt;code&gt;tune2fs -l&lt;/code&gt; on the root device settles it in one command.&lt;/p&gt;

&lt;p&gt;A real node is never all small files. Layer blobs and logs are big, which is why disk was the higher number here. But image GC was waiting for 85%, the file count was climbing at its own pace, and nothing in kubelet compares one against the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the millions of files
&lt;/h2&gt;

&lt;p&gt;Knowing the inodes are going doesn't tell you where they went. This does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;--inodes&lt;/span&gt; &lt;span class="nt"&gt;-xS&lt;/span&gt; /var | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rh&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both flags earn their place. &lt;code&gt;-x&lt;/code&gt; stops &lt;code&gt;du&lt;/code&gt; at filesystem boundaries, so it doesn't wander into the tmpfs and volume mounts kubelet sets up under &lt;code&gt;/var/lib/kubelet/pods&lt;/code&gt;. &lt;code&gt;-S&lt;/code&gt; counts each directory on its own instead of adding in its subdirectories. Leave it off and &lt;code&gt;/var&lt;/code&gt;, &lt;code&gt;/var/lib&lt;/code&gt; and every other parent sit at the top of the list, above the directory you're actually looking for.&lt;/p&gt;

&lt;p&gt;On this node the top of that list wasn't logs and it wasn't pod volumes. It was containerd's snapshot store, &lt;code&gt;/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/&lt;/code&gt;, where each numbered directory is one unpacked image layer. Several of those snapshots held the same path: &lt;code&gt;app/node_modules/@mui/icons-material&lt;/code&gt;, at 21,553 files each.&lt;/p&gt;

&lt;p&gt;That's one package directory out of one image. The image as a whole carried more than 40,000 files: the icon package is over half of it on its own, and the remainder is the rest of the dependency tree, the application source, and the build output, all of which shipped in the same image. Multiply 40,000 by every version a node is still holding, and millions of inodes stop looking mysterious.&lt;/p&gt;

&lt;p&gt;It's 21,553 per snapshot, not across all of them. containerd's content store dedupes compressed layers by digest, but the overlayfs snapshotter unpacks each distinct layer into its own directory and shares nothing between them. Two builds that put an identical &lt;code&gt;node_modules&lt;/code&gt; into layers with different digests leave two full copies on disk.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;node_modules&lt;/code&gt; is the well-known offender, but it isn't the only one. Any image that ships a dependency tree full of small files behaves the same way, whether that's a Python virtualenv, vendored PHP packages or a pile of Ruby gems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem was the image, not the cluster
&lt;/h2&gt;

&lt;p&gt;Nothing on the cluster was set up wrong. Kubelet, containerd and the node image were all running on defaults.&lt;/p&gt;

&lt;p&gt;The image was a single-stage build, so everything the build needed ended up in what shipped: the source, the build output, and all of &lt;code&gt;node_modules&lt;/code&gt;, dev dependencies included, since &lt;code&gt;npm install&lt;/code&gt; pulls those in by default. A single-stage build like that usually looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now look at that &lt;code&gt;COPY . .&lt;/code&gt; and ask whether the repo has a &lt;code&gt;.dockerignore&lt;/code&gt;. If &lt;code&gt;node_modules&lt;/code&gt; isn't excluded, &lt;code&gt;COPY . .&lt;/code&gt; puts a second copy of it into a layer that changes on every single commit. That's the fastest way to multiply snapshots, and it's easy to miss because the build still works.&lt;/p&gt;

&lt;p&gt;Even with a &lt;code&gt;.dockerignore&lt;/code&gt;, a CI pipeline that builds without a warm layer cache reruns &lt;code&gt;npm install&lt;/code&gt; every time. That produces a new layer digest even when &lt;code&gt;package.json&lt;/code&gt; hasn't changed, because the files inside the layer carry new timestamps. Every new digest becomes a new snapshot on every node that pulls it. containerd keeps those snapshots until the image is removed, and kubelet only removes unused images when image GC runs, which brings you straight back to that 85% byte threshold.&lt;/p&gt;

&lt;p&gt;It went unnoticed because the number most people watch, disk usage, never crossed the line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing it
&lt;/h2&gt;

&lt;p&gt;The first fix belongs in the build: a multi-stage Dockerfile. One stage installs dependencies and builds. The final image is a small web server that only receives the compiled output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nginx:alpine&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/build /usr/share/nginx/html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That should take the deployed image from more than 40,000 files down to the compiled assets plus the base image, which for a React build is typically a few dozen files rather than tens of thousands. I haven't measured both images side by side, so treat that as the expected shape rather than a benchmark. If you try it, &lt;code&gt;docker run --rm &amp;lt;image&amp;gt; find / -xdev -type f | wc -l&lt;/code&gt; on each will give you the real before-and-after.&lt;/p&gt;

&lt;p&gt;The second fix is monitoring, and credit where it's due: the upstream alert did its job here. &lt;code&gt;NodeFilesystemFilesFillingUp&lt;/code&gt; ships with the node_exporter mixin, and it's what caught this at 67%. It's a prediction, though, and predictions go quiet when growth pauses for a while. I'd add a flat threshold alongside it that fires well before kubelet's 95% eviction point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 - node_filesystem_files_free{mountpoint="/"}
  / node_filesystem_files{mountpoint="/"}
  &amp;gt; 0.80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give it &lt;code&gt;for: 30m&lt;/code&gt; so a burst of image pulls during a rollout doesn't page anyone. The 80% line is a judgement call; pick whatever leaves you enough room to act before 95%. You won't find anything like it tied to image garbage collection upstream, and that's the distinction worth holding on to: image GC's thresholds are percentages of bytes, and the inode thresholds kubelet does have belong to eviction. They're separate mechanisms, and only one of them runs early.&lt;/p&gt;

&lt;p&gt;If you run Prometheus, this one is worth having on a dashboard. It subtracts disk usage from inode usage per node, so anything near the top is burning through inodes faster than bytes, and those are the nodes where the byte thresholds will fail you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sort_desc(
  (1 - node_filesystem_files_free{mountpoint="/"} / node_filesystem_files{mountpoint="/"})
  -
  (1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"})
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adjust the &lt;code&gt;mountpoint&lt;/code&gt; label if your node_exporter reports the root filesystem differently.&lt;/p&gt;

&lt;p&gt;Neither fix does anything about what's already sitting on your nodes. Old snapshots stay until their images go. &lt;code&gt;crictl rmi --prune&lt;/code&gt; removes every image no container is currently using, and those images simply get pulled again the next time something needs them. You'll also see &lt;code&gt;crictl rm --all&lt;/code&gt; suggested for this kind of cleanup. It clears every stopped container and takes &lt;code&gt;kubectl logs --previous&lt;/code&gt; with it for anything that restarted recently. Fine in an emergency. I wouldn't put it in a cron job.&lt;/p&gt;

&lt;p&gt;If you only remember one thing from this: kubelet's background cleanup runs on bytes, so a workload that's heavy on files rather than bytes gets no early intervention at all. Something does eventually fire, at 5% inodes free, and it fires by evicting pods. That's the emergency brake, not the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your own cluster in five minutes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step one.&lt;/strong&gt; Run the fleet query above and look at anything positive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step two.&lt;/strong&gt; On the worst node, find the directories holding the most files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;--inodes&lt;/span&gt; &lt;span class="nt"&gt;-xS&lt;/span&gt; /var/lib/containerd | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rh&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step three.&lt;/strong&gt; If &lt;code&gt;node_modules&lt;/code&gt; shows up under the snapshot store, list the images on that node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;crictl images
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then go and look at how those images are built.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The inode ratio.&lt;/strong&gt; No root needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 1G img16 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mkfs.ext4 &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; 16384 img16
&lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 1G img8  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mkfs.ext4 &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; 8192  img8
tune2fs &lt;span class="nt"&gt;-l&lt;/span&gt; img16 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(Inode count|Block count):'&lt;/span&gt;
tune2fs &lt;span class="nt"&gt;-l&lt;/span&gt; img8  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(Inode count|Block count):'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get 65,536 and 131,072 inodes, both on 262,144 blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running out of inodes with most of the disk free.&lt;/strong&gt; No root needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;files &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 4200&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;printf &lt;/span&gt;x &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; files/f&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 64M img &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mkfs.ext4 &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; 16384 &lt;span class="nt"&gt;-d&lt;/span&gt; files img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fails with &lt;code&gt;Could not allocate inode&lt;/code&gt;. Drop the loop to 4,000 files and it succeeds with 84 inodes free and two thirds of the blocks unused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot duplication on a node.&lt;/strong&gt; You'll need a registry and a cluster you can deploy to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; &amp;lt;registry&amp;gt;/app:v1 &lt;span class="nb"&gt;.&lt;/span&gt;
docker build &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; &amp;lt;registry&amp;gt;/app:v2 &lt;span class="nb"&gt;.&lt;/span&gt;
docker push &amp;lt;registry&amp;gt;/app:v1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker push &amp;lt;registry&amp;gt;/app:v2
&lt;span class="c"&gt;# deploy v1, then v2, onto the same node, then on that node:&lt;/span&gt;
&lt;span class="nb"&gt;sudo du&lt;/span&gt; &lt;span class="nt"&gt;--inodes&lt;/span&gt; &lt;span class="nt"&gt;-xS&lt;/span&gt; /var/lib/containerd | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rh&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see two &lt;code&gt;node_modules/@mui/icons-material&lt;/code&gt; directories with the same file count under two different snapshot IDs.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>linux</category>
      <category>docker</category>
    </item>
  </channel>
</rss>
