Ceph osd_memory_target: cache eviction, RSS growth, and OSD OOM kills

An OSD repeatedly killed by the kernel OOM killer, or a host where OSDs flap up and down shortly after systemd restarts them, very often traces back to one knob: osd_memory_target. The same knob set too low for the working set produces a quieter failure: read latency climbs on HDD-backed OSDs as BlueStore evicts onodes and RocksDB block-cache pages the workload actually needs.

osd_memory_target bounds the BlueStore onode cache and RocksDB block cache. The OSD uses tcmalloc and trims its caches to stay near the target, but the target is a soft limit, not a hard cap. Ceph documents that RSS may exceed the target by up to roughly 20%. Spikes from PG log bloat, RocksDB compaction, or many PGs peering at once can push RSS higher still. When target plus overshoot meets the host RAM budget or a container cgroup limit, the OOM killer fires, the OSD goes down, and from the cluster’s perspective it looks like a disk failure or heartbeat timeout.

The reverse failure is just as common and easier to miss. Set the target too low, or disable bluestore_cache_autotune and leave the fixed bluestore_cache_size small, and BlueStore trims faster than the workload can reuse cached entries. On HDD OSDs the result is a steady climb in apply latency that never trips a health check. The OSD is up, the PGs are active+clean, the cluster reports HEALTH_OK. Only the cache hit ratio and per-OSD latency expose the problem.

What this means

osd_memory_target is the cache budget BlueStore uses when bluestore_cache_autotune is enabled (the default). The default target is 4 GiB. With autotune on, BlueStore dynamically splits the budget between the onode cache (metadata served via RocksDB) and the data buffer cache. With autotune off, BlueStore falls back to fixed bluestore_cache_size values, and osd_memory_target no longer drives cache sizing. On any modern cluster, autotune is on, and osd_memory_target is the knob.

Two operator errors dominate incident reports.

The first is setting the target higher than the host can deliver. On a dense host with twelve HDDs, twelve 4 GiB targets already require 48 GiB just for cache, before PG metadata, PG logs, tcmalloc overhead, and the OS. The math gets worse in containers. If the OSD does not detect the cgroup limit correctly, it sizes its cache against host RAM, then gets killed when it crosses the much smaller container limit.

The second is leaving no headroom for spikes. Recovery, backfill, deep-scrub, and PG peering after a topology change all push memory consumption above the target. If the gap between target and the real ceiling is thin, the next recovery event OOM-kills the OSD, recovery restarts, memory spikes again, and the OSD flaps.

The headroom rule is straightforward: each OSD needs at least 1.5x its osd_memory_target available on the host, plus at least 2 GB for the OS. That ratio is not enforced by Ceph. It is the slack tcmalloc fragmentation, delayed kernel reclaim, and recovery spikes need.

flowchart TD
    A[osd_memory_target] --> B{Sized for workload and ceiling?}
    B -- Too low for working set --> C[Aggressive cache eviction]
    C --> D[Low BlueStore cache hit ratio]
    D --> E[Apply latency climbs on HDD]
    B -- Too high or no headroom --> F[RSS approaches ceiling]
    F --> G[Kernel OOM killer fires]
    G --> H[OSD marked down]
    H --> I[Peering storm on its PGs]
    I --> J[Restart, spike, flap]
    B -- Container misreads limit --> F

Common causes

CauseWhat it looks likeFirst thing to check
Target too low for working setApply latency climbing on a few OSDs, low cache hit ratio, HEALTH_OKceph daemon osd.X perf dump BlueStore cache hits vs misses
Target too high for host RAMOOM kills in dmesg, OSDs flapping on one dense hostps RSS per OSD, free -m, sum against host RAM
Container does not see real cgroup limitOSD repeatedly OOMKilled, RSS sized to host RAM not pod limitPod memory limit, /sys/fs/cgroup/memory.max inside the container
osd_memory_target_autotune on hyperconverged hostCo-located VMs or pods OOM after OSD restartceph config get mgr mgr/cephadm/autotune_memory_target_ratio
THP enabled with tcmallocRSS drifts above target with no workload change/sys/kernel/mm/transparent_hugepage/enabled
Recovery or backfill memory spikeOSD OOM-killed during rebalance, fine in steady stateTime-correlate OOM with ceph pg dump recovering/backfilling count

Quick checks

These are read-only. None of them modify cluster state.

# Per-OSD RSS, in MB, sorted
ps -eo pid,rss,args | grep 'ceph-osd' | grep -v grep \
  | awk '{print $1, $2/1024 "MB"}' | sort -k2 -n

