Home · ASPLOS 2023 · Concurrency & Parallel Programming

Cooperative Concurrency Control for Write-Intensive Key-Value Workloads

Mark Sutherland, Babak Falsafi and Alexandros Daglis

Conference
ASPLOS 2023
Published
2022
Pages
30-46
Citations
6

Key-value stores — the fast in-memory databases behind online services — are all tuned for read-heavy traffic, but real workloads increasingly have many writes, and the standard trick of pinning each key's writes to one thread to avoid locking then wrecks load balance; this work from EPFL and Georgia Tech builds C-4, a co-design that teaches the network card to split writes into independent ones it can spread across threads and dependent ones it funnels to a single thread that batches them into one update, cutting tail latency by up to 5 times and lifting throughput up to 1.7 times while keeping strong consistency.

The argument in one glance

  1. Problem

    Key-value stores are tuned for reads, but writes are increasingly common

    A key-value store is the fast in-memory database that sits behind almost every online service, and its slowest responses — its tail latency — directly shape how snappy a website feels. For years these systems have been optimized for read-mostly traffic. But production studies now show write-heavy workloads are common: more than a third of one company's workloads have more writes than reads, and some are 92 percent writes. Today's stores handle these badly.

  2. What people did

    Pin each key's writes to one thread so writers never need locks

    On a manycore server, the way to get high throughput is to avoid synchronization between threads, because locking a shared item costs atomic instructions and cache-coherence traffic. So the standard design statically assigns each key to one writer thread: reads can happen on any thread concurrently, but a given key is only ever written by its one owner. No locks, no contention between writers — a clean, fast rule.

  3. Why it fell short

    That static pinning forbids balancing write load, and it hurts two ways

    The problem is that pinning writes to fixed threads makes it impossible to balance write load. When a workload is write-heavy but spread evenly, half or more of the requests cannot be moved off a busy thread, so they pile up in transient queues and tail latency balloons. When a workload is heavily skewed, the single thread that owns the hottest key is buried under writes while every other thread sits idle, capping throughput. Both failures trace to the same root cause: static write partitioning.

  4. This paper's idea

    Split writes into independent and dependent, and handle each with the network card and software

    C-4 breaks the tradeoff by noticing that two writes truly conflict only when they hit the same key. So it teaches the network card to treat a key as owned by a thread only while a write to it is actually in flight — letting it freely balance the independent writes across threads — and it adds a software layer that lets the one thread handling a flood of writes to the same hot key compact them into a single batched update. Both moves preserve strong consistency, and together they cut tail latency up to 5 times and lift throughput up to 1.7 times.

Why this problem is worth solving

The whole difficulty lives in one design decision, so it is worth being precise about it.

A key-value store on a manycore server runs many worker threads, and the central question is how to let them touch shared data without corrupting it. Locking every item is safe but slow: acquiring a lock means atomic instructions and bouncing the item’s cache line between cores, which at microsecond service times is crippling. The dominant answer is Concurrent Read, Exclusive Write (CREW): reads can run on any thread at once, but each key is statically assigned to exactly one writer thread, chosen by hashing the key. Because a key has a single fixed writer, that writer never needs a lock — no other thread will ever write the same key. This is why top stores use it. But the same fixed assignment that eliminates locking also freezes write placement: a load balancer that could spread work across idle threads is not allowed to touch writes, because moving a write to another thread would break the single-writer guarantee. The performance of the whole system is thus hostage to a hash function decided in advance.

The reason this matters more than it used to is that the workloads have changed, in two distinct ways.

Key-value workloads can be placed on two axes: how large a fraction of requests are writes, and how skewed the popularity of keys is. Read-mostly workloads, skewed or not, are well handled by today’s stores. Two write-heavy regions are not. The first is uniform and write-heavy — say half or more writes, with no single hot key. Here static partitioning simply cannot balance the majority of requests, so under bursts they queue on whatever threads their keys hash to, and tail latency inflates several-fold. The second is read-write but extremely skewed — most traffic hitting a few keys. Here the one thread owning the hottest key is overwhelmed even at single-digit write fractions, capping the store’s throughput while other threads idle, and the constant writes to that key thrash its cache line against the many readers. Production traces show both regions are now common, yet current designs address neither — which is what makes revisiting concurrency control worthwhile.

