The Comprehensive Ceph Guide
فارسیGet started
On this page
Chapter 8

Ceph Under the Hood: CRUSH, Maps, Authentication and PGs

35 min readEdit this page
TL;DR this chapter in a few lines
  • CRUSH replaces a central lookup table: a hierarchy of buckets plus rules computes the OSDs of each PG, so clients find data themselves.
  • An object hashes to a PG and the PG maps to OSDs in different failure domains; every change of the maps is versioned by an epoch.
  • Tunables fix old CRUSH behaviours but change mappings, so enabling them moves data.
  • cephx gives Kerberos-like mutual authentication with time-limited tickets; device classes put a pool on chosen OSDs.

8-1 Before you start#

8-1-1 What questions does this chapter answer?#

So far we have seen Ceph from the outside: commands, services and interfaces. This chapter goes inside. Every important decision in designing and troubleshooting Ceph (why the data is where it is, why a PG is stuck, why a CRUSH rule does not work) comes back to a few basic mechanisms, which we examine in this chapter:

  • How does data find its place without a central table?
  • What structure does the CRUSH map have, and how do we write rules?
  • What are tunables, and why does the "non-optimal tunables" warning appear?
  • Which cluster maps are there, and how do the MONs stay available?
  • How does cephx work, and how do we restrict capabilities?
  • What is the path of a write from the client to the disk, and what states do PGs have?
  • How do we place a pool on specific OSDs?

8-2 Scaling without a central table#

8-2-1 The placement problem#

Most older distributed storage systems had a metadata server or lookup table that said where each block was. At large scale this table has three problems: it becomes a performance bottleneck, it is a single point of failure, and it must be updated with every hardware change. Ceph has removed this table: every client and every OSD computes the location of the data itself. The inputs to this computation are only the object name, the pool id and the cluster map, which everyone has.

Figure 8-1: The two placement stages: the object name is mapped to a PG by a hash, and CRUSH maps that PG to a set of OSDs
Figure 8-1: The two placement stages: the object name is mapped to a PG by a hash, and CRUSH maps that PG to a set of OSDs

8-2-2 The two mapping stages#

  1. 1. Object to PG: the object name is hashed, and its remainder modulo the pool's PG count (more precisely, with the stable mod algorithm) gives the PG number. A PG id combines the pool number and the PG number, such as 3.1f.
  2. 2. PG to OSD: the CRUSH algorithm, given the PG id, the CRUSH map and the pool's rule, returns an ordered list of OSDs. The first active OSD is the primary.
bashReef+
ceph osd map rbd rbd_data.12ab.0000000000000004
# osdmap e1234 pool 'rbd' (3) object '...' -> pg 3.8c7e1b1f (3.1f) -> up ([12,4,27], p12) acting ([12,4,27], p12)
ceph pg map 3.1f
◆ Deeper

Why the intermediate PG layer? If we ran CRUSH directly on each object, the cluster would have to track replication, recovery and scrub state separately for each object; for billions of objects that is impossible. PGs divide objects into a few thousand groups, and all the data protection mechanisms (peering, log, recovery, scrub) work at the PG level. The PG count is a trade-off: too few and the distribution becomes uneven; too many and OSD memory and CPU and the peering load go up.

8-2-3 up and acting#

The up set is CRUSH's pure output. The acting set is the set that actually serves requests. The two are usually identical; a difference means Ceph is temporarily using other OSDs (for example pg_temp during backfill until the new OSD has all the data) or an explicit upmap has changed CRUSH's result for that PG.

8-3 The CRUSH mechanism#

8-3-1 The hierarchy#

Figure 8-2: A sample CRUSH hierarchy: root, datacenter, rack, host and OSDs with device classes
Figure 8-2: A sample CRUSH hierarchy: root, datacenter, rack, host and OSDs with device classes

The CRUSH map consists of two parts: the hierarchy (a tree of buckets showing the physical location of each OSD) and the rules, which say how to spread replicas across this tree. The default bucket types, from bottom to top, are osd, host, chassis, rack, row, pdu, pod, room, datacenter, zone, region and root. You do not need to use all of them; define only the levels that really are independent failure domains.

bashReef+
ceph osd crush tree --show-shadow
ceph osd crush add-bucket rack1 rack
ceph osd crush move rack1 root=default
ceph osd crush move ceph-node1 rack=rack1
ceph osd crush set-device-class nvme osd.20
ceph osd crush class ls
💡 Tip

With cephadm you can set the host's CRUSH location in the host spec (location: {rack: rack1}) so that new OSDs land directly under the right rack. Otherwise every OSD goes under its host, directly under root=default, and after you move it by hand the data moves once more.

8-3-2 Weight#

Each OSD has a CRUSH weight (by default equal to its capacity in terabytes), and each bucket's weight is the sum of its children's weights. CRUSH spreads data in proportion to weight. There is also another weight called reweight (between 0 and 1) that used to be used to correct the distribution temporarily; today the balancer does this more precisely with upmap.

bashReef+
ceph osd crush reweight osd.20 3.49309     # permanent CRUSH weight
ceph osd reweight 20 0.9                   # temporary correction (prefer the balancer)

8-3-3 The bucket algorithm#

