The 6.2% Revelation: How a CPU Profile Exposed a Hidden I/O Bottleneck in a Distributed Storage System

Introduction

In the middle of a late-night debugging session on a distributed storage system, an engineer ran a five-second CPU profile on a live production node. The result was innocuous on its face—a mere 310 milliseconds of sampled CPU activity out of five seconds, yielding a utilization of just 6.2%. But this single data point, captured in a single message from an AI coding assistant to its human partner, became the fulcrum on which an entire performance investigation turned. What began as a hunt for lock contention, blocked goroutines, and mutex pressure ended with the deletion of a single line of code: a stray fmt.Println left over from development that was serializing a string to stdout on every single write operation.

This article examines that pivotal message in detail—the reasoning behind it, the assumptions it rested on, the knowledge it required, and the chain of discovery it set in motion.


The Message

The subject message, sent by the AI assistant at index 2867 of the conversation, reads as follows:

[assistant] Not many goroutines blocked on locks. Let me look at the CPU profile to see where time is being spent: [bash] curl -s "http://10.1.232.83:9010/debug/pprof/profile?seconds=5" -o /tmp/cpu.prof 2>&1 && go tool pprof -top /tmp/cpu.prof 2>&1 | head -40 File: kuri Build ID: c4dddfbc87c299bc4971370df6a0710dfaa58265 Type: cpu Time: 2026-02-05 01:17:08 CET Duration: 5s, Total samples = 310ms ( 6.20%) Showing nodes accounting for 310ms, 100% of 310ms total flat flat% sum% cum cum% 40ms 12.90% 12.90% 40ms 12.90% internal/runtime/syscall.Syscall6 40ms 12.90% 25.81% 40ms 12.90% runtime.futex 20ms 6.45% 32.26% 20ms 6.45% github.com/yugabyte/gocql.(*Conn).exec 20ms 6.45% 38.71% 20ms...

Context: The Slow Write Investigation

To understand why this message was written, we must reconstruct the debugging context. The conversation leading up to this point had been intensely operational. The user had just enabled parallel writes in a QA environment—a feature allowing multiple storage groups to accept writes concurrently instead of serializing through a single group. Two Kuri nodes (the storage backend nodes in this Filecoin Gateway architecture) had been configured with RIBS_ENABLE_PARALLEL_WRITES=true and RIBS_MAX_PARALLEL_GROUPS=2.

Shortly after enabling this feature, the user reported: "Writes failing now, investigate." The assistant dutifully checked systemd journals, grepped for errors, and tested a direct S3 write via rclone. It found no explicit failures—the parallel write stats showed TotalWrites: 2481, ParallelWrites: 2481, and WriteErrors: 0. The user pushed back: "Pretty slow and bursty writes, where are the write paths mostly sitting?"

This question reframed the investigation. The user wasn't reporting hard failures; they were reporting degraded performance—slow, bursty throughput. The assistant's first diagnostic step was to examine goroutine profiles for blocked goroutines, looking for goroutines stuck on mutexes, semaphores, or channel operations. The goroutine dump showed roughly 879 goroutines, mostly in network I/O, bitswap, yamux, and quic—normal patterns for a p2p-enabled storage node. No goroutines appeared stuck on write locks.

This is where the subject message begins.


Reasoning and Motivation: Why Switch to CPU Profiling?

The assistant's decision to pivot from goroutine profiling to CPU profiling reflects a sophisticated diagnostic intuition. The goroutine profile had ruled out one class of problem (blocked goroutines on locks), but the user's report of "slow and bursty" writes suggested the system was not keeping up with demand. The natural next question was: is the CPU busy doing work, or is it idle?

The assistant's opening line—"Not many goroutines blocked on locks. Let me look at the CPU profile to see where time is being spent"—reveals the logical chain. If goroutines are not blocked, the system is either:

  1. CPU-bound: spending time computing, which would show high CPU utilization in a profile.
  2. I/O-bound: waiting on disk, network, or other I/O, which would show low CPU utilization and many goroutines in syscall or poll states.
  3. Idle: simply not receiving enough work, which would also show low utilization. The CPU profile would distinguish between these cases. A high CPU utilization would point to algorithmic inefficiency or excessive computation in the write path. A low CPU utilization would suggest the system is waiting on something external—database queries, disk flushes, network round-trips, or some other I/O operation. The result was striking: 6.2% CPU utilization. Over five seconds, only 310 milliseconds of CPU time were sampled. This is an extraordinarily low figure for a system that should be actively processing writes. The top consumers were syscall.Syscall6 (12.9%), runtime.futex (12.9%), and gocql.(*Conn).exec (6.45%)—all I/O-related functions. The system was not CPU-bound; it was spending most of its time waiting.

Assumptions Made

The assistant's diagnostic approach rested on several assumptions:

  1. The performance issue is systemic, not environmental. The assistant assumed that the slow writes were caused by something in the software stack, not by external factors like network bandwidth, disk latency, or resource contention from other processes on the same hosts. This was a reasonable assumption given that the user had just enabled a new feature (parallel writes) and was reporting degraded performance immediately after.
  2. Standard Go profiling tools are sufficient. The assistant assumed that pprof—Go's built-in profiling framework—would reveal the bottleneck. This assumption proved correct, but it's worth noting that pprof's CPU profiler uses statistical sampling at 100 Hz (on Linux), which means very short operations can be missed. The 6.2% figure itself is a statistical estimate, not an exact measurement.
  3. The bottleneck is in the write path, not the read path or background maintenance. The user specifically asked about "write paths," so the assistant focused on write-related goroutines and functions. This was appropriate given the context, but it's possible that background tasks (like the CIDgravity deal-check loop or bitswap protocol maintenance) were contributing to the perception of slowness.
  4. No contention means no lock problems. The assistant checked the block profile and mutex profile and found them empty, concluding there was no significant contention. While this is a valid inference, it's possible that contention was occurring at a granularity too fine for the default sampling rate to capture, or that the contention was in a code path not exercised during the profiling window.