What people did before, and where it broke

  • Static exclusive write, no balancing (EREW). Hash every request, read or write, to a fixed thread; never balance anything. Where it broke. With no load balancing at all, transient queues form on busy threads and throughput saturates well below the hardware’s potential.
  • Concurrent readers, partitioned writes (CREW, the state of the art). Let reads run on any thread and balance freely, but keep each key’s writes pinned to one thread. Where it broke. It handles read-heavy traffic beautifully but cannot balance writes, so as the write fraction rises its tail latency climbs toward EREW and skew overwhelms the hot writer.
  • Frequency-scale the overloaded core. Detect the buried hot-writer thread and clock it faster. Where it broke. A single core’s clock headroom cannot absorb the load a badly-skewed workload dumps on it; it is a band-aid, not a fix.
  • Multi-version concurrency frameworks (RLU, MV-RLU). Let writers create new versions so readers never lock, enabling write balancing in software. Where it broke. Version management — logging, commit, garbage collection — adds overhead ruinous at microsecond scale: ported to a key-value store, write latency rose several-fold to dozens of times, and the store could not even meet its latency target at low load.
  • Software delegation (flat combining, remote core locking). Funnel all writes to a shared key to one core through software queues. Where it broke. The software shuffling of requests between cores to serialize them is itself the overhead CREW was trying to avoid, essentially reimplementing partitioning with extra cost.
  • The gap. Every approach either forbade write balancing to stay lock-free, or bought balancing with synchronization overhead that microsecond serving cannot afford. What was missing was a way to balance the writes that are safe to move and efficiently absorb the ones that are not, without locks — which is exactly what write-intensive key-value serving needs.

How C-4 works

Splitting writes into independent and dependent, then handling each

  1. new

  2. new

  3. new

  4. new

  5. new

The idea

Sort the writes into two kinds and give each its own remedy. The insight that unlocks everything is that static partitioning treats all writes as if they conflict, when in truth two writes clash only if they touch the same key. So C-4 separates writes into independent ones — to different keys, which can safely run on any thread — and dependent ones — to the same key, which must be serialized. The two classes get opposite treatments: independent writes are load-balanced to spread the work, while dependent writes are deliberately concentrated on one thread so they can be batched. Classifying a stream of operations into the kind you can freely rearrange and the kind you must handle carefully, then applying a different mechanism to each, is the same strategy the distributed-training communication work covered elsewhere on this site uses, splitting communication into the part it can overlap by reordering and the part it must hide by decomposing, and treating each on its own terms.

Make the exclusive lock exist only during the instant of a real conflict. Static partitioning is permanently conservative: a key belongs to its one thread forever, whether or not anyone is writing it. C-4’s dynamic policy keeps the single-writer guarantee but shrinks it to the moment it is actually needed — a key becomes exclusive to a thread only while a write to it is in flight, and the instant that write completes the key is free to be balanced anywhere again. The network card holds these ephemeral ownerships in a tiny table, so the restriction applies precisely when two writes genuinely collide and never otherwise. Separating the fixed question of what owns a resource from the fleeting question of when it is truly in use, and imposing the constraint only in that narrow window, is the same discipline the memory-resizing work covered elsewhere on this site follows, decoupling what to resize from when so the costly rule fires only at the moment it matters.

Turn the overloaded hot thread’s burden into a chance to do less. When skew dumps a torrent of writes to one key onto a single thread, the naive response is to make that thread faster; C-4 instead makes it do less work. Because all those writes target the same item, the thread gathers them into one private log and applies them as a single combined update, so instead of paying the coherence cost of writing the shared cache line once per request, it pays it once per batch. Counterintuitively, the busier the hot key gets, the more the thread can batch, so its cost per write falls as load rises. Letting a single unit absorb many correlated requests together, rather than paying a fresh cost for each, is the same move the serverless orchestration work covered elsewhere on this site makes, reusing one busy container to serve a queue of requests for the same function instead of spinning up a new one every time.

Following a burst of writes through C-4

Serving a flood of writes to one hot key

Tracing writes to the hottest key shows the NIC and software cooperating.

Step 1 of 5

A burst hits the hottest key

Writes pour in for the hottest key, alongside writes to many other keys. In a standard store, all the hot key's writes are pinned to one thread, which becomes the bottleneck while other threads sit idle — the classic skew problem.

Under heavy skew this can cap throughput even when writes are only a few percent of traffic.

Why it works

  • Only truly conflicting writes need to be serialized. Static partitioning pins every key’s writes to one thread to avoid locks, forbidding all write balancing → but two writes actually conflict only when they hit the same key, which is rare under uniform load → so the network card can freely balance independent writes and make a key exclusive only while a write to it is in flight, reaching near-ideal tail latency.
  • Deciding write placement in the card keeps it lock-free and cheap. Balancing writes dynamically would normally need synchronization, or broadcasting ownership to thousands of clients → but a small table in the network card tracks which thread owns each active key and routes writes accordingly → so threads still avoid atomic instructions while the store gains write balancing, at a hardware cost of a fraction of a percent.
  • Skew that overwhelms one thread becomes an opportunity to batch. Heavy skew funnels a flood of writes to the hottest key onto a single thread, saturating it while others idle → but because those writes all target the same item, that thread can compact them into one update instead of applying each separately → so the bottleneck thread does less work per write as load rises, not more.
  • Batching in a private log avoids the coherence traffic that makes hot writes slow. Applying each write to the shared datastore repeatedly invalidates and refetches the hot cache line, so service time climbs with load → but gathering writes into a thread-private batch touches no shared lines and writes the datastore once → so the hot thread’s per-write time falls, dropping from 908 to 243 nanoseconds.
  • Deferring the batch’s responses keeps the store linearizable. Acknowledging a compacted write before its batch is applied would let a later read miss it → but holding every response until the window closes makes concurrent reads legally free to return the old value → so batching preserves strong consistency instead of weakening it.