CRUSH has an algorithm for choosing a child in each bucket. Today in practice only straw2 is used (since Hammer, the default for new buckets). The key advantage of straw2 is that changing one item's weight moves data only between that item and the rest, not among the other items; that is, the minimum necessary movement.

8-3-4 Rules#

A CRUSH rule defines the selection steps. Most of the time you do not need to write a rule by hand; the helper commands are enough:

bashReef+
# Replicated: replicas on different hosts, SSD only
ceph osd crush rule create-replicated rep-ssd-host default host ssd
# Replicated with a rack failure domain
ceph osd crush rule create-replicated rep-rack default rack
# EC from a profile
ceph osd erasure-code-profile set ec42-rack k=4 m=2 crush-failure-domain=rack crush-device-class=hdd
ceph osd crush rule create-erasure ec42-rack ec42-rack
ceph osd crush rule ls
ceph osd crush rule dump rep-ssd-host

For more complex cases, extract the map, edit it and test it before applying:

bashReef+
ceph osd getcrushmap -o crush.bin
crushtool -d crush.bin -o crush.txt
# edit crush.txt
crushtool -c crush.txt -o crush.new
crushtool -i crush.new --test --rule 1 --num-rep 3 --show-statistics --show-bad-mappings
ceph osd setcrushmap -i crush.new

An example of a hand-written rule that puts two replicas in one datacenter and the third in another datacenter:

text
rule rep-2dc {
    id 5
    type replicated
    step take default
    step choose firstn 2 type datacenter
    step chooseleaf firstn 2 type host
    step emit
}
⚠ Warning

With size=3 the rule above puts two replicas in the first datacenter and one in the second, but which datacenter gets two replicas differs for each PG. If you want both sites to be certain to hold a replica and writes to continue when one site is cut off, use stretch mode (chapter 18) instead of a hand-written rule; it also manages the quorum and min_size logic.

✎ Note

Squid added a new rule type called MSR (Multi-Step Retry) which, according to the release notes, enables more flexible EC configurations; its common use is controlled placement of several shards in one failure domain when the number of domains is less than k+m. Because this rule type is recorded in the CRUSH map, require_min_compat_client also applies to it; check your clients' versions before using it.

8-3-5 Device classes and the shadow tree#

Since Luminous each OSD has a device class (hdd, ssd, nvme or any name you like). CRUSH builds a shadow tree for each class, for example default~ssd, containing only the OSDs of that class. A rule with a class runs on that shadow tree. This feature replaces the old trick from the 2019 book of building two separate roots to separate SSDs and HDDs.

8-4 Tunables#

8-4-1 Why tunables?#

CRUSH was not perfect from the start. In some hierarchies (for example small trees, or when many OSDs are out) it could not find enough replicas for every PG, or it moved a lot of data for small changes. Fixing these behaviours changes the mapping result, so they could not be applied silently: every client must compute the same result the OSDs compute. Ceph's solution is tunables: flags in the CRUSH map that determine the algorithm's behaviour, grouped into profiles named after releases.

ProfileMain featureMinimum client kernel (per the documentation)
argonaut (legacy)Original behaviourAll
bobtail (CRUSH_TUNABLES2)Fewer failures to find a mapping and fixed retries3.9
firefly (CRUSH_TUNABLES3)Fixed chooseleaf behaviour with re-descent3.15
hammer (CRUSH_V4)straw2 bucket type4.1
jewel (CRUSH_TUNABLES5)chooseleaf_stable: less movement when an OSD goes out4.5
bashReef+
ceph osd crush show-tunables
ceph osd crush tunables optimal       # causes data movement!
ceph features                         # version and features of connected clients
ceph osd get-require-min-compat-client
⚠ Warning

Changing the tunable profile on a cluster full of data moves a large part of the data. Do it in off-peak hours, with a suitable mClock profile, and after making sure all clients are compatible (especially old kernel RBD and CephFS clients). Clusters built with recent releases have optimal tunables from the start, and this section matters mostly for old upgraded clusters.

8-4-2 The non-optimal warning#

If the cluster's tunables lag behind the optimal profile of the current release, the OLD_CRUSH_TUNABLES warning is shown. You can fix it by updating the tunables or, if you have old clients you cannot upgrade, silence it temporarily with mon_warn_on_legacy_crush_tunables=false. A similar warning, OLD_CRUSH_STRAW_CALC_VERSION, appears when the map still uses the old, buggy version of the weight calculation in straw buckets.

bashReef+
# convert straw buckets to straw2 with minimal movement
ceph osd crush set-all-straw-buckets-to-straw2

8-5 Cluster maps and MONs#

Figure 8-3: Cluster maps are kept in the MONs and versioned by epoch; clients and OSDs get the new map from the MON or from each other
Figure 8-3: Cluster maps are kept in the MONs and versioned by epoch; clients and OSDs get the new map from the MON or from each other

8-5-1 The maps#

The cluster state is kept in several maps, all versioned with an epoch (an increasing version number):

MapContentsCommand to view
MonMapfsid, the list of MONs and their addressesceph mon dump
OSDMappools, OSDs and their up/in state, pg_temp and upmaps, flags and the CRUSH mapceph osd dump
MgrMapactive MGR, standbys and modulesceph mgr dump
FSMapfile systems and MDSsceph fs dump
PGMapPG statistics (in the MGR, not the MON)ceph pg dump
◆ Deeper

