Top
Best
New

Posted by f311a 14 hours ago

Saving another 100TB of RAM(blog.cloudflare.com)
351 points | 68 commentspage 2
variety8675 8 hours ago|
It’s nice to see Cloudflare is letting humans write the blog posts again after all the fallout from their LLM slop blogs
jiggawatts 6 hours ago||
I'm surprised to see no mention of hierarchical rendezvous hashing in either the article or the comments here.

It is purpose-designed for exactly this type of proxy/cache load-balancing scenario!

sfink 5 hours ago||
Um.

I read the article thinking it would make for a great brain puzzle, but I quickly decided there's something wrong with the question setup because the initial solution didn't make sense. I assumed it was just missing a constraint that would be revealed later, but I'm still not seeing it -- the article just kept patching up the flaws in the wrong solution, the one that is more complicated than the straightforward one.

I'm probably still missing something obvious? It's probably something to do with "...in a way that does not require large changes when servers are added or removed."

But let's start with the problem as initially posed: you have an infinite stream of tasks and you need to deterministically assign them to N servers. (Perhaps you have to shard the collections of servers, so not every load balancer knows about all of them? But no, that would break the solution in the article.) Ok, then hash the task request (I assume that you hash it, the article doesn't explicitly say, but that's how you'd get determinism) and take that hash mod N, that's your server index.

Why hash the servers too? If you roll 6 dice, and then another one to choose which die to use, you're not getting any more randomness. You're matching up two sides, the tasks on one side and the servers on the other; no need to randomize both.

Ooh, but that's not a perfect distribution? Ok, if the hash value is large enough to be in the at most N-1 slop values at the top of UINT_MAX, then roll again (compute another hash). But CF is happy with 8% unevenness, there should be no problem with this 0.1% or whatever.

Also, how do they find the nearest server hash to a task hash? Surely it's not a log(n) binary search through sorted server hashes, I hope?

Weights break this scheme. Now each server has some number of tickets. So you compute hash % T (where T=total tickets) and have to figure out what server that is. There's probably a more clever way, but you could make a big array of (2-byte!) server indexes, one per ticket, and just fill them in and look up at index hash % T.

That's 2 bytes per ticket, which feels uncomfortably wasteful if weights can be large. That's where things get more complicated for me: since the tasks are hashed, it doesn't matter what order a server's indexes come in relative to other servers', so sort them by descending weight. [I'm starting to suspect I'm making a fool of myself here by missing something obvious with the whole setup...] Now you can make an array of indexes for servers with the highest weight, then the next lower, then the next. Record the number of servers of each weight. Then you can take the hash % T and figure out which array it's in, then divide by the weight to give the index within that array.

To reduce the number of per-weight arrays, you can restrict the weights allowed. If you restrict weights to be powers of two, you can eliminate a division by using a shift. If you really want more flexible weights, you can allow servers to be in more than one of the arrays. Let the arrays be powers of two, and then add an entry to each array corresponding to 1 bits in the binary representation of the weights. That increases the total memory usage of the arrays, so you could somewhat restrict the allowed weights by rounding to the nearest number with, say, 2 or 3 "on" bits at most. With at most 2 bits, that means weights are 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 16, 17, .... The error really isn't bad.

And this should all be easily doable without any branches, I'm pretty sure. As long as you statically cap the max weight.

Anyway, that's just plowing through with the straightforward approach, and I still think I'm probably missing something major here. I imagine with large numbers of servers, some go down, so fast deletions are probably important. You can get by a little while by marking dead servers and if you "roll" one, just roll again. (Yes, deterministically, assuming other load balancers agree that the server is down.) But when more than some number of servers go down, you'd want to kick off a background task to rebuild a new set of tables -- so that's a factor 2 in size usage to have them both in memory during the rebuild.

Adding is trickier, you'd probably want to do a 2-level structure where first you use the hash to decide whether it's in the old set that the table is built for or the set of servers that hasn't been incorporated yet (you'd collect these over time, and empty them out on the next table rebuild.) It's a little weird, because the load balancers' outputs would only agree when the added and deleted sets agreed, but I don't see how to do better than that. (I think you could set up some kind of synchronization scheme so that the old sets would agree, which would make them usually agree on which of the old set of machines gets it.)

Somebody, feel free to tell me I'm being stupid! I'm sure there's a constraint that I'm missing, given that my understanding of the initial problem doesn't require any memory at all except for the servers' info.

(Or if not, I'll let you know where I'd like to receive shipment of 1% of the memory I've saved...)

procaryote 2 hours ago|
You hash the servers because then adding or removing a server doesn't directly affect other servers position on the ring; adding a server just takes some load from som servers.

This is useful because you want stickiness, so requests for the same key mostly go to the same server.

Sorting servers by weight means that removing or adding a server will shift a lot of traffic from the servers it used to go to. A flapping server early in the list will break stickiness for the whole set of servers.

The simplicity of stable hashing means you don't have to think about new sets, old sets, table rebuilds, synchronisation schemes etc, and that's useful because every such extra step adds bugs and corner cases

kingleopold 11 hours ago||
anyone remember 100tb hosting company?
opsnotes80 8 hours ago||
[flagged]
opsnotes80 8 hours ago||
[flagged]
jamesforestwest 12 hours ago||
[dead]
aaron695 8 hours ago||
[dead]
officialchicken 13 hours ago||
[flagged]
cyberpunk 12 hours ago||
Anyone have an idea how it behaves differently from google's jump hash algorithm? The cool thing about google's one is it's so short I can include it in a HN comment:

    int32_t JumpConsistentHash(uint64_t key, int32_t num_buckets) {
      int64_t b = 1, j = 0;
      while (j < num_buckets) {
        b = j;
        key = key * 2862933555777941757ULL + 1;
        j = (b + 1) * (double(1LL << 31) / double((key >> 33) + 1));
      }
      return b;
    }
https://arxiv.org/pdf/1406.2294
cyberpunk 12 hours ago|||
Well I looked it up; nginx, apparently, uses ketama -- it's a ring-style hash probably works better for web backends than the jch above, as when given [0,1,2,3] and replacing the server in slot 1 you're going to have a lot of hash moves. With ketama, you'd only have the '1' hashes moving. You can't really beat google's for brevity, though.
prirun 11 hours ago|||
I have used Google's jump hash. As I recall, one of the main differences is that jump hash doesn't have a mechanism to remove targets, eg, a server dies and you don't want to route requests to it. Traditional consistent hashing can do that. I guess if you had 4 servers, server #4 dies, then you can go back to 3 servers by just changing num_buckets from 4 to 3. But if server 1 dies, you can't.

Jump hash does allow adding more targets and preserves the property that most request targets stay the same when adding a new target, so if you had 3 targets and add a fourth, ~8% of the requests that would have been sent to targets 1-3 are sent to target 4, evenly chosen from servers 1-3.

globnomulous 12 hours ago|||
If you have a real, actual, substantive critique of either the post or package itself, I'd be interested in reading that. What you posted doesn't provide that. I'm not sure who you're talking to or what you expect your comment to accomplish.
terabyteoff 12 hours ago|||
You got me! I keep meaning to do this.

https://github.com/cloudflare/pingora/issues/1014

I have now solved the problem once and for all

agosta 12 hours ago||
We can tell you didn't read the post because it is definitively NOT garbage. Very interesting write up by the Cloudflare team - the man literally did calculus to improve something. When's the last time any of us did Calculus to improve anything? Bang up job Kevin and everyone!!
johnnyApplePRNG 14 hours ago|
[flagged]
More comments...