# Configured target on a specific OSD
ceph daemon osd.0 config show | grep -E 'osd_memory_target|bluestore_cache_autotune|bluestore_cache_size'

# BlueStore cache hit/miss counters
# <!-- TODO: verify exact onode counter names across Ceph versions -->
ceph daemon osd.0 perf dump \
  | jq '.bluestore | {hit: .bluestore_cache_hit, miss: .bluestore_cache_miss,
                      onode_hits: .bluestore_onode_hits, onode_misses: .bluestore_onode_misses}'

# tcmalloc mempool breakdown (more accurate than RSS for what the OSD thinks it holds)
# <!-- TODO: verify dump_mempools is a valid admin socket command on target versions -->
ceph daemon osd.0 dump_mempools

# OOM kills in kernel ring buffer
dmesg -T | grep -iE 'killed process|out of memory|oom'

# OSD state and recent flapping
ceph osd tree
ceph health detail | grep -iE 'OSD_DOWN|OSD_FLAPPING'

# Host memory budget
free -m

# THP setting
cat /sys/kernel/mm/transparent_hugepage/enabled

# Cgroup memory limit visible to this OSD (v2 path; v1 uses memory/memory.limit_in_bytes)
cat /sys/fs/cgroup/memory.max 2>/dev/null || cat /sys/fs/cgroup/memory/memory.limit_in_bytes

# BlueFS slow-device stats
# <!-- TODO: verify exact admin socket command name for BlueFS stats -->
ceph daemon osd.0 bluefs stats

How to diagnose it

  1. Confirm the OSD is actually being OOM-killed, not crashing for another reason. Check dmesg -T | grep -iE 'killed process|out of memory' and the unit journal: journalctl -u ceph-<fsid>@osd.X.service --since '2 hours ago'. A Killed process ... ceph-osd line is conclusive. A segfault or assertion is a different problem.

  2. Compare RSS against the configured target. Read the OSD’s PID via ps and read osd_memory_target from ceph daemon osd.X config show. RSS near or above 1.2x the target is expected in steady state. RSS climbing monotonically over hours with no workload change suggests tcmalloc fragmentation or a leak.

  3. Compare the sum of OSD targets against what the host can give. Sum the per-OSD targets on the host, add 2 GB for the OS, add headroom for any non-OSD workload (MON, MDS, RGW, VM, hypervisor), and compare against total RAM. If the sum is within 20% of total RAM, the host has no spike headroom.

  4. In containers, verify the OSD sees the right limit. ceph daemon osd.X config get osd_memory_target shows the resolved value. If it equals roughly 0.8x host RAM rather than 0.8x the pod limit, cgroup detection is broken. This is a known failure mode on cgroup v2 hosts when container tooling hardcodes the v1 path.

  5. For the low-target failure mode, look at cache hit ratio and apply latency together. A sustained drop in onode hit ratio, especially combined with rising apply latency on HDD OSDs, indicates the cache budget is too small for the working set.

  6. Time-correlate OOM events with cluster events. Recovery, backfill, and PG peering all push memory up. If OOMs cluster around recovery windows, the issue is spike headroom, not steady-state target.

Metrics and signals to monitor

SignalWhy it mattersWarning sign
OSD RSS (ps, cgroup memory.usage)Crosses target by 20% in normal use, more under spikesSteady growth with no workload change, or RSS pegged at the cgroup limit
osd_memory_target resolved valueContainer detection bugs silently set this to host RAMResolved target close to host total RAM on a constrained container
BlueStore onode hit ratioOnode misses require RocksDB lookups, expensive on HDDHit ratio falling while object access pattern is unchanged
OSD apply latency (ceph_osd_apply_latency_ms)The user-visible symptom of cache thrashingOne OSD at 5x cluster median for the same device class
OSD commit latency (ceph_osd_commit_latency_ms)WAL/DB device or replication slowness, often co-occursSpike without correlated apply-latency spike points at the DB device
OOM kills (host kernel logs, container OOMKilled counter)The hard failureAny kill of a ceph-osd process
OSD up/down transitions (ceph_osd_up gauge edges)Each OOM restart is a flapRepeated transitions on the same OSD minutes apart
Recovery rate (ceph_pool_recovering_bytes_per_sec)Memory spikes during recoveryOOM correlates with elevated recovery rate
Swap usage on hostKernel swaps OSD heap instead of evicting page cacheAny sustained swap usage on an OSD host
THP enabled flagInteracts with tcmalloc to inflate RSS[always] rather than [never] or madvise

Fixes

Target set higher than the host can deliver

Lower the target to fit the budget, or move OSDs off the host. Compute the safe target as (host_RAM - 2GB - non_OSD_workload) / OSD_count / 1.5. Apply per-OSD:

ceph config set osd.X osd_memory_target 4294967296

This is a live config change; the OSD will trim back down without a restart, but it must be running and healthy to honor the new value. If the host is fundamentally oversubscribed, no target tuning fixes it. Add RAM or reduce OSD density.

Target set too low for the working set

Raise the target, after confirming the host has the headroom. Pools with many small objects (CephFS metadata, RGW bucket indexes) need proportionally more onode cache. Track onode hit ratio after the change. If raising the target does not move the hit ratio, the working set may be so random that no practical cache helps, and faster media (NVMe for DB) is the actual fix.

Container cgroup limit not reflected in the target

In Rook, set explicit memory requests and limits in the CephCluster spec. Without a request, each OSD can see the entire node and resolve osd_memory_target to a large fraction of host RAM. In cephadm, the container path uses POD_MEMORY_REQUEST and POD_MEMORY_LIMIT. If only the limit is set, the target resolves to limit * osd_memory_target_cgroup_limit_ratio (default 0.8). Make sure both are set and that the limit is what you actually want the OSD to fit in. On cgroup v2 hosts where the OSD misreads the limit, pin to a known-good Ceph image or set the target explicitly per OSD.

Autotune fighting hyperconverged workloads

osd_memory_target_autotune is enabled by default in cephadm since Quincy. It sizes OSD targets against mgr/cephadm/autotune_memory_target_ratio, default 0.7 of host RAM. On hyperconverged hosts (Proxmox, Rook co-located with workloads, OSDs alongside MON or MDS on the same box), that ratio assumes all host RAM belongs to OSDs and will over-allocate. Either disable per-OSD autotune with ceph config set osd.X osd_memory_target_autotune false and set the target explicitly, or lower the global ratio to reflect what the OSDs actually get.

THP and tcmalloc RSS growth

Transparent Huge Pages interact with tcmalloc to inflate RSS above what the OSD thinks it is holding. The practical fix is the same one ceph-ansible and cephadm apply by default: disable THP.

# Requires root; ephemeral, will not survive reboot
echo never > /sys/kernel/mm/transparent_hugepage/enabled

# Persist via tuned, udev rule, or systemd unit depending on your host setup
This does not lower the target, but it removes a known source of overshoot.

vm.swappiness

With default swappiness of 60, the kernel may swap OSD heap pages instead of evicting page cache, producing latency spikes when those pages are faulted back in. Setting vm.swappiness=0 or 1 is operator consensus for Ceph hosts. This is host-level tuning, not a Ceph config.

Prevention

Capacity-plan OSD memory the same way you plan raw storage. For each host, write down: number of OSDs, target per OSD, sum of targets, 1.5x headroom multiplier, OS reserve, and any co-located workload. If the result is more than 90% of host RAM, the host is oversubscribed.

For containerized deployments, treat the pod memory limit as the ceiling. Set explicit requests and limits in the spec, verify the OSD resolves the target against the limit, and alert on the resolved value drifting toward host RAM.

Track OOM kills of ceph-osd as a first-class signal. A single OOM is a warning. Repeated OOMs on the same OSD, or OOMs across multiple OSDs on the same host, are an incident even when the cluster reports HEALTH_OK between kills.

Track cache hit ratio alongside apply latency. A cluster with HEALTH_OK, all PGs active+clean, and a steadily falling onode hit ratio is drifting toward a latency incident.

How Netdata helps

  • Per-second OSD RSS collection (via the ceph collector and cgroup memory metrics) shows the steady-state RSS vs target ratio and the spike pattern around recovery events that point-in-time ps snapshots miss.
  • BlueStore cache hit/miss counters and per-OSD apply and commit latency let you correlate cache eviction with the latency consequence. A falling hit ratio with rising apply latency is the signature of a too-low target.
  • ML anomaly detection on RSS highlights monotonic growth (tcmalloc fragmentation, leaks) before it crosses the cgroup limit and becomes an OOM.
  • Cgroup memory usage on the same host as the OSD lets you compare the resolved target against the actual container ceiling, surfacing the misconfigured-container failure before the first OOM.
  • OOM kill events from the kernel logs and OSD up/down transitions from the Ceph collector appear on the same timeline as recovery rate, so spike-driven OOMs are distinguishable from steady-state oversubscription at a glance.