OSDs do not necessarily get the new map from the MON: every message between OSDs and clients carries the sender's epoch, and the side with the newer map sends the changes (incrementals) to the other. This "gossip" reduces the load on the MONs. The practical consequence: an OSD that has been off for a while must process all the missed epochs when it comes up, and if the MONs keep old epochs (for example because the cluster has been unhealthy for a long time) the MON database grows.

8-5-2 MON availability#

MONs reach consensus with the Paxos algorithm: a map change is valid only when a majority of MONs (a quorum) accepts it. One MON is the leader and the rest are peons.

bashReef+
ceph mon stat
ceph quorum_status -f json-pretty
ceph mon dump
ceph tell mon.ceph-node1 mon_status
Number of MONsMajority neededFailures tolerated
110
321
532
743
⚠ Warning

An even number of MONs does not increase fault tolerance: four MONs also tolerate only one failure. MONs are sensitive to accurate time (the MON_CLOCK_SKEW warning); configure and monitor chrony or NTP on all nodes. Put the MON database (RocksDB) on SSD and monitor its disk space; when free space runs low, the MON_DISK_LOW warning and eventually a MON shutdown follow.

✎ Note

Umbrella added an optional MON RocksDB backup and restore mechanism (the mon_backup_interval setting, the ceph tell mon.* backup command and offline restore with ceph-mon --restore-backup). Before any risky operation on the MONs (removal, rebuild or a major upgrade), be aware of this option or of the documented methods for rebuilding the MON database; losing all MONs without a backup is Ceph's worst scenario.

8-6 Inside the code: placement, authentication and messaging#

8-6-1 From object name to OSD#

Figure 8-4: The three stages of mapping an object to an OSD: in the client the object name is hashed with ceph_str_hash and turned into a PG number with ceph_stable_mod; then the PG number is combined with the pool id and given to crush_do_rule as input x; finally the OSDMap removes non-existent OSDs and applies upmap and pg_temp to obtain the acting set and the primary
Figure 8-4: The three stages of mapping an object to an OSD: in the client the object name is hashed with ceph_str_hash and turned into a PG number with ceph_stable_mod; then the PG number is combined with the pool id and given to crush_do_rule as input x; finally the OSDMap removes non-existent OSDs and applies upmap and pg_temp to obtain the acting set and the primary

The first stage happens in pg_pool_t::hash_key() (src/osd/osd_types.cc): the object name, together with the namespace if there is one, is turned into a 32-bit number with the pool's hash function (rjenkins by default). This number is then mapped to the PG range with ceph_stable_mod(). This small function is one of the cleverest pieces of Ceph:

c
/* from ceph/ceph, LGPL — src/include/rados.h (excerpt) */
static inline int ceph_stable_mod(int x, int b, int bmask)
{
	if ((x & bmask) < b)
		return x & bmask;
	else
		return x & (bmask >> 1);
}

b is the number of PGs and bmask is the smallest power of 2 minus one that is at least b. If pg_num is not a power of 2 (for example 12 with bmask=15), the values 12 to 15, which have no corresponding PG, are "folded" into the lower half (4 to 7). The important result: when pg_num grows from 12 to 13, only the objects that went to PG 5 and whose hash falls on 13 move; that is, PG splitting is local and the data is not all reshuffled. This property is what made gradual pg_num increases (since Nautilus) possible.

✎ Note

For this reason, when pg_num is not a power of 2, PG sizes are unequal: the "folded" PGs have roughly twice as much data. This is one of the reasons for the power-of-2 recommendation and for the autoscaler's behaviour (chapter 19).

8-6-2 straw2: choosing by drawing exponential lots#

Figure 8-5: The straw2 algorithm in three stages: for each bucket item a sixteen-bit pseudo-random number is built from the input hash, the item id and the attempt number, and its natural logarithm is computed with a table and divided by the weight; the item with the largest value is chosen, which is mathematically equivalent to picking the minimum of exponential variables with rate equal to the weight and makes each item's selection probability exactly proportional to its weight; the consequence is minimal data movement when weights change
Figure 8-5: The straw2 algorithm in three stages: for each bucket item a sixteen-bit pseudo-random number is built from the input hash, the item id and the attempt number, and its natural logarithm is computed with a table and divided by the weight; the item with the largest value is chosen, which is mathematically equivalent to picking the minimum of exponential variables with rate equal to the weight and makes each item's selection probability exactly proportional to its weight; the consequence is minimal data movement when weights change

bucket_straw2_choose() in src/crush/mapper.c computes a "draw" for each bucket item and chooses the largest. The core of the computation:

c
/* from ceph/ceph, LGPL — src/crush/mapper.c, generate_exponential_distribution() (excerpt) */
unsigned int u = crush_hash32_3(type, x, y, z);
u &= 0xffff;
__s64 ln = crush_ln(u) - 0x1000000000000ll;
return div64_s64(ln, weight);