What it costs (trade-offs and scope)

  • It is evaluated entirely in simulation. The results come from cycle-accurate full-system simulation of a 64-core server with a modeled network card; the hardware table’s area and power are analytical estimates, and no physical system was built.
  • It needs a specialized network card and RPC stack. The dynamic balancing requires a card that terminates network protocols in hardware, parses application headers, and runs a programmable load balancer — features the authors expect in future products, not commodity cards today.
  • The two mechanisms are workload-specific and can hurt off-target. Dynamic balancing helps uniform write-heavy traffic; compaction helps skewed traffic; and compaction actually slows uniform workloads by a few percent through fruitless queue scanning, so the system must apply the right one for the workload.
  • Compaction trades a latency floor for throughput. Under light load, windows open but gather few writes, and closing only at the latency deadline creates a tail-latency jump; an adaptive early-close would fix it but is not implemented. It also deliberately delays write acknowledgements until a window closes.
  • The benefit shrinks for small items. When key-value pairs are tiny, the fixed cost of building a batch approaches the cost of a single store, so the hot-thread speedup falls to modest levels, though throughput still improves.
  • It preserves only single-key consistency. Linearizability holds per key; multi-key atomicity is out of scope, left to higher transaction layers.
  • The concurrency-framework baselines are outside their design point. General multi-versioning frameworks look very poor here, but they were not built for microsecond-scale key-value serving, so the comparison flatters C-4’s specialization.

The evaluation

Concurrency control for write-intensive key-value stores

PropertyEREWCREW (baseline)RLU / MV-RLUC-4
Load-balance readsYesYes
Load-balance independent writesNo
Writer overheadNone, but no balancingNone, but no write balancing
Write-heavy tail latencyHigh
Skewed throughputHot thread caps itHot thread caps itPoor at microsecond scale
Concurrency control policies for an in-memory key-value store, evaluated in cycle-accurate simulation of a 64-core server running MICA with a modeled NIC. C-4's dynamic write balancing cuts 99th-percentile tail latency by 1.3 to 5 times on uniform write-heavy workloads (near the ideal of full concurrency with no synchronization), and its write compaction lifts throughput up to 1.7 times on heavily-skewed read-write workloads (76 to 125 million requests per second in one case) — all while preserving linearizability.Underlined values explain themselves when clicked.

Check yourself

Check your understanding

  1. 1. Why do state-of-the-art key-value stores statically pin each key's writes to one thread?

  2. 2. How does C-4 split the write-handling problem in two?

  3. 3. Why does write compaction make the overloaded hot thread faster, not just less loaded?

  4. 4. Which statement best reflects the limits of C-4?

Sources

The paper is “Cooperative Concurrency Control for Write-Intensive Key-Value Workloads” by Mark Sutherland, Babak Falsafi, and Alexandros Daglis, published at ASPLOS 2023, DOI 10.1145/3567955.3567957. The authors are at EcoCloud, EPFL and the Georgia Institute of Technology.

This explainer is written from the full 17-page text. Everything above comes from the paper’s own sections: the introduction and motivation establishing the rise of write-intensive workloads and the KVS workload taxonomy (the read-mostly regions that are handled and the uniform-write-heavy and skewed read-write regions that are not); the tradeoff analysis showing static write partitioning as the shared root cause and the two insights (independent writes can be balanced, dependent writes can be compacted); the queueing model quantifying that dynamic partitioning reaches near-ideal tail latency and compaction boosts skewed-workload throughput 1.5 to 2 times; the C-4 design describing dynamic CREW realized in the network card with the Exclusive Writer Table and software write compaction with its linearizability argument (deferring responses until a window closes); the implementation on the NeBuLa architecture with MICA (adding only 105 lines for compaction) and the hardware-cost analysis (a 128-entry table at 0.004 square millimeters and 6.85 milliwatts, 0.002 percent of a 64-core chip); and the evaluation reporting up to 5 times lower 99th-percentile tail latency for write-heavy workloads, up to 1.7 times higher throughput for skewed workloads, the hot thread’s service time falling from 908 to 243 nanoseconds under compaction, and the item-size sensitivity showing gains from 1.33 to 1.6 times across key-value sizes.

Related papers on Concurrency & Parallel Programming

See all 43 →