Input Knowledge Required

To fully understand this message, a reader needs familiarity with several concepts:


Output Knowledge Created

This message produced several pieces of actionable knowledge:

  1. CPU utilization is extremely low (6.2%). This ruled out CPU-bound computation as the cause of slow writes. The system was not working hard; it was waiting.
  2. The top CPU consumers are I/O-related. syscall.Syscall6, runtime.futex, and gocql.(*Conn).exec dominate the profile. This suggests the system is spending its (limited) CPU time entering system calls and waiting on futex locks—both indicators of I/O or synchronization overhead.
  3. No single function dominates. The flat percentages are small (12.9%, 12.9%, 6.45%), meaning no hot loop or expensive computation is consuming CPU. The bottleneck is not algorithmic.
  4. The profile is a starting point, not a conclusion. The truncated output (ending with "20ms...") shows only the top entries. A deeper analysis—looking at the call graph, examining cumulative percentages, or generating a flame graph—would be needed to trace the full chain of waiting.

The Chain of Discovery: From CPU Profile to fmt.Println

The subject message was not the end of the investigation; it was the critical redirection. After seeing the CPU profile, the assistant checked the block profile (empty) and mutex profile (empty), then turned to the systemd journal to observe live write behavior. There, it noticed a recurring log line: "syncing group 201" appearing on every write operation.

A grep search traced this to line 264 of rbstor/group.go:

fmt.Println("syncing group", m.id)

This was a debug print statement—a fmt.Println call that serializes a string to stdout on every group sync. Every single write operation triggered a call to fmt.Println, which involves:


Mistakes and Incorrect Assumptions

While the assistant's diagnostic approach was largely sound, several aspects merit critical examination:

  1. The initial focus on lock contention was partially misdirected. The assistant spent time examining goroutine dumps for blocked goroutines, block profiles, and mutex profiles before turning to the CPU profile. While this is a reasonable systematic approach, the user's report of "slow and bursty" writes could have been investigated more directly by looking at the write path's actual I/O patterns first. The goroutine dump showed no obvious contention, but the assistant could have saved time by jumping to the CPU profile earlier.
  2. The assistant initially dismissed the user's report. When the user said "Writes failing now," the assistant checked for error logs and tested a write, found no errors, and essentially told the user "writes are working fine." This was a miscommunication—the user was reporting performance degradation, not hard failures. The assistant's literal interpretation of "failing" caused a brief disconnect that required the user to clarify ("Pretty slow and bursty writes").
  3. The CPU profile was not fully analyzed. The assistant truncated the profile output with head -40 and only showed the top entries. A complete analysis would have included a flame graph or call graph to trace the full chain of waiting. The assistant relied on subsequent log inspection to find the actual bottleneck, which worked but was not the most efficient path.
  4. The assumption that the parallel write feature caused the problem may have been incorrect. The debug fmt.Println was not introduced by the parallel write feature—it was a pre-existing development artifact. The timing of the user's report (right after enabling parallel writes) was coincidental. The parallel write feature may have simply increased the write volume enough to make the existing bottleneck visible.

The Thinking Process: A Window into Diagnostic Reasoning

The assistant's reasoning in this message sequence reveals a structured diagnostic methodology:

  1. Formulate hypothesis: "Writes are slow and bursty. Where is the time being spent?"
  2. Check for blocked goroutines: If goroutines are stuck on locks, that explains slowness. Result: no significant blocking.
  3. Pivot to CPU profile: If goroutines aren't blocked, is the CPU busy or idle? Result: 6.2% utilization—extremely idle.
  4. Check for contention: Block and mutex profiles confirm no lock contention. Result: confirmed.
  5. Observe live behavior: Look at journal logs during writes. Result: "syncing group 201" appears on every write.
  6. Trace to source: Grep for the log line. Result: fmt.Println in group.go.
  7. Verify impact: Check how often Sync is called. Result: called on every write flush.
  8. Remove the bottleneck: Delete the debug print.
  9. Verify no other instances: Grep for remaining fmt.Print calls in the package. This is textbook performance debugging: rule out classes of problems systematically, use the right tool for each class, and follow the evidence wherever it leads. The assistant's willingness to pivot from one profiling tool to another—goroutine dump → CPU profile → block profile → mutex profile → journal logs → source code grep—demonstrates fluency with the Go profiling ecosystem and a methodical approach to root cause analysis.

Conclusion

The message at index 2867—a simple CPU profile showing 6.2% utilization—is a masterclass in diagnostic efficiency. In a single command, the assistant ruled out CPU-bound computation as the cause of slow writes and redirected the investigation toward I/O waiting. The subsequent discovery of a debug fmt.Println in the write path's sync function was the direct consequence of this redirection.

The lesson is both technical and philosophical: in complex distributed systems, the bottleneck is rarely where you expect it. The assistant began by looking for lock contention, a common culprit in concurrent Go programs. But the real problem was a single line of code left over from development—a fmt.Println that had no business being in a production write path. The CPU profile, with its stark 6.2% figure, was the clue that forced the investigation to look beyond the obvious suspects.

For anyone debugging performance issues in Go systems, this message serves as a reminder: start with the CPU profile. It tells you immediately whether the system is working hard or waiting. And if it's waiting, the next step is to find out what it's waiting for—whether that's a database query, a network round-trip, a disk flush, or, in this case, a forgotten debug print serializing a string to stdout on every single write.