The mathematics behind it (in this book's words): suppose U is uniform on (0, 1]. Then −ln(U) / w is an exponential variable with rate w. The minimum of several independent exponential variables with rates w₁ to wₙ belongs to item i with probability wᵢ / Σw. Because ln(U) is negative, ln(U)/w is largest for the same item for which −ln(U)/w is smallest. Therefore:

text
draw_i = ln(u_i / 65536) / w_i          (u_i = hash(x, id_i, r) mod 2^16)
choose argmax_i draw_i   ⇒   P(i) = w_i / Σ_j w_j

crush_ln() approximates the logarithm with fixed-point tables (no floating point, because the same code also runs in the Linux kernel); its output is in the range [0, 2⁴⁸), and subtracting 2⁴⁸ turns it into the negative value equivalent to ln(u/65536). Weights are in 16.16 fixed-point format (0x10000 means 1.0).

◆ Deeper

Why does straw2 minimise movement? Each item's draw depends only on its own hash and its own weight. If item A's weight increases, only its draw grows; PGs that had already chosen A still choose A and some other PGs move to A, but no PG moves between two other items (B and C). In the old straw algorithm the length of the "straws" depended on the weights of all items and this property did not hold; that is why straw2 was introduced in Hammer and later became the default.

Lab

CRUSH playground

Build a root, hosts and OSDs, choose a rule, and watch straw2 place every PG; mark an OSD out or down or change a weight and see which PGs move.

64 × 3PGs × replicas
9.9%data moved by the last change
19PGs whose up set changed
0PGs with fewer copies than size
0empty slots (NONE)

Last change: osd.4 marked out. Moved shards are highlighted in the list.

osd.0: 29 (≈24)029osd.1: 14 (≈24)114osd.2: 21 (≈24)221osd.3: 0 (≈0)30osd.4: 38 (≈24)438osd.5: 26 (≈24)526osd.6: 27 (≈24)627osd.7: 14 (≈24)714osd.8: 23 (≈24)823
PG shards on each OSD (bars) against the share its weight would give (line). Red: out; amber: down.
$ ceph osd map pool1 rbd_data.10226b8b4567.0000000000000000ceph_str_hash_rjenkins → 0x87bd7b9a · ceph_stable_mod(·, 64, 63) → pg 1.1a · pps 0x8d2145b9 · up [4,2,8] p4
PG → up set (64 PGs; primary underlined)
  1. 1.0[0,6,5]
  2. 1.1[7,4,0]
  3. 1.2[4,7,0]
  4. 1.3[4,8,2]
  5. 1.4[1,8,5]
  6. 1.5[4,8,0]
  7. 1.6[6,4,2]
  8. 1.7[1,7,5]
  9. 1.8[6,0,4]
  10. 1.9[8,4,2]
  11. 1.a[1,5,7]
  12. 1.b[1,8,5]
  13. 1.c[6,5,2]
  14. 1.d[7,4,2]
  15. 1.e[1,8,4]
  16. 1.f[6,2,4]
  17. 1.10[5,2,8]
  18. 1.11[4,8,0]
  19. 1.12[0,6,5]
  20. 1.13[4,6,1]
  21. 1.14[5,8,1]
  22. 1.15[8,5,2]
  23. 1.16[5,0,6]
  24. 1.17[8,0,5]
  25. 1.18[6,2,4]
  26. 1.19[6,0,4]
  27. 1.1a[4,2,8]
  28. 1.1b[8,1,4]
  29. 1.1c[8,4,1]
  30. 1.1d[6,0,5]
  31. 1.1e[8,4,0]
  32. 1.1f[8,5,2]
  33. 1.20[2,6,5]
  34. 1.21[6,0,4]
  35. 1.22[2,4,6]
  36. 1.23[0,5,7]
  37. 1.24[0,4,6]
  38. 1.25[8,5,0]
  39. 1.26[1,4,6]
  40. 1.27[1,7,5]
  41. 1.28[5,7,0]
  42. 1.29[8,2,4]
  43. 1.2a[5,2,6]
  44. 1.2b[8,0,4]
  45. 1.2c[7,0,4]
  46. 1.2d[2,7,4]
  47. 1.2e[6,2,4]
  48. 1.2f[0,4,6]
  49. 1.30[6,5,0]
  50. 1.31[0,6,4]
  51. 1.32[0,6,4]
  52. 1.33[4,6,2]
  53. 1.34[8,0,5]
  54. 1.35[8,2,4]
  55. 1.36[7,0,4]
  56. 1.37[4,7,0]
  57. 1.38[6,4,2]
  58. 1.39[0,5,6]
  59. 1.3a[0,8,4]
  60. 1.3b[4,1,6]
  61. 1.3c[5,6,0]
  62. 1.3d[5,8,2]
  63. 1.3e[5,1,7]
  64. 1.3f[7,4,1]

Follows:

  • src/crush/mapper.c :: crush_do_rule runs the rule steps (take, chooseleaf, emit)
  • src/crush/mapper.c :: crush_choose_firstn, crush_choose_indep the replicated (firstn) and EC (indep) choice loops with retries
  • src/crush/mapper.c :: bucket_straw2_choose, generate_exponential_distribution, crush_ln straw2: the largest ln(hash)/weight wins
  • src/crush/mapper.c :: is_out the reweight check that rejects an out OSD
  • src/crush/hash.c :: crush_hash32_rjenkins1_2, _3 the Jenkins hash that CRUSH uses
  • src/osd/osd_types.cc :: pg_pool_t::raw_pg_to_pps, hash_key the PG's seed that enters CRUSH
  • src/include/rados.h :: ceph_stable_mod object hash to PG while pg_num is not a power of two
  • src/common/ceph_hash.cc :: ceph_str_hash_rjenkins the object-name hash
  • src/osd/OSDMap.cc :: _pg_to_raw_osds, _raw_to_up_osds raw set to up set (down OSDs are dropped or become NONE)
  • src/crush/CrushWrapper.cc :: add_simple_rule_at, insert_item; CrushWrapper.h :: set_tunables_jewel the rule ceph osd pool create makes and the jewel tunables
A fixed snapshot of the simulator; the interactive version is in the web edition's lab. Interactive lab: CRUSH playground

8-6-3 Retrying: collision and reject#

crush_choose_firstn() descends through a bucket for each replica (rep) until it reaches the desired type. If the chosen item has already been chosen (collide) or the OSD is out or overfull (reject), the ftotal counter is incremented and the choice is repeated with r' = r + ftotal, up to the choose_total_tries limit (a tunable, default 50 in the newer profiles).

text
pseudocode: crush_choose_firstn (this book's summary)
  for rep in 0..numrep-1:
      ftotal = 0
      loop:
          r = rep + ftotal
          item = descend(bucket, x, r) until type matches     # straw2 at each level
          collide = item in out[0..rep-1]
          reject  = is_out(item, x) or (chooseleaf and leaf selection failed)
          if not collide and not reject: out[rep] = item; break
          ftotal += 1
          if ftotal >= choose_total_tries: give up on this rep

The is_out() function is interesting: an OSD with a reweight between 0 and 1 is rejected probabilistically; hash(x, item) & 0xffff is compared with the reweight value (16.16). So ceph osd reweight 12 0.8 means that about 20% of the PGs CRUSH gave to osd.12 go to another OSD after a retry. This is why reweight differs from the CRUSH weight (which changes the share in straw2).

💡 Tip

crushtool --test runs the same code outside the cluster. To see the effect of a change before applying it: ceph osd getcrushmap -o cm, edit, then crushtool -i cm --test --show-statistics --rule 0 --num-rep 3 --min-x 0 --max-x 100000, and compare --show-mappings before and after.

8-6-4 cephx: tickets and rotating keys#

Figure 8-6: The cephx authentication flow: by proving knowledge of the shared key, the client gets a session key and an auth ticket from the MON, then obtains a service ticket for the OSD service encrypted with that service's rotating key; the OSD, which has received the rotating keys from the MON, decrypts the ticket, checks its expiry and caps, and replies so that mutual authentication is complete
Figure 8-6: The cephx authentication flow: by proving knowledge of the shared key, the client gets a session key and an auth ticket from the MON, then obtains a service ticket for the OSD service encrypted with that service's rotating key; the OSD, which has received the rotating keys from the MON, decrypts the ticket, checks its expiry and caps, and replies so that mutual authentication is complete

The main structures are defined in src/auth/cephx/CephxProtocol.h: CephXServerChallenge, CephXAuthenticate, CephXTicketBlob, CephXServiceTicket, CephXServiceTicketInfo and CephXAuthorizer. The three main request types are identified by the constants CEPHX_GET_AUTH_SESSION_KEY (0x0100), CEPHX_GET_PRINCIPAL_SESSION_KEY (0x0200) and CEPHX_GET_ROTATING_KEY (0x0400).

  • The client's shared key is never sent over the network; the client only proves that it has it (by answering a challenge).
  • The service ticket is encrypted with the service's rotating key, which the MON changes periodically and the OSDs fetch; so the OSD does not need to contact the MON to check a ticket.
  • Ticket lifetimes are controlled by auth_mon_ticket_ttl (default 72 hours) and auth_service_ticket_ttl (default 1 hour); the client renews before expiry.
⚠ Warning

Because tickets expire, clock skew between the client, MON and OSD can break authentication. Messages such as "could not get service secret", or authentication errors after rebooting a host with a wrong clock, are often caused by this. NTP/chrony on all hosts and clients is mandatory.

8-6-5 msgr2: frames and secure mode#

Figure 8-7: The frame structure in the msgr2 protocol: a fixed-size preamble containing the frame type tag, the number of segments up to four, the length and alignment of each segment and a separate CRC; then up to four segments containing the header, front, middle and data; and finally the epilogue, which in crc mode holds each segment's CRC and in secure mode the AES-GCM encryption and authentication of the whole frame
Figure 8-7: The frame structure in the msgr2 protocol: a fixed-size preamble containing the frame type tag, the number of segments up to four, the length and alignment of each segment and a separate CRC; then up to four segments containing the header, front, middle and data; and finally the epilogue, which in crc mode holds each segment's CRC and in secure mode the AES-GCM encryption and authentication of the whole frame

In src/msg/async/frames_v2.h, each frame starts with a preamble_block_t holding the tag (enum class Tag: HELLO, AUTH_REQUEST, AUTH_REQUEST_MORE, ..., SESSION_RECONNECT, MESSAGE, KEEPALIVE2, COMPRESSION_REQUEST and so on), the number of segments (at most MAX_NUM_SEGMENTS = 4) and the length and alignment of each segment. Separating the data segment with a specific alignment lets the data buffer reach BlueStore directly without copying (zero-copy).

bashReef+
ceph config get mon ms_cluster_mode          # crc or secure (and the order of preference)
ceph config get mon ms_service_mode
ceph config get mon ms_client_mode
ceph config set global ms_cluster_mode "secure"   # encrypt traffic between OSDs
ceph mon dump | grep -E "v2:|v1:"                 # the addresses of the two protocols
✎ Note

secure mode costs CPU, but on processors with AES-NI it is usually tolerable. Before enabling it on a busy cluster network, measure the effect in a lab. The default msgr2 port on the MON is 3300 and msgr1 is 6789.

8-7 Data movement by the numbers: adding a host and changing a weight#

This section answers two practical questions with real CRUSH mappings: "how much data moves when a host is added?" and "what moves when an OSD's weight changes?". The map was built with crushtool --build --num_osds 132 … host straw2 12 root straw2 0. The ten-host version of the same map was obtained with --remove-item so that the ids of all buckets and OSDs stay fixed, because straw2 uses the item id in the hash. A three-replica pool with 8192 PGs was defined with osdmaptool --createsimple 256 --pg-bits 5 --pgp-bits 5, and the mapping of each case was taken with --test-map-pgs-dump. The tools were from Ceph 18.2.7 and the tunables were the defaults: chooseleaf_vary_r 1, chooseleaf_stable 1 and choose_total_tries 50.

✎ Note

A trap in osdmaptool: the --mark-up-in option gives a weight of 1.0 to every OSD with a CRUSH weight of zero (src/tools/osdmaptool.cc, the if (mark_up_in) block with adjust_item_weightf(…, 1.0)). So to simulate "the host has not been added yet" you cannot set the OSDs' weight to zero; you must remove them from the map. We made exactly this mistake at first and saw no movement at all.

8-7-1 Adding a host#

∑ Algorithm: Counting movement by comparing two mappings

Source in code: osdmaptool --test-map-pgs-dump before and after the change; bucket_straw2_choose and crush_choose_firstn in src/crush/mapper.c

Complexity: O(pg_num × size)

text
pseudocode
A = up(pg) with the old map ; B = up(pg) with the new map          # for each pg in [0, pg_num)
moved = Σ_pg |set(B[pg]) − set(A[pg])|                         # new placements, ignoring order
to_new = Σ_pg |{o ∈ B[pg] − A[pg] : o on the new host}|
ideal = pg_num × size × (new host weight / total weight)               # 24576 × 12/132

Result. Of 24576 replica placements, 2184 (8.89%) went to the new host. The ideal is 2234 (9.09%). This is exactly the behaviour straw2 guarantees: each item has an independent "draw" ln(u)/w (the generate_exponential_distribution function), and adding an item changes the winner only where the new item's draw is higher.

But another 328 placements (1.33%) moved between old OSDs. A real example from this mapping: PG 1.58 went from [76, 113, 46] to [76, 125, 116]. The new host took the second position and host 9 was pushed to the third. In crush_choose_firstn the leaf within a host is chosen with sub_r = r >> (vary_r − 1), i.e. it depends on the position's r. So within the same host 9, osd.116 was chosen instead of osd.113. The practical consequence is that adding capacity causes about 15% more movement than the ideal (2512 versus 2234). Take this into account when estimating backfill time.

Figure 8-8: Adding an eleventh host (12 OSDs) to a 10 × 12 cluster with straw2, a three-replica pool and 8192 PGs, measured with osdmaptool. Grey is the ideal share, green the replicas that went to the new host, and orange the movement between old OSDs caused by the position's r changing in chooseleaf
Figure 8-8: Adding an eleventh host (12 OSDs) to a 10 × 12 cluster with straw2, a three-replica pool and 8192 PGs, measured with osdmaptool. Grey is the ideal share, green the replicas that went to the new host, and orange the movement between old OSDs caused by the position's r changing in chooseleaf

8-7-2 Changing an OSD's weight#

∑ Algorithm: Classifying movements after a reweight

Source in code: crushtool --reweight-item (also updates the parents' weights); the same A and B comparison

Complexity: O(pg_num × size)

text
pseudocode
for each new placement y ∈ B[pg] − A[pg]:
    if osd.5 is the incoming or outgoing OSD of this PG      → "osd.5 itself"
    else if the incoming or outgoing OSD is on host0       → "other OSDs of host0"   (host0's weight in root changed)
    else                                          → "unrelated hosts"   (position/r shift)

Result. When osd.5's weight goes from 1.0 to 2.0, 411 placements change: 214 are osd.5 itself, 169 are other OSDs of host0 and 28 are unrelated hosts. When the weight drops to 0.8, 84 placements change: 42, 40 and 2.

The large "other OSDs of host0" share is not a flaw of straw2. crushtool --reweight-item also raises host0's weight in the root from 12 to 13, and host0 gets a larger share of the PGs. straw2 spreads this share within host0 in the ratio 2/13 for osd.5 and 1/13 for each other OSD.

In this experiment the old straw algorithm gave almost the same result (400 versus 411 for weight 2.0), because all items but one had equal weights. The documented flaw of old straw (movement between the other items of the same bucket when one item's weight changes) shows up in buckets with unequal weights. Even so, straw2 is the only algorithm that guarantees this property for any weight distribution.

💡 Tip

Before any weight change or host addition on a real cluster, do the same comparison offline: ceph osd getmap -o om, then osdmaptool om --export-crush cm, change cm with crushtool, osdmaptool om --import-crush cm2 on a copy, and compare the two --test-map-pgs-dump outputs. In this case --mark-up-in is not needed, because the real osdmap has the up and in state.

Figure 8-9: Changing the CRUSH weight of one OSD (osd.5 on host0) from 1.0 to five values and classifying the moved placements: blue is osd.5 itself, yellow the other OSDs of host0, which gain or lose share as the host's weight changes, and red unrelated hosts. Left straw2 and right old straw, with 8192 PGs, measured with osdmaptool
Figure 8-9: Changing the CRUSH weight of one OSD (osd.5 on host0) from 1.0 to five values and classifying the moved placements: blue is osd.5 itself, yellow the other OSDs of host0, which gain or lose share as the host's weight changes, and red unrelated hosts. Left straw2 and right old straw, with 8192 PGs, measured with osdmaptool

8-8 Authentication and authorisation#

8-8-1 cephx#

Ceph uses the cephx protocol for mutual authentication between clients and daemons. Its idea is similar to Kerberos: the secret key is never sent over the network, and the MON, as the authority, issues time-limited tickets with which the client talks directly to OSDs and MDSs.

Figure 8-10: The cephx flow: the client proves its identity to the MON, gets a ticket and uses it to work directly with the OSD
Figure 8-10: The cephx flow: the client proves its identity to the MON, gets a ticket and uses it to work directly with the OSD
◆ Deeper

If every I/O request went to the MON for an authorisation check, the MONs would become a bottleneck. With tickets, the OSD checks a ticket's validity using only its own service key (obtained from the MON and rotated periodically). This is also why clock skew between nodes can lead to authentication errors: tickets expire.

8-8-2 Users and capabilities#

Every identity in Ceph has the form type.id (for example client.backup or osd.3) and has a key and a set of capabilities. Capabilities are defined separately for each daemon type:

bashReef+
ceph auth ls
ceph auth get client.admin
# a user for RBD in one pool only
ceph auth get-or-create client.vmhost mon 'profile rbd' osd 'profile rbd pool=vms' mgr 'profile rbd pool=vms'
# a read-only user for monitoring
ceph auth get-or-create client.monitor mon 'allow r' mgr 'allow r'
# a user restricted to one namespace
ceph auth get-or-create client.tenant1 mon 'allow r' osd 'allow rw pool=shared namespace=tenant1'
ceph auth caps client.vmhost mon 'profile rbd' osd 'profile rbd pool=vms, profile rbd-read-only pool=images'
ceph auth rm client.old-app
💡 Tip

Always use profiles (profile rbd, profile rbd-read-only, profile crash, profile bootstrap-osd) instead of writing low-level capabilities by hand. Profiles also include the capabilities needed for operations such as blocklisting the previous client (for the exclusive-lock in RBD), which are usually forgotten in hand-written capabilities.

8-8-3 Key rotation and encryption#

  • A user's key can be rotated with ceph auth rotate (in recent releases) or by creating a new key and replacing it gradually.
  • Umbrella hardened cephx: the new AES256KRB5 cipher became the default in place of older key types, and online key rotation was added. Before upgrading, check the compatibility of old clients (especially kernel clients).
  • To encrypt network traffic, enable secure mode in msgr2 (ms_cluster_mode, ms_service_mode, ms_client_mode). This mode has CPU overhead; measure it on real hardware.
bashReef+
ceph config set global ms_client_mode secure
ceph config set global ms_service_mode secure
ceph config get mon ms_cluster_mode

8-9 The I/O path and PGs#

8-9-1 The path of a write#

Figure 8-11: A write to a replicated pool: the client talks only to the primary; the primary replicates to the secondaries in parallel and acks after all replicas have committed
Figure 8-11: A write to a replicated pool: the client talks only to the primary; the primary replicates to the secondaries in parallel and acks after all replicas have committed
  1. 1. The client (librados) computes the PG and the primary with the map it has.
  2. 2. It sends the request only to the primary.
  3. 3. The primary records the operation in that PG's log and sends it to the secondaries in parallel.
  4. 4. Each OSD commits the transaction in BlueStore.
  5. 5. The primary acks the client after receiving confirmation from all OSDs in the acting set.

So write latency equals the slowest OSD in the acting set plus two network round trips. This is why one slow disk slows down the whole cluster (chapter 17). In an EC pool the primary splits the data into k data chunks and m parity chunks and sends each chunk to one OSD (chapter 13).

✎ Note

By default reads are served by the primary. RBD and RGW clients can, under certain conditions, read from the nearest replica (for example the rbd_read_from_replica_policy option with the values balance and localize), which is useful in clusters stretched across sites.

8-9-2 PG states#

Figure 8-12: The main PG states and the common transitions between them
Figure 8-12: The main PG states and the common transitions between them
StateMeaningWorrying?
active+cleanAll replicas present and in syncNo; the desired state
peeringOSDs agreeing on the PG's stateOnly if it takes long
degradedSome objects lack enough replicasHigher risk until recovery completes
recoveringSynchronising from the logNormal after a short failure
backfilling, backfill_waitFull copy of the PG to a new OSDNormal after a map change
remappedThe acting set temporarily differs from the up setNormal during backfill
undersizedFewer OSDs than sizeShould be checked
inconsistentScrub found an inconsistencyYes; must be investigated and repaired
incomplete, downNot enough valid copies to become activeYes; I/O to this PG is stopped
staleThe primary has not sent stats for a whileYes; check the OSDs
bashReef+
ceph pg stat
ceph pg dump_stuck inactive
ceph pg dump_stuck unclean
ceph pg ls degraded
ceph pg 3.1f query | jq '.recovery_state'
✚ Troubleshooting

For a PG stuck in peering or incomplete, read the output of ceph pg <id> query from the bottom up. The recovery_state section usually says which OSD it is waiting for (blocked_by or down_osds_we_would_probe). If that OSD is lost for good, decisions such as ceph osd lost or marking objects as lost risk data loss; read chapter 17 before running them.

8-9-3 min_size and size#

size is the number of replicas and min_size is the minimum number of healthy replicas a PG needs to accept I/O. For a three-way replicated pool, min_size=2 is standard. Lowering it to 1 keeps the cluster "available" during a failure but sharply raises the risk of data loss, because a write might exist on only one disk.

8-10 A pool on specific OSDs#

The modern way to place a pool on a specific group of OSDs is the device class. If the default classes are not enough (for example you want to set aside the NVMe disks of a particular model for one tenant), define a custom class:

bashReef+
for id in 30 31 32 33 34 35; do
  ceph osd crush rm-device-class osd.$id
  ceph osd crush set-device-class fastnvme osd.$id
done
ceph osd crush rule create-replicated fast-host default host fastnvme
ceph osd pool create tenant-a-db 64 64 replicated fast-host
ceph osd pool application enable tenant-a-db rbd
ceph pg ls-by-pool tenant-a-db | head
💡 Tip

In the cephadm OSD spec you can also set crush_device_class so that new OSDs get the right class from the start and you do not need to rerun the commands above.

SRE notes for this chapter
  • Data location is computed, not looked up; ceph osd map is your first tool.
  • Define only real failure domains in CRUSH, and give the host location in the spec.
  • Run crushtool --test before applying any hand-edited map.
  • Use device classes for SSD/HDD, not separate roots.
  • A tunable change means a large movement; check client compatibility with ceph features.
  • An odd number of MONs (3 or 5), on SSD, with accurate clocks.
  • Capabilities with profiles and least privilege; do not lower min_size.

8-11 Supplementary and up to date#

  • Device classes and the shadow tree (Luminous) replaced the separate SSD/HDD roots seen in older sources.
  • upmap and the balancer have replaced manual reweighting (chapter 7).
  • The MSR rule (Squid) gives more flexibility for EC.
  • cephx in Umbrella: the AES256KRB5 cipher as the default and online key rotation.
  • MON RocksDB backup (Umbrella).
  • The read balancer (Reef) for distributing primaries.

8-12 Hands-on exercise#

  1. 1. Run ceph osd map for three different objects and compare the PG and OSD paths with ceph pg map.
  2. 2. Build a two-rack hierarchy, move the hosts, and apply the rep-rack rule to a test pool. Observe the amount of movement.
  3. 3. Extract the CRUSH map, add the two-datacenter rule, and check the distribution with crushtool --test before applying it.
  4. 4. Create a user with profile rbd for one pool only and prove that it has no access to another pool.
  5. 5. Stop an OSD and record the PG states over time with ceph pg stat.
  6. 6. Using a custom device class, create a pool on three specific OSDs and check the PG distribution.

8-13 Review questions#

  1. 1. What are the two stages of mapping an object to an OSD, and why is the PG needed?
  2. 2. What is the difference between the up set and the acting set?
  3. 3. What is the advantage of straw2 over the earlier algorithms?
  4. 4. Why does changing tunables cause data movement, and what must be checked beforehand?
  5. 5. Why do four MONs not tolerate more failures than three?
  6. 6. Where do OSDs receive the new map from?
  7. 7. Why does cephx use tickets?
  8. 8. Why does write latency depend on the slowest OSD in the acting set?
  9. 9. How do the incomplete and inconsistent states differ?
  10. 10. What is the recommended way to place a pool on specific OSDs?

Answers to these questions are in Appendix E: Answers to Review Questions. Answers to this chapter's questions →

8-14 Sources for this chapter#

The Comprehensive Ceph Guide · 1.6 · November 2026
© 2026 Ramin Najarbashi · CC BY-NC-SA 4.0

Privacy: this site sets no cookies. Visits are counted with Cloudflare Web Analytics, which is cookie-less and does not track individuals.