<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Performance of Systems]]></title><description><![CDATA[Performance of Systems]]></description><link>https://perfguru.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 20 Oct 2026 11:53:48 GMT</lastBuildDate><atom:link href="https://perfguru.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I Doubled Block Store IO Performance: A Bottleneck Detective Story using AI
]]></title><description><![CDATA[During the development of our Block Store (User space block management layer)—a storage engine layer that uses dynamic B-tree metadata lookups for block…]]></description><link>https://perfguru.hashnode.dev/how-i-doubled-block-store-io-performance-a-bottleneck-detective-story-using-ai</link><guid isPermaLink="true">https://perfguru.hashnode.dev/how-i-doubled-block-store-io-performance-a-bottleneck-detective-story-using-ai</guid><category><![CDATA[Performance Optimization]]></category><category><![CDATA[Performance Tuning]]></category><category><![CDATA[performance metrics]]></category><category><![CDATA[performance]]></category><category><![CDATA[sequential-io]]></category><category><![CDATA[virtualization]]></category><category><![CDATA[Nutanix]]></category><category><![CDATA[Nutanix AHV]]></category><dc:creator><![CDATA[Karthik Dwarakanath]]></dc:creator><pubDate>Tue, 13 Oct 2026 21:19:23 GMT</pubDate><content:encoded><![CDATA[<p>During the development of our <strong>Block Store (User space block management layer)</strong>—a storage engine layer that uses dynamic B-tree metadata lookups for block allocation—I encountered a massive performance gap in sequential IO, compared to our internal performance target of pass-through mode without block management.<br />To resolve this disparity, I launched a systematic performance investigation. This post details how I tracked down the root cause, why early CPU profiling pointed in the wrong direction, and how targeted optimizations at the submission layer ultimately delivered a <strong>3x throughput boost</strong>—along with insights on where AI assisted along the way.</p>
<h2><strong>Setting the Goal: Block Store vs. Pass-Through</strong></h2>
<p>In a pass-through mode, guest VM I/O operations map almost directly to external storage LUNs with minimal metadata intervention. In contrast, our Block Store routes every read and write through an indexed metadata B-tree map to translate virtual disk offsets into physical block locations:</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6ace969d3241d22c3c996e03/42ab7348-400a-49f7-90eb-6e8b6d18f311.png" alt="block_store_baseline_vs_passthrough_target" /></p>
<p>The target goal for Block Store performance was to reach parity with the pass-through mode.</p>
<h2><strong>Red Herring #1: CPU Flamegraphs and Thread Offloading</strong></h2>
<p>Initial flamegraph analysis driven by AI-generated scripts flagged Map::Find as a major bottleneck in the Block Store—accounting for 6% of execution time compared to just 0.3% in the pass-through path. However, architectural expectations and code inspection suggested that key lookup overhead should have been nearly identical. It was only upon manually comparing the visual execution stacks of both Find operations that the underlying issue became clear.<br />The manual flamegraph analysis revealed that metadata lookups (Map::Find) and data write submissions were executing on the same frontend thread context. In contrast, the high-performance pass-through path processed data I/O submissions on a separate thread context, avoiding lock contention on the frontend worker thread.<br /><strong>The Fix:</strong> I refactored the execution path to offload data write submissions to a separate frontend thread. The AI LLM model was great at generating code for this patch.<br /><strong>The Result:</strong> The flamegraph stack looked much cleaner and closely mirrored the direct path. However, <strong>user level throughput barely moved</strong>. Fixing thread-level contention reduced CPU overhead, but it wasn't the primary bottleneck choking system throughput.</p>
<h2><strong>Red Herring #2 and #3: Thread Pool Queuing and I/O Pollers</strong></h2>
<p>Next, I evaluated thread pool queuing metrics and I/O event pollers (such as SPDK NVMe-oF worker threads):</p>
<ol>
<li><p><strong>IO Engine Thread Queueing Stats:</strong> I analyzed thread pool run-time versus queue wait time ratios. This stat showed a &gt;3x queue/run ratio for the baseline block store case, compared to the pass-through mode - indicating that there was a lot of queueing on the VM disk threads. <strong>The Fix:</strong> One solution I considered was to offload some of the work from the individual disk threads to the threadpool. Here again the AI LLM model was great at generating working code for the patch. I was able to confirm through stats that the offload was working as expected.<br /><strong>The Result:</strong> Offloading to control thread pools lowered queue wait times drastically (reducing queue/run ratios from ~2.5 down to ~1.02). But overall storage throughput remained capped at ~2.76 GB/s</p>
</li>
<li><p><strong>I/O Poller Serialization:</strong> I suspected that because the block store used a single consolidated storage volume connection, I/O submission/completion callbacks might be serialized on a single poller thread. To isolate this, I tested a minimal benchmark: a single VM, single disk running a 1MB sequential write workload. The pass-through path reached 2.59 GB/s, while the block store plateaued at 1.12 GB/s. Poller thread imbalance was not <strong>yet</strong> the root cause either. Some bottlenecks are secondary - they have a tendency to show up when the major one is solved.</p>
</li>
</ol>
<h2><strong>The Breakthrough: Inspecting Storage Array Telemetry</strong></h2>
<p>When internal CPU profiles and queue stats fail to reveal why system throughput is throttled, it is time to look downstream at storage-level telemetry. This highlighted the limits of current AI LLM models. Despite full access to the codebase and targeted questions, the model did not pinpoint the root bottleneck. It was only after I manually identified the underlying flaw and requested analysis of specific code paths that the model confirmed the implementation detail.<br />While issuing a <strong>1MB sequential write workload</strong> from the host application, I observed the following metrics on the storage array:</p>
<ul>
<li><p><strong>Latency:</strong> ~0.33 ms</p>
</li>
<li><p><strong>Throughput:</strong> ~3.55 GiB/s</p>
</li>
<li><p><strong>Array IOPS:</strong> <strong>~220,000 IOPS</strong></p>
</li>
</ul>
<p>1 MB / 220,000 IOPS ≈ 4 KB per I/O operation</p>
<p>The storage array was receiving <strong>256 individual 4KB I/O requests</strong> instead of single, contiguous 1MB block writes!</p>
<h2><strong>Root Cause: High-Level Block Fragmentation</strong></h2>
<p>Traced through the block engine layer, the difference between the pass-through path and the metadata-backed store became clear. With specific prompting, the model provided a detailed difference between the two implementations:</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6ace969d3241d22c3c996e03/fd1b525b-bc5f-4c93-8dc3-9e824108da40.png" alt="write_path_comparison" /></p>
<p>In the metadata-backed block store, the indexing layer allocated space by looking up block maps in 4KB metadata units. Even though the block IDs allocated for the 1MB write were physically contiguous, the engine issued <strong>256 discrete I/O calls</strong> to the storage driver.<br />This caused severe serialization and protocol-handling overhead at the user-space storage driver (SPDK) level, saturating the storage controller with millions of tiny I/Os instead of large sequential streams.</p>
<h2><strong>The Solution: Intelligent I/O Coalescing</strong></h2>
<p>To eliminate this overhead, I updated the write path to aggregate contiguous block requests before passing them to the underlying storage hardware. Here too, the model was very useful in generating a working coalescing algorithm that I could use to test and validate the theory.<br />Instead of issuing an I/O call for every 4KB metadata descriptor, the submission engine now evaluates descriptors in sequence and merges adjacent blocks:</p>
<blockquote>
<ul>
<li><strong>I/O Coalescing Logic:</strong> The write path evaluates descriptors sequentially to merge contiguous block IDs into a single combined buffer, dispatching consolidated I/O requests upon detecting continuity breaks instead of issuing hundreds of fragmented sub-block calls.</li>
</ul>
</blockquote>
<p>After validating this coalescing logic on writes, I applied a similar contiguous block aggregation algorithm to the <strong>sequential read path</strong>.</p>
<h2><strong>Results</strong></h2>
<p>Coalescing contiguous block requests eliminated I/O driver submission overhead and dramatically increased system throughput:</p>
<ul>
<li><p><strong>Sequential Read Performance:</strong> Increased from <strong>4.75 GB/s to 11.87 GB/s</strong>—a ~<strong>2.5x increase</strong>, matching pass-through performance.</p>
</li>
<li><p><strong>Sequential Write Performance:</strong> Increased from <strong>2.76 GB/s to 7.57 GB/s</strong>—a <strong>~2.7x increase</strong>.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/uploads/covers/6ace969d3241d22c3c996e03/5f3a7401-9e97-4aad-afc3-d765598c0a46.png" alt="labeled_block_store_throughput_comparison" /></p>
<table>
<thead>
<tr>
<th>Workload Metric</th>
<th>Baseline Block Store</th>
<th>Block Store + Coalescing Fix</th>
<th>Performance Gain</th>
<th>Pass-Through Target</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Seq Read Throughput</strong></td>
<td>4.75 GB/s</td>
<td><strong>11.87 GB/s</strong></td>
<td><strong>2.5x</strong></td>
<td>11.85 GB/s</td>
</tr>
<tr>
<td><strong>Seq Write Throughput</strong></td>
<td>2.76 GB/s</td>
<td><strong>7.57 GB/s</strong></td>
<td><strong>2.7x</strong></td>
<td>9.6 GB/s</td>
</tr>
</tbody></table>
<h2><strong>Engineering Key Takeaways</strong></h2>
<blockquote>
<ol>
<li><p><strong>Trace the Complete I/O Path:</strong> Insufficient metrics within the SPDK layer left a critical blind spot across the end-to-end I/O journey. Gaining full visibility into every stage of the request path is essential for uncovering true root causes.</p>
</li>
<li><p><strong>Don't Stop at CPU Flamegraphs:</strong> A clean CPU stack does not guarantee efficient I/O. CPU profiles show where threads spend time, but on a workload that is not CPU bound, I would look elsewhere.</p>
</li>
<li><p><strong>AI Tools—Double-Edged but Valuable:</strong> Although over-relying on AI suggestions led to several red herrings, these same tools significantly accelerated patch iteration and sped up the overall debugging process. The key takeaway is that asking targeted, precise questions is essential to guide AI in the right direction.</p>
</li>
</ol>
</blockquote>
]]></content:encoded></item></channel></rss>