
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[ The Cloudflare Blog ]]></title>
        <description><![CDATA[ Get the latest news on how products at Cloudflare are built, technologies used, and join the teams helping to build a better Internet. ]]></description>
        <link>https://blog.cloudflare.com</link>
        <atom:link href="https://blog.cloudflare.com/" rel="self" type="application/rss+xml"/>
        <language>en-us</language>
        <image>
            <url>https://blog.cloudflare.com/favicon.png</url>
            <title>The Cloudflare Blog</title>
            <link>https://blog.cloudflare.com</link>
        </image>
        <lastBuildDate>Thu, 09 Apr 2026 08:48:50 GMT</lastBuildDate>
        <item>
            <title><![CDATA[Quicksilver v2: evolution of a globally distributed key-value store (Part 2)]]></title>
            <link>https://blog.cloudflare.com/quicksilver-v2-evolution-of-a-globally-distributed-key-value-store-part-2-of-2/</link>
            <pubDate>Thu, 17 Jul 2025 13:00:00 GMT</pubDate>
            <description><![CDATA[ This is part two of a story about how we overcame the challenges of making a complex system more scalable. ]]></description>
            <content:encoded><![CDATA[ 
    <div>
      <h2>What is Quicksilver?</h2>
      <a href="#what-is-quicksilver">
        
      </a>
    </div>
    <p>Cloudflare has servers in <a href="https://www.cloudflare.com/network"><u>330 cities spread across 125+ countries</u></a>. All of these servers run Quicksilver, which is a key-value database that contains important configuration information for many of our services, and is queried for all requests that hit the Cloudflare network.</p><p>Because it is used while handling requests, Quicksilver is designed to be very fast; it currently responds to 90% of requests in less than 1 ms and 99.9% of requests in less than 7 ms. Most requests are only for a few keys, but some are for hundreds or even more keys.</p><p>Quicksilver currently contains over five billion key-value pairs with a combined size of 1.6 TB, and it serves over three billion keys per second, worldwide. Keeping Quicksilver fast provides some unique challenges, given that our dataset is always growing, and new use cases are added regularly.</p><p>Quicksilver used to store all key-values on all servers everywhere, but there is obviously a limit to how much disk space can be used on every single server. For instance, the more disk space used by Quicksilver, the less disk space is left for content caching. Also, with each added server that contains a particular key-value, the cost of storing that key-value increases.</p><p>This is why disk space usage has been the main battle that the Quicksilver team has been waging over the past several years. A lot was done over the years, but we now think that we have finally created an architecture that will allow us to get ahead of the disk space limitations and finally make Quicksilver scale better.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2mGYCFdZIWdYl1TuDzvlAf/45d1b975adce0ec4eb5cc2842c8bd185/image1.png" />
          </figure><p><sup>The size of the Quicksilver database has grown by 50% to about 1.6 TB in the past year</sup></p>
    <div>
      <h2>What we talked about previously</h2>
      <a href="#what-we-talked-about-previously">
        
      </a>
    </div>
    <p><a href="https://blog.cloudflare.com/quicksilver-v2-evolution-of-a-globally-distributed-key-value-store-part-1/"><u>Part one of the story</u></a> explained how Quicksilver V1 stored all key-value pairs on each server all around the world. It was a very simple and fast design, it worked very well, and it was a great way to get started. But over time, it turned out to not scale well from a disk space perspective.</p><p>The problem was that disk space was running out so fast that there was not enough time to design and implement a fully scalable version of Quicksilver. Therefore, Quicksilver V1.5 was created first. It halved the disk space used on each server compared to V1.</p><p>For this, a new <i>proxy </i>mode was introduced for Quicksilver. In this mode, Quicksilver does not contain the full dataset anymore, but only contains a cache. All cache misses are looked up on another server that runs Quicksilver with a full dataset. Each server runs about ten separate <i>instances</i> of Quicksilver, and all have different databases with different sets of key-values. We call Quicksilver instances with the full data set <i>replicas</i>.</p><p>For Quicksilver V1.5, half of those instances on a particular server would run Quicksilver in proxy mode, and therefore would not have the full dataset anymore. The other half would run in replica mode. This worked well for a time, but it was not the final solution.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2GHG0tl5diZ7w09qNkblyd/6fa0ba250477cb9027496669640a0acb/image4.png" />
          </figure><p>Building this intermediate solution had the added benefit of allowing the team to gain experience running an even more distributed version of Quicksilver.</p>
    <div>
      <h2>The problem</h2>
      <a href="#the-problem">
        
      </a>
    </div>
    <p>There were a few reasons why Quicksilver V1.5 was not fully scalable.</p><p>First, the size of the separate instances were not very stable. The key-space is owned by the teams that use Quicksilver, not by the Quicksilver team, and the way those teams use Quicksilver changes frequently. Furthermore, while most instances grow in size over time, some instances have actually gotten smaller, such as when the use of Quicksilver is optimised by teams. The result of this is that the split of instances that was well-balanced at the start, quickly became unbalanced.</p><p>Second, the analyses that were done to estimate how much of the key space would need to be in cache on each server assumed that taking all keys that were accessed in a three-day period would represent a good enough cache. This assumption turned out to be wildly off. This analysis estimated that we needed about 20% of the key space in cache, which turned out to not be entirely accurate. Whereas most instances did have a good cache hit rate, with 20% or less of the key space in cache, some instances turned out to need a much higher percentage.</p><p>The main issue, however, was that reducing the disk space used by Quicksilver on our network by as much as 40% does not actually make it more scalable. The number of key-values that are stored in Quicksilver keeps growing. It only took about two years before disk space was running low again.</p>
    <div>
      <h2>The solution</h2>
      <a href="#the-solution">
        
      </a>
    </div>
    
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4G3Y1t9OeF7BlsASIwlpI7/8df9e21affde869fbdd41f203cbcd256/image6.png" />
          </figure><p><sup>Except for a handful of special storage servers, Quicksilver does not contain the full dataset anymore, but only cache. Any cache misses will be looked up in replicas on our storage servers, which do have the full dataset.</sup></p><p>The solution to the scalability problem was brought on by a new insight. As it turns out, numerous key-values were actually almost never used. We call these <i>cold keys</i>. There are different reasons for these cold keys: some of them were old and not well cleaned up, some were used only in certain regions or in certain data centers, and some were not used for a very long time or maybe not at all (a domain name that is never looked up for example or a script that was uploaded but never used).</p><p>At first, the team had been considering solving our scalability problem by splitting up the entire dataset into shards and distributing those across the servers in the different data centers. But sharding the full dataset adds a lot of complexity, corner cases, and unknowns. Sharding also does not optimize for data locality. For example, if the key-space is split into 4 shards and each server gets one shard, that server can only serve 25% of the requested keys from its local database. The cold keys would also still be contained in those shards and would take up disk space unnecessarily.</p><p>Another data structure that is much better at data locality and explicitly avoids storing keys that are never used is a cache. So it was decided that only a handful of servers with large disks would maintain the full data set, and all other servers would only have a cache. This was an obvious evolution from Quicksilver V1.5. Caching was already being done on a smaller scale, so all the components were already available. The caching proxies and the inter-data center discovery mechanisms were already in place. They had been used since 2021 and were therefore thoroughly battle tested. However, one more component needed to be added.</p><p>There was a concern that having all instances on all servers connect to a handful of storage nodes with replicas would overload them with too many connections. So a Quicksilver <i>relay</i> was added. For each instance, a few servers would be elected within each data center on which Quicksilver would run in relay mode. The relays would maintain the connections to the replicas on the storage nodes. All proxies inside a data center would discover those relays and all cache misses would be relayed through them to the replicas.</p><p>This new architecture worked very well. The cache hit rates still needed some improvement, however.</p>
    <div>
      <h3>Prefetching the future</h3>
      <a href="#prefetching-the-future">
        
      </a>
    </div>
    
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/48FbryG5MCUAmQNY34jtOK/5e57c62fafa81fa323cf4ef77ccded55/Prefetching_the_future.png" />
          </figure><p><sup>Every resolved cache miss is prefetched by all servers in the data center</sup></p><p>We had a hypothesis that prefetching all keys that were cache misses on the other servers inside the same data center would improve the cache hit rate. So an analysis was done, and it indeed showed that every key that was a cache miss on one server in a data center had a very high probability of also being a cache miss on another server in the same data center sometime in the near future. Therefore, a mechanism was built that distributed all resolved cache misses on relays to all other servers.</p><p>All cache misses in a data center are resolved by requesting them from a relay, which subsequently forwards the requests to one of the replicas on the storage nodes. Therefore, the prefetching mechanism was implemented by making relays publish a stream of all resolved cache misses, to which all Quicksilver proxies in the same data center subscribe. The resulting key-values were then added to the proxy local caches.</p><p>This strategy is called <i>reactive</i> prefetching, because it fills caches only with the key-values that directly resulted from cache misses inside the same data center. Those prefetches are a <i>reaction</i> to the cache misses. Another way of prefetching is called <i>predictive</i> prefetching, in which an algorithm tries to predict which keys that have not yet been requested will be requested in the near future. A few approaches for making these predictions were tried, but they did not result in any improvement, and so this idea was abandoned.</p><p>With the prefetching enabled, cache hit rates went up to about 99.9% for the worst performing instance. This was the goal that we were trying to reach. But while rolling this out to more of our network, it turned out that there was one team that needed an even higher cache hit rate, because the tail latencies they were seeing with this new architecture were too high.</p><p>This team was using a Quicksilver instance called <i>dnsv2</i>. This is a very latency sensitive instance, because it is the one from which DNS queries are served. Some of the DNS queries under the hood need multiple queries to Quicksilver, so any added latency to Quicksilver multiplies for them. This is why it was decided that one more improvement to the Quicksilver cache was needed.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7fKjcIASCIsaW52NDTF6OX/25cc40046536b28fca5b44249234cc48/image12.png" />
          </figure><p><sup>The level 1 cache hit-rate is 99.9% or higher, on average.</sup></p>
    <div>
      <h3>Back to the sharding</h3>
      <a href="#back-to-the-sharding">
        
      </a>
    </div>
    
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/10ur3qVEVDt4carNCqldch/0777d16f53fd501354f7196caac4d9fe/back-to-sharding.png" />
          </figure><p><sup>Before going to a replica in another data center, a cache miss is first looked up in a data center-wide sharded cache</sup></p><p>The instance on which higher cache hit rates were required was also the instance on which the cache performed the worst. The cache works with a retention time, defined as the number of days a key-value is kept in cache after it was last accessed, after which it is evicted from the cache. An analysis of the cache showed that this instance needed a much longer retention time. But, a higher retention time also causes the cache to take up more disk space — space that was not available.</p><p>However, while running Quicksilver V1.5, we had already noticed the pattern that caches generally performed much better in smaller data centers as compared to larger ones. This sparked the hypothesis that led to the final improvement.</p><p>It turns out that smaller data centers, with fewer servers, generally needed less disk space for their cache. Vice versa, the more servers there are in a data center, the larger the Quicksilver cache needs to be. This is easily explained by the fact that larger data centers generally serve larger populations, and therefore have a larger diversity of requests. More servers also means more total disk space available inside the data center. To be able to make use of this pattern the concept of sharding was reintroduced.</p><p>Our key space was split up into multiple shards. Each server in a data center was assigned one of the shards. Instead of those shards containing the full dataset for their part of the key space, they contain a cache for it. Those cache shards are populated by all cache misses inside the data center. This all forms a data center-wide cache that is distributed using sharding.</p><p>The data locality issue that sharding the full dataset has, as described above, is solved by keeping the local per-server caches as well. The sharded cache is in addition to the local caches. All servers in a data center contain both their local cache and a cache for one physical shard of the sharded cache. Therefore, each requested key is first looked up in the server’s local cache, after that the data center-wide sharded cache is queried, and finally if both caches miss the requested key, it is looked up on one of the storage nodes.</p><p>The key space is split up into separate shards by first dividing hashes of the keys by range into 1024 logical shards. Those logical shards are then divided up into physical shards, again by range. Each server gets one physical shard assigned by repeating the same process on the server hostname.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4fKK9EPdwP69l4VtIErNGW/5481fb93b8e89fff6f5bdc6dd2dfbb44/image7.png" />
          </figure><p><sup>Each server contains one physical shard. A physical shard contains a range of logical shards. A local shard contains a range of the ordered set that result from hashing all keys.</sup></p><p>This approach has the advantage that the sharding factor can be scaled up by factors of two without the need for copying caches to other servers. When the sharding factor is increased in this way, the servers will automatically get a new physical shard assigned that contains a subset of the key space that the previous physical shard on that server contained. After this has happened, their cache will contain supersets of the needed cache. The key-values that are not needed anymore will be evicted over time.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2IBc0EMQEDeenGX2ShbrrM/d661ef58d7114caa3c77b9eb5b18b736/image10.png" />
          </figure><p><sup>When the number of physical shards are doubled the servers will automatically get new physical shards that are subsets of their previous physical shards, therefore still have the relevant key-values in cache.</sup></p><p>This approach means that the sharded caches can easily be scaled up when needed as the number of keys that are in Quicksilver grows, and without any need for relocating data. Also, shards are well-balanced due to the fact that they contain uniform random subsets of a very large key-space.</p><p>Adding new key-values to the physical cache shards piggybacks on the prefetching mechanism, which already distributes all resolved cache misses to all servers in a data center. The keys that are part of the key space for a physical shard on a particular server are just kept longer in cache than the keys that are not part of that physical shard.</p><p>Another reason why a sharded cache is simpler than sharding the full key-space is that it is possible to cut some corners with a cache. For instance, looking up older versions of key-values (as used for  multiversion concurrency control) is not supported on cache shards. As explained in an <a href="https://blog.cloudflare.com/quicksilver-v2-evolution-of-a-globally-distributed-key-value-store-part-1/"><u>earlier blog post</u></a>, this is needed for consistency when looking up key-values on different servers, when that server has a newer version of the database. It is not needed in the cache shards, because lookups can always fall back to the storage nodes when the right version is not available.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7qKie4ZFES4yJw1Sa2K26X/3d326d4e8e7553dcf96738ceae7229fe/image9.png" />
          </figure><p><sup>Proxies have a recent keys window that contains all recently written key-values. A cache shard only has its cached key-values. Storage replicas contain all key-values and on top of that they contain multiple versions for recently written key-values. When the proxy, that has database version 1000, has a cache miss for </sup><sup><i>key1</i></sup><sup> it can be seen that the version of that key on the cache shard was written at database version 1002 and therefore is too new. This means that it is not consistent with the proxy’s database version. This is why the relay will fetch that key from a replica instead, which can return the earlier consistent version. In contrast, </sup><sup><i>key2</i></sup><sup> on the cache shard can be used, because it was written at index 994, well below the database version of the proxy.</sup></p><p>There is only one very specific corner case in which a key-value on a cache shard cannot be used. This happens when the key-value in the cache shard was written at a more recent database version than the version of the proxy database at that time. This would mean that the key-value probably has a different value than it had at the correct version. Because, in general, the cache shard and the proxy database versions are very close to each other, and this only happens for key-values that were written in between those two database versions, this happens very rarely. As such, deferring the lookup to storage nodes has no noticeable effect on the cache hit rate.</p>
    <div>
      <h3>Tiered Storage</h3>
      <a href="#tiered-storage">
        
      </a>
    </div>
    
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2bLnTL9WWzQfMqjyXv5o6Q/1c1bf1cb348aa736fb320c77b52e10dd/image3.png" />
          </figure><p>To summarize, Quicksilver V2 has three levels of storage.</p><ol><li><p>Level 1: The local cache on each server that contains the key-values that have most recently been accessed.</p></li><li><p>Level 2: The data center wide sharded cache that contains key-values that haven’t been accessed in a while, but do have been accessed.</p></li><li><p>Level 3: The replicas on the storage nodes that contain the full dataset, which live on a handful of storage nodes and are only queried for the <i>cold</i> keys.</p></li></ol>
    <div>
      <h2>The results</h2>
      <a href="#the-results">
        
      </a>
    </div>
    <p>The percentage of keys that can be resolved within a data center improved significantly by adding the second caching layer. The worst performing instance has a cache hit rate higher than 99.99%. All other instances have a cache hit rate that is higher than 99.999%.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6V5BgOVPsTrYQf4WuUKdmg/67756312764cdab789eff1ff33caf0f3/image5.png" />
          </figure><p><sup>The combined level 1 and level 2 cache hit-rate is 99.99% or higher for the worst caching instance.</sup></p>
    <div>
      <h2>Final notes</h2>
      <a href="#final-notes">
        
      </a>
    </div>
    <p>It took the team quite a few years to go from the old Quicksilver V1, where all data was stored on each server to the tiered caching Quicksilver V2, where all but a handful of servers only have cache. We faced many challenges, including migrating hundreds of thousands of live databases without interruptions, while serving billions of requests per second. A lot of code changes were rolled out, with the result that Quicksilver now has a significantly different architecture. All of this was done transparently to our customers. It was all done iteratively, always learning from the previous step before taking the next one. And always making sure that, if at all possible, all changes are easy to revert. These are important strategies for migrating complex systems safely.</p><p>If you like these kinds of stories, keep an eye out for more development stories on our blog. And if you are enthusiastic about solving these kinds of problems, <a href="https://www.cloudflare.com/en-gb/careers/jobs/"><u>we are hiring for multiple types of roles across the organization</u></a></p>
    <div>
      <h2>Thank you</h2>
      <a href="#thank-you">
        
      </a>
    </div>
    <p>And finally, a big thanks to the rest of the Quicksilver team, because we all do this together: Aleksandr Matveev, Aleksei Surikov, Alex Dzyoba, Alexandra (Modi) Stana-Palade, Francois Stiennon, Geoffrey Plouviez, Ilya Polyakovskiy, Manzur Mukhitdinov, Volodymyr Dorokhov.</p> ]]></content:encoded>
            <category><![CDATA[Cache]]></category>
            <category><![CDATA[Quicksilver]]></category>
            <category><![CDATA[Storage]]></category>
            <category><![CDATA[Key Value]]></category>
            <guid isPermaLink="false">5oxVTbLoVGabfWNbMtNbs</guid>
            <dc:creator>Marten van de Sanden</dc:creator>
            <dc:creator>Anton Dort-Golts</dc:creator>
        </item>
        <item>
            <title><![CDATA[Quicksilver v2: evolution of a globally distributed key-value store (Part 1)]]></title>
            <link>https://blog.cloudflare.com/quicksilver-v2-evolution-of-a-globally-distributed-key-value-store-part-1/</link>
            <pubDate>Thu, 10 Jul 2025 14:00:00 GMT</pubDate>
            <description><![CDATA[ This blog post is the first of a series, in which we share our journey in redesigning Quicksilver — Cloudflare’s distributed key-value store that serves over 3 billion keys per second globally.  ]]></description>
            <content:encoded><![CDATA[ <p>Quicksilver is a key-value store developed internally by Cloudflare to enable fast global replication and low-latency access on a planet scale. It was <a href="https://blog.cloudflare.com/introducing-quicksilver-configuration-distribution-at-internet-scale/"><u>initially designed</u></a> to be a global distribution system for configurations, but over time it gained popularity and became the foundational storage system for many products in Cloudflare.</p><p>A previous <a href="https://blog.cloudflare.com/moving-quicksilver-into-production/"><u>post</u></a> described how we moved Quicksilver to production and started replicating on all machines across our global network. That is what we called Quicksilver v1: each server has a full copy of the data and updates it through asynchronous replication. The design served us well for some time. However, as our business grew with an ever-expanding data center footprint and a growing dataset, it became more and more expensive to store everything everywhere.</p><p>We realized that storing the full dataset on every server is inefficient. Due to the uniform design, data accessed in one region or data center is replicated globally, even if it's never accessed elsewhere. This leads to wasted disk space. We decided to introduce a more efficient system with two new server roles: <b><i>replica</i></b>, which stores the full dataset and <b><i>proxy</i></b>, which acts as a persistent cache, evicting unused key-value pairs to free up some disk space. We call this design <b>Quicksilver v1.5</b> – an interim step towards a more sophisticated and scalable system.</p><p>To understand how those two roles helped us reduce disk space usage, we first need to share some background on our setup and introduce some terminology. Cloudflare is architected in a way where we have a few hyperscale core data centers that form our <a href="https://www.cloudflare.com/learning/network-layer/what-is-the-control-plane/"><u>control plane</u></a>, and many smaller data centers distributed across the globe where resources are more constrained. Quicksilver has dozens of servers in the core data centers with terabytes of storage called <b><i>root nodes</i></b>. In the smaller data centers, though, things are different. A typical data center has two types of nodes: <b><i>intermediate nodes </i></b><i>and </i><b><i>leaf nodes.</i></b> Intermediate servers replicate data either from the other intermediate nodes or directly from the root nodes. Leaf nodes serve end user traffic, and receive updates from intermediate servers, effectively being leaves of a replication tree. Disk capacity varies significantly between node types. While root nodes aren't facing an imminent disk space bottleneck, it's a definite concern for leaf nodes.</p><p>Every server – whether it’s a root, intermediate, or leaf – hosts 10 Quicksilver <b><i>instances</i></b>. These are independent databases, each used by specific Cloudflare services or products such as the <a href="https://www.cloudflare.com/application-services/products/dns/"><u>DNS</u></a>, <a href="https://www.cloudflare.com/application-services/products/cdn/"><u>CDN</u></a>  or <a href="https://www.cloudflare.com/application-services/products/waf/"><u>WAF</u></a>. </p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/336Tl3Q00d5MVe29nzkQcY/3781ee725f2da6a847677235c1c3c960/image5.png" />
          </figure><p><sup>Figure 1. Global Quicksilver</sup></p><p>Let’s consider the role distribution. Instead of hosting ten full datasets on every machine within a data center, what if we deploy only a few replicas in each?  The remaining servers would be proxies, maintaining a persistent cache of hot keys and querying replicas for any cache misses.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3RzCZ7Yr1Nx3wd28DL5uco/5e4bd0cd0ce649080e77814ab6f048c0/image1.png" />
          </figure><p><sup>Figure 2. Role allocation for different Quicksilver instances</sup></p><p>Data centers across our network are very different in size, ranging from hundreds of servers to a single rack with just a few servers. To ensure every data center has at least one replica, the simplest initial step is an even split: on each server, place five replicas of some instances and five proxies for others. The change immediately frees up disk space, as the cached hot dataset on a proxy should be smaller than a full replica. While it doesn’t remove the bottleneck entirely, it could, in theory, lead to an up to 50% reduction in disk space usage. More importantly, it lays the foundation for a new distributed design of Quicksilver, where queries can be served by multiple machines in a data center, paving the way for further horizontal scaling. Additionally, an iterative approach helps to battle-proof the code changes earlier.</p>
    <div>
      <h2>Can it even work?</h2>
      <a href="#can-it-even-work">
        
      </a>
    </div>
    <p>Before committing to building Quicksilver v1.5, we wanted to be sure that the proxy/replica design would actually work for our workload. If proxies needed to cache the entire dataset for good performance, then it would be a dead end, offering no potential disk space benefits. To assess this, we built a data pipeline which pushes accessed keys from all across our network to <a href="https://clickhouse.com"><u>ClickHouse</u></a>. This allowed us to estimate typical sizes of working sets. Our analysis revealed that:</p><ul><li><p>in large data centers approximately, 20% of the keyspace was in use</p></li><li><p>in small data centers this number dropped to just about 1%</p></li></ul><p>These findings gave us confidence that the caching approach should work, though it wouldn’t be without its challenges.</p>
    <div>
      <h2>Persistent caching</h2>
      <a href="#persistent-caching">
        
      </a>
    </div>
    <p>When talking about caches, the first thing that comes to mind is an in-memory cache. However, this cannot work for Quicksilver for two main reasons: memory usage and the “cold cache” problem.</p><p>Indeed, with billions of stored keys, even a fraction of them would lead to an unmanageable increase in memory usage. System restarts should not affect performance, which means that cache data must be preserved somewhere anyway. So we decided to make the cache persistent and store it in the same way as full datasets: in our embedded <a href="https://rocksdb.org/"><u>RocksDB</u></a>. Thus, cached keys normally sit on disk and can be retrieved on-demand with low memory footprint.</p><p>When a key cannot be found in the proxy’s cache, we request it from a replica using our internal <i>distributed key-value protocol</i>, and put it into a local cache after processing.</p><p>Evictions are based on RocksDB <a href="https://github.com/facebook/rocksdb/wiki/Compaction-Filter"><u>compaction filters</u></a>. Compaction filters allow defining custom logic executed in background RocksDB threads responsible for compacting files on disk. Each key-value pair is processed with a filter on a regular basis, evicting least recently used data from the disk when available disk space drops below a certain threshold called a <i>soft limit</i>. To track keys accessed on disk, we have an LRU-like in-memory data structure, which is passed to the compaction filter to set last access date in key metadata and inform potential evictions.</p><p>However, with some specific workloads there is still a chance that evictions will not keep up with disk space growth, and for this scenario we have a <i>hard limit</i>: when available disk space drops below a critical threshold, we temporarily stop adding new keys to the cache. This hurts performance, but it acts as a safeguard, ensuring our proxies remain stable and don't overflow under a massive surge of requests.</p>
    <div>
      <h2>Consistency and asynchronous replication</h2>
      <a href="#consistency-and-asynchronous-replication">
        
      </a>
    </div>
    <p>Quicksilver has, from the start, provided <a href="https://jepsen.io/consistency/models/sequential"><u>sequential consistency</u></a> to clients: if key A was written before B, it’s not possible to read B and not A. We are committed to maintaining this guarantee in the new design. We have experienced <a href="https://www.hyrumslaw.com/"><u>Hyrum's Law</u></a> first hand, with Quicksilver being so widely adopted across the company that every property we introduced in earlier versions is now relied upon by other teams. This means that changing behaviour would inevitably break existing functionality and introduce bugs.</p><p>However, there is one thing standing in our way: asynchronous replication. Quicksilver replication is asynchronous mainly because machines in different parts of the world replicate at different speeds, and we don’t want a single server to slow down the entire tree. But it turns out in a proxy-replica design, independent replication progress can result in non-<a href="https://jepsen.io/consistency/models/monotonic-reads"><u>monotonic</u></a> reads!</p><p>Consider the following scenario: a client sequentially writes keys A, B, C, .. K one after another to the Quicksilver root node. These keys are asynchronously replicated through data centers across our network with varying latency. Imagine we have a proxy on index 5, which has observed keys from A to E, and two replicas: </p><ul><li><p>replica_1 is at index 2 (slightly behind the proxy), having only received A and B</p></li><li><p>replica_2 at index 9, which is slightly ahead due to a faster replication path and has received all keys from A to I
</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3miaQrw1UCUypXhra7mada/e713ebf0299e4b6f0a15bb25bbfa451a/image6.jpg" />
          </figure></li></ul><p><sup>Figure 3. Asynchronous replication in QSv1.5</sup></p><p>Now, a client performs two successive requests on a proxy, each time reading the keys E, F, G, H and I. For simplicity, we assume these keys are not cacheable (for example, due to low disk space). The proxy’s first remote request is routed to replica_2, which already has all keys and responds back with values. To prevent hot spots in a data center, we load balance requests from proxies, and the next one lands on replica_1, which hasn’t received any of the requested keys yet, and responds with a “not found” error.</p><p>So, which result is correct?</p><p>The correct behavior here is that of Quicksilver v1, which we aim to preserve. If the server on replication index 5 were a replica instead of a proxy, it would have seen updates for keys A through E inclusive, resulting in E being the only key in both replies, while all other keys cannot be found yet. Which means <i>responses from both replica_1 and replica_2 are wrong!</i></p><p>Therefore, to maintain previous guarantees and API backwards compatibility, Quicksilver v1.5 must address two crucial consistency problems: cases where the replica is ahead of the proxy, and conversely, where it lags behind. For now let’s focus on the case where a proxy lags behind a replica.</p>
    <div>
      <h2>Multiversion concurrency control</h2>
      <a href="#multiversion-concurrency-control">
        
      </a>
    </div>
    <p>In our example, replica_2 responds to a request from a proxy “from the past”. We cannot use any locks for synchronizing two servers, as it would introduce undesirable delays to the replication tree, defeating the purpose of asynchronous replication. The only option is for replicas to maintain a history of recent updates. This naturally leads us to implementing <b>multiversion concurrency control </b>(<a href="https://en.wikipedia.org/wiki/Multiversion_concurrency_control"><u>MVCC</u></a>), a popular database mechanism for tracking changes in a non-blocking fashion, where for any key we can keep multiple versions of its values for different points in time.</p><p>With MVCC, we no longer overwrite the latest value of a key in the default <a href="https://github.com/facebook/rocksdb/wiki/column-families"><u>column family</u></a> for every update. Instead, we introduced a new MVCC column family in RocksDB, where all updates are stored with a corresponding replication index. Lookup for a key at some index in the past goes as follows:</p><ol><li><p>First we search in the default column family. If a key is found and the write timestamp is not greater than the index of a requesting proxy, we can use it straight away.</p></li><li><p>Otherwise, we begin scanning the MVCC column family, where keys have unique suffixes based on latest timestamps for which they are still valid.</p></li></ol><p>In the example above, replica_2 has MVCC enabled and has keys A@1 .. K@11 in its default column family. The MVCC is initially empty, because no keys have been overwritten yet. When it receives a request for, say, key H with target index 5, it first makes a lookup in a default column family and finds the given key, but its timestamp is 8, which means this version should not be visible to the proxy yet. It then scans the MVCC, finds no matching previous versions and responds with “not found” to the proxy. Should key H be updated twice at indexes 4 and 8, we would have placed the initial version into MVCC before overwriting it in the default column family, and the proxy would receive the first version in response.</p><p>If a key E is requested at index 5, replica_2 can find it quickly in the default column family and return it back to the proxy. There is no need to read from MVCC, as the timestamp of the latest version (5) satisfies the request.</p><p>Another corner case to consider is deletions. When a key is deleted and then re-written, we need to explicitly mark the period of removal in MVCC. For that we’ve implemented <a href="https://en.wikipedia.org/wiki/Tombstone_(data_store)"><u>tombstones</u></a> – a special value format for absent keys.</p><p>Finally, we need to make sure that key history is not growing uncontrollably, using up all of the disk space available. Luckily we don’t actually need to record history for a long period of time, it just needs to cover the maximum replication index difference between any two machines. And in practice, a two-hour interval turned out to be way more than enough, while adding only about 500 MB of extra disk space usage. All records in the MVCC column family older than two hours are garbage collected, and for that again we use custom RocksDB compaction filters.</p>
    <div>
      <h2>Sliding window</h2>
      <a href="#sliding-window">
        
      </a>
    </div>
    <p>Now we know how to deal with proxies lagging behind replicas. But what about the opposite case, when a proxy is ahead of replicas?</p><p>The simplest solution is for replicas to not serve requests with a target index higher than its own. After all, it cannot know about keys from the future, whether they will be added, updated, or removed. In fact, our first implementation just returned an error when the proxy was ahead, as we expected it to happen quite infrequently. But after rolling out gradually to a few data centers, our metrics made it clear that the approach was not going to work.</p><p>This led us to analyze which keys are affected by this kind of replication asymmetry. It’s definitely not keys added or updated a long time ago, because replicas would already have the changes replicated. The only problematic keys are those updated very recently, which the proxy already knows about, but the replica does not.</p><p>With this insight, we realized that the issue should be solved on the proxies rather than on the replica side. By preserving <i>all</i> recent updates locally, the proxy can avoid querying the replica. This became known as the <b>sliding window</b> approach.</p><p>The sliding window retains all recent updates written in a short, rolling timeframe. Unlike cached keys, items in the window cannot be evicted until they move outside of the window. Internally, the sliding window is defined by lower and upper boundary pointers. These are kept in memory, and can easily be restored after a reload from the current database index and the pre-configured window size.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3kj7NdphFy8BQoJmsRH8fH/37ca06ac1c1800e4e97073815e754e51/image4.jpg" />
          </figure><p><sup>Figure 4. The sliding window shifts when replication updates arrive</sup></p><p>When a new update event arrives from the replication layer we add it to the sliding window by moving both the upper and lower boundary one position higher. Thereby, we maintain the fixed size of the window. Keys written before the lower bound can be evicted by the compaction filter, which is aware of current sliding window boundaries.</p>
    <div>
      <h2>Negative lookups</h2>
      <a href="#negative-lookups">
        
      </a>
    </div>
    <p>Another problem arising with our distributed replica-proxy design is negative lookups – requests for keys which don't exist in the database. Interestingly, in our workloads we see about ten times more negative lookups than positive ones!</p><p>But why is it a problem? Unfortunately, each negative lookup will be a cache miss on a proxy, requiring a request to a replica. Given the volume of requests and proportion of such lookups, it would be a disaster for performance, with overloaded replicas, overused data center networks, and massive latency degradation. We needed a fast and efficient approach to identifying non-existing keys directly at the proxy level.</p><p>In v1, negative lookups are the quickest type of requests. We rely on a special probabilistic data structure – <a href="https://en.wikipedia.org/wiki/Bloom_filter"><u>Bloom filters</u></a> – used in RocksDB to determine if the requested key might belong to a certain data file containing a range of sorted keys (called Sorted Sequence Table or SST) or definitely not. 99% of the time, negative lookups are served using only this in-memory data structure, avoiding the need for disk I/O.</p><p>One approach we considered for proxies was to cache negative lookups. Two problems immediately arise:</p><ul><li><p>How big is the keyspace of negative lookups? In theory, it’s infinite, but the real size was unclear. We can store it in our cache only if it is small enough.</p></li><li><p>Cached negative lookups would no longer be served by the fast Bloom filters. We have row and block caches in RocksDB, but the hit rate is nowhere near the filters for SSTs, which means negative lookups would end up going to disk more often.</p></li></ul><p>These turned out to be dealbreakers: not only was the negative keyspace vast, greatly exceeding the actual keyspace (by a thousand times for some instances!), but clients also need lookups to be <i>really</i> fast, ideally served from memory.</p><p>In pursuit of probabilistic data structures which could give us a dynamic compact representation of a full keyspace on proxies, we spent some time exploring <a href="https://en.wikipedia.org/wiki/Cuckoo_filter"><u>Cuckoo filters</u></a>. Unfortunately, with 5 billion keys it takes about 18 GB to have a false positive rate similar to Bloom filters (which only require 6 GB). And this is not only about wasted disk space — to be fast we have to keep it all in memory too!</p><p>Clearly some other solution was needed.</p><p>Finally, we decided to implement <b>key and value separation</b>, storing all keys on proxies, but persisting values only for cached keys. Evicting a key from the cache actually results in the removal of its value.</p><p>But wait, don’t the keys, even stripped of values, take a lot of space? Well, yes and no.</p><p>The total size of pure keys in Quicksilver is approximately 11 times smaller than the full dataset. Of course, it’s larger than any representation by probabilistic data structure, but there are some very desirable properties to such a solution. Firstly, we continue to enjoy fast Bloom filter lookups in RocksDB. Another benefit is that it unlocks some cool optimizations for range queries in a distributed context.</p><p>We may revisit it one day, but so far it has worked great for us.</p>
    <div>
      <h2>Discovery mechanism</h2>
      <a href="#discovery-mechanism">
        
      </a>
    </div>
    <p>Having solved all of the above challenges, one bit remained to be sorted out to make distributed query execution work: how can proxies discover replicas?</p><p>Within the local data center it is fairly easy. Each one runs its own <a href="https://www.consul.io/"><u>consul</u></a> cluster, where machines are registered as services. Consul is well integrated with our internal DNS resolvers, and with a single DNS request, we can get the names of all replicas running in a data center, which proxies can directly connect to.</p><p>However, data centers vary in size, servers are constantly added and removed, and having only local discovery would not be enough for the system to work reliably. Proxies also need to find replicas in other nearby data centers.</p><p>We had previously encountered a similar problem with our replication layer. Initially, the replication topology was statically defined in a configuration and distributed to all servers, such that they know from which sources they should replicate. While simple, this approach was quite fragile and tedious to operate. It led to a rigid replication tree with suboptimal overall performance, unable to adapt to network changes.</p><p>Our solution to this problem was the <b>Network Oracle</b> – a special overlay network based on a <a href="https://en.wikipedia.org/wiki/Gossip_protocol"><u>gossip</u></a> protocol and consisting of intermediate nodes in our data centers. Each member of this overlay constantly exchanges status and metainformation with other nodes, which helps us see active members in near-real time. Each member runs network probes measuring round-trip time to its peers, making it easy to find closest (in terms of <a href="https://www.cloudflare.com/learning/cdn/glossary/round-trip-time-rtt/"><u>RTT</u></a>) active intermediate nodes to form a low-latency replication tree. Introducing the Network Oracle was a major improvement: we no longer needed to reconfigure the topology, watch intermediate nodes or entire data centers go down, or investigate frequent replication issues. Replication is now a completely self-organized and self-healing dynamic system.</p><p>Naturally, we decided to reuse the Network Oracle for our discovery mechanism. It consists of two subproblems: data center discovery and specific service lookup. We use the Network Oracle to find the closest data centers. Adding all machines running Quicksilver to the same overlay would be inefficient because of significant increase of network traffic and message delivery times. Instead, we use intermediate nodes as sources of <b>network proximity</b> information for the leaf nodes. Knowing which data centers are nearby, we can directly send DNS queries there to resolve specific services – Quicksilver replicas in this case.</p><p>Proxies maintain a pool of connections to active replicas and distribute requests among them to smooth out the load and avoid hotspots in a data center. Proxies also have a health-tracking mechanism, monitoring the state of connections and errors coming from replicas, and temporarily deprioritizing or isolating potentially faulty ones.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/70olkvpfiCY3aDNn1owv1j/a188c0b9253c0e23f0341463a31b5f8e/image3.png" />
          </figure><p><sup>Figure 5. Internal replica request errors</sup></p><p>To demonstrate its efficiency, we graphed errors coming from replica requests, which showed that such errors almost disappeared after introducing the new discovery system.</p>
    <div>
      <h2>Results</h2>
      <a href="#results">
        
      </a>
    </div>
    <p>Our objective with Quicksilver v1.5 was simple: gain some disk space without losing request latency, because clients rely heavily on us being fast. While the replica-proxy design delivered significant space savings, what about latencies?</p><p>Proxy</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/jyrzamGb5K05r7EOSJbrq/05a30c951ad2c9c91edd72f898bd09f1/image7.png" />
          </figure><p>Replica</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4ry4erxQ6vf7WF6wSfPpit/0de9d46bbdb4a69f2d210ec6c8baadcd/image2.png" />
          </figure><p><sup>Figure 6. Proxy-replica latency comparison</sup></p><p>Above, we have the 99.9% percentile of request latency on both a replica and proxy during a 24-hour window. One can hardly find a difference between the two. Surprisingly, proxies can even be slightly faster than replicas sometimes, likely because of smaller datasets on disk!</p><p>Quicksilver v1.5 is released but our journey to a highly scalable and efficient solution is not over. In the next post we’ll share what challenges we faced with the following iteration. Stay tuned!</p>
    <div>
      <h2>Thank you</h2>
      <a href="#thank-you">
        
      </a>
    </div>
    <p>This project was a big team effort, so we’d like to thank everyone on the Quicksilver team – it would not have come true without you all.</p><ul><li><p>Aleksandr Matveev</p></li><li><p>Aleksei Surikov</p></li><li><p>Alex Dzyoba</p></li><li><p>Alexandra (Modi) Stana-Palade</p></li><li><p>Francois Stiennon</p></li><li><p>Geoffrey Plouviez</p></li><li><p>Ilya Polyakovskiy</p></li><li><p>Manzur Mukhitdinov</p></li><li><p>Volodymyr Dorokhov</p></li></ul><p></p> ]]></content:encoded>
            <category><![CDATA[Quicksilver]]></category>
            <category><![CDATA[Cache]]></category>
            <category><![CDATA[RocksDB]]></category>
            <category><![CDATA[Storage]]></category>
            <category><![CDATA[Replication]]></category>
            <category><![CDATA[Distributed]]></category>
            <guid isPermaLink="false">4gVdi0wvV700DVpfnnBo7j</guid>
            <dc:creator>Anton Dort-Golts</dc:creator>
            <dc:creator>Marten van de Sanden</dc:creator>
        </item>
    </channel>
</rss>