
<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>Sat, 04 Apr 2026 09:32:40 GMT</lastBuildDate>
        <item>
            <title><![CDATA[Eliminating Cold Starts 2: shard and conquer]]></title>
            <link>https://blog.cloudflare.com/eliminating-cold-starts-2-shard-and-conquer/</link>
            <pubDate>Fri, 26 Sep 2025 13:00:00 GMT</pubDate>
            <description><![CDATA[ We reduced Cloudflare Workers cold starts by 10x by optimistically routing to servers with already-loaded Workers. Learn how we did it here. ]]></description>
            <content:encoded><![CDATA[ <p>Five years ago, we announced that we were <a href="https://blog.cloudflare.com/eliminating-cold-starts-with-cloudflare-workers/"><u>Eliminating Cold Starts with Cloudflare Workers</u></a>. In that episode, we introduced a technique to pre-warm Workers during the <a href="https://www.cloudflare.com/learning/ssl/what-happens-in-a-tls-handshake/"><u>TLS handshake</u></a> of their first request. That technique takes advantage of the fact that the <a href="https://www.cloudflare.com/learning/ssl/what-is-sni/"><u>TLS Server Name Indication (SNI)</u></a> is sent in the very first message of the TLS handshake. Armed with that SNI, we often have enough information to pre-warm the request’s target Worker.</p><p>Eliminating cold starts by pre-warming <a href="https://www.cloudflare.com/developer-platform/products/workers/"><u>Workers</u></a> during TLS handshakes was a huge step forward for us, but “eliminate” is a strong word. Back then, Workers were still relatively small, and had cold starts constrained by limits explained later in this post. We’ve relaxed those limits, and users routinely deploy complex applications on Workers, often replacing origin servers. Simultaneously, TLS handshakes haven’t gotten any slower. In fact, <a href="https://www.cloudflare.com/learning/ssl/why-use-tls-1.3/"><u>TLS 1.3</u></a> only requires a single round trip for a handshake – compared to three round trips for TLS 1.2 – and is more widely used than it was in 2021.</p><p>Earlier this month, we finished deploying a new technique intended to keep pushing the boundary on cold start reduction. The new technique (or old, depending on your perspective) uses a consistent hash ring to take advantage of our global <a href="https://www.cloudflare.com/network"><u>network</u></a>. We call this mechanism “Worker sharding”.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3wfKoSIUzu20UtrJ3thfLh/c5821aa90f72a344962b83dbbfbb4508/image12.png" />
          </figure>
    <div>
      <h3>What’s in a cold start?</h3>
      <a href="#whats-in-a-cold-start">
        
      </a>
    </div>
    <p>A Worker is the basic unit of compute in our <a href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"><u>serverless computing</u></a> platform. It has a simple lifecycle. We instantiate it from source code (typically JavaScript), make it serve a bunch of requests (often HTTP, but not always), and eventually shut it down some time after it stops receiving traffic, to re-use its resources for other Workers. We call that shutdown process “eviction”.</p><p>The most expensive part of the Worker’s lifecycle is the initial instantiation and first request invocation. We call this part a “cold start”. Cold starts have several phases: fetching the script source code, compiling the source code, performing a top-level execution of the resulting JavaScript module, and finally, performing the initial invocation to serve the incoming HTTP request that triggered the whole sequence of events in the first place.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4dsRI0IS9GmJFRzCWeaQDw/db362a6962e20976565d6ed0fc11cdf2/image11.png" />
          </figure>
    <div>
      <h3>Cold starts have become longer than TLS handshakes</h3>
      <a href="#cold-starts-have-become-longer-than-tls-handshakes">
        
      </a>
    </div>
    <p>Fundamentally, our TLS handshake technique depends on the handshake lasting longer than the cold start. This is because the duration of the TLS handshake is time that the visitor must spend waiting, regardless, so it’s beneficial to everyone if we do as much work during that time as possible. If we can run the Worker’s cold start in the background while the handshake is still taking place, and if that cold start finishes <i>before</i> the handshake, then the request will ultimately see zero cold start delay. If, on the other hand, the cold start takes <i>longer</i> than the TLS handshake, then the request will see some part of the cold start delay – though the technique still helps reduce that visible delay.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/54wEBpC1hCRXN97zMiAXu1/f1434aac21b40093bc12d8931c7bc1ec/image7.png" />
          </figure><p>In the early days, TLS handshakes lasting longer than Worker cold starts was a safe bet, and cold starts typically won the race. One of our <a href="https://blog.cloudflare.com/cloud-computing-without-containers/#cold-starts"><u>early blog posts explaining how our platform works</u></a> mentions 5 millisecond cold start times – and that was correct, at the time!</p><p>For every limit we have, our users have challenged us to relax them. Cold start times are no different. </p><p>There are two crucial limits which affect cold start time: Worker script size and the startup CPU time limit. While we didn’t make big announcements at the time, we have quietly raised both of those limits since our last <i>Eliminating Cold Starts</i> blog post:</p><ul><li><p>Worker script size (compressed) increased from <a href="https://github.com/cloudflare/cloudflare-docs/pull/6613"><u>1 MB to 5 MB</u></a>, then again from <a href="https://github.com/cloudflare/cloudflare-docs/pull/9083"><u>5 MB to 10 MB</u></a>, for paying users.</p></li><li><p>Worker script size (compressed) increased from <a href="https://github.com/cloudflare/cloudflare-docs/pull/18400"><u>1 MB to 3 MB</u></a> for free users.</p></li><li><p>Startup CPU time increased from <a href="https://github.com/cloudflare/cloudflare-docs/pull/9154"><u>200ms to 400ms</u></a>.</p></li></ul><p>We relaxed these limits because our users wanted to deploy increasingly complex applications to our platform. And deploy they did! But the increases have a cost:</p><ul><li><p>Increasing script size increases the amount of data we must transfer from script storage to the Workers runtime.</p></li><li><p>Increasing script size also increases the time complexity of the script compilation phase.</p></li><li><p>Increasing the startup CPU time limit increases the maximum top-level execution time.</p></li></ul><p>Taken together, cold starts for complex applications began to lose the TLS handshake race.</p>
    <div>
      <h3>Routing requests to an existing Worker</h3>
      <a href="#routing-requests-to-an-existing-worker">
        
      </a>
    </div>
    <p>With relaxed script size and startup time limits, optimizing cold start time directly was a losing battle. Instead, we needed to figure out how to reduce the absolute <i>number</i> of cold starts, so that requests are simply less likely to incur one.</p><p>One option is to route requests to existing Worker instances, where before we might have chosen to start a new instance.</p><p>Previously, we weren’t particularly good at routing requests to existing Worker instances. We could trivially coalesce requests to a single Worker instance if they happened to land on a machine which already hosted a Worker, because in that case it’s not a distributed systems problem. But what if a Worker already existed in our data center on a different server, and some other server received a request for the Worker? We would always choose to cold start a new Worker on the machine which received the request, rather than forward the request to the machine with the already-existing Worker, even though forwarding the request would avoid the cold start.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5Yf9jgJcUGIppzhobkbG3j/4113c3a1910961b014f4f4ff9a1866be/image8.png" />
          </figure><p>To drive the point home: Imagine a visitor sends one request per minute to a data center with 300 servers, and that the traffic is load balanced evenly across all servers. On average, each server will receive one request every five hours. In particularly busy data centers, this span of time could be long enough that we need to evict the Worker to re-use its resources, resulting in a 100% cold start rate. That’s a terrible experience for the visitor.</p><p>Consequently, we found ourselves explaining to users, who saw high latency while prototyping their applications, that their latency would counterintuitively <i>decrease</i> once they put sufficient traffic on our network. This highlighted the inefficiency in our original, simple design.</p><p>If, instead, those requests were all coalesced onto one single server, we would notice multiple benefits. The Worker would receive one request per minute, which is short enough to virtually guarantee that it won’t be evicted. This would mean the visitor may experience a single cold start, and then have a 100% “warm request rate.” We would also use 99.7% (299 / 300) less memory serving this traffic. This makes room for other Workers, decreasing their eviction rate, and increasing <i>their</i> warm request rates, too – a virtuous cycle!</p><p>There’s a cost to coalescing requests to a single instance, though, right? After all, we’re adding latency to requests if we have to proxy them around the data center to a different server.</p><p>In practice, the added time-to-first-byte is less than one millisecond, and is the subject of continual optimization by our IPC and performance teams. One millisecond is far less than a typical cold start, meaning it’s always better, in every measurable way, to proxy a request to a warm Worker than it is to cold start a new one.</p>
    <div>
      <h3>The consistent hash ring</h3>
      <a href="#the-consistent-hash-ring">
        
      </a>
    </div>
    <p>A solution to this very problem lies at the heart of many of our products, including one of our oldest: the HTTP cache in our <a href="https://www.cloudflare.com/application-services/products/cdn/"><u>Content Delivery Network</u></a>.</p><p>When a visitor requests a cacheable web asset through Cloudflare, the request gets routed through a pipeline of proxies. One of those proxies is a caching proxy, which stores the asset for later, so we can serve it to future requests without having to request it from the origin again.</p><p>A Worker cold start is analogous to an HTTP cache miss, in that a request to a warm Worker is like an HTTP cache hit.</p><p>When our standard HTTP proxy pipeline routes requests to the caching layer, it chooses a cache server based on the request's cache key to optimize the HTTP cache hit rate. <a href="https://developers.cloudflare.com/cache/how-to/cache-keys/"><u>The cache key is the request’s URL, plus some other details</u></a>. This technique is often called “sharding”. The servers are considered to be individual shards of a larger, logical system – in this case a data center’s HTTP cache. So, we can say things like, “Each data center contains one logical HTTP cache, and that cache is sharded across every server in the data center.”</p><p>Until recently, we could not make the same claim about the set of Workers in a data center. Instead, each server contained its own standalone set of Workers, and they could easily duplicate effort.</p><p>We borrow the cache’s trick to solve that. In fact, we even use the same type of data structure used by our HTTP cache to choose servers: a <a href="https://en.wikipedia.org/wiki/Consistent_hashing"><u>consistent hash ring</u></a>. A naive sharding implementation might use a classic hash table mapping Worker script IDs to server addresses. That would work fine for a set of servers which never changes. But servers are actually ephemeral and have their own lifecycle. They can crash, get rebooted, taken out for maintenance, or decommissioned. New ones can come online. When these events occur, the size of the hash table would change, necessitating a re-hashing of the whole table. Every Worker’s home server would change, and all sharded Workers would be cold started again!</p><p>A consistent hash ring improves this scenario significantly. Instead of establishing a direct correspondence between script IDs and server addresses, we map them both to a number line whose end wraps around to its beginning, also known as a ring. To look up the home server of a Worker, first we hash its script, and then we find where it lies on the ring. Next, we take the server address which comes directly on or after that position on the ring, and consider that the Worker’s home.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3UJuyYc8D1CYDrRKgBmz7e/ce1da2614c2a5d712a3ab471a0ec7675/image2.png" />
          </figure><p>If a new server appears for some reason, all the Workers that lie before it on the ring get re-homed, but none of the other Workers are disturbed. Similarly, if a server disappears, all the Workers which lay before it on the ring get re-homed.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/61yVG9Cd7z9R7pCbDQKUXT/42a1d5f6f02b33b4c321dc3788a8d724/image6.png" />
          </figure><p>We refer to the Worker’s home server as the “shard server”. In request flows involving sharding, there is also a “shard client”. It’s also a server! The shard client initially receives a request, and, using its consistent hash ring, looks up which shard server it should send the request to. I’ll be using these two terms – shard client and shard server – in the rest of this post.</p>
    <div>
      <h3>Handling overload</h3>
      <a href="#handling-overload">
        
      </a>
    </div>
    <p>The nature of HTTP assets lend themselves well to sharding. If they are cacheable, they are static, at least for their cache <a href="https://www.cloudflare.com/learning/cdn/glossary/time-to-live-ttl/"><u>Time to Live (TTL)</u></a> duration. So, serving them requires time and space complexity which scales linearly with their size.</p><p>But Workers aren’t JPEGs. They are live units of compute which can use up to five minutes of CPU time per request. Their time and space complexity do not necessarily scale with their input size, and can vastly outstrip the amount of computing power we must dedicate to serving even a huge file from cache.</p><p>This means that individual Workers can easily get overloaded when given sufficient traffic. So, no matter what we do, we need to keep in mind that we must be able to scale back up to infinity. We will never be able to guarantee that a data center has only one instance of a Worker, and we must always be able to horizontally scale at the drop of a hat to support burst traffic. Ideally this is all done without producing any errors.</p><p>This means that a shard server must have the ability to refuse requests to invoke Workers on it, and shard clients must always gracefully handle this scenario.</p>
    <div>
      <h3>Two load shedding options</h3>
      <a href="#two-load-shedding-options">
        
      </a>
    </div>
    <p>I am aware of two general solutions to shedding load gracefully, without serving errors.</p><p>In the first solution, the client asks politely if it may issue the request. It then sends the request if it  receives a positive response. If it instead receives a “go away” response, it handles the request differently, like serving it locally. In HTTP, this pattern can be found in <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/100"><u>Expect: 100-continue semantics</u></a>. The main downside is that this introduces one round-trip of latency to set the expectation of success before the request can be sent. (Note that a common naive solution is to just retry requests. This works for some kinds of requests, but is not a general solution, as requests may carry arbitrarily large bodies.)</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4e7OUD3T8kT5ct6OiETfwb/595c92d02312956921d379ceb4268139/image4.png" />
          </figure><p>The second general solution is to send the request without confirming that it can be handled by the server, then count on the server to forward the request elsewhere if it needs to. This could even be back to the client. This avoids the round-trip of latency that the first solution incurs, but there is a tradeoff: It puts the shard server in the request path, pumping bytes back to the client. Fortunately, we have a trick to minimize the amount of bytes we actually have to send back in this fashion, which I’ll describe in the next section.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1Cb7zWYrMfmQnMBooe8g2o/2a3d2b906b85cf8eff7666630ed78b9e/image9.png" />
          </figure>
    <div>
      <h3>Optimistically sending sharded requests</h3>
      <a href="#optimistically-sending-sharded-requests">
        
      </a>
    </div>
    <p>There are a couple of reasons why we chose to optimistically send sharded requests without waiting for permission.</p><p>The first reason of note is that we expect to see very few of these refused requests in practice. The reason is simple: If a shard client receives a refusal for a Worker, then it must cold start the Worker locally. As a consequence, it can serve all future requests locally without incurring another cold start. So, after a single refusal, the shard client won’t shard that Worker any more (until traffic for the Worker tapers off enough for an eviction, at least).</p><p>Generally, this means we expect that if a request gets sharded to a different server, the shard server will most likely accept the request for invocation. Since we expect success, it makes a lot more sense to optimistically send the entire request to the shard server than it does to incur a round-trip penalty to establish permission first.</p><p>The second reason is that we have a trick to avoid paying too high a cost for proxying the request back to the client, as I mentioned above.</p><p>We implement our cross-instance communication in the Workers runtime using <a href="https://capnproto.org/"><u>Cap’n Proto RPC</u></a>, whose distributed object model enables some incredible features, like <a href="https://blog.cloudflare.com/javascript-native-rpc/"><u>JavaScript-native RPC</u></a>. It is also the elder, spiritual sibling to the just-released <a href="https://blog.cloudflare.com/capnweb-javascript-rpc-library/"><u>Cap’n Web</u></a>.</p><p>In the case of sharding, Cap’n Proto makes it very easy to implement an optimal request refusal mechanism. When the shard client assembles the sharded request, it includes a handle (<a href="https://capnproto.org/rpc.html#distributed-objects"><u>called a </u><i><u>capability</u></i></a> in Cap’n Proto) to a lazily-loaded local instance of the Worker. This lazily-loaded instance has the <a href="https://github.com/cloudflare/workerd/blob/f93fd2625f8d4131d9d50762c09deddb01bb4c70/src/workerd/io/worker-interface.capnp#L582"><u>same exact interface as any other Worker exposed over RPC</u></a>. The difference is just that it’s lazy – it doesn’t get cold started until invoked. In the event the shard server decides it must refuse the request, it does not return a “go away” response, but instead returns the shard client’s own lazy capability!</p><p>The shard client’s application code only sees that it received a capability from the shard server. It doesn’t know where that capability is actually implemented. But the shard client’s <i>RPC system</i> does know where the capability lives! Specifically, it recognizes that the returned capability is actually a local capability – the same one that it passed to the shard server. Once it realizes this, it also realizes that any request bytes it continues to send to the shard server will just come looping back. So, it stops sending more request bytes, waits to receive back from the shard server all the bytes it already sent, and shortens the request path as soon as possible. This takes the shard server entirely out of the loop, preventing a “trombone effect.”</p>
    <div>
      <h3>Workers invoking Workers</h3>
      <a href="#workers-invoking-workers">
        
      </a>
    </div>
    <p>With load shedding behavior figured out, we thought the hard part was over.</p><p>But, of course, Workers may invoke other Workers. There are many ways this could occur, most obviously via <a href="https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/"><u>Service Bindings</u></a>. Less obviously, many of our favorite features, such as <a href="https://developers.cloudflare.com/kv/"><u>Workers KV</u></a>, are actually cross-Worker invocations. But there is one product, in particular, that stands out for its powerful ability to invoke other Workers: <a href="https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/"><u>Workers for Platforms</u></a>.</p><p>Workers for Platforms allows you to run your own functions-as-a-service on Cloudflare infrastructure. To use the product, you deploy three special types of Workers:</p><ul><li><p>a dynamic dispatch Worker</p></li><li><p>any number of user Workers</p></li><li><p>an optional, parameterized <a href="https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/"><u>outbound Worker</u></a></p></li></ul><p>A typical request flow for Workers for Platforms goes like so: First, we invoke the dynamic dispatch Worker. The dynamic dispatch Worker chooses and invokes a user Worker. Then, the user Worker invokes the outbound Worker to intercept its subrequests. The dynamic dispatch Worker chose the outbound Worker's arguments prior to invoking the user Worker.</p><p>To really amp up the fun, the dynamic dispatch Worker could have a <a href="https://developers.cloudflare.com/workers/observability/logs/tail-workers/"><u>tail Worker</u></a> attached to it. This tail Worker would need to be invoked with traces related to all the preceding invocations. Importantly, it should be invoked one single time with all events related to the request flow, not invoked multiple times for different fragments of the request flow.</p><p>You might further ask, can you nest Workers for Platforms? I don’t know the official answer, but I can tell you that the code paths do exist, and they do get exercised.</p><p>To support this nesting doll of Workers, we keep a context stack during invocations. This context includes things like ownership overrides, resource limit overrides, trust levels, tail Worker configurations, outbound Worker configurations, feature flags, and so on. This context stack was manageable-ish when everything was executed on a single thread. For sharding to be truly useful, though, we needed to be able to move this context stack around to other machines.</p><p>Our choice of Cap’n Proto RPC as our primary communications medium helped us make sense of it all. To shard Workers deep within a stack of invocations, we serialize the context stack into a Cap’n Proto data structure and send it to the shard server. The shard server deserializes it into native objects, and continues the execution where things left off.</p><p>As with load shedding, Cap’n Proto’s distributed object model provides us simple answers to otherwise difficult questions. Take the tail Worker question – how do we coalesce tracing data from invocations which got fanned out across any number of other servers back to one single place? Easy: create a capability (a live Cap’n Proto object) for a reportTraces() callback on the dynamic dispatch Worker’s home server, and put that in the serialized context stack. Now, that context stack can be passed around at will. That context stack will end up in multiple places: At a minimum, it will end up on the user Worker’s shard server and the outbound Worker’s shard server. It may also find its way to other shard servers if any of those Workers invoked service bindings! Each of those shard servers can call the reportTraces() callback, and be confident that the data will make its way back to the right place: the dynamic dispatch Worker’s home server. None of those shard servers need to actually know <i>where</i> that home server is. Phew!</p>
    <div>
      <h3>Eviction rates down, warm request rates up</h3>
      <a href="#eviction-rates-down-warm-request-rates-up">
        
      </a>
    </div>
    <p>Features like this are always satisfying to roll out, because they produce graphs showing huge efficiency gains.</p><p>Once fully rolled out, only about 4% of total requests from enterprise traffic ended up being sharded. To put that another way, 96% of all enterprise requests are to Workers which are sufficiently loaded that we <i>must</i> run multiple instances of them in a data center.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3BsNColqVqUYNpfUv1wz8W/d80edb0c93cf741eab3603a9b2ef57e8/image5.png" />
          </figure><p>Despite that low total rate of sharding, we reduced our global Worker eviction rate by 10x. </p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1w3sQH8iQRP6oQFCCTm9BQ/815c49d51c473aafebfb001f58762797/image10.png" />
          </figure><p>Our eviction rate is a measure of memory pressure within our system. You can think of it like garbage collection at a macro level, and it has the same implications. Fewer evictions means our system uses memory more efficiently. This has the happy consequence of using less CPU to clean up our memory. More relevant to Workers users, the increased efficiency means we can keep Workers in memory for an order of magnitude longer, improving their warm request rate and reducing their latency.</p><p>The high leverage shown – sharding just 4% of our traffic to improve memory efficiency by 10x – is a consequence of the power-law distribution of Internet traffic.</p><p>A <a href="https://en.wikipedia.org/wiki/Power_law"><u>power law distribution</u></a> is a phenomenon which occurs across many fields of science, including linguistics, sociology, physics, and, of course, computer science. Events which follow power law distributions typically see a huge amount clustered in some small number of “buckets”, and the rest spread out across a large number of those “buckets”. Word frequency is a classic example: A small handful of words like “the”, “and”, and “it” occur in texts with extremely high frequency, while other words like “eviction” or “trombone” might occur only once or twice in a text.</p><p>In our case, the majority of Workers requests goes to a small handful of high-traffic Workers, while a very long tail goes to a huge number of low-traffic Workers. The 4% of requests which were sharded are all to low-traffic Workers, which are the ones that benefit the most from sharding.</p><p>So did we eliminate cold starts? Or will there be an <i>Eliminating Cold Starts 3</i> in our future?</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/419NdQonMOOAsirFmMl885/28b0266ee311ebffb342002e5e1cf54e/image3.png" />
          </figure><p>For enterprise traffic, our warm request rate increased from 99.9% to 99.99% – that’s three 9’s to four 9’s. Conversely, this means that the cold start rate went from 0.1% to 0.01% of requests, a 10x decrease. A moment’s thought, and you’ll realize that this is coherent with the eviction rate graph I shared above: A 10x decrease in the number of Workers we destroy over time must imply we’re creating 10x fewer to begin with.</p><p>Simultaneously, our warm request rate became less volatile throughout the course of the day.</p><p>Hmm.</p><p>I hate to admit this to you, but I still notice a little bit of space at the top of the graph. 😟</p><p><a href="https://www.cloudflare.com/careers/"><u>Can you help us get to five 9’s?</u></a></p> ]]></content:encoded>
            <category><![CDATA[Birthday Week]]></category>
            <category><![CDATA[Cap'n Proto]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Engineering]]></category>
            <category><![CDATA[TLS]]></category>
            <guid isPermaLink="false">1mLzSCaF2U02LD3DDd97Mn</guid>
            <dc:creator>Harris Hancock</dc:creator>
        </item>
        <item>
            <title><![CDATA[HTTP Analytics for 6M requests per second using ClickHouse]]></title>
            <link>https://blog.cloudflare.com/http-analytics-for-6m-requests-per-second-using-clickhouse/</link>
            <pubDate>Tue, 06 Mar 2018 13:00:00 GMT</pubDate>
            <description><![CDATA[ One of our large scale data infrastructure challenges here at Cloudflare is around providing HTTP traffic analytics to our customers. HTTP Analytics is available to all our customers via two options: ]]></description>
            <content:encoded><![CDATA[ <p>One of our large scale data infrastructure challenges here at Cloudflare is around providing HTTP traffic analytics to our customers. HTTP Analytics is available to all our customers via two options:</p><ul><li><p>Analytics tab in Cloudflare dashboard</p></li><li><p>Zone Analytics API with 2 endpoints</p><ul><li><p><a href="https://api.cloudflare.com/#zone-analytics-dashboard">Dashboard endpoint</a></p></li><li><p><a href="https://api.cloudflare.com/#zone-analytics-analytics-by-co-locations">Co-locations endpoint</a> (Enterprise plan only)</p></li></ul></li></ul><p>In this blog post I'm going to talk about the exciting evolution of the Cloudflare analytics pipeline over the last year. I'll start with a description of the old pipeline and the challenges that we experienced with it. Then, I'll describe how we leveraged ClickHouse to form the basis of a new and improved pipeline. In the process, I'll share details about how we went about schema design and performance tuning for ClickHouse. Finally, I'll look forward to what the Data team is thinking of providing in the future.</p><p>Let's start with the old data pipeline.</p>
    <div>
      <h3>Old data pipeline</h3>
      <a href="#old-data-pipeline">
        
      </a>
    </div>
    <p>The previous pipeline was built in 2014. It has been mentioned previously in <a href="https://blog.cloudflare.com/scaling-out-postgresql-for-cloudflare-analytics-using-citusdb/">Scaling out PostgreSQL for CloudFlare Analytics using CitusDB</a> and <a href="https://blog.cloudflare.com/more-data-more-data/">More data, more data</a> blog posts from the Data team.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5MY5UJgdkM35pwDy4mMOxt/2c08b73e37001788547db620f00a5a92/Old-system-architecture.jpg" />
          </figure><p>It had following components:</p><ul><li><p><b>Log forwarder </b>- collected Cap'n Proto formatted logs from the edge, notably DNS and Nginx logs, and shipped them to Kafka in Cloudflare central datacenter.</p></li><li><p><b>Kafka cluster </b>- consisted of 106 brokers with x3 replication factor, 106 partitions, ingested Cap'n Proto formatted logs at average rate 6M logs per second.</p></li><li><p><b>Kafka consumers</b> - each of 106 partitions had dedicated Go consumer (a.k.a. Zoneagg consumer), which read logs and produced aggregates per partition per zone per minute and then wrote them into Postgres.
<b>Postgres database</b> - single instance PostgreSQL database (a.k.a. RollupDB), accepted aggregates from Zoneagg consumers and wrote them into temporary tables per partition per minute. It then rolled-up the aggregates into further aggregates with aggregation cron. More specifically:</p><ul><li><p>Aggregates per partition, minute, zone → aggregates data per minute, zone</p></li><li><p>Aggregates per minute, zone → aggregates data per hour, zone</p></li><li><p>Aggregates per hour, zone → aggregates data per day, zone</p></li><li><p>Aggregates per day, zone → aggregates data per month, zone</p></li></ul></li><li><p><b>Citus Cluster</b> - consisted of Citus main and 11 Citus workers with x2 replication factor (a.k.a. Zoneagg Citus cluster), the storage behind Zone Analytics API and our BI internal tools. It had replication cron, which did remote copy of tables from Postgres instance into Citus worker shards.</p></li><li><p><b>Zone Analytics API</b> - served queries from internal PHP API. It consisted of 5 API instances written in Go and queried Citus cluster, and was not visible to external users.</p></li><li><p><b>PHP API </b>- 3 instances of proxying API, which forwarded public API queries to internal Zone Analytics API, and had some business logic on zone plans, error messages, etc.</p></li><li><p><b>Load Balancer </b>- nginx proxy, forwarded queries to PHP API/Zone Analytics API.</p></li></ul><p>Cloudflare has grown tremendously since this pipeline was originally designed in 2014. It started off processing under 1M requests per second and grew to current levels of 6M requests per second. The pipeline had served us and our customers well over the years, but began to split at the seams. Any system should be re-engineered after some time, when requirements change.</p><p>Some specific disadvantages of the original pipeline were:</p><ul><li><p><b>Postgres SPOF</b> - single PostgreSQL instance was a SPOF (Single Point of Failure), as it didn't have replicas or backups and if we were to lose this node, whole analytics pipeline could be paralyzed and produce no new aggregates for Zone Analytics API.</p></li><li><p><b>Citus main SPOF</b> - Citus main was the entrypoint to all Zone Analytics API queries and if it went down, all our customers' Analytics API queries would return errors.</p></li><li><p><b>Complex codebase</b> - thousands of lines of bash and SQL for aggregations, and thousands of lines of Go for API and Kafka consumers made the pipeline difficult to maintain and debug.</p></li><li><p><b>Many dependencies</b> - the pipeline consisted of many components, and failure in any individual component could result in halting the entire pipeline.</p></li><li><p><b>High maintenance cost</b> - due to its complex architecture and codebase, there were frequent incidents, which sometimes took engineers from the Data team and other teams many hours to mitigate.</p></li></ul><p>Over time, as our request volume grew, the challenges of operating this pipeline became more apparent, and we realized that this system was being pushed to its limits. This realization inspired us to think about which components would be ideal candidates for replacement, and led us to build new data pipeline.</p><p>Our first design of an improved analytics pipeline centred around the use of the <a href="https://flink.apache.org/">Apache Flink</a> stream processing system. We had previously used Flink for other data pipelines, so it was a natural choice for us. However, these pipelines had been at a much lower rate than the 6M requests per second we needed to process for HTTP Analytics, and we struggled to get Flink to scale to this volume - it just couldn't keep up with ingestion rate per partition on all 6M HTTP requests per second.</p><p>Our colleagues on our DNS team had already built and productionized DNS analytics pipeline atop ClickHouse. They wrote about it in <a href="https://blog.cloudflare.com/how-cloudflare-analyzes-1m-dns-queries-per-second/">"How Cloudflare analyzes 1M DNS queries per second"</a> blog post. So, we decided to take a deeper look at ClickHouse.</p>
    <div>
      <h3>ClickHouse</h3>
      <a href="#clickhouse">
        
      </a>
    </div>
    <blockquote><p>"ClickHouse не тормозит."
Translation from Russian: ClickHouse doesn't have brakes (or isn't slow)
© ClickHouse core developers</p></blockquote><p>When exploring additional candidates for replacing some of the key infrastructure of our old pipeline, we realized that using a column oriented database might be well suited to our analytics workloads. We wanted to identify a column oriented database that was horizontally scalable and fault tolerant to help us deliver good uptime guarantees, and extremely performant and space efficient such that it could handle our scale. We quickly realized that ClickHouse could satisfy these criteria, and then some.</p><p><a href="https://clickhouse.yandex/">ClickHouse</a> is an open source column-oriented database management system capable of real time generation of analytical data reports using SQL queries. It is blazing fast, linearly scalable, hardware efficient, fault tolerant, feature rich, highly reliable, simple and handy. ClickHouse core developers provide great help on solving issues, merging and maintaining our PRs into ClickHouse. For example, engineers from Cloudflare have contributed a whole bunch of code back upstream:</p><ul><li><p>Aggregate function <a href="https://clickhouse.com/docs/en/sql-reference/aggregate-functions/reference/topk">topK</a> by <a href="https://github.com/vavrusa">Marek Vavruša</a></p></li><li><p>IP prefix dictionary by Marek Vavruša</p></li><li><p>SummingMergeTree engine optimizations by Marek Vavruša</p></li><li><p><a href="https://clickhouse.com/docs/en/engines/table-engines/integrations/kafka">Kafka table Engine</a> by Marek Vavruša. We're thinking to replace Kafka Go consumers with this engine when it will be stable enough and ingest from Kafka into ClickHouse directly.</p></li><li><p>Aggregate function <a href="https://clickhouse.yandex/docs/en/single/index.html#summapkey-value">sumMap</a> by <a href="https://github.com/bocharov">Alex Bocharov</a>. Without this function it would be impossible to build our new Zone Analytics API.</p></li><li><p><a href="https://github.com/yandex/ClickHouse/pull/1636">Mark cache fix</a> by Alex Bocharov</p></li><li><p><a href="https://github.com/yandex/ClickHouse/pull/1844">uniqHLL12 function fix</a> for big cardinalities by Alex Bocharov. The description of the issue and its fix should be an interesting reading.</p></li></ul>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6mN4cq6YrpiHjBfbP6vpCh/7181a8e68b4a63cd48e42e0eaf807191/ClickHouse-uniq-functions.png" />
          </figure><p>Along with filing many bug reports, we also report about every issue we face in our cluster, which we hope will help to improve ClickHouse in future.</p><p>Even though DNS analytics on ClickHouse had been a great success, we were still skeptical that we would be able to scale ClickHouse to the needs of the HTTP pipeline:</p><ul><li><p>Kafka DNS topic has on average 1.5M messages per second vs 6M messages per second for HTTP requests topic.</p></li><li><p>Kafka DNS topic average uncompressed message size is 130B vs 1630B for HTTP requests topic.</p></li><li><p>DNS query ClickHouse record consists of 40 columns vs 104 columns for HTTP request ClickHouse record.</p></li></ul><p>After unsuccessful attempts with Flink, we were skeptical of ClickHouse being able to keep up with the high ingestion rate. Luckily, early prototype showed promising performance and we decided to proceed with old pipeline replacement. The first step in replacing the old pipeline was to design a schema for the new ClickHouse tables.</p>
    <div>
      <h3>ClickHouse schema design</h3>
      <a href="#clickhouse-schema-design">
        
      </a>
    </div>
    <p>Once we identified ClickHouse as a potential candidate, we began exploring how we could port our existing Postgres/Citus schemas to make them compatible with ClickHouse.</p><p>For our <a href="https://api.cloudflare.com/#zone-analytics-dashboard">Zone Analytics API</a> we need to produce many different aggregations for each zone (domain) and time period (minutely / hourly / daily / monthly). For deeper dive about specifics of aggregates please follow Zone Analytics API documentation or this handy <a href="https://docs.google.com/spreadsheets/d/1zQ3yI3HB2p8hiM-Jwvq1-MaeEyIouix2I-iUAPZtJYw/edit#gid=1788221216">spreadsheet</a>.</p><p>These aggregations should be available for any time range for the last 365 days. While ClickHouse is a really great tool to work with non-aggregated data, with our volume of 6M requests per second we just cannot afford yet to store non-aggregated data for that long.</p><p>To give you an idea of how much data is that, here is some "napkin-math" capacity planning. I'm going to use an average insertion rate of 6M requests per second and $100 as a cost estimate of 1 TiB to calculate storage cost for 1 year in different message formats:</p><table><tr><th><p><b>Metric</b></p></th><th><p><b>Cap'n Proto</b></p></th><th><p><b>Cap'n Proto (zstd)</b></p></th><th><p><b>ClickHouse</b></p></th></tr><tr><td><p>Avg message/record size</p></td><td><p>1630 B</p></td><td><p>360 B</p></td><td><p>36.74 B</p></td></tr><tr><td><p>Storage requirements for 1 year</p></td><td><p>273.93 PiB</p></td><td><p>60.5 PiB</p></td><td><p>18.52 PiB (RF x3)</p></td></tr><tr><td><p>Storage cost for 1 year</p></td><td><p>$28M</p></td><td><p>$6.2M</p></td><td><p>$1.9M</p></td></tr></table><p>And that is if we assume that requests per second will stay the same, but in fact it's growing fast all the time.</p><p>Even though storage requirements are quite scary, we're still considering to store raw (non-aggregated) requests logs in ClickHouse for 1 month+. See "Future of Data APIs" section below.</p>
    <div>
      <h4>Non-aggregated requests table</h4>
      <a href="#non-aggregated-requests-table">
        
      </a>
    </div>
    <p>We store over <a href="https://docs.google.com/spreadsheets/d/1zQ3yI3HB2p8hiM-Jwvq1-MaeEyIouix2I-iUAPZtJYw/edit?usp=sharing">100+ columns</a>, collecting lots of different kinds of metrics about each request passed through Cloudflare. Some of these columns are also available in our <a href="https://support.cloudflare.com/hc/en-us/articles/216672448-Enterprise-Log-Share-Logpull-REST-API">Enterprise Log Share</a> product, however ClickHouse non-aggregated requests table has more fields.</p><p>With so many columns to store and huge storage requirements we've decided to proceed with the aggregated-data approach, which worked well for us before in old pipeline and which will provide us with backward compatibility.</p>
    <div>
      <h4>Aggregations schema design #1</h4>
      <a href="#aggregations-schema-design-1">
        
      </a>
    </div>
    <p>According to the <a href="https://api.cloudflare.com/#zone-analytics-dashboard">API documentation</a>, we need to provide lots of different requests breakdowns and to satisfy these requirements we decided to test the following approach:</p><ol><li><p>Create Cickhouse <a href="https://clickhouse.com/docs/en/sql-reference/statements/create/view">materialized views</a> with <a href="https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/aggregatingmergetree">ReplicatedAggregatingMergeTree</a> engine pointing to non-aggregated requests table and containing minutely aggregates data for each of the breakdowns:</p><ul><li><p><b>Requests totals</b> - containing numbers like total requests, bytes, threats, uniques, etc.</p></li><li><p><b>Requests by colo</b> - containing requests, bytes, etc. breakdown by edgeColoId - each of 120+ Cloudflare datacenters</p></li><li><p><b>Requests by http status</b> - containing breakdown by HTTP status code, e.g. 200, 404, 500, etc.</p></li><li><p><b>Requests by content type</b> - containing breakdown by response content type, e.g. HTML, JS, CSS, etc.</p></li><li><p><b>Requests by country</b> - containing breakdown by client country (based on IP)</p></li><li><p><b>Requests by threat type</b> - containing breakdown by threat type</p></li><li><p><b>Requests by browser</b> - containing breakdown by browser family, extracted from user agent</p></li><li><p><b>Requests by ip class</b> - containing breakdown by client IP class</p></li></ul></li><li><p>Write the code gathering data from all 8 materialized views, using two approaches:</p><ul><li><p>Querying all 8 materialized views at once using JOIN</p></li><li><p>Querying each one of 8 materialized views separately in parallel</p></li></ul></li><li><p>Run performance testing benchmark against common Zone Analytics API queries</p></li></ol>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3iQvOQSik5xLDR7lkG0X9H/ee505f826947f6844c3e001bb83c7e7b/Schema-design--1-1.jpg" />
          </figure><p>Schema design #1 didn't work out well. ClickHouse JOIN syntax forces to write monstrous query over 300 lines of SQL, repeating the selected columns many times because you can do only <a href="https://github.com/yandex/ClickHouse/issues/873">pairwise joins</a> in ClickHouse.</p><p>As for querying each of materialized views separately in parallel, benchmark showed prominent, but moderate results - query throughput would be a little bit better than using our Citus based old pipeline.</p>
    <div>
      <h4>Aggregations schema design #2</h4>
      <a href="#aggregations-schema-design-2">
        
      </a>
    </div>
    <p>In our second iteration of the schema design, we strove to keep a similar structure to our existing Citus tables. To do this, we experimented with the SummingMergeTree engine, which is described in detail by the excellent ClickHouse <a href="https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/summingmergetree">documentation</a>:</p><blockquote><p>In addition, a table can have nested data structures that are processed in a special way. If the name of a nested table ends in 'Map' and it contains at least two columns that meet the following criteria... then this nested table is interpreted as a mapping of key =&gt; (values...), and when merging its rows, the elements of two data sets are merged by 'key' with a summation of the corresponding (values...).</p></blockquote><p>We were pleased to find this feature, because the SummingMergeTree engine allowed us to significantly reduce the number of tables required as compared to our initial approach. At the same time, it allowed us to match the structure of our existing Citus tables. The reason was that the ClickHouse Nested structure ending in 'Map' was similar to the <a href="https://www.postgresql.org/docs/9.6/static/hstore.html">Postgres hstore</a> data type, which we used extensively in the old pipeline.</p><p>However, there were two existing issues with ClickHouse maps:</p><ul><li><p>SummingMergeTree does aggregation for all records with same primary key, but final aggregation across all shards should be done using some aggregate function, which didn't exist in ClickHouse.</p></li><li><p>For storing uniques (uniques visitors based on IP), we need to use AggregateFunction data type, and although SummingMergeTree allows you to create column with such data type, it will not perform aggregation on it for records with same primary keys.</p></li></ul><p>To resolve problem #1, we had to create a new aggregation function <a href="https://clickhouse.yandex/docs/en/single/index.html#summapkey-value">sumMap</a>. Luckily, ClickHouse source code is of excellent quality and its core developers are very helpful with reviewing and merging requested changes.</p><p>As for problem #2, we had to put uniques into separate materialized view, which uses the ReplicatedAggregatingMergeTree Engine and supports merge of AggregateFunction states for records with the same primary keys. We're considering adding the same functionality into SummingMergeTree, so it will simplify our schema even more.</p><p>We also created a separate materialized view for the Colo endpoint because it has much lower usage (5% for Colo endpoint queries, 95% for Zone dashboard queries), so its more dispersed primary key will not affect performance of Zone dashboard queries.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2ZmeMrcqIgAI99UJgfx1HK/80923c6f467d95a1bd8d74e838f91d99/Schema-design--2.jpg" />
          </figure><p>Once schema design was acceptable, we proceeded to performance testing.</p>
    <div>
      <h3>ClickHouse performance tuning</h3>
      <a href="#clickhouse-performance-tuning">
        
      </a>
    </div>
    <p>We explored a number of avenues for performance improvement in ClickHouse. These included tuning index granularity, and improving the merge performance of the SummingMergeTree engine.</p><p>By default ClickHouse recommends to use 8192 index granularity. There is <a href="https://medium.com/@f1yegor/clickhouse-primary-keys-2cf2a45d7324">nice article</a> explaining ClickHouse primary keys and index granularity in depth.</p><p>While default index granularity might be excellent choice for most of use cases, in our case we decided to choose the following index granularities:</p><ul><li><p>For the main non-aggregated requests table we chose an index granularity of 16384. For this table, the number of rows read in a query is typically on the order of millions to billions. In this case, a large index granularity does not make a huge difference on query performance.</p></li><li><p>For the aggregated requests_* stables, we chose an index granularity of 32. A low index granularity makes sense when we only need to scan and return a few rows. It made a huge difference in API performance - query latency decreased by 50% and throughput increased by ~3 times when we changed index granularity 8192 → 32.</p></li></ul><p>Not relevant to performance, but we also disabled the min_execution_speed setting, so queries scanning just a few rows won't return exception because of "slow speed" of scanning rows per second.</p><p>On the aggregation/merge side, we've made some ClickHouse optimizations as well, like <a href="https://github.com/yandex/ClickHouse/pull/1330">increasing SummingMergeTree maps merge speed</a> by x7 times, which we contributed back into ClickHouse for everyone's benefit.</p><p>Once we had completed the performance tuning for ClickHouse, we could bring it all together into a new data pipeline. Next, we describe the architecture for our new, ClickHouse-based data pipeline.</p>
    <div>
      <h3>New data pipeline</h3>
      <a href="#new-data-pipeline">
        
      </a>
    </div>
    <p>The new pipeline architecture re-uses some of the components from old pipeline, however it replaces its most weak components.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2UaoX6fPvFuhN1ecJpbe6v/8aee3bd1ab395cb4a4ac2b8db9575a23/New-system-architecture.jpg" />
          </figure><p>New components include:</p><ul><li><p><b>Kafka consumers </b>- 106 Go consumers per each partition consume Cap'n Proto raw logs and extract/prepare needed 100+ ClickHouse fields. Consumers no longer do any aggregation logic.</p></li><li><p><b>ClickHouse cluster</b> - 36 nodes with x3 replication factor. It handles non-aggregate requests logs ingestion and then produces aggregates using materialized views.</p></li><li><p><b>Zone Analytics API</b> - rewritten and optimized version of API in Go, with many meaningful metrics, healthchecks, failover scenarios.</p></li></ul><p>As you can see the architecture of new pipeline is much simpler and fault-tolerant. It provides Analytics for all our 7M+ customers' domains totalling more than 2.5 billion monthly unique visitors and over 1.5 trillion monthly page views.</p><p>On average we process 6M HTTP requests per second, with peaks of upto 8M requests per second.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/zjubA3CgetykRE8Angxln/7509dd5dea82ad7408339a5f78561b41/HTTP-Logfwdr-throughput.png" />
          </figure><p>Average log message size in <a href="https://capnproto.org/">Cap’n Proto</a> format used to be ~1630B, but thanks to amazing job on Kafka compression by our Platform Operations Team, it decreased significantly. Please see <a href="https://blog.cloudflare.com/squeezing-the-firehose/">"Squeezing the firehose: getting the most from Kafka compression"</a> blog post with deeper dive into those optimisations.</p>
    <div>
      <h4>Benefits of new pipeline</h4>
      <a href="#benefits-of-new-pipeline">
        
      </a>
    </div>
    <ul><li><p><b>No SPOF</b> - removed all SPOFs and bottlenecks, everything has at least x3 replication factor.</p></li><li><p><b>Fault-tolerant</b> - it's more fault-tolerant, even if Kafka consumer or ClickHouse node or Zone Analytics API instance fails, it doesn't impact the service.</p></li><li><p><b>Scalable</b> - we can add more Kafka brokers or ClickHouse nodes and scale ingestion as we grow. We are not so confident about query performance when cluster will grow to hundreds of nodes. However, Yandex team managed to scale their cluster to 500+ nodes, distributed geographically between several data centers, using two-level sharding.</p></li><li><p><b>Reduced complexity</b> - due to removing messy crons and consumers which were doing aggregations and <a href="https://www.cloudflare.com/learning/cloud/how-to-refactor-applications/">refactoring</a> API code we were able to:</p><ul><li><p>Shutdown Postgres RollupDB instance and free it up for reuse.</p></li><li><p>Shutdown Citus cluster 12 nodes and free it up for reuse. As we won't use Citus for serious workload anymore we can reduce our operational and support costs.</p></li><li><p>Delete tens of thousands of lines of old Go, SQL, Bash, and PHP code.</p></li><li><p>Remove WWW PHP API dependency and extra latency.</p></li></ul></li><li><p><b>Improved API throughput and latency </b>- with previous pipeline Zone Analytics API was struggling to serve more than 15 queries per second, so we had to introduce temporary hard rate limits for largest users. With new pipeline we were able to remove hard rate limits and now we are serving ~40 queries per second. We went further and did intensive load testing for new API and with current setup and hardware we are able serve up to ~150 queries per second and this is scalable with additional nodes.
</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1TCHFdGAndAank2N3agHQj/5db6811ef0918e8fc66b8543531f9733/Zone-Analytics-API-requests-latency-quantiles.png" />
          </figure><p></p></li><li><p><b>Easier to operate</b> - with shutdown of many unreliable components, we are finally at the point where it's relatively easy to operate this pipeline. ClickHouse quality helps us a lot in this matter.</p></li><li><p><b>Decreased amount of incidents</b> - with new more reliable pipeline, we now have fewer incidents than before, which ultimately has reduced on-call burden. Finally, we can sleep peacefully at night :-).</p></li></ul><p>Recently, we've improved the throughput and latency of the new pipeline even further with better hardware. I'll provide details about this cluster below.</p>
    <div>
      <h4>Our ClickHouse cluster</h4>
      <a href="#our-clickhouse-cluster">
        
      </a>
    </div>
    <p>In total we have 36 ClickHouse nodes. The new hardware is a big upgrade for us:</p><ul><li><p><b>Chassis</b> - Quanta D51PH-1ULH chassis instead of Quanta D51B-2U chassis (2x less physical space)</p></li><li><p><b>CPU</b> - 40 logical cores E5-2630 v3 @ 2.40 GHz instead of 32 cores E5-2630 v4 @ 2.20 GHz</p></li><li><p><b>RAM</b> - 256 GB RAM instead of 128 GB RAM</p></li><li><p><b>Disks</b> - 12 x 10 TB Seagate ST10000NM0016-1TT101 disks instead of 12 x 6 TB Toshiba TOSHIBA MG04ACA600E</p></li><li><p><b>Network</b> - 2 x 25G Mellanox ConnectX-4 in MC-LAG instead of 2 x 10G Intel 82599ES</p></li></ul><p>Our Platform Operations team noticed that ClickHouse is not great at running heterogeneous clusters yet, so we need to gradually replace all nodes in the existing cluster with new hardware, all 36 of them. The process is fairly straightforward, it's no different than replacing a failed node. The problem is that <a href="https://github.com/yandex/ClickHouse/issues/1821">ClickHouse doesn't throttle recovery</a>.</p><p>Here is more information about our cluster:</p><ul><li><p><b>Avg insertion rate</b> - all our pipelines bring together 11M rows per second.</p></li><li><p><b>Avg insertion bandwidth</b> - 47 Gbps.</p></li><li><p><b>Avg queries per second</b> - on average cluster serves ~40 queries per second with frequent peaks up to ~80 queries per second.</p></li><li><p><b>CPU time</b> - after recent hardware upgrade and all optimizations, our cluster CPU time is quite low.
</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/61sIThxxM4s9mgA8nQSibn/4e09df1a744f0c1e2cd92b8bf5bfdd5f/ClickHouse-CPU-usage.png" />
          </figure><p></p></li><li><p><b>Max disk IO</b> (device time) - it's low as well.
</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1rNrJvGvXd6VNvPRH2qe0l/f7813d61e834b420515047062e25e791/Max-disk-IO.png" />
          </figure><p></p><p></p></li></ul><p>In order to make the switch to the new pipeline as seamless as possible, we performed a transfer of historical data from the old pipeline. Next, I discuss the process of this data transfer.</p>
    <div>
      <h4>Historical data transfer</h4>
      <a href="#historical-data-transfer">
        
      </a>
    </div>
    <p>As we have 1 year storage requirements, we had to do one-time ETL (Extract Transfer Load) from the old Citus cluster into ClickHouse.</p><p>At Cloudflare we love Go and its goroutines, so it was quite straightforward to write a simple ETL job, which:</p><ul><li><p>For each minute/hour/day/month extracts data from Citus cluster</p></li><li><p>Transforms Citus data into ClickHouse format and applies needed business logic</p></li><li><p>Loads data into ClickHouse</p></li></ul><p>The whole process took couple of days and over 60+ billions rows of data were transferred successfully with consistency checks. The completion of this process finally led to the shutdown of old pipeline. However, our work does not end there, and we are constantly looking to the future. In the next section, I'll share some details about what we are planning.</p>
    <div>
      <h3>Future of Data APIs</h3>
      <a href="#future-of-data-apis">
        
      </a>
    </div>
    
    <div>
      <h4>Log Push</h4>
      <a href="#log-push">
        
      </a>
    </div>
    <p>We're currently working on something called "Log Push". Log push allows you to specify a desired data endpoint and have your HTTP request logs sent there automatically at regular intervals. At the moment, it's in private beta and going to support sending logs to:</p><ul><li><p>Amazon S3 bucket</p></li><li><p>Google Cloud Service bucket</p></li><li><p>Other storage services and platforms</p></li></ul><p>It's expected to be generally available soon, but if you are interested in this new product and you want to try it out please contact our Customer Support team.</p>
    <div>
      <h4>Logs SQL API</h4>
      <a href="#logs-sql-api">
        
      </a>
    </div>
    <p>We're also evaluating possibility of building new product called Logs SQL API. The idea is to provide customers access to their logs via flexible API which supports standard SQL syntax and JSON/CSV/TSV/XML format response.</p><p>Queries can extract:</p><ul><li><p><b>Raw requests logs fields</b> (e.g. SELECT field1, field2, ... FROM requests WHERE ...)</p></li><li><p><b>Aggregated data from requests logs</b> (e.g. SELECT clientIPv4, count() FROM requests GROUP BY clientIPv4 ORDER BY count() DESC LIMIT 10)</p></li></ul><p>Google BigQuery provides similar <a href="https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/query">SQL API</a> and Amazon has product callled <a href="https://docs.aws.amazon.com/kinesisanalytics/latest/sqlref/analytics-sql-reference.html">Kinesis Data analytics</a> with SQL API support as well.</p><p>Another option we're exploring is to provide syntax similar to <a href="https://api.cloudflare.com/#dns-analytics-properties">DNS Analytics API</a> with filters and dimensions.</p><p>We're excited to hear your feedback and know more about your analytics use case. It can help us a lot to build new products!</p>
    <div>
      <h3>Conclusion</h3>
      <a href="#conclusion">
        
      </a>
    </div>
    <p>All this could not be possible without hard work across multiple teams! First of all thanks to other Data team engineers for their tremendous efforts to make this all happen. Platform Operations Team made significant contributions to this project, especially Ivan Babrou and Daniel Dao. Contributions from Marek Vavruša in DNS Team were also very helpful.</p><p>Finally, Data team at Cloudflare is a small team, so if you're interested in building and operating distributed services, you stand to have some great problems to work on. Check out the <a href="https://boards.greenhouse.io/cloudflare/jobs/613800">Distributed Systems Engineer - Data</a> and <a href="https://boards.greenhouse.io/cloudflare/jobs/688056">Data Infrastructure Engineer</a> roles in London, UK and San Francisco, US, and let us know what you think.</p> ]]></content:encoded>
            <category><![CDATA[Analytics]]></category>
            <category><![CDATA[Data]]></category>
            <category><![CDATA[Speed & Reliability]]></category>
            <category><![CDATA[Kafka]]></category>
            <category><![CDATA[Cap'n Proto]]></category>
            <category><![CDATA[API]]></category>
            <category><![CDATA[php]]></category>
            <category><![CDATA[Load Balancing]]></category>
            <category><![CDATA[NGINX]]></category>
            <guid isPermaLink="false">6VEE3i8wXN2CDKWJJ16uXS</guid>
            <dc:creator>Alex Bocharov</dc:creator>
        </item>
        <item>
            <title><![CDATA[How Cloudflare analyzes 1M DNS queries per second]]></title>
            <link>https://blog.cloudflare.com/how-cloudflare-analyzes-1m-dns-queries-per-second/</link>
            <pubDate>Wed, 10 May 2017 21:50:20 GMT</pubDate>
            <description><![CDATA[ On Friday, we announced DNS analytics for all Cloudflare customers. Because of our scale –– by the time you’ve finished reading this, Cloudflare DNS will have handled millions of DNS queries –– we had to be creative in our implementation.  ]]></description>
            <content:encoded><![CDATA[ <p>On Friday, we <a href="/dns-analytics/">announced DNS analytics</a> for all Cloudflare customers. Because of our scale –– by the time you’ve finished reading this, Cloudflare DNS will have handled millions of DNS queries –– we had to be creative in our implementation. In this post, we’ll describe the systems that make up DNS Analytics which help us comb through trillions of these logs each month.</p>
    <div>
      <h3>How logs come in from the edge</h3>
      <a href="#how-logs-come-in-from-the-edge">
        
      </a>
    </div>
    
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/43iWrS8JtJwUtuUMJoO4Bx/27e5021c786337d38293d03f8260a104/Screen-Shot-2017-05-10-at-2.35.39-PM.png" />
            
            </figure><p>Cloudflare already has a <a href="/scaling-out-postgresql-for-cloudflare-analytics-using-citusdb">data pipeline for HTTP logs</a>. We wanted to utilize what we could of that system for the new DNS analytics. Every time one of our edge services gets an HTTP request, it generates a structured log message in the <a href="https://capnproto.org">Cap’n Proto</a> format and sends it to a local multiplexer service. Given the volume of the data, we chose not to record the full DNS message payload, only telemetry data we are interested in such as response code, size, or query name, which has allowed us to keep only ~150 bytes on average per message. It is then fused with processing metadata such as timing information and exceptions triggered during query processing. The benefit of fusing data and metadata at the edge is that we can spread the computational cost across our thousands of edge servers, and log only what we absolutely need.</p><p>The multiplexer service (known as “log forwarder”) is running on each edge node, assembling log messages from multiple services and transporting them to our warehouse for processing over a TLS secured channel. A counterpart service running in the warehouse receives and demultiplexes the logs into several Apache Kafka clusters. Over the years, Apache Kafka has proven to be an invaluable service for buffering between producers and downstream consumers, preventing data loss when consumers fail over or require maintenance. Since version 0.10, Kafka allows <a href="https://issues.apache.org/jira/browse/KAFKA-1215">rack-aware allocation of replicas</a>, which improves resilience against rack or site failure, giving us fault tolerant storage of unprocessed messages.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/39oKpjRECU2vqTqBI6FM4h/cbe9e83d633142933d6d2291a28acc57/data-pipeline-lg.png" />
            
            </figure><p>Having a queue with structured logs has allowed us to investigate issues retrospectively without requiring access to production nodes, but it has proven to be quite laborious at times. In the early days of the project we would skim the queue to find offsets for the rough timespan we needed, and then extract the data into HDFS in <a href="https://parquet.apache.org">Parquet</a> format for offline analysis.</p>
    <div>
      <h3>About aggregations</h3>
      <a href="#about-aggregations">
        
      </a>
    </div>
    <p>The HTTP analytics service was built around stream processors generating aggregations, so we planned to leverage Apache Spark to stream the logs to HDFS automatically. As Parquet doesn’t natively support indexes or arranging the data in a way that avoids full table scans, it’s impractical for on-line analysis or serving reports over an API. There are extensions like <a href="https://github.com/lightcopy/parquet-index">parquet-index</a> that create indexes over the data, but not on-the-fly. Given this, the initial plan was to only show aggregated reports to customers, and keep the raw data for internal troubleshooting.</p><p>The problem with aggregated summaries is that they only work on columns with low cardinality (a number of unique values). With aggregation, each column in a given time frame explodes to the number of rows equal to the number of unique entries, so it’s viable to aggregate on something like response code which only has 12 possible values, but not a query name for example. <a href="https://www.cloudflare.com/learning/dns/glossary/what-is-a-domain-name/">Domain names</a> are subject to popularity, so if, for example, a popular domain name gets asked 1,000 times a minute, one could expect to achieve 1000x row reduction for per-minute aggregation, however in practice it is not so.</p><p>Due to how DNS caching works, resolvers will answer identical queries from cache without going to the authoritative server for the duration of the TTL. The TTL tends to be longer than a minute. So, while authoritative servers see the same request many times, our data is skewed towards non-cacheable queries such as typos or random prefix subdomain attacks. In practice, we see anywhere between 0 - 60x row reduction when aggregating by query names, so storing aggregations in multiple resolutions almost negates the row reduction. Aggregations are also done with multiple resolution and key combinations, so aggregating on a high cardinality column can even result in more rows than original.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2jfGlvqdn84rSStt6KYsPO/eb0b6c634a9cf130335ff77443b474b8/data-agg-lg-1.png" />
            
            </figure><p>For these reasons we started by only aggregating logs at the zone level, which was useful enough for trends, but too coarse for root cause analysis. For example, in one case we were investigating short bursts of unavailability in one of our data centers. Having unaggregated data allowed us to narrow the issue down to the specific DNS queries experiencing latency spikes, and then correlated the queries with a misconfigured firewall rule. Cases like these would be much harder to investigate with only aggregated data as it only affected a tiny percentage of requests that would be lost in the aggregations.</p><p>So we started looking into several OLAP (Online Analytical Processing) systems. The first system we looked into was <a href="http://druid.io">Druid</a>. We were really impressed with the capabilities and how the front-end (Pivot and formerly Caravel) is able to slice and dice the data, allowing us to generate reports with arbitrary dimensions. Druid has already been deployed in similar environments with over 100B events/day, so we were confident it could work, but after testing on sampled data we couldn’t justify the hardware costs of hundreds of nodes. Around the same time Yandex open-sourced their OLAP system, <a href="https://clickhouse.yandex/">ClickHouse</a>.</p>
    <div>
      <h3>And then it Clicked</h3>
      <a href="#and-then-it-clicked">
        
      </a>
    </div>
    <p>ClickHouse has a much simpler system design - all the nodes in a cluster have equal functionality and use only <a href="https://zookeeper.apache.org/">ZooKeeper</a> for coordination. We built a small cluster of several nodes to start kicking the tires, and found the performance to be quite impressive and true to the results advertised in the <a href="https://clickhouse.yandex/benchmark.html">performance comparisons of analytical DBMS</a>, so we proceeded with building a proof of concept. The first obstacle was a lack of tooling and the small size of the community, so we delved into the ClickHouse design to understand how it works.</p><p>ClickHouse doesn’t support ingestion from Kafka directly, as it’s only a database, so we wrote an adapter service in Go. It read Cap’n Proto encoded messages from Kafka, converted them into TSV, and inserted into ClickHouse over the HTTP interface in batches. Later, we rewrote the service to use a <a href="https://github.com/kshvakov/clickhouse">Go library</a> using the native ClickHouse interface to boost performance. Since then, we’ve contributed some performance improvements back to the project. One thing we learned during the ingestion performance evaluation is that ClickHouse ingestion performance highly depends on batch size - the number of rows you insert at once. To understand why, we looked further into how ClickHouse stores data.</p><p>The most common table engine ClickHouse uses for storage is the MergeTree family. It is conceptually similar to <a href="https://en.wikipedia.org/wiki/Log-structured_merge-tree">LSM algorithm</a> used in <a href="https://www.igvita.com/2012/02/06/sstable-and-log-structured-storage-leveldb">Google’s BigTable</a> or Apache Cassandra, however it avoids an intermediate memory table, and writes directly to disk. This gives it excellent write throughput, as each inserted batch is only sorted by “primary key”, compressed, and written to disk to form a segment. The absence of a memory table or any notion of “freshness” of the data also means that it is append-only and data modification or deletion isn’t supported. The only way to delete data currently is to remove data by calendar months, as segments never overlap a month boundary. The ClickHouse team is actively working on making this configurable. On the other hand, this makes writing and segment merging conflict-free, so the ingestion throughput scales linearly with the number of parallel inserts until the I/O or cores are saturated. This, however, also means it is not fit for tiny batches, which is why we rely on Kafka and inserter services for buffering. ClickHouse then keeps constantly merging segments in the background, so many small parts will be merged and written more times (thus increasing write amplification) and too many unmerged parts will trigger aggressive throttling of insertions until the merging progresses. We have found that several insertions per table per second work best as a tradeoff between real-time ingestion and ingestion performance.</p><p>The key to table read performance is indexing and the arrangement of the data on disk. No matter how fast processing is, when the engine needs to scan terabytes of data from disk and use only a fraction of it, it’s going to take time. ClickHouse is a columnar store, so each segment contains a file for each column, with sorted values for each row. This way whole columns not present in the query can be skipped, and then multiple cells can be processed in parallel with vectorized execution. In order to avoid full scans, each segment also has a sparse index file. Given that all columns are sorted by the “primary key”, the index file only contains marks (captured rows) of every Nth row in order to be able to keep it in memory even for very large tables. For example the default settings is to make a mark of every 8,192th row. This way only 122,070 marks are required to sparsely index a table with 1 trillion rows, which easily fits in memory. See <a href="https://medium.com/@f1yegor/clickhouse-primary-keys-2cf2a45d7324">primary keys in ClickHouse</a> for a deeper dive into how it works.</p><p>When querying the table using primary key columns, the index returns approximate ranges of rows considered. Ideally the ranges should be wide and contiguous. For example, when the typical usage is to generate reports for individual zones, placing the zone on the first position of the primary key will result in rows sorted by zone in each column, making the disk reads for individual zones contiguous, whereas sorting primarily by timestamp would not. The rows can be sorted in only one way, so the primary key must be chosen carefully with the typical query load in mind. In our case, we optimized read queries for individual zones and have a separate table with sampled data for exploratory queries. The lesson learned is that instead of trying to optimize the index for all purposes and splitting the difference, we have made several tables instead.</p><p>One such specialisation are tables with aggregations over zones. Queries across all rows are significantly more expensive, as there is no opportunity to exclude data from scanning. This makes it less practical for analysts to compute basic aggregations on long periods of time, so we decided to use <a href="https://clickhouse.yandex/reference_en.html#CREATE%20VIEW">materialized views</a> to incrementally compute predefined aggregations, such as counters, uniques, and quantiles. The materialized views leverage the sort phase on batch insertion to do productive work - computing aggregations. So after the newly inserted segment is sorted, it also produces a table with rows representing dimensions, and columns representing <a href="https://github.com/yandex/ClickHouse/blob/master/doc/developers/architecture.md#aggregate-functions">aggregation function state</a>. The difference between aggregation state and final result is that we can generate reports using an arbitrary time resolution without actually storing the precomputed data in multiple resolutions. In some cases the state and result can be the same - for example basic counters, where hourly counts can be produced by summing per-minute counts, however it doesn’t make sense to sum unique visitors or latency quantiles. This is when aggregation state is much more useful, as it allows meaningful merging of more complicated states, such as <a href="https://en.wikipedia.org/wiki/HyperLogLog">HyperLogLog (HLL)</a> bitmap to produce hourly unique visitors estimate from minutely aggregations. The downside is that storing state can be much more expensive than final values - the aforementioned HLL state tends to be 20-100 bytes / row when compressed, while a counter is only 8 bytes (1 byte compressed on average). These tables are then used to quickly visualise general trends across zones or sites, and also by our API service that uses them opportunistically for simple queries. Having both incrementally aggregated and unaggregated data in the same place allowed us simplify the architecture by deprecating stream processing altogether.</p>
    <div>
      <h3>Infrastructure and data integrity</h3>
      <a href="#infrastructure-and-data-integrity">
        
      </a>
    </div>
    <p>We started with RAID-10 composed of 12 6TB spinning disks on each node, but reevaluated it after the first inevitable disk failures. In the second iteration we migrated to RAID-0, for two reasons. First, it wasn’t possible to hot-swap just the faulty disks, and second the array rebuild took tens of hours which degraded I/O performance. It was significantly faster to replace a faulty node and use internal replication to populate it with data over the network (2x10GbE), than to wait for an array to finish rebuilding. To compensate for higher probability of node failure, we switched to 3-way replication and allocated replicas of each shard to different racks, and started planning for replication to a separate data warehouse.</p><p>Another disk failure <a href="https://github.com/yandex/ClickHouse/issues/520">highlighted a problem</a> with the filesystem we used. Initially we used XFS, but it started to lock up during replication from 2 peers at the same time, thus breaking replication of segments before it completed. This issue has manifested itself with a lot of I/O activity with little disk usage increase as broken parts were deleted, so we gradually migrated to ext4 that didn’t have the same issue.</p>
    <div>
      <h3>Visualizing Data</h3>
      <a href="#visualizing-data">
        
      </a>
    </div>
    <p>At the time we relied solely on <a href="http://pandas.pydata.org">Pandas</a> and <a href="https://clickhouse.yandex/reference_en.html#HTTP%20interface">ClickHouse’s HTTP interface</a> for ad-hoc analyses, but we wanted to make it more accessible for both analysis and monitoring. Since we knew Caravel - now renamed to <a href="https://github.com/airbnb/superset">Superset</a> - from the experiments with Druid, we started working on an integration with ClickHouse.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4flBsPFls2o0tCyU2N5MqV/3e92f7508c3a869c7ed13438607bc2de/superset-1.png" />
            
            </figure><p>Superset is a data visualisation platform designed to be intuitive, and allows analysts to interactively slice and dice the data without writing a single line of SQL. It was initially <a href="https://medium.com/airbnb-engineering/caravel-airbnb-s-data-exploration-platform-15a72aa610e5">built and open sourced by AirBnB</a> for Druid, but over time it has gained support for SQL-based backends using <a href="https://www.sqlalchemy.org">SQLAlchemy</a>, an abstraction and ORM for tens of different database dialects. So we wrote and open-sourced a <a href="https://github.com/cloudflare/sqlalchemy-clickhouse">ClickHouse dialect</a> and finally a native <a href="https://github.com/airbnb/superset/pull/1844">Superset integration</a> that has been merged a few days ago.</p><p>Superset has served us well for ad-hoc visualisations, but it is still not polished enough for our monitoring use case. At Cloudflare we’re heavy users of Grafana for visualisation of all our metrics, so we wrote and open-sourced a <a href="https://github.com/vavrusa/grafana-sqldb-datasource">Grafana integration</a> as well.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6HRlF5lQlrcikqgwFWM6cs/fdefd2e8f8d23a1fb371fbc9c72cb7a6/Screen-Shot-2017-05-08-at-12.39.47-1.png" />
            
            </figure><p>It has allowed us to seamlessly extend our existing monitoring dashboards with the new analytical data. We liked it so much that we wanted to give the same ability to look at the analytics data to you, our users. So we built a <a href="/grafana-plugin">Grafana app</a> to visualise data from Cloudflare DNS Analytics. Finally, we made it available in your <a href="/dns-analytics/">Cloudflare dashboard analytics</a>. Over time we’re going to add new data sources, dimensions, and other useful ways how to visualise your data from Cloudflare. Let us know what you’d like to see next.</p><p>Does solving these kinds of technical and operational challenges excite you? Cloudflare is always hiring for talented specialists and generalists within our <a href="https://www.cloudflare.com/careers/jobs/?department=Engineering">Engineering</a>, <a href="https://www.cloudflare.com/careers/jobs/">Technical Operations</a> and <a href="https://www.cloudflare.com/careers">other teams</a>.</p> ]]></content:encoded>
            <category><![CDATA[DNS]]></category>
            <category><![CDATA[Analytics]]></category>
            <category><![CDATA[Cap'n Proto]]></category>
            <category><![CDATA[Speed & Reliability]]></category>
            <guid isPermaLink="false">6vsdAcIrh7NQu1zjXcxRFT</guid>
            <dc:creator>Marek Vavruša</dc:creator>
        </item>
        <item>
            <title><![CDATA[Introducing lua-capnproto: better serialization in Lua]]></title>
            <link>https://blog.cloudflare.com/introducing-lua-capnproto-better-serialization-in-lua/</link>
            <pubDate>Fri, 28 Feb 2014 09:00:00 GMT</pubDate>
            <description><![CDATA[ When we need to transfer data from one program to another program, either within a machine or from one data center to another some form of serialization is needed. ]]></description>
            <content:encoded><![CDATA[ <p>When we need to transfer data from one program to another program, either within a machine or from one data center to another some form of serialization is needed. Serialization converts data stored in memory into a form that can be sent across a network or between processes and then converted back into data a program can use directly.</p><p>At CloudFlare, we have data centers all over the world. When transferring data from one data center to another, we need a very efficient way of serializing data, saving us both time and network bandwidth.</p><p>We've looked at a few serialization projects. For example, one popular serialization format is JSON, for some of our Go programs we use gob, and we've made use of Protocol Buffers in the past. But lately we've been using a new serialization protocol called Cap'n Proto.</p><p><a href="http://kentonv.github.io/capnproto/">Cap'n Proto</a> attracted us because of its very high performance compared to other serialization projects. It looks a little like a better version of Protocol Buffers, and the author of Cap'n Proto, Kenton, was the primary author of Protocol Buffers version 2.</p><p>At CloudFlare, we use NGINX in conjunction with Lua for front-line web serving, proxying and traffic filtering. We need to serialize our data in Lua and transport it across the Internet. But unfortunately, there was no Lua module for <a href="http://kentonv.github.io/capnproto/index.html">Cap'n Proto</a>. So, we decided to write <a href="https://github.com/cloudflare/lua-capnproto">lua-capnproto</a> and release it as yet another <a href="http://cloudflare.github.io/">CloudFlare open source</a> contribution.</p><p><a href="https://github.com/cloudflare/lua-capnproto">lua-capnproto</a> provides very fast data serialization and a really easy to use API. Here I'll show you how to use <a href="https://github.com/cloudflare/lua-capnproto">lua-capnproto</a> to do serialization and deserialization.</p>
    <div>
      <h3>Install lua-capnproto</h3>
      <a href="#install-lua-capnproto">
        
      </a>
    </div>
    <p>To install lua-capnproto, you need to install <a href="http://kentonv.github.io/capnproto/install.html">Cap'n Proto</a>, <a href="http://luajit.org/install.html">LuaJIT 2.1</a> and <a href="http://luarocks.org/en/Download">luarocks</a> first.</p><p>Then you can install lua-capnproto using the following commands:</p><p>git clone <a href="https://github.com/cloudflare/lua-capnproto.git">https://github.com/cloudflare/lua-capnproto.git</a>cd lua-capnprotosudo luarocks make</p><p>To test whether lua-capnproto was installed successfully, you can use the <code>capnp</code> compiler to generate a Lua version of one of the Cap'n Proto examples as follows:</p><p>capnp compile -olua proto/example.capnp</p><p>If everything goes well, you should see no errors and a file named <code>example_capnp.lua</code> generated under the <code>proto</code> directory.</p>
    <div>
      <h3>Write a Cap’n Proto definition</h3>
      <a href="#write-a-capn-proto-definition">
        
      </a>
    </div>
    <p>Here's a sample Cap’n Proto definition that would be stored in a file called <code>AddressBook.capnp</code>:</p>
            <pre><code>    @0xdbb9ad1f14bf0b36;  # unique file ID, generated by capnp id

    struct Person {
      id @0 :UInt32;
      name @1 :Text;
      email @2 :Text;
      phones @3 :List(PhoneNumber);

      struct PhoneNumber {
        number @0 :Text;
        type @1 :Type;

        enum Type {
          mobile @0;
          home @1;
          work @2;
        }
      }

      employment :union {
        unemployed @4 :Void;
        employer @5 :Text;
        school @6 :Text;
        selfEmployed @7 :Void;
        # We assume that a person is only one of these.
      }
    }

    struct AddressBook {
      people @0 :List(Person);
    }</code></pre>
            <p>We have a root structure named <code>AddressBook</code> containing a list named <code>people</code> whose members are also structures. What we are going to do is to serialize an <code>AddressBook</code> structure and then read the structure from serialized data. For more details about the <a href="http://kentonv.github.io/capnproto/index.html">Cap'n Proto</a> definition, you can checkout its documentation at <a href="http://kentonv.github.io/capnproto/index.html">here</a>.</p>
    <div>
      <h3>Prepare your data</h3>
      <a href="#prepare-your-data">
        
      </a>
    </div>
    <p>Preparing data is pretty simple. All you need to do is to generate a Lua table corresponding to the root structure. The following list gives rules to help you write this table. On the left is a <a href="http://kentonv.github.io/capnproto/index.html">Cap'n Proto</a> data type, on the right is its corresponding Lua data type.</p><ul><li><p>struct -&gt; Lua hash table</p></li><li><p>list -&gt; Lua array table</p></li><li><p>bool -&gt; Lua boolean</p></li><li><p>int8/16/32 or uint8/16/32 or float32/64 -&gt; Lua number</p></li><li><p>int64/uint64 -&gt; LuaJIT 64bit number</p></li><li><p>enum -&gt; Lua string</p></li><li><p>void -&gt; Lua string <code>“Void”</code></p></li><li><p>group -&gt; Lua hash table (the same as struct)</p></li><li><p>union -&gt; Lua hash table which has only one value set</p></li></ul>
    <div>
      <h3>A few notices:</h3>
      <a href="#a-few-notices">
        
      </a>
    </div>
    <p>Because Lua number type represents real (double-precision floating-point) numbers and Lua has no integer type, so by default you can't store a 64 bit integer using number type without losing precision. LuaJIT has an <a href="http://luajit.org/ext_ffi_api.html#literals">extension</a> which supports 64-bit integers. You need to add <code>ULL</code> or <code>LL</code> to the end of the number (<code>ULL</code> is for unsigned integers, <code>LL</code> for signed). So, if you need to serialize a 64-bit integer, remember to append <code>ULL</code> or <code>LL</code> to your number.</p><p>For example:</p><p>id @0 :Int64;                   -&gt;   id = 12345678901234LL</p><p>Enum values are automatically converted from strings to their values, you don’t need to do it yourself. By default, enums will be converted to the uppercase with underscores form. You can change this behavior using annotations. The <a href="https://github.com/cloudflare/lua-capnproto">lua-capnproto</a> <a href="https://github.com/cloudflare/lua-capnproto/blob/master/README.md">document</a> has more details.</p><p>Here is an example:</p><p>type @0 :Type;
enum Type {
binaryAddr @0;
textAddr @1;
}                               -&gt;    type = “TEXT_ADDR”</p><p><code>void</code> is a special type in Cap’n Proto. For simplicity, we just use a string <code>"Void"</code> to represent <code>void</code> (actually, when serializing, any value other than <code>nil</code> will work, but we use <code>"Void"</code> for consistency).</p><p>A sample data table looks like this:</p>
            <pre><code>    local data = {
        people = {
            {
                id = "123",
                name = "Alice",
                email = "alice@example.com",
                phones = {
                    {
                        number = "555-1212",
                        ["type"] = "MOBILE",
                    },
                },
                employment = {
                    school = "MIT",
                },
            },
            {
                id = "456",
                name = "Bob",
                email = "bob@example.com",
                phones = {
                    {
                        number = "555-4567",
                        ["type"] = "HOME",
                    },
                    {
                        number = "555-7654",
                        ["type"] = "WORK",
                    },
                },
                employment = {
                    unemployed = "Void",
                },
            },
        }
    }</code></pre>
            
    <div>
      <h3>Compile and run</h3>
      <a href="#compile-and-run">
        
      </a>
    </div>
    <p>Now let's compile the Cap'n Proto file:</p><p>capnp compile -olua AddressBook.capnp</p><p>You shouldn't see any errors and a file named <code>AddressBook_capnp.lua</code> is generated under the current directory.</p><p>To use this file, we only need to write a file named <code>main.lua</code> (or whatever name you desire), and get all the required modules ready.</p>
            <pre><code>    local addressBook = require "AddressBook_capnp"                                                                       
    local capnp = require "capnp"</code></pre>
            <p>Now we can start to serialize using our already prepared data.</p>
            <pre><code>    local bin = addressBook.AddressBook.serialize(data)</code></pre>
            <p>That’s it. Just one line of code. All the serialization work is done. Now variable <code>bin</code> (a Lua string) holds the serialized binary data. You can write this string to a file or send it through network.</p><p>Want about deserialization? It's as easy as serialization.</p>
            <pre><code>    local decoded = addressBook.AddressBook.parse(bin)</code></pre>
            <p>Now variable <code>decoded</code> holds a table which is identical to <code>data</code>. You can find the complete code <a href="https://github.com/cloudflare/lua-capnproto/tree/master/example">here</a>. Note that you need LuaJIT to run the code.</p>
    <div>
      <h3>Performance</h3>
      <a href="#performance">
        
      </a>
    </div>
    <p>If you are happy with the API, here is even better news. We chose Cap'n Proto because of its impressively high performance. So when writing lua-capnproto, I also made every effort to make it fast.</p><p>In one project we switched to lua-capnproto from <a href="http://www.kyne.com.au/~mark/software/lua-cjson.php">lua-cjson</a> (a quite fast JSON library for Lua) for serialization. So let's see how fast lua-capnproto is compared to lua-cjson.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/59GtNOY9Ix3jO9tFNn53ms/8b88605c6a817d02dd525926adebee8b/lua-capnproto.png" />
            
            </figure><p>You can also run <a href="https://github.com/cloudflare/lua-capnproto/blob/master/benchmark.lua">benchmark.lua</a> yourself (included in the source code) to find out how fast lua-capnproto is compared to <a href="http://www.kyne.com.au/~mark/software/lua-cjson.php">lua-cjson</a> on your machine.</p>
    <div>
      <h3>The future</h3>
      <a href="#the-future">
        
      </a>
    </div>
    <p>We are already using <a href="https://github.com/cloudflare/lua-capnproto">lua-capnproto</a> in production at CloudFlare and it has been running very well for the past month. But <a href="https://github.com/cloudflare/lua-capnproto">lua-capnproto</a> is still a very young project. Some features are missing and there's a lot of work to do in the future. We will continue to make <a href="https://github.com/cloudflare/lua-capnproto">lua-capnproto</a> more stable and more reliable, and would be happy to receive contributions from the open source community.</p> ]]></content:encoded>
            <category><![CDATA[Data]]></category>
            <category><![CDATA[Speed & Reliability]]></category>
            <category><![CDATA[Cap'n Proto]]></category>
            <category><![CDATA[LUA]]></category>
            <guid isPermaLink="false">5n9qvrLgdQCTRLUe0NlTiB</guid>
            <dc:creator>Jiale Zhi</dc:creator>
        </item>
    </channel>
</rss>