The symptom: 100 Gbyte uploads stalling
A microservice that managed and processed large files — encrypting them before storing them on S3 — was taking hours to upload 100 Gbyte files, while files as large as 40 Gbytes completed in minutes. Cloud-wide monitoring in Atlas showed a high rate of pageins during the large uploads.
Pageins are disk reads of a page of memory, and they are ordinary for many workloads, but the gap between the 40 Gbyte and 100 Gbyte cases pointed at something size-dependent.
Checking the storage layer
The 60-second performance checklist starts with disk behavior, so iostat(1) came first. The r_await column stood out: the average wait for reads was 33 ms, which is on the high side, and it tracked with queueing visible in avgqu-sz. Request sizes worked out to roughly 128 Kbytes (divide rkB/s by r/s).
# iostat -xz 1
Linux 4.4.0-1072-aws (xxx) 12/18/2018 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
5.03 0.00 0.83 1.94 0.02 92.18
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
xvda 0.00 0.29 0.21 0.17 6.29 3.09 49.32 0.00 12.74 6.96 19.87 3.96 0.15
xvdb 0.00 0.08 44.39 9.98 5507.39 1110.55 243.43 2.28 41.96 41.75 42.88 1.52 8.25
avg-cpu: %user %nice %system %iowait %steal %idle
14.81 0.00 1.08 29.94 0.06 54.11
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
xvdb 0.00 0.00 745.00 0.00 91656.00 0.00 246.06 25.32 33.84 33.84 0.00 1.35 100.40
avg-cpu: %user %nice %system %iowait %steal %idle
14.86 0.00 0.89 24.76 0.06 59.43
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
xvdb 0.00 0.00 739.00 0.00 92152.00 0.00 249.40 24.75 33.49 33.49 0.00 1.35 100.00
avg-cpu: %user %nice %system %iowait %steal %idle
14.95 0.00 0.89 28.75 0.06 55.35
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
xvdb 0.00 0.00 734.00 0.00 91704.00 0.00 249.87 24.93 34.04 34.04 0.00 1.36 100.00
avg-cpu: %user %nice %system %iowait %steal %idle
14.54 0.00 1.14 29.40 0.06 54.86
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
xvdb 0.00 0.00 750.00 0.00 92104.00 0.00 245.61 25.14 33.37 33.37 0.00 1.33 100.00
^C
Reads tend to have applications waiting on them, unlike writes that can be absorbed by write-back caching, so that latency was worth investigating before blaming the device.
Latency distribution and I/O sizes
Averages hide outliers, and outliers would be the signature of a failing device. The biolatency eBPF tool from bcc produced a latency histogram showing most I/O landing between 16 and 127 ms, with some stragglers out to 0.5–1.0 seconds.
# /usr/share/bcc/tools/biolatency -m
Tracing block device I/O... Hit Ctrl-C to end.
^C
msecs : count distribution
0 -> 1 : 83 | |
2 -> 3 : 20 | |
4 -> 7 : 0 | |
8 -> 15 : 41 | |
16 -> 31 : 1620 |******* |
32 -> 63 : 8139 |****************************************|
64 -> 127 : 176 | |
128 -> 255 : 95 | |
256 -> 511 : 61 | |
512 -> 1023 : 93 | |
Nothing there looked like hardware. The spread was consistent with the queueing seen in iostat(1), and bitesize confirmed the request pattern: I/O concentrated in the 128–255 Kbyte bucket, exactly what the earlier output implied.
# /usr/share/bcc/tools/bitesize
Tracing... Hit Ctrl-C to end.
^C
Process Name = java
Kbytes : count distribution
0 -> 1 : 0 | |
2 -> 3 : 0 | |
4 -> 7 : 0 | |
8 -> 15 : 31 | |
16 -> 31 : 15 | |
32 -> 63 : 15 | |
64 -> 127 : 15 | |
128 -> 255 : 1682 |****************************************|
Memory and the page cache
free(1) was the next checklist item, and it reframed the problem. Only 349 Mbytes of memory remained free, but 48,643 Mbytes — 48 Gbytes — sat in the buffer/page cache on a 64-Gbyte system.
# free -m
total used free shared buff/cache available
Mem: 64414 15421 349 5 48643 48409
Swap: 0 0 0
Combine that with the file sizes and a theory emerges: do the 100 Gbyte files exceed what the page cache can hold, while 40 Gbyte files fit comfortably? If so, the backing reads that iostat(1) and biolatency were recording are the file being pulled back off disk after eviction.
Page cache statistics
cachestat, a tool built on Ftrace that has since been ported to bcc/eBPF, reports hit and miss statistics for the page cache. It showed hit ratios bouncing between 6.5% and 74% — far below the upper-90s range that indicates healthy caching. This is cache busting: the 100 Gbyte file does not fit within 48 Gbytes of page cache, so misses generate disk I/O and drag down throughput.
# /apps/perf-tools/bin/cachestat
Counting cache functions... Output every 1 seconds.
HITS MISSES DIRTIES RATIO BUFFERS_MB CACHE_MB
1811 632 2 74.1% 17 48009
1630 15132 92 9.7% 17 48033
1634 23341 63 6.5% 17 48029
1851 13599 17 12.0% 17 48019
1941 3689 33 34.5% 17 48007
1733 23007 154 7.0% 17 48034
1195 9566 31 11.1% 17 48011
[...]
Two remedies follow. The immediate one is to run on a larger-memory instance that can hold 100 Gbyte files. The longer-term one belongs to the developers: rework the code around the memory constraint, for example by processing parts of the file instead of making repeated passes over the whole thing.
Confirming with a smaller file
A 32 Gbyte upload collected against the same tooling served as the control. cachestat reported a hit ratio near 100%.
# /apps/perf-tools/bin/cachestat
Counting cache functions... Output every 1 seconds.
HITS MISSES DIRTIES RATIO BUFFERS_MB CACHE_MB
61831 0 126 100.0% 41 33680
53408 0 78 100.0% 41 33680
65056 0 173 100.0% 41 33680
65158 0 79 100.0% 41 33680
55052 0 107 100.0% 41 33680
61227 0 149 100.0% 41 33681
58669 0 71 100.0% 41 33681
33424 0 73 100.0% 41 33681
^C
At that size the service can perform all of its passes over the file from memory without going back to disk. free(1) showed the file fitting in the page cache.
# free -m
total used free shared buff/cache available
Mem: 64414 18421 11218 5 34773 45407
Swap: 0 0 0
And iostat(1) showed little disk I/O, which is what the caching theory predicts.
# iostat -xz 1
Linux 4.4.0-1072-aws (xxx) 12/19/2018 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.25 0.00 1.24 0.19 0.03 86.29
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
xvda 0.00 0.32 0.31 0.19 7.09 4.85 47.59 0.01 12.58 5.44 23.90 3.09 0.15
xvdb 0.00 0.07 0.01 11.13 0.10 1264.35 227.09 0.91 82.16 3.49 82.20 2.80 3.11
avg-cpu: %user %nice %system %iowait %steal %idle
57.43 0.00 2.95 0.00 0.00 39.62
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
avg-cpu: %user %nice %system %iowait %steal %idle
53.50 0.00 2.32 0.00 0.00 44.18
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
xvdb 0.00 0.00 0.00 2.00 0.00 19.50 19.50 0.00 0.00 0.00 0.00 0.00 0.00
avg-cpu: %user %nice %system %iowait %steal %idle
39.02 0.00 2.14 0.00 0.00 58.84
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
[...]
Where the tooling stands
cachestat was the tool that cracked this case, but it remains experimental. It was written for Ftrace under two constraints: low overhead, and use of the Ftrace function profiler only. The statistics it produces are needed often enough that they arguably belong in /proc rather than in a separate tool. Kernel memory-management engineers, present when this was raised at the LSFMMBPF 2019 keynote in Puerto Rico, pointed to challenges in exposing them properly, and a robust solution would likely need their expertise.
Kernel support may never arrive, since these are hot-path counters and adding them is not free. In the meantime cachestat works, at the cost of regular maintenance to keep it functional.



