<?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: Parsa Gheiratian</title>
    <description>The latest articles on DEV Community by Parsa Gheiratian (@vexsx).</description>
    <link>https://dev.to/vexsx</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%2F4110880%2F4f24b02b-8987-445d-958f-53ca4cb98cdd.jpg</url>
      <title>DEV Community: Parsa Gheiratian</title>
      <link>https://dev.to/vexsx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vexsx"/>
    <language>en</language>
    <item>
      <title>Detecting Ceph LARGE_OMAP_OBJECTS Before Deep Scrub Finds It</title>
      <dc:creator>Parsa Gheiratian</dc:creator>
      <pubDate>Sat, 05 Sep 2026 14:49:51 +0000</pubDate>
      <link>https://dev.to/vexsx/detecting-ceph-largeomapobjects-before-deep-scrub-finds-it-4cpg</link>
      <guid>https://dev.to/vexsx/detecting-ceph-largeomapobjects-before-deep-scrub-finds-it-4cpg</guid>
      <description>&lt;p&gt;&lt;code&gt;LARGE_OMAP_OBJECTS&lt;/code&gt; has an annoying property: it tells you about a problem that started days ago. An RGW bucket index shard, a CephFS metadata object, or an application object built on librados grows past the threshold, and nothing in &lt;code&gt;ceph health&lt;/code&gt; changes until a deep scrub happens to walk that PG. With the default deep-scrub interval of seven days, the warning can trail the actual growth by most of a week.&lt;/p&gt;

&lt;p&gt;I wanted to know exactly what Ceph knows about OMAP size before deep scrub, what it only learns during deep scrub, and which of the commonly suggested "check it earlier" techniques actually work and at what cost. This article is the result of reading the scrub code and then reproducing the condition on a Tentacle lab cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  The behavior I wanted to verify
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Is the health warning purely a product of deep scrub, or does a normal scrub contribute?&lt;/li&gt;
&lt;li&gt;What exactly is compared against the two thresholds: key count, value bytes, or both?&lt;/li&gt;
&lt;li&gt;Does the OSD keep a live per-object OMAP size anywhere that I can query?&lt;/li&gt;
&lt;li&gt;What do &lt;code&gt;ceph pg dump&lt;/code&gt;, &lt;code&gt;ceph df detail&lt;/code&gt;, &lt;code&gt;ceph osd df&lt;/code&gt;, the OSD admin socket, and the Prometheus module expose, and which of those numbers are live versus scrub-time snapshots?&lt;/li&gt;
&lt;li&gt;What does &lt;code&gt;rados listomapkeys | wc -l&lt;/code&gt; actually cost on the OSD?&lt;/li&gt;
&lt;li&gt;Is there any supported way to ask "top N objects by OMAP keys" without reading every object?&lt;/li&gt;
&lt;li&gt;What is different for RGW bucket indexes?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What actually triggers LARGE_OMAP_OBJECTS
&lt;/h2&gt;

&lt;p&gt;Two OSD options define "large". On the lab cluster both are at their defaults:&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="nv"&gt;$ &lt;/span&gt;ceph config get osd osd_deep_scrub_large_omap_object_key_threshold
200000
&lt;span class="nv"&gt;$ &lt;/span&gt;ceph config get osd osd_deep_scrub_large_omap_object_value_sum_threshold
1073741824
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ceph config show-with-defaults osd.0&lt;/code&gt; reports both with source &lt;code&gt;default&lt;/code&gt;, so nobody has tuned them here. The key threshold is also read by the MDS: the open-file table code caps the number of entries it puts in one object at exactly this value and spreads the rest across up to 1024 objects. That is a useful hint about how the Ceph developers themselves avoid the warning: shard at the application layer.&lt;/p&gt;

&lt;p&gt;The detection path, traced through the Tentacle source, is entirely inside deep scrub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deep scrub of PG
    │
    ▼
for each object in the scrub chunk (5..15 objects per chunk by default)
    │
    ├── stat + xattrs                       (shallow and deep)
    │
    └── OMAP walk, 1024 keys per step   (deep only, osd_deep_scrub_keys)
          │  header → crc
          │  every key and value → crc, key count, value bytes
          ▼
        keys  &amp;gt; osd_deep_scrub_large_omap_object_key_threshold
        OR
        bytes &amp;gt; osd_deep_scrub_large_omap_object_value_sum_threshold
          │
          ▼
        object flagged in the scrub map
          │
          ▼
after each chunk: ScrubBackend::omap_checks()
    ├── sums keys/bytes into per-scrub counters
    ├── increments large_omap_objects
    └── clog WARN "Large omap object found. Object: ... PG: ...
                   Key count: ... Size (bytes): ..."
          │
          ▼
scrub_finish(), only if the scrub was deep:
    stats.sum.num_large_omap_objects = counter
    stats.sum.num_omap_keys          = sum
    stats.sum.num_omap_bytes         = sum
          │
          ▼
OSD publishes PG stats → mgr/mon PGMap
          │
          ▼
