# Range merges **Last update:** April 10, 2019 **Original author:** Nikhil Benesch This document serves as an end-to-end description of the implementation of range merges. The target reader is someone who is reasonably familiar with core but unfamiliar with either the "how" or the "why" of the range merge implementation. The most complete documentation, of course, is in the code, tests, and the surrounding comments, but those pieces are necessarily split across several files and packages. That scattered knowledge is centralized here, without excessive detail that is likely to become stale. ## Table of Contents * [Overview](#overview) * [Implementation details](#implementation-details) * [Preconditions](#preconditions) * [Initiating a merge](#initiating-a-merge) * [AdminMerge race](#adminmerge-race) * [Merge transaction](#merge-transaction) * [Transfer of power](#transfer-of-power) * [Snapshots](#snapshots) * [Merge queue](#merge-queue) * [Subtle complexities](#subtle-complexities) * [Range descriptor generation](#range-descriptor-generations) * [Misaligned replica sets](#misaligned-replica-sets) * [Replica GC](#replica-gc) * [Transaction record GC](#transaction-record-gc) * [Unanimity](#unanimity) * [Safety recap](#safety-recap) * [Appendix](#appendix) * [Key encoding oddities](#key-encoding-oddities) ## Overview A range merge begins when two adjacent ranges are selected to be merged together. For example, suppose our adjacent ranges are _P_ and _Q_, somewhere in the middle of the keyspace: ``` --+-----+-----+-- | P | Q | --+-----+-----+-- ``` We'll call _P_ the left-hand side (LHS) of the merge, and _Q_ the right-hand side (RHS) of the merge. For reasons that will become clear later, we also refer to _P_ as the subsuming range and _Q_ as the subsumed range. The merge is coordinated by the LHS. The coordinator begins by verifying that a) the two ranges are, in fact, adjacent, and b) that the replica sets of the two ranges are aligned. Replica set alignment is a term that is currently only relevant to merges; it means that the set of stores with replicas of the LHS exactly matches the set of stores with replicas of the RHS. For example, this replica set is aligned: ``` Store 1 Store 2 Store 3 Store 4 +-----+ +-----+ +-----+ +-----+ | P Q | | P Q | | | | P Q | +-----+ +-----+ +-----+ +-----+ ``` By requiring replica set alignment, the merge operation is reduced to a metadata update, albeit a tricky one, as the stores that will have a copy of the merged range _PQ_ already have all the constituent data, by virtue of having a copy of both _P_ and _Q_ before the merge begins. Note that replicas of _P_ and _Q_ do not need to be fully up-to-date before the merge begins; they'll be caught up as necessary during the [transfer of power](#transfer-of-power). After verifying that the merge is sensible, the coordinator transactionally updates the implicated range descriptors, adjusting P's range descriptor so that it extends to _Q_'s end and deleting _Q_'s range descriptor. Then, the coordinator needs to [atomically move responsibility](#transfer-of-power) for the data in the RHS to the LHS. This is tricky, as the lease on the LHS may be held by a different store than the lease on the RHS. The coordinator notifies the RHS that it is about to be subsumed and it is prohibited from serving any additional read or write traffic. Only when the coordinator has received an acknowledgement from _every_ replica of the RHS, indicating that no traffic is possibly being served on the RHS, does the coordinator commit the merge transaction. Like with splits, the merge transaction is committed with a special "commit trigger" that instructs the receiving store to update its in-memory bookkeeping to match the updates to the range descriptors in the transaction. The moment the merge transaction is considered committed, the merge is complete! At the time of writing, merges are only initiated by the merge queue, which is responsible both for locating ranges that are in need of a merge and aligning their replica sets before initiating the merge. The remaining sections cover each of these steps in more detail. ## Implementation details ### Preconditions Not any two ranges can be merged. The first and most obvious criterion is that the two ranges must be adjacent. Suppose a simplified cluster that has only three ranges, _A_, _B_, and _C_: ``` +-----+-----+-----+ | A | B | C | +-----+-----+-----+ ``` Ranges _A_ and _B_ can be merged, as can ranges _B_ and _C_, but not ranges _A_ and _C_, as they are not adjacent. Note that adjacent ranges are equivalently referred to as "neighbors", as in, range _B_ is range _A_'s right-hand neighbor. The second criterion is that the replica sets must be aligned. To illustrate, consider a four node cluster with the default 3x replication. The allocator has attempted to balance ranges as evenly as possible: ``` Node 1 Node 2 Node 3 Node 4 +-----+ +-----+ +-----+ +-----+ | P Q | | P | | Q | | P Q | +-----+ +-----+ +-----+ +-----+ ``` Notice how node 2 does not have a copy of _Q_, and node 3 does not have a copy of _P_. These replica sets are considered "misaligned." Aligning them requires rebalancing Q from node 3 to node 2, or rebalancing _P_ from node 2 to node 3: ``` Node 1 Node 2 Node 3 Node 4 +-----+ +-----+ +-----+ +-----+ | P Q | | P Q | | | | P Q | +-----+ +-----+ +-----+ +-----+ Node 1 Node 2 Node 3 Node 4 +-----+ +-----+ +-----+ +-----+ | P Q | | | | P Q | | P Q | +-----+ +-----+ +-----+ +-----+ ``` We explored an alternative merge implementation that did not require aligned replica sets, but found it to be unworkable. See the [misaligned replica sets misstep](#misaligned-replica-sets) for details. ### Initiating a merge A merge is initiated by sending a AdminMerge request to a range. Like other admin commands, DistSender will automatically route the request to the leaseholder of the range, but there is no guarantee that the store will retain its lease while the admin command is executing. Note that an AdminMerge request takes no arguments, as there is no choice in what range will be merged. The recipient of the AdminMerge will always be the LHS, subsuming range, and its right neighbor at the moment that the AdminMerge command begins executing will always be the RHS, subsumed range. It would have been reasonable to have instead used the RHS to coordinate the merge. That is, the RHS would have been the subsuming range, and the LHS would have been the subsumed range. Using the LHS to coordinate, however, yields a nice symmetry with splits, where the range that coordinates a split becomes the LHS of the split. Maintaining this symmetry means that a range's start key never changes during its lifetime, while its end key may change arbitrarily in response to splits and merges. There is another reason to prefer using the LHS to coordinate involving an oddity of key encoding and range bounds. It is trivial for a range to send a request to its right neighbor, as it simply addresses the request to its end key, but it is difficult to send a request to its left neighbor, as there is no function to get the key that immediately precedes the range's start key. See the [key encoding oddities](#key-encoding-oddities) section of the appendix for details. At the time of writing, only the [merge queue](#merge-queue) initiates merges, and it does so by bypassing DistSender and invoking the AdminMerge command directly on the local replica. At some point in the future, we may wish to expose manual merges via SQL, at which point the SQL layer will need to send proper AdminMerge requests through the KV API. #### AdminMerge race At present, AdminMerge requests are subject to a small race. It is possible for the ranges implicated by an AdminMerge request to split or merge between when the client decides to send an AdminMerge request and when the AdminMerge request is processed. For example, suppose the client decides that _P_ and _Q_ should be merged and sends an AdminMerge request to _P_. It is possible that, before the AdminMerge request is processed, _P_ splits into _P1_ and _P2_. The AdminMerge request will thus result in _P1_ and _P2_ merging together, and not the desired _P_ and _Q_. The race could have been avoided if the AdminMerge request required that the descriptors for the implicated ranges were provided as arguments to the request. Then the merge could be aborted if the merge transaction discovered that either of the implicated ranges did not match the corresponding descriptor in the AdminMerge request arguments, forming a sort of optimistic lock. Fortunately, the race is rare in practice. If it proves to be a problem, the scheme described above would be easy to implement while maintaining backwards compatibility. ### Merge transaction The merge transaction piggybacks on CockroachDB's serializability to provide much of the necessary synchronization for the bookkeeping updates. For example, merges cannot occur concurrently with any splits or replica changes on the implicated ranges, because the merge transaction will naturally conflict with those split transaction and change replicas transactions, as both transactions will attempt to write updated range descriptors and conflict. No additional code was needed to enforce this, as our standard transaction conflict detection mechanisms kick in here (write intents, the timestamp cache, the span latch manager, etc.). Note that there was one surprising synchronization problem that was not immediately handled by serializability. See [range descriptor generations](#range-descriptor-generations) for details. The standard KV operations that the merge transaction performs are: * Reading the LHS descriptor and RHS descriptor, and verifying that their replica sets are aligned. * Updating the local and meta copy of the LHS descriptor to reflect the widened end key. * Deleting the local and meta copy of the RHS descriptor. * Writing an entry to the `system.rangelog` table. These operations are the essence of a merge, and in fact update all necessary on-disk data! All the remaining complexity exists to update in-memory metadata while the cluster is live. Note that the merge transaction's KV operations are not fundamentally dependent and so could theoretically be performed in any order. There are, however, several implementation details that enforce some ordering constraints. First, the merge transaction record needs to be located on the LHS. Specifically, the transaction record needs to live on the subsuming range, as the commit trigger that actually applies the merge to the replica's in-memory state runs on the range where the transaction record lives. The transaction record is created on the range that the transaction writes first; therefore, the merge transaction is careful to update the local copy of the LHS descriptor as its first operation, since the local copy of the LHS descriptor lives on the LHS. --- **UPDATE in v21.1:** As of v21.1, the merge transaction uses a locking read on the LHS range's transaction record as its first course of action. This eliminates thrashing and livelock in the face of concurrent merge attempts, which we typically only see in testing scenarios but which we'd like to handle effectively. It also has the effect of locating the transaction record on the LHS earlier in the transaction. Because of this, the merge transaction no longer needs to be quite as deliberate about the order in which it updates descriptors later on because that order does not impact the location of the transaction record. --- Second, the merge transaction must ensure that, when it issues the delete request to remove the local copy of the RHS descriptor, the resulting intent is actually written to disk. (See the [transfer of power](#transfer-of-power) subsection for why this is required.) Thanks to [transactional pipelining][#26599], KV writes can return early, before their intents have actually been laid down. The intents are not required to make it to disk until the moment before the transaction commits. The merge transaction simply disables pipelining to avoid this hazard. As the last step before the commit, the merge transaction needs to freeze the RHS, then wait for _every_ replica of the RHS to apply all outstanding commands. This ensures that, when the merge commits, every LHS replica can blindly assume that it has perfectly up-to-date data for the RHS. To quickly recap, this is guaranteed because 1) the replica sets were aligned when the merge transaction started, 2) rebalances that would misalign the replica sets will conflict with the merge transaction, causing one of the transactions to abort, 3) the RHS is frozen and cannot process any new commands, and 4) every replica of the RHS is caught up on all commands. The process of freezing the RHS and waiting for every replica to catch up is covered more thoroughly in the [transfer of power](#transfer-of-power) subsection. Finally, the merge transaction commits, attaching a special [merge commit trigger] to the end transaction request. This trigger has three responsibilities: 1. It ensures the end transaction request knows which intents it can resolve locally. Intents that live on the RHS range would naively appear to belong to a different range than the one containing the transaction record (i.e., the LHS), but if the merge is committing then the LHS is subsuming the RHS and thus the intents can be resolved locally. In fact, it's critical that these intents are considered local, because local intents are resolved synchronously while remote intents are resolved asynchronously. We need to maintain the invariant that, when a store boots up and discovers an intent on its local copy of a range descriptor, it can simply ignore the intent. Because we enforce that these intents are resolved synchronously with the commit of the merge transaction, we are guaranteed that, if we see an intent on a local range descriptor, this replica has not yet applied the `EndTransaction` request for the merge transaction, and it is therefore safe to load the replica. If the intent were instead resolved asynchronously, we could observe the state where the `EndTransaction` request for the merge had applied but the intent resolution had not applied, in which case we would attempt to load both the post-merge subsuming replica, and the subsumed replica, which would overlap and crash the node. 2. It adjusts the LHS's MVCCStats to incorporate the subsumed range's MVCCstats. 3. It copies necessary range-ID local data from the RHS and LHS, rekeying each key to use LHS's range ID. At the moment, the only necessary data is the [transaction abort span]. 4. It attaches a [merge payload to the replicated proposal result][pd-flag]. When each replica of the LHS applies the command, it will notice the merge payload and adjust the store's in-memory state to match the changes to the on-disk state that were committed by the transaction. This entails atomically removing the RHS replica from the store and widening the LHS replica. This operation involves a delicate dance of acquiring store locks and locks for both replicas, in a certain order, at various points in the Raft command application flow. The details are too intricate to be worth describing here, especially considering that these tangled interactions between a store and its replicas are due for a refactor. The best thing to do, if you're interested in the details, is to trace through all references to `storagepb.ReplicatedEvalResult.Merge`. [#26599]: https://github.com/cockroachdb/cockroach/pull/26599 [transaction abort span]: https://github.com/cockroachdb/cockroach/blob/82bcd948384e6a482cdd7c916c0aaca32367a7b0/pkg/storage/abortspan/abortspan.go [merge commit trigger]: https://github.com/cockroachdb/cockroach/blob/82bcd948384e6a482cdd7c916c0aaca32367a7b0/pkg/storage/batcheval/cmd_end_transaction.go#L984-L994 [pd-flag]: https://github.com/cockroachdb/cockroach/blob/82bcd948384e6a482cdd7c916c0aaca32367a7b0/pkg/storage/batcheval/cmd_end_transaction.go#L1033-L1035 #### Transfer of power The trickiest part of the merge transaction is making the LHS responsible for the keyspace that was previously owned by the RHS. The transfer of power must be performed atomically; otherwise, all manner of consistency violations can occur. This is hopefully obvious, but here's a quick example to drive the point home. Suppose _P_ and _Q_ simultaneously consider themselves responsible for the key _q_. _Q_ could then allow a write to _q_ at time 1 at the same time that _P_ allowed a read of _q_ at time 2. Consistency requires that either the read see the write, as the read is executing at a higher timestamp, or that the write is bumped to time 3. But because _P_ and _Q_ have separate span latch managers, no synchronization will occur, and the read might fail to see the write! The transfer of power is complicated by the fact that there is no guarantee that the leases on the LHS and the RHS will be aligned, nor is there any straightforward way to provide such a guarantee. (Aligned leaseholders would allow the merge transaction to use a simple in-memory lock to make the transfer of power atomic.) The leaseholder of either range might fail at any moment, at which point the lease can be acquired, after it expires, by any other live member of the range. Since leaseholder assignment is infeasible, the merge transaction implements what is essentially a distributed lock. The lock is initialized by sending a [Subsume][subsume-request] request to the RHS. This is a single-purpose request that exists solely for use in the merge transaction. It is unlikely to ever be useful in another situation, and (ab)uses several implementation details to provide the necessary synchronization. When the Subsume request returns, the RHS has made three important promises: 1. There are no commands in flight. 2. Any future requests will block until the merge transaction completes. If the merge transaction commits, the requests will be bounced with a RangeNotFound error. If the merge transaction aborts, the requests will be processed as normal. 3. If the RHS loses its lease, the new leaseholder will adhere to promises 1 and 2. The Subsume request provides promise 1 by declaring that it reads and writes all addressable keys in the range. This is a bald-faced lie, as the Subsume request only reads one key and writes no keys, but it forces synchronization with all latches in the span latch manager, as no other commands can possibly execute in parallel with a command that claims to write all keys. It provides promise 2 by flipping [a bit][merge-bit] on the replica that indicates that a subsumption is in progress. When the bit is active, the replica blocks processing of all requests. Importantly, the bit needs to be cleared when the merge transaction completes, so that the requests are not blocked forever. This is the responsibility of the [merge watcher goroutine][watcher]. Determining whether a transaction has committed or not is conceptually simple, but the details are brutally complicated. See the [transaction record GC](#transaction-record-gc) section for details. Note that the Subsume request only runs on the leaseholder, and therefore the merge bit is only set on the leaseholder and the watcher goroutine only runs on the leaseholder. This is perfectly safe, as none of the follower replicas can process requests. Promise 3 is actually not provided by the Subsume request at all, but by a hook in the lease acquisition code path. Whenever a replica acquires a lease, it checks to see whether its local descriptor has a deletion intent. If it does, it can infer that a subsumption is in progress, as nothing else leaves a deletion intent on a range descriptor. In that case, the replica, instead of serving traffic, flips the merge bit and launches its own merge watcher goroutine, just as the Subsume command would have. This means there can actually be multiple replicas of the RHS with the merge bit set and a merge watcher goroutine running—assuming the old leaseholder did not crash but lost its lease for other reasons—but this does not cause any problems. [subsume-request]: https://github.com/cockroachdb/cockroach/blob/d6adf24cae788d7cd967feadae8e9c0388ce5273/pkg/storage/batcheval/cmd_subsume.go#L56-L86 [merge-bit]: https://github.com/cockroachdb/cockroach/blob/d6adf24cae788d7cd967feadae8e9c0388ce5273/pkg/storage/replica.go#L361-L364 [watcher]: https://github.com/cockroachdb/cockroach/blob/d6adf24cae788d7cd967feadae8e9c0388ce5273/pkg/storage/replica.go#L2817-L2926 ### Snapshots The LHS might be advanced over the command that commits a merge with a snapshot. That means that all the complicated bookkeeping that normally takes place when a replica processes a command with a non-nil `ReplicatedEvalResult.Merge` is entirely skipped! Most problematically, the snapshot will need to widen the receiving replica, but there will be a replica of the RHS in the way—remember, this is guaranteed by replica set alignment. In fact, the snapshot could be advancing over several merges, or a combination of several merges and splits, in which case there will be several RHSes to subsume. This turns out to be relatively straightforward to handle. If an initialized replica receives a snapshot that widens it, it can infer that a merge occurred, and it simply subsumes all replicas that are overlapped by the snapshot in one shot. This requires the same delicate synchronization dance, mentioned at the end of the [merge transaction](#merge-transaction) section, to update bookkeeping information. After all, applying a widening snapshot is simply the bulk version of applying a merge command directly. The details are too complicated to go into here, but you can begin your own exploration by starting with this call to [`Replica.maybeAcquireSnapshotMergeLock`][code-start] and tracing how the returned `subsumedRepls` value is used. [code-start]: https://github.com/cockroachdb/cockroach/blob/82bcd948384e6a482cdd7c916c0aaca32367a7b0/pkg/storage/replica.go#L4071-L4072 ### Merge queue The merge queue, like most of the other queues in the system, runs on every store and periodically scans all replicas for which that store holds the lease. For each replica, the merge queue evaluates whether it should be merged with its right neighbor. Looking rightward is a natural fit for the reasons described in the [key encoding oddities](#key-encoding-oddities) section of the appendix. In some ways, the merge queue has an easy job. For any given range _P_ and its right neighbor _Q_, the merge queue synthesizes the hypothetical merged range _PQ_ and asks whether the split queue would immediately split that merged range. If the split queue would immediately split _PQ_, then obviously _P_ and _Q_ should not be merged; otherwise, the ranges _should_ be merged! This means that any improvement to our split heuristics also improves our merge heuristics with essentially no extra work. For example, load-based splitting hardly required any changes to the merge queue. Note that, to avoid thrashing, ranges at or above the minimum size threshold (8MB) are never considered for a merge. The minimum size threshold is configurable on a per-zone basis. Unfortunately, constructing the hypothetical merged range _PQ_ requires information about _Q_ that only _Q_'s leaseholder maintains, like the amount of load that _Q_ is currently experiencing. The merge queue must send a RangeStats RPC to collect this information from _Q_'s leaseholder, because there is no guarantee that the current store is _Q_'s leaseholder—or that the current store even has a replica of _Q_ at all. To prevent unnecessary RPC chatter, the merge queue uses several heuristics to void sending RangeStats requests when it seems like the merge is unlikely to be permitted. For example, if it determines that _P_ and _Q_ store different tables, the split between them is mandatory and it won't bother sending the RangeStats requests. Similarly, if _P_ is above the minimum size threshold, it doesn't bother asking about _Q_. ## Subtle complexities ### Range descriptor generations There was one knotty race that was not immediately eliminated by transaction serializability. Suppose we have our standard aligned replica set situation: ``` Store 1 Store 2 Store 3 Store 4 +-----+ +-----+ +-----+ +-----+ | P Q | | P Q | | | | P Q | +-----+ +-----+ +-----+ +-----+ ``` In an unfortunate twist of fate, a rebalance of P from store 2 to store 3 begins at the same time as a merge of P and Q begins. Let's quickly cover the valid outcomes of this race. 1. The rebalance commits before the merge. The merge must abort, as the replica sets of P and Q are no longer aligned. 2. The merge commits before the rebalance starts. The rebalance should voluntarily abort, as the decision to rebalance P needs to be updated in light of the merge. It is not, however, a correctness problem if the rebalance commits; it simply results in rebalancing a larger range than may have been intended. 3. The merge commits before the rebalance ends, but after the rebalance has sent a preemptive snapshot to store 3. The rebalance must abort, as otherwise the preemptive snapshot it sent to store 3 is a ticking time bomb. To see why, suppose the rebalance commits. Since the preemptive snapshot predates the commit of the merge transaction, the new replica on store 3 will need to be streamed the Raft command that commits the merge transaction. But applying this merge command is disastrous, as store 3 does not have a replica of Q to merge! This is a very subtle way in which replica set alignment can be subverted. Guaranteeing the correct outcome in case 1 is easy. The merge transaction simply checks for replica set alignment by transactionally reading the range descriptor for _P_ and the range descriptor for _Q_ and verifying that they list the same replicas. Serializability guarantees the rest. Case 2 is similarly easy to handle. The rebalance transaction simply verifies that the range descriptor used to make the rebalance decision matches the range descriptor that it reads transactionally. Case 3, however, has an extremely subtle pitfall. It seems like the solution for case 2 should apply: simply abort the transaction if the range descriptor changes between when the preemptive snapshot is sent and when the rebalance transaction starts. But, as it turns out, this is not quite foolproof. What if, between when the preemptive snapshot is sent and when the rebalance transaction starts, _P_ and _Q_ merge together and then split at exactly the same key? The range descriptor for _P_ will look entirely unchanged to the rebalance transaction! The solution was to add a generation counter to the range descriptor: ```protobuf message RangeDescriptor { // ... // generation is incremented on every split and every merge, i.e., whenever // the end_key of this range changes. It is initialized to zero when the range // is first created. optional int64 generation = 6; } ``` It is no longer possible for a range descriptor to be unchanged by a sequence of splits and merges, as every split and merge will bump the generation counter. Rebalances can thus detect if a merge commits between when the preemptive snapshot is sent and when the transaction begins, and abort accordingly. ### Misaligned replica sets An early implementation allowed merges between ranges with misaligned replica sets. The intent was to simplify matters by avoiding replica rebalancing. Consider again our example misaligned replica set: ``` Store 1 Store 2 Store 3 Store 4 +-----+ +-----+ +-----+ +-----+ | P Q | | P | | Q | | P Q | +-----+ +-----+ +-----+ +-----+ P: (s1, s2, s4) Q: (s1, s3, s4) ``` Note that there are two perspectives shown here. The store boxes represent the replicas that are *actually* present on that store, from the perspective of the store itself. The descriptor tuples at the bottom represent the stores that are considered to be members of the range, from the perspective of the most recently committed range descriptor. Now, to merge _P_ and _Q_ in this situation without aligning their replica sets, store 2 needed to be provided a copy of store 3's data. To accomplish this, a copy of _Q_'s data was stashed in the merge trigger, and _P_ would write this data into its store when applying the merge trigger. There was, sadly, a large and unforeseen problem with lagging replicas. Suppose store 2 loses network connectivity a moment before ranges _P_ and _Q_ are merged. Note that store 2 is not required for _P_ and _Q_ to merge, because only a quorum is required on the LHS to commit a merge. Now the situation looks like this: ``` Store 1 Store 2 Store 3 Store 4 +-----+ xxxxxxx +-----+ +-----+ | PQ | | P | | | | PQ | +-----+ xxxxxxx +-----+ +-----+ PQ: (s1, s2, s4) ``` There is nothing stopping the newly merged _PQ_ range from immediately splitting into _P_ and _Q'_. Note that _P_ is the same range as the original _P_ (i.e., it has the same range ID) and so the replica on _P_ is still considered a member, while _Q'_ is a new range, with a new ID, that is unrelated to _Q_: ``` Store 1 Store 2 Store 3 Store 4 +-----+ xxxxxxx +-----+ +-----+ | P Q'| | P | | | | P Q'| +-----+ xxxxxxx +-----+ +-----+ P: (s1, s2, s4) Q': (s1, s2, s4) ``` When store 2 comes back online, it will start catching up on missed messages. But notice how the meta ranges consider store 2 to be a member of _Q'_, because it was a member of _P_ before the split. The leaseholder for _Q'_ will notice that store 2's replica is out of date and send over a snapshot so that store 2 can initialize its replica... and all that might happen before store 2 manages to apply the merge command for _PQ_. If so, applying the merge command for _PQ_ will explode, because the keyspace of the merged range _PQ_ intersects with the keyspace of _Q'_! By requiring aligned replica sets, we sidestep this problem. The RHS is, in effect, a lock on the post-merge keyspace. Suppose we find ourselves in the analogous situation with replica sets aligned: ``` Store 1 Store 2 Store 3 Store 4 +-----+ xxxxxxx +-----+ +-----+ | P Q'| | P Q | | | | P Q'| +-----+ xxxxxxx +-----+ +-----+ P: (s1, s2, s4) Q': (s1, s2, s4) ``` Here, _PQ_ split into _P_ and _Q'_ immediately after merging, but notice how store 2 has a replica of both _P_ and _Q_ because we required replica set alignment during the merge. That replica of _Q_ prevents store 2 from initializing a replica of _Q'_ until either store 2's replica of _P_ applies the merge command (to _PQ_) and the split command (to _P_ and _Q'_), or store 2's replica of _P_ is rebalanced away. ### Replica GC Per the discussion in the last section, we use the replica of the RHS as a lock on the keyspace extension. This means that we need to be careful not to GC this replica too early. It's easiest to see why this is a problem if we consider the case where one replica is extremely slow in applying a merge: ``` Store 1 Store 2 Store 3 Store 4 +-----+ +-----+ +-----+ +-----+ | PQ | | PQ | | | | P Q | +-----+ +-----+ +-----+ +-----+ PQ: (s1, s2, s4) ``` Here, _P_ and _Q_ have just merged. Store 4 hasn't yet processed the merge while stores 1 and 2 have. The replica GC queue is continually scanning for replicas that are no longer a member of their range. What if the replica GC queue on store 4 scans its replica of _Q_ at this very moment? It would notice that the _Q_ range has been merged away and, conceivably, conclude that _Q_ could be garbage collected. This would be disastrous, as when _P_ finally applied the merge trigger it would no longer have a replica of _Q_ to subsume! One potential solution would be for the replica GC queue to refuse to GC replicas for ranges that have been merged away. But that could result in replicas getting permanently stuck. Suppose that, before store 4 applies the merge transaction, the _PQ_ range is rebalanced away to store 3: ``` Store 1 Store 2 Store 3 Store 4 +-----+ +-----+ +-----+ +-----+ | PQ | | PQ | | PQ | | P Q | +-----+ +-----+ +-----+ +-----+ PQ: (s1, s2, s3) ``` Store 4's replica of _P_ will likely never hear about the merge, as it is no longer a member of the range and therefore not receiving any additional Raft messages from the leader, so it will never subsume _Q_. The replica GC queue _must_ be capable of garbage collecting _Q_ in this case. Otherwise _Q_ will be stuck on store 4 forever, permanently preventing the store from ever acquiring a new replica that overlaps with that keyspace. Solving this problem turns out to be quite tricky. What the replica GC queue wants to know when it discovers that _Q_'s range has been subsumed is whether the local replica of _Q_ might possibly still be subsumed by its local left neighbor _P_. It can't just ask the local _P_ whether it's about to apply a merge, since _P_ might be lagging behind, as it is here, and have no idea that a merge is about to occur. So the problem reduces to proving that _P_ cannot apply a merge trigger that will subsume _Q_. The chosen approach is to fetch the current range descriptor for _P_ from the meta index. If that descriptor exactly matches the local descriptor, thanks to [range descriptor generations](#range-descriptor-generations), we are assured that there are no merge triggers that _P_ has yet to apply, and _Q_ can safely be GC'd. Note that it is possible to form long chains of replicas that can only be GC'd from left to right; the GC queue is not aware of these dependencies and therefore processes such chains extremely inefficiently (i.e., by processing replicas in an arbitrary order instead of the necessary order). These chains turn out to be extremely rare in practice. There is one additional subtlety here. Suppose we have two adjacent ranges, _O_ and _Q_. _O_ has just split into _O_ and _P_, but store 3 is lagging and has not yet processed the split. ``` STATE 1 Store 1 Store 2 Store 3 Store 4 +-------+ +-------+ +-------+ +-------+ | O P Q | | O P Q | | O Q | | | +-------+ +-------+ +-------+ +-------+ O: (s1, s2, s3) P: (s1, s2, s3) Q: (s1, s2, s3) ``` At this point, suppose the leader for the new range _P_ decides that store 3 will need a snapshot to catch up, and starts sending the snapshot over the network. This will be important later. At the same time, _P_ and _Q_ merge while store 3 is still lagging. It may seem strange that this merge is permitted, but notice how the replica sets are aligned according to the descriptors, even though store 3 does not physically have a replica of _P_ yet. Here's the new state of the world: ``` STATE 2 Store 1 Store 2 Store 3 Store 4 +-------+ +-------+ +-------+ +-------+ | O PQ | | O PQ | | O Q | | | +-------+ +-------+ +-------+ +-------+ O: (s1, s2, s3) PQ: (s1, s2, s3) ``` Finally, _O_ is rebalanced from store 3 to store 4 and garbage collected on store 4: ``` STATE 3 Store 1 Store 2 Store 3 Store 4 +-------+ +-------+ +-------+ +-------+ | O PQ | | O PQ | | Q | | O | +-------+ +-------+ +-------+ +-------+ O: (s1, s2, s4) PQ: (s1, s2, s3) ``` The replica GC queue might reasonably think that store 3's replica of _Q_ is out of date, as _Q_ has no left neighbor that could subsume it. But, at any moment in time, store 3 could finish receiving the snapshot for _P_ that was started between state 1 and state 2. Crucially, this snapshot predates the merge, so it will need to apply the merge trigger... and the replica for _Q_ had better be present on the store! This hazard is avoided by requiring that all replicas of the LHS are initialized before a merge begins. This prevents a transition from state 1 to state 2, as the merge of _P_ and _Q_ cannot occur until store 3 initializes its replica of _P_. The AdminMerge command will wait a few seconds in the hope that store 3 catches up quickly; otherwise, it will refuse to launch the merge transaction. It is therefore impossible to end up in a dangerous state, like state 3, and it is thus safe for the replica GC queue to GC _Q_ if its left neighbor is generationally up to date. ### Transaction record GC The merge watcher goroutine needs to wait until the merge transaction completes, and determine whether or not the transaction committed or aborted. This turns out to be brutally complicated, thanks to the aggressive garbage collection of transaction records. The watcher goroutine begins by sending a PushTxn request to the merge transaction. It can easily discover the necessary arguments for the PushTxn request, that is, the ID and key of the current merge transaction record, because they're recorded in the intent that the merge transaction has left on the RHS's local copy of the descriptor. If the PushTxn request reports that the merge transaction committed, we're guaranteed that the merge transaction did, in fact, complete. That means that we can mark the RHS replica as destroyed, bounce all requests back to DistSender (where they'll get retried on the subsuming range), and clean up the watcher goroutine. The RHS replica will be cleaned up either when the LHS replica subsumes it or when the replica GC queue notices that it has been abandoned. If the PushTxn request instead reports that the merge transaction aborted, we're not guaranteed that the merge transaction actually aborted. The merge transaction may have committed so quickly that its transaction record was garbage collected before our PushTxn request arrived. The PushTxn incorrectly interprets this state to mean that the transaction was aborted, when, in fact, it was committed and GCed. To be fair, we're somewhat abusing the PushTxn request here. Outside of merges, a PushTxn request is only sent when a pending intent is discovered, and transaction records can't be GCed until all their intents have been resolved. So we need some way to determine whether a merge transaction was actually aborted. What we do is look for the effects of the merge transaction in meta2. If the merge aborted, we'll see our own range descriptor, with our range ID, in meta2. By contrast, if the merge committed, we'll see a range descriptor for a different range in meta2. This complexity is extremely unfortunate, and turns what should be a simple goroutine, spawned on the RHS leader for every merge transaction ```go go func() { <-txn.Done() // wait for txn to complete if txn.Committed() { repl.MarkDestroyed("replica subsumed") } repl.UnblockRequests() } ``` into [150 lines of hard to follow code][code]. [code]: https://github.com/cockroachdb/cockroach/blob/82bcd948384e6a482cdd7c916c0aaca32367a7b0/pkg/storage/replica.go#L2813-L2920 ### Unanimity The largest conceptual incongruity with the current merge implementation is the fact that it requires unanimous consent from all replicas, instead of a majority quorum, like everything else in Raft. Further confusing matters, only the RHS needs unanimous consent; a merge can proceed with only majority consent from the LHS. In fact, it's even a bit more subtle: while only a majority of the LHS replicas need to vote on the merge command, all LHS and RHS replicas need to confirm that they are initialized for the merge to start. There is no theoretical reason that merges need unanimous consent, but the complexity of the implementation quickly skyrockets without it. For example, suppose you adjusted the transfer of power so that only a majority of replicas on the RHS need to be fully up to date before the merge commits. Now, when applying the merge trigger, the LHS needs to check to see if its copy of the RHS is up to date; if it's not, the LHS needs to throw away its entire Raft state and demand a snapshot from the leader because its copy of the RHS may never catch up (its Raft group may have already been destroyed by this point). This is both unsightly—our code is worse off every time we reach into Raft—and less efficient than the existing implementation, as it requires sending a potentially multi-megabyte snapshot if one replica of the RHS is just a little bit behind in applying the latest commands. It's possible that these problems could be mitigated while retaining the ability to merge with a minority of replicas offline, but an obvious solution did not present itself. On the bright side, having too many ranges is unlikely to cause acute performance problems; that is, a situation where a merge is critical to the health of a cluster is difficult to imagine. Unlike large ranges, which can appear suddenly and require an immediate split or log truncation, merges are only required when there are on the order of tens of thousands of excessively small ranges, which takes a long time to build up. ## Safety recap This section is a recap of the various mechanisms, which are described in detail above, that work together to ensure that merges do not violate consistency. The first broad safety mechanism is replica set alignment, which is required so that every store participating in the merge has a copy of both halves of the data in the merged range. Replica sets are first optimistically aligned by the merge queue. The replicas might drift apart, e.g., because the ranges in question were also targeted for a rebalance by the replicate queue, so the merge transaction verifies that the replica sets are still aligned from within the transaction. If a concurrent split or rebalance were to occur on the implicated ranges, transactional isolation kicks in and aborts one of the transactions, so we know that the replica sets are still aligned at the moment that the merge commits. Crucially, we need to maintain alignment until the merge applies on all replicas that were present at the time of the merge. This is enforced by refusing to GC a replica of the RHS of a merge unless it can be proven that the store does not have a replica of the LHS that predates the merge, _nor_ will it acquire a replica of the LHS that predates the merge. Proving that it does not currently have a replica of the LHS that predates the merge is fairly straightforward: we simply prove that the local left neighbor's generation is the newest generation, as indicated by the LHS's meta descriptor. Proving that the store will _never_ acquire a replica of the LHS that predates the merge is harder— there could be a snapshot in flight that the LHS is entirely unaware of. So instead we require that replicas of the LHS in a merge are initialized on every store before the merge can begin. The second broad safety mechanism is the range freeze. This ensures that the subsuming range and the subsumed range do not serve traffic at the same time, which would lead to clear consistency violations. The mechanism works by tying the freeze to the lifetime of the merge transaction; the merge will not commit until all replicas of the RHS are verified to be frozen, and the replicas of the RHS will not unfreeze unless the merge transaction is verified to be aborted. Lease transfers are freeze-aware, so the freeze will persist even if the lease moves around on the RHS during the merge or if the leaseholder restarts. The implementation of the freeze ab(uses) the span latch manager, to flush out in-flight commands on the RHS, an intent on the local range descriptor, to ensure the freeze persists if the lease is transferred, and an RPC that repeatedly polls the RHS to wait until it is fully caught up. ## Appendix The appendix contains digressions that are not directly pertinent to range merges, but are not covered in documentation elsewhere. ### Key encoding oddities Lexicographic ordering of keys of unbounded length has the interesting property that it is always possible to construct the key that immediately succeeds a given key, but it is not always possible to construct the key that immediately precedes a given key. In the following diagrams `\xNN` represents a byte whose value in hexadecimal is `NN`. The null byte is thus `\x00` and the maximal byte is thus `\xff`. Now consider a sequence of keys that has no gaps: ``` a a\x00 a\x00\x00 ``` No gaps means that there are no possible keys that can sort between any of the members of the sequence. For example, there is, simply, no key that sorts between `a` and `a\x00`. Because we can construct such a sequence, we must have next and previous operations over the sequence, which, given a key, construct the immediately following key and the immediately preceding key, respectively. We can see from the diagram that the next operation appends a null byte (`\x00`), while the previous operation strips off that null byte. But what if we want to perform the previous operation on a key that does not end in a null byte? For example, what is the key that immediately precedes `b`? It's not `a`, because `a\x00` sorts between `a` and `b`. Similarly, it's not `a\xff`, because `a\xff\xff` sorts between `a\xff` and `b`. This process continues inductively until we conclude the key that immediately precedes `b` is `a\xff\xff\xff...`, where there are an infinite number of trailing `\xff` bytes. It is not possible to represent this key in CockroachDB without infinite space. You could imagine designing the key encoding with an additional bit that means, "pretend this key has an infinite number of trailing maximal bytes," but CockroachDB does not have such a bit. The upshot is that it is trivial to advance in the keyspace using purely lexical operations, but it is impossible to reverse in the keyspace with purely lexical operations. This problem pervades the system. Given a range that spans from `StartKey`, inclusive, to `EndKey`, exclusive, it is trivial to address a request to following range, but *not* the preceding range. To route a request to a range, we must construct a key that lives inside that range. Constructing such a key for the following range is trivial, as the end key of a range is, by definition, contained in the following range. But constructing such a key for the preceding range would require constructing the key that immediately precedes `StartKey`, which is not possible with CockroachDB's key encoding.