mon health check: if pg_sum.num_large_omap_objects &amp;gt; 0
    → LARGE_OMAP_OBJECTS (HEALTH_WARN), "N large objects found in pool 'X'"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Details worth knowing because they affect how you interpret the numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A shallow scrub never touches OMAP.&lt;/strong&gt; The object scan calls &lt;code&gt;stat&lt;/code&gt; and &lt;code&gt;getattrs&lt;/code&gt; and only descends into the OMAP walk when the scrub is deep. A shallow scrub also leaves &lt;code&gt;num_omap_keys&lt;/code&gt;, &lt;code&gt;num_omap_bytes&lt;/code&gt;, and &lt;code&gt;num_large_omap_objects&lt;/code&gt; untouched in the PG stats, so it neither raises nor clears the warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Size (bytes)" is the sum of value lengths only.&lt;/strong&gt; Key bytes are not counted toward the value-sum threshold. An object with 200000 keys and empty values trips the key threshold with a reported size of 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The comparison is strictly greater-than.&lt;/strong&gt; Exactly 200000 keys is not "large".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One warning per scrub chunk.&lt;/strong&gt; &lt;code&gt;omap_checks()&lt;/code&gt; stops at the first flagged object in each chunk, so if two large objects sit in the same chunk of 5 to 15 objects, only one is logged and counted. The health message count can be lower than the truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PG OMAP stats are a snapshot.&lt;/strong&gt; Nothing decrements or increments them on the write path. They only change at the next deep scrub of that PG.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lab environment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ceph 20.2.3 Tentacle (daemons), 20.2.4 client tools, deployed with cephadm.&lt;/li&gt;
&lt;li&gt;3 hosts, 3 mons, 2 mgrs, 6 BlueStore OSDs, all replicated pools with size 3.&lt;/li&gt;
&lt;li&gt;No RGW deployed. &lt;code&gt;radosgw-admin&lt;/code&gt; from the same release was used only to verify command availability.&lt;/li&gt;
&lt;li&gt;Health was &lt;code&gt;HEALTH_OK&lt;/code&gt; before the test and again after cleanup.&lt;/li&gt;
&lt;li&gt;Test pool &lt;code&gt;test-omap&lt;/code&gt;: 1 PG, &lt;code&gt;pg_autoscale_mode off&lt;/code&gt;, application &lt;code&gt;rados&lt;/code&gt;. Single test object &lt;code&gt;test-object&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;All scrub and threshold settings at defaults. No configuration was changed for the reproduction.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproducing the condition
&lt;/h2&gt;

&lt;p&gt;The default key threshold is 200000, which is small enough to hit directly. I wrote 250000 keys with 32-byte values using librados from Python, batching 5000 keys per write op:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;rados&lt;/span&gt;
&lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;250000&lt;/span&gt;
&lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rados&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rados&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conffile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/ceph/ceph.conf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;io&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open_ioctx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test-omap&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;start&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;rados&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WriteOpCtx&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;op&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key-%08d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;vals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_omap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;operate_write_op&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test-object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole write took 0.7 seconds. The object has no data payload at all:&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="nv"&gt;$ &lt;/span&gt;rados &lt;span class="nt"&gt;-p&lt;/span&gt; test-omap &lt;span class="nb"&gt;stat &lt;/span&gt;test-object
test-omap/test-object mtime 0.000000, size 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rados stat&lt;/code&gt; says size 0. OMAP is invisible to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before any scrub
&lt;/h3&gt;

&lt;p&gt;Twenty seconds after the write, the PG stats already know that the object &lt;em&gt;has&lt;/em&gt; OMAP, but not how much:&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="nv"&gt;$ &lt;/span&gt;ceph pg 7.0 query | jq &lt;span class="s1"&gt;'.info.stats.stat_sum
    | {num_objects, num_objects_omap,
       num_omap_bytes, num_omap_keys, num_large_omap_objects}'&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"num_objects"&lt;/span&gt;: 1,
  &lt;span class="s2"&gt;"num_objects_omap"&lt;/span&gt;: 1,
  &lt;span class="s2"&gt;"num_omap_bytes"&lt;/span&gt;: 0,
  &lt;span class="s2"&gt;"num_omap_keys"&lt;/span&gt;: 0,
  &lt;span class="s2"&gt;"num_large_omap_objects"&lt;/span&gt;: 0
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;num_objects_omap&lt;/code&gt; is maintained on the write path, so it is live. The other three are zero because no deep scrub has run.&lt;/p&gt;

&lt;p&gt;The pool-level view, on the other hand, is live and already large:&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="nv"&gt;$ &lt;/span&gt;ceph &lt;span class="nb"&gt;df &lt;/span&gt;detail | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'POOL|test-omap'&lt;/span&gt;
POOL       ID  PGS   STORED   &lt;span class="o"&gt;(&lt;/span&gt;DATA&lt;span class="o"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;(&lt;/span&gt;OMAP&lt;span class="o"&gt;)&lt;/span&gt;  OBJECTS     USED   &lt;span class="o"&gt;(&lt;/span&gt;DATA&lt;span class="o"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;(&lt;/span&gt;OMAP&lt;span class="o"&gt;)&lt;/span&gt;  %USED  MAX AVAIL ...
test-omap   7    1   16 MiB      0 B   16 MiB        1   48 MiB      0 B   48 MiB   0.02     95 GiB ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Health:&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="nv"&gt;$ &lt;/span&gt;ceph health detail
HEALTH_OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Shallow scrub
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ceph pg scrub 7.0
instructing pg 7.0 on osd.5 to scrub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It completed in about two seconds. &lt;code&gt;last_scrub_stamp&lt;/code&gt; advanced, &lt;code&gt;last_deep_scrub_stamp&lt;/code&gt; did not, all OMAP counters stayed at zero, and health stayed &lt;code&gt;HEALTH_OK&lt;/code&gt;. This matches the source: no OMAP walk in a shallow scrub.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deep scrub
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ceph pg deep-scrub 7.0
instructing pg 7.0 on osd.5 to deep-scrub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also about two seconds for one object with 250000 small keys. Now the stats are populated:&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="nv"&gt;$ &lt;/span&gt;ceph pg 7.0 query | jq &lt;span class="s1"&gt;'.info.stats.stat_sum
    | {num_objects, num_objects_omap,
       num_omap_bytes, num_omap_keys, num_large_omap_objects}'&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"num_objects"&lt;/span&gt;: 1,
  &lt;span class="s2"&gt;"num_objects_omap"&lt;/span&gt;: 1,
  &lt;span class="s2"&gt;"num_omap_bytes"&lt;/span&gt;: 8000000,
  &lt;span class="s2"&gt;"num_omap_keys"&lt;/span&gt;: 250000,
  &lt;span class="s2"&gt;"num_large_omap_objects"&lt;/span&gt;: 1
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;8000000 bytes is exactly 250000 keys times 32-byte values, confirming that keys are not counted toward the byte sum. The cluster log has the object name, and the monitor raised the health check five seconds after the OSD logged it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ceph log last 300 warn cluster | grep -i 'large omap'
2026-09-05T14:16:07.644528+0000 osd.5 (osd.5) 55 : cluster [WRN] Large omap object found. Object: 7:5756f1fd:::test-object:head PG: 7.bf8f6aea (7.0) Key count: 250000 Size (bytes): 8000000
2026-09-05T14:16:12.723891+0000 mon.ceph01 (mon.0) 39043 : cluster [WRN] Health check failed: 1 large omap objects (LARGE_OMAP_OBJECTS)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ceph health detail
HEALTH_WARN 1 large omap objects
&lt;span class="o"&gt;[&lt;/span&gt;WRN] LARGE_OMAP_OBJECTS: 1 large omap objects
    1 large objects found &lt;span class="k"&gt;in &lt;/span&gt;pool &lt;span class="s1"&gt;'test-omap'&lt;/span&gt;
    Search the cluster log &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s1"&gt;'Large omap object found'&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;more details.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ceph pg ls-by-pool&lt;/code&gt; now shows the numbers too, with the asterisk footnote that Ceph itself prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ceph pg ls-by-pool test-omap
PG   OBJECTS  ...  BYTES  OMAP_BYTES*  OMAP_KEYS*  ...  SCRUB_STAMP                      DEEP_SCRUB_STAMP
7.0        1  ...      0      8000000      250000  ...  2026-09-05T14:16:07.651563+0000  2026-09-05T14:16:07.651563+0000

* NOTE: Omap statistics are gathered during deep scrub and may be inaccurate soon afterwards depending on utilization.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The snapshot goes stale immediately
&lt;/h3&gt;

&lt;p&gt;I then added 50000 more keys and waited fifteen seconds:&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="nv"&gt;$ &lt;/span&gt;rados &lt;span class="nt"&gt;-p&lt;/span&gt; test-omap listomapkeys test-object | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
300000
&lt;span class="nv"&gt;$ &lt;/span&gt;ceph pg ls-by-pool test-omap | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NR==2 {print "OMAP_KEYS*=" $8}'&lt;/span&gt;
OMAP_KEYS&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;250000
&lt;span class="nv"&gt;$ &lt;/span&gt;ceph &lt;span class="nb"&gt;df &lt;/span&gt;detail &lt;span class="nt"&gt;--format&lt;/span&gt; json &lt;span class="se"&gt;\&lt;/span&gt;
    | jq &lt;span class="s1"&gt;'.pools[] | select(.name=="test-omap") | .stats.stored_omap'&lt;/span&gt;
20410766
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PG stat is frozen at the last deep scrub. The pool-level OMAP figure moved. This is the core asymmetry of the whole problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clearing also needs a deep scrub
&lt;/h3&gt;

&lt;p&gt;After &lt;code&gt;rados -p test-omap rm test-object&lt;/code&gt;, the warning stayed for as long as I waited. Another &lt;code&gt;ceph pg deep-scrub 7.0&lt;/code&gt; reset all counters to zero and the health check cleared within a few seconds. Deleting a large OMAP object does not clear the warning by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ceph knows before deep scrub
&lt;/h2&gt;

&lt;p&gt;This is the inventory I ended up with, split by when the information is produced.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                live, no scrub needed
                ─────────────────────
pool   ├── STORED/USED (OMAP) bytes     ceph df detail       RocksDB estimate
       ├── num_objects_omap             ceph pg dump pools   write-path count
       └── store_stats.omap_allocated   ceph pg dump pools   same as df detail

PG     └── num_objects_omap             ceph pg dump pgs / pg query

OSD    ├── OMAP column                  ceph osd df          estimate, all pools
       ├── omap_allocated per pool      ceph tell osd.N dump_pool_statfs &amp;lt;id&amp;gt;
       └── bluestore omap_* counters    ceph tell osd.N perf dump   aggregates

object └── has-omap flag only           (drives num_objects_omap)

                only produced by deep scrub
                ───────────────────────────
PG     ├── num_omap_keys                ceph pg dump / pg ls   snapshot
       ├── num_omap_bytes                                      snapshot
       └── num_large_omap_objects                              health check

object └── key count and value bytes    cluster log line only

                not stored anywhere
                ───────────────────
object ── per-object OMAP key count or byte size, outside a full key walk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some specifics on the live sources, because they are the only early signals Ceph gives you for free:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pool-level OMAP bytes.&lt;/strong&gt; With per-pool OMAP enabled in BlueStore, which has been the default for newly created OSDs for several releases, each OSD stores every pool's OMAP under a pool-prefixed RocksDB key range. &lt;code&gt;ceph df detail&lt;/code&gt; asks each OSD for &lt;code&gt;estimate_prefix_size&lt;/code&gt; on that range, which is a RocksDB &lt;code&gt;GetApproximateSizes&lt;/code&gt; call over SST files plus memtables. It is an estimate of on-disk footprint, not a key count, and it includes tombstoned data until compaction runs. In the lab, deleting the test pool left the OSD-level OMAP column elevated for a while afterwards for exactly that reason. It is cheap, it is continuous, and it is the only OMAP size that Ceph updates without a scrub.&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="nv"&gt;$ &lt;/span&gt;ceph &lt;span class="nb"&gt;df &lt;/span&gt;detail &lt;span class="nt"&gt;--format&lt;/span&gt; json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.pools[]
    | [.name, .stats.stored_omap, .stats.omap_bytes_used] | @tsv'&lt;/span&gt;
.mgr      1215    3647
images    6823    20471
volumes   20659   61979
vms       0       0
backups   0       0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Which PGs and pools have OMAP objects at all.&lt;/strong&gt; &lt;code&gt;num_objects_omap&lt;/code&gt; is incremented and decremented by the OSD as objects gain or lose OMAP. On this cluster it immediately reveals the RBD metadata objects in the image pools. The &lt;code&gt;num_omap_keys&lt;/code&gt; column next to them is whatever the last deep scrub saw, which for these empty RBD pools is legitimately zero:&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="nv"&gt;$ &lt;/span&gt;ceph pg dump pgs &lt;span class="nt"&gt;--format&lt;/span&gt; json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.pg_stats[]
    | select(.stat_sum.num_objects_omap &amp;gt; 0)
    | [.pgid, .stat_sum.num_objects_omap, .stat_sum.num_omap_keys,
       .last_deep_scrub_stamp] | @tsv'&lt;/span&gt;
3.1c    1    0    2026-09-05T09:12:57.106912+0000
2.1c    1    0    2026-09-04T14:08:45.216107+0000
3.3     1    0    2026-09-04T14:08:54.384848+0000
2.3     1    0    2026-09-04T14:08:45.216107+0000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Per-OSD, per-pool.&lt;/strong&gt; &lt;code&gt;ceph tell osd.N dump_pool_statfs &amp;lt;poolid&amp;gt;&lt;/code&gt; returns the same &lt;code&gt;omap_allocated&lt;/code&gt; estimate for one OSD's share of one pool. In the lab the primary for the test PG reported &lt;code&gt;12594436&lt;/code&gt; bytes for 250000 keys, before any scrub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is not there.&lt;/strong&gt; I checked the obvious places for a per-object figure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ceph tell osd.N bluestore onode metadata &amp;lt;ghobject&amp;gt;&lt;/code&gt; prints the onode: nid, size, shards, blobs, xattrs. No OMAP key count, no OMAP byte count. BlueStore does not store one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ceph tell osd.N calc_objectstore_db_histogram&lt;/code&gt; walks the entire RocksDB and reports key and value size histograms per prefix. It is an aggregate, and it is a full DB scan.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ceph tell osd.N perf dump&lt;/code&gt; has &lt;code&gt;bluestore.omap_setkeys_records&lt;/code&gt;, &lt;code&gt;omap_setkeys_bytes&lt;/code&gt;, &lt;code&gt;omap_get_keys_lat&lt;/code&gt;, &lt;code&gt;omap_next_lat&lt;/code&gt; and friends. All per-OSD aggregates. Useful for spotting an OSD receiving a flood of OMAP writes, useless for naming the object.&lt;/li&gt;
&lt;li&gt;RADOS has no "count my OMAP keys" operation. The read side of the OMAP op set is get-keys, get-vals, get-vals-by-keys, get-header, and compare. Nothing returns a count; every read that could tell you the size does so by iterating.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ceph-objectstore-tool&lt;/code&gt; can list OMAP for an object, but only against a stopped OSD. It is a forensic tool, not a monitoring one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the answer to "does Ceph provide a top-N-by-OMAP API" is no. Not in the OSD, not in the mgr, not in the mon. The only per-object numbers ever produced are the ones deep scrub writes into the cluster log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1 — Targeted OMAP inspection
&lt;/h2&gt;

&lt;p&gt;If you already suspect a specific object, counting its keys is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rados &lt;span class="nt"&gt;-p&lt;/span&gt; &amp;lt;pool&amp;gt; listomapkeys &amp;lt;object&amp;gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the 250000-key test object this returned in 0.32 seconds. But look at what the primary OSD did during that call. I snapshotted its perf counters before and after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;osd.op_r                 985  →  1230     (+245 read ops)
osd.op_r_out_bytes  32153954  →  36155179 (+4.0 MB returned)
bluestore.omap_next_lat.avgcount  1408 → 1653  (+245 iterator batches)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;245 ops for 250000 keys is the batching: the &lt;code&gt;rados&lt;/code&gt; tool asks for 1024 keys per request, and the OSD caps a single OMAP read at &lt;code&gt;osd_max_omap_entries_per_request&lt;/code&gt;, also 1024 by default. Each of those requests opens a RocksDB iterator positioned after the last key returned and walks forward. The OSD reads every key. It also reads the values from the same SST blocks because that is how RocksDB stores them, even though only keys go on the wire.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wc -l&lt;/code&gt; therefore changes nothing about server-side cost. It only stops 250000 lines from hitting your terminal. The distinction matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;small output on the client    ≠    low OSD / RocksDB read cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cost is linear in key count and, for value-heavy objects, in bytes. A bucket index shard with 200000 entries of a few hundred bytes each is tens of megabytes of RocksDB reads on one OSD, on the primary, in the client I/O path with no scrub throttling. That is acceptable for a handful of candidate objects. It is not acceptable as a loop over a pool.&lt;/p&gt;

&lt;p&gt;Other &lt;code&gt;rados&lt;/code&gt; subcommands for reference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rados listomapvals &amp;lt;object&amp;gt;&lt;/code&gt; returns keys and values. Same iteration cost, plus the values on the wire. The same test object produced 50 MB of output. Do not use it for counting.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rados getomapheader &amp;lt;object&amp;gt;&lt;/code&gt; returns only the header blob. It says nothing about key count or total size.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rados stat &amp;lt;object&amp;gt;&lt;/code&gt; reports data size and mtime. OMAP does not appear, as shown above with &lt;code&gt;size 0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ceph tell osd.N getomap &amp;lt;pool&amp;gt; &amp;lt;object&amp;gt;&lt;/code&gt; exists on the admin socket and dumps the entire map. Strictly worse than &lt;code&gt;listomapkeys&lt;/code&gt; for this purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Method 2 — Narrowing down suspicious PGs/objects
&lt;/h2&gt;

&lt;p&gt;There is no supported API that ranks objects by OMAP size, so "narrowing down" means combining the live aggregates with the stale scrub snapshot and with knowledge of the application. The funnel that actually works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. pools with OMAP         ceph df detail                    live bytes per pool
        │
        ▼
2. growth trend            ceph df detail --format json      sample every few
                           .stats.stored_omap per pool       minutes, alert on
                                                             rate or level
        │
        ▼
3. PGs holding OMAP        ceph pg dump pgs --format json    num_objects_omap&amp;gt;0
   objects                 plus num_omap_keys and            last deep-scrub
                           last_deep_scrub_stamp             snapshot as a prior
        │
        ▼
4. candidate objects       application knowledge             RGW index shards,
                           (never rados ls + listomapkeys    your own key layout
                            across the pool)                 MDS/RBD metadata
        │
        ▼
5. targeted count          rados listomapkeys &amp;lt;obj&amp;gt; | wc -l  a few objects only
        │
        ▼
6. targeted deep scrub     ceph pg deep-scrub &amp;lt;pgid&amp;gt;         authoritative;
                           ceph osd pool deep-scrub &amp;lt;pool&amp;gt;   updates pg stats and
                                                             the health check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 3 deserves a note. The last deep scrub gave you &lt;code&gt;num_omap_keys&lt;/code&gt; per PG. A PG that already had 150000 keys across three OMAP objects a few days ago, in a pool whose live OMAP bytes have since doubled, is a much better candidate than a PG with zero. That prior is stale, but it is free, and it is per PG rather than per pool.&lt;/p&gt;

&lt;p&gt;Step 6 is the part that people underrate. A deep scrub of a single PG is a bounded, throttled operation that runs through the OSD's scrub machinery with chunking and &lt;code&gt;osd_scrub_sleep&lt;/code&gt;, and it produces the authoritative answer in the same place the periodic scrub would: &lt;code&gt;num_large_omap_objects&lt;/code&gt; in the PG stats, the object name in the cluster log, and the health check. On the lab PG it took two seconds. On a production PG it is a full read of that PG's data as well as its OMAP, so pick PGs, not pools, unless the pool is an index-only pool that is small in bytes.&lt;/p&gt;

&lt;p&gt;Options for making that scrub happen sooner without doing it by hand:&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="c"&gt;# one PG, now&lt;/span&gt;
ceph pg deep-scrub &amp;lt;pgid&amp;gt;

&lt;span class="c"&gt;# every PG of a pool&lt;/span&gt;
ceph osd pool deep-scrub &amp;lt;pool&amp;gt;

&lt;span class="c"&gt;# schedule rather than force, on the primary OSD&lt;/span&gt;
ceph tell osd.&amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; schedule-deep-scrub &amp;lt;pgid&amp;gt;

&lt;span class="c"&gt;# per-pool deep-scrub interval in seconds (overrides osd_deep_scrub_interval)&lt;/span&gt;
ceph osd pool &lt;span class="nb"&gt;set&lt;/span&gt; &amp;lt;pool&amp;gt; deep_scrub_interval &amp;lt;seconds&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pool-level &lt;code&gt;deep_scrub_interval&lt;/code&gt; is the closest thing Ceph has to "watch this pool harder". Index pools are small in bytes and expensive in OMAP, which is exactly the profile where a shorter deep-scrub interval costs little.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rados -p &amp;lt;pool&amp;gt; ls&lt;/code&gt; is not part of the funnel. It lists names with no sizes, and a pool of any size makes the follow-up per-object listing the very scan that the section on full scans below argues against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3 — RGW bucket-index monitoring
&lt;/h2&gt;

&lt;p&gt;RGW was not deployed in this lab, so this section is verified against the Tentacle source and the &lt;code&gt;radosgw-admin&lt;/code&gt; binary from the same release, not against live buckets.&lt;/p&gt;

&lt;p&gt;A bucket index shard is one RADOS object named &lt;code&gt;.dir.&amp;lt;bucket_instance_id&amp;gt;.&amp;lt;shard&amp;gt;&lt;/code&gt; in the zone's &lt;code&gt;.rgw.buckets.index&lt;/code&gt; pool, and every object in the bucket is at least one OMAP key in one of those shards. That mapping is what makes RGW the one workload where an early check exists that does not read OMAP at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;radosgw-admin bucket limit check
radosgw-admin bucket limit check &lt;span class="nt"&gt;--warnings-only&lt;/span&gt;
radosgw-admin bucket limit check &lt;span class="nt"&gt;--uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;user&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per bucket it prints &lt;code&gt;num_objects&lt;/code&gt;, &lt;code&gt;num_shards&lt;/code&gt;, &lt;code&gt;objects_per_shard&lt;/code&gt;, and a &lt;code&gt;fill_status&lt;/code&gt; of &lt;code&gt;OK&lt;/code&gt;, &lt;code&gt;WARN &amp;lt;pct&amp;gt;%&lt;/code&gt;, or &lt;code&gt;OVER &amp;lt;pct&amp;gt;%&lt;/code&gt;. The numbers come from the per-shard index headers, which RGW maintains on every index update, so the check reads one small header per shard rather than walking the keys. The percentage is &lt;code&gt;objects_per_shard&lt;/code&gt; against &lt;code&gt;rgw_safe_max_objects_per_shard&lt;/code&gt; (default 102400), and &lt;code&gt;WARN&lt;/code&gt; starts at &lt;code&gt;rgw_shard_warning_threshold&lt;/code&gt; (default 90, so 92160 objects per shard).&lt;/p&gt;

&lt;p&gt;Compare that to the OSD threshold of 200000 keys per object. Dynamic resharding, on by default via &lt;code&gt;rgw_dynamic_resharding&lt;/code&gt;, triggers at &lt;code&gt;rgw_max_objs_per_shard&lt;/code&gt; (default 100000) and is evaluated by the reshard thread every &lt;code&gt;rgw_reshard_thread_interval&lt;/code&gt; seconds (default 600). When it is working, shards are split at roughly half the OSD's key threshold, and &lt;code&gt;LARGE_OMAP_OBJECTS&lt;/code&gt; should never fire for a bucket index. When it does fire for an index object, one of these is usually true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic resharding is disabled, or the bucket exceeded &lt;code&gt;rgw_max_dynamic_shards&lt;/code&gt; (default 1999).&lt;/li&gt;
&lt;li&gt;The deployment is multisite on a release before Reef, where dynamic resharding is not supported.&lt;/li&gt;
&lt;li&gt;Resharding is queued but not completing. Check &lt;code&gt;radosgw-admin reshard list&lt;/code&gt; and &lt;code&gt;radosgw-admin reshard status --bucket=&amp;lt;name&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The index holds more entries than the object count suggests, for example versioned buckets with many versions per name or a backlog of incomplete multipart uploads. Treat &lt;code&gt;objects_per_shard&lt;/code&gt; as a proxy with margin, not as a key count.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To go from the cluster log line back to a bucket, take the instance id out of the &lt;code&gt;.dir.&amp;lt;bucket_instance_id&amp;gt;.&amp;lt;shard&amp;gt;&lt;/code&gt; object name and match it against the &lt;code&gt;id&lt;/code&gt; field in &lt;code&gt;radosgw-admin bucket stats&lt;/code&gt;. To go the other way and count a specific shard's keys, the generic tool still applies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rados &lt;span class="nt"&gt;-p&lt;/span&gt; &amp;lt;zone&amp;gt;.rgw.buckets.index listomapkeys .dir.&amp;lt;bucket_instance_id&amp;gt;.&amp;lt;shard&amp;gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with the same cost caveats as Method 1. Manual resharding is &lt;code&gt;radosgw-admin bucket reshard --bucket=&amp;lt;name&amp;gt; --num-shards=&amp;lt;n&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The practical RGW rule: run &lt;code&gt;bucket limit check --warnings-only&lt;/code&gt; on a schedule and alert on any output, verify that &lt;code&gt;reshard list&lt;/code&gt; drains, and let the OSD-level warning be the backstop rather than the detector.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Prometheus can and cannot tell us
&lt;/h2&gt;

&lt;p&gt;I enabled the mgr &lt;code&gt;prometheus&lt;/code&gt; module on the lab for the duration of the test and pulled the endpoint while the large object existed and the health check was active. Grepping the 113 metric families for &lt;code&gt;omap&lt;/code&gt; matched my pool's name in a &lt;code&gt;ceph_pool_metadata&lt;/code&gt; label and exactly one real series:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ceph_health_detail{name="LARGE_OMAP_OBJECTS",severity="HEALTH_WARN"} 1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the only one. It went from 0 to 1 after the deep scrub, and the underlying health check cleared after the object was removed and the PG deep-scrubbed again. It is a correct and useful alert, and it is exactly as late as the health check because it is the health check.&lt;/p&gt;

&lt;p&gt;What the module does not export, verified in its source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No per-pool OMAP bytes. The pool DF series are &lt;code&gt;ceph_pool_stored&lt;/code&gt;, &lt;code&gt;ceph_pool_stored_raw&lt;/code&gt;, &lt;code&gt;ceph_pool_bytes_used&lt;/code&gt;, &lt;code&gt;ceph_pool_objects&lt;/code&gt; and similar. &lt;code&gt;stored_omap&lt;/code&gt; and &lt;code&gt;omap_bytes_used&lt;/code&gt; exist in &lt;code&gt;ceph df detail --format json&lt;/code&gt; but are not in the list the module publishes.&lt;/li&gt;
&lt;li&gt;No per-PG &lt;code&gt;num_omap_keys&lt;/code&gt; or &lt;code&gt;num_omap_bytes&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;num_objects_omap&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No per-object anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Daemon perf counters are not served by the mgr module either. &lt;code&gt;mgr/prometheus/exclude_perf_counters&lt;/code&gt; defaults to true and the expected source is &lt;code&gt;ceph-exporter&lt;/code&gt;. Those counters include the BlueStore &lt;code&gt;omap_*&lt;/code&gt; series shown earlier. Even when exported, they are per-OSD totals of calls and records. A spike in OMAP set records on one OSD tells you that something is writing OMAP through that OSD. It does not name a pool, a PG, or an object.&lt;/p&gt;

&lt;p&gt;So the honest monitoring picture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;metric exists?            per pool  per PG  per object  live?
------------------------  --------  ------  ----------  -----------------
OMAP bytes (df detail)    yes*      no      no          yes
OMAP key count            no        no      no          deep scrub only†
large omap object count   no        no      no          deep scrub only†
LARGE_OMAP_OBJECTS        cluster   -       -           after deep scrub

* in ceph df detail JSON, not exported by the Prometheus module
† and not exported by the Prometheus module at all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one PromQL expression I can honestly recommend on a stock Tentacle cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ceph_health_detail{name="LARGE_OMAP_OBJECTS"} == 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want the live pool-level OMAP figure in Prometheus, you have to put it there yourself, for example a small textfile-collector script that runs &lt;code&gt;ceph df detail --format json&lt;/code&gt; and emits &lt;code&gt;stored_omap&lt;/code&gt; per pool. That is cheap because the OSDs already compute the number for every stats report. It is not something Ceph exports for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why scanning every object is a bad monitoring strategy
&lt;/h2&gt;

&lt;p&gt;The tempting cron job looks like this:&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="k"&gt;for &lt;/span&gt;obj &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;rados &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pool&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt;&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="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;rados &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pool&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; listomapkeys &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$obj&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 150000 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$obj&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Measured against what the OSD actually does, this is a deep scrub of the pool's OMAP, minus everything that makes deep scrub safe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It reads every key of every object, so the cost is the total OMAP key count of the pool, not the number of objects. An index pool with a few thousand shards of 50000 entries each is hundreds of millions of RocksDB iterations per run.&lt;/li&gt;
&lt;li&gt;It runs in the client op path on the primary OSDs, competing with real I/O, with no chunking, no &lt;code&gt;osd_scrub_sleep&lt;/code&gt;, no scrub reservations, no load or time-window gating.&lt;/li&gt;
&lt;li&gt;It runs from one client, so the OSD-side reads are serialized behind whatever concurrency the script has and the client becomes a bottleneck long before the cluster does.&lt;/li&gt;
&lt;li&gt;The result is stale the moment it finishes, exactly like the scrub snapshot, but without the cluster-log entry and health check that a real deep scrub gives you for free.&lt;/li&gt;
&lt;li&gt;On erasure-coded pools OMAP is not supported at all, and on replicated pools you are still reading only the primary copy, so it checks less than scrub does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deep scrub already implements the full-walk detector, with backpressure, and writes the answer into the PG stats and cluster log. If you believe a whole pool needs checking now, the correct full-walk tool is &lt;code&gt;ceph osd pool deep-scrub &amp;lt;pool&amp;gt;&lt;/code&gt;, not a shell loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical early-warning strategy
&lt;/h2&gt;

&lt;p&gt;Everything above collapses into a workflow that leans on the live aggregates for detection, the application for candidates, and deep scrub for confirmation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 OMAP-heavy workload
                         │
                         ▼
        ┌── inventory: which pools carry OMAP ──┐
        │   ceph df detail  (STORED/USED OMAP)  │
        │   ceph pg dump pools  num_objects_omap│
        └───────────────────┬───────────────────┘
                            ▼
              trend pool-level OMAP bytes
              (stored_omap per pool, sampled)
                            │
             ┌──────────────┴──────────────┐
             │                             │
            RGW                     generic RADOS
             │                             │
   bucket limit check            application-level counters
   --warnings-only               (keys per object, per shard)
   reshard list drains?          shard keys across objects
             │                             │
             └──────────────┬──────────────┘
                            ▼
             candidate objects / PGs identified
                            │
                            ▼
          rados listomapkeys &amp;lt;obj&amp;gt; | wc -l   (a few objects)
                            │
                            ▼
                  ceph pg deep-scrub &amp;lt;pgid&amp;gt;
                            │
                            ▼
      pg stats num_large_omap_objects, cluster log entry,
      LARGE_OMAP_OBJECTS health check (or its absence)
                            │
                            ▼
      backstop: alert on ceph_health_detail LARGE_OMAP_OBJECTS
      and, for index-style pools, a shorter pool deep_scrub_interval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Concretely:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Know your OMAP pools.&lt;/strong&gt; Run the &lt;code&gt;ceph df detail&lt;/code&gt; and &lt;code&gt;num_objects_omap&lt;/code&gt; queries once and write down which pools are OMAP-bearing. On this RGW-less lab the answer is the RBD pools, whose &lt;code&gt;rbd_directory&lt;/code&gt; and related metadata objects carry OMAP. On yours it is probably &lt;code&gt;.rgw.buckets.index&lt;/code&gt;, the CephFS metadata pool, and whatever your librados applications use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trend the live number.&lt;/strong&gt; Sample &lt;code&gt;stored_omap&lt;/code&gt; per pool every few minutes. Alert on growth rate for pools that should be flat and on absolute level for index pools. This is the only continuous signal Ceph gives you and it costs nothing extra.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instrument the application.&lt;/strong&gt; If you write OMAP through librados, you already know the key count per object because you wrote the keys. Count them where they are produced and cap them: the MDS uses the OSD threshold itself as the per-object limit and fans out. Do the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RGW: check shards, not OMAP.&lt;/strong&gt; &lt;code&gt;bucket limit check --warnings-only&lt;/code&gt; on a schedule, plus confirmation that dynamic resharding is enabled and &lt;code&gt;reshard list&lt;/code&gt; is draining.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect, then confirm.&lt;/strong&gt; For the few candidates that the trend or the application points to, count keys with &lt;code&gt;listomapkeys | wc -l&lt;/code&gt;, and deep-scrub the owning PG so the authoritative stats, the cluster log, and the health check all agree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shorten the interval where it is cheap.&lt;/strong&gt; Set &lt;code&gt;deep_scrub_interval&lt;/code&gt; on index-style pools that are small in bytes and heavy in OMAP. You are paying for OMAP walks either way; paying more often on a small pool is the cheapest generic way to move the warning earlier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the backstop.&lt;/strong&gt; Alert on &lt;code&gt;ceph_health_detail{name="LARGE_OMAP_OBJECTS"} == 1&lt;/code&gt;. It is late, but it is authoritative.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Commands cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Live or snapshot&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Thresholds in force&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ceph config get osd osd_deep_scrub_large_omap_object_key_threshold&lt;/code&gt; / &lt;code&gt;..._value_sum_threshold&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;config&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-pool OMAP bytes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ceph df detail&lt;/code&gt;, or the &lt;code&gt;stored_omap&lt;/code&gt; field of &lt;code&gt;ceph df detail --format json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;live estimate&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-OSD OMAP bytes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ceph osd df&lt;/code&gt; (OMAP column)&lt;/td&gt;
&lt;td&gt;live estimate&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One OSD, one pool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ceph tell osd.N dump_pool_statfs &amp;lt;poolid&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;live estimate&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PGs holding OMAP objects&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ceph pg dump pgs --format json&lt;/code&gt;, filter &lt;code&gt;stat_sum.num_objects_omap &amp;gt; 0&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;live count&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PG OMAP keys/bytes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ceph pg ls-by-pool &amp;lt;pool&amp;gt;&lt;/code&gt; or &lt;code&gt;ceph pg &amp;lt;pgid&amp;gt; query&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;last deep scrub&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health check&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ceph health detail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;last deep scrub&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which object&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ceph log last 300 warn cluster&lt;/code&gt;, search for &lt;code&gt;Large omap&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;last deep scrub&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Count keys of one object&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rados -p &amp;lt;pool&amp;gt; listomapkeys &amp;lt;obj&amp;gt;&lt;/code&gt; piped to &lt;code&gt;wc -l&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;live&lt;/td&gt;
&lt;td&gt;full key walk on primary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deep scrub one PG&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ceph pg deep-scrub &amp;lt;pgid&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;produces snapshot&lt;/td&gt;
&lt;td&gt;full PG read, throttled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deep scrub a pool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ceph osd pool deep-scrub &amp;lt;pool&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;produces snapshot&lt;/td&gt;
&lt;td&gt;full pool read, throttled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pool-specific interval&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ceph osd pool set &amp;lt;pool&amp;gt; deep_scrub_interval &amp;lt;sec&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;scheduling&lt;/td&gt;
&lt;td&gt;ongoing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RGW shard fill&lt;/td&gt;
&lt;td&gt;&lt;code&gt;radosgw-admin bucket limit check --warnings-only&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;live from index headers&lt;/td&gt;
&lt;td&gt;one header read per shard, no key walk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RGW reshard backlog&lt;/td&gt;
&lt;td&gt;&lt;code&gt;radosgw-admin reshard list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;live&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prometheus backstop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ceph_health_detail{name="LARGE_OMAP_OBJECTS"} == 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;last deep scrub&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Final result
&lt;/h2&gt;

&lt;p&gt;Back to the scenario: an OMAP object is growing, deep scrub has not run, and &lt;code&gt;ceph health&lt;/code&gt; is clean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Ceph already has.&lt;/strong&gt; Per pool, a live estimate of OMAP bytes on disk, visible in &lt;code&gt;ceph df detail&lt;/code&gt; and in the pool stats. Per PG and per pool, a live count of objects that have OMAP. Per OSD, aggregate OMAP byte estimates and OMAP operation counters. All of these update without any scrub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Ceph only learns during deep scrub.&lt;/strong&gt; Per-PG OMAP key count and value bytes, the per-PG count of objects over the threshold, and the name, key count, and value size of the offending object. Shallow scrub contributes nothing. These are snapshots that do not move until the next deep scrub of that PG, in either direction: growth after the scrub is invisible, and deleting the object does not clear the warning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What can be inspected proactively.&lt;/strong&gt; The live aggregates, at zero cost. The last deep-scrub snapshot per PG, at zero cost, as a prior. Individual objects with &lt;code&gt;listomapkeys&lt;/code&gt;, at the cost of the OSD iterating every key of that object.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How expensive.&lt;/strong&gt; Aggregates are free. A single targeted &lt;code&gt;listomapkeys&lt;/code&gt; is linear in the object's key count and runs unthrottled on the primary; fine for a handful, wrong for a loop. A targeted PG deep scrub is a full throttled read of one PG and yields the authoritative answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there a scalable cluster-wide solution.&lt;/strong&gt; Not a generic one. Ceph 20.2 does not expose an inexpensive per-object OMAP inventory outside of scrub, and there is no top-N API. Cluster-wide early detection therefore has to come from pool-level trending, application awareness, RGW-specific tooling, and more frequent or targeted deep scrubs of the pools that matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is different for RGW.&lt;/strong&gt; The index-shard-to-object mapping is fixed and known, bucket stats track entries per bucket without reading OMAP, and &lt;code&gt;bucket limit check&lt;/code&gt; plus dynamic resharding keep shards well under the OSD threshold when they work. RGW is the one workload with a cheap, early, per-shard check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What monitoring to implement.&lt;/strong&gt; Trend &lt;code&gt;stored_omap&lt;/code&gt; per pool from &lt;code&gt;ceph df detail&lt;/code&gt;; alert on &lt;code&gt;bucket limit check&lt;/code&gt; warnings and a non-draining reshard queue; instrument key counts in your own librados writers; shorten &lt;code&gt;deep_scrub_interval&lt;/code&gt; on small OMAP-heavy pools; and keep &lt;code&gt;ceph_health_detail{name="LARGE_OMAP_OBJECTS"}&lt;/code&gt; as the authoritative, late backstop.&lt;/p&gt;

</description>
      <category>database</category>
      <category>devops</category>
      <category>infrastructure</category>
      <category>sre</category>
    </item>
  </channel>
</rss>
