Saving another 100TB of RAM
blog.cloudflare.com
blog.cloudflare.com
But costs on the cloud are real too, especially now. I’ve been living in JVM land for a very long time, but now it’s especially clear how important lean services are. Especially now that the bar for writing lean code is so much lower: let the borrow checker figure it out, etc.
I just spent a couple days wringing out more performance/memory efficiency for our services. Nice gains to be sure, but it’s still so immensely wasteful compared to something well written running native. If it was my money, I’d be going native for sure.
I don't think even Cloudflare bothers with this waste of time. If they did, they would certainly not have built their global infrastructure on JavaScript running on V8. They'd have done what Google and old-time Facebook did and built their whole infrastructure on low-level system languages, and hiring the world's leading minds on the subject to milk the last drop of performance from their hardware.
Even Google stopped to look at the problem and came up with Go. Not V8.
> The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.
From Rob Pike
Also, it really does matter what you're building. Yet another CRUD app? Don't waste time on (super) lean languages - use Go or something else you like and move on.
Building a Kafka replacement? Making something that processes gobs of data quickly? Perhaps think about using something lean and efficient, as the economics have changed. "waste of time" (or memory) also applies to the cloud and your (or your company's) bill. And if you haven't noticed, you're being ripped off on the cloud, running something 10-50x less efficient has real bottom line impact.
Some of us find production and optimization more interesting than marketing and distribution.
The problem statement is of applications using up expensive RAM. Incidentally, expensive RAM is just one of the problems we face in the computing space. Forced obsolescence is another, when running hardware needs to be replaced because software is built for only newer CPUs.
You are right, but it reads (to me, could be just me) you mean newer specs which used to be true when I shipped software in the 70-90s; you mean faster machines / more memory right? Like installing a new version of software or OS and suddenly all memory is used, system is swapping and you did not ask for that but some obscure feature you didn't need needed to be shipped fast.
You need to stop and think about the problem. Nowadays RAM is expensive because many people now want to max out their computers with RAM to run LLMs and AI coding assistants. Today's software is still the same software that ran perfectly well half a dozen years ago. Cloudflare happens to operate a large global computer infrastructure, and it's scale is such that 1% gains are lauded as fantastic cost savers. But that's the bean counter's perspective, pointing out that they saved a bean.
This statement is quite honestly not true at all. Just take the two largest OS from 6 years ago and compare resource usage between them and you will find you are incorrect, never mind the software running on it.
Source? I imagine the amount of people trying to run local LLMs is miniscule. RAM is expensive because a handful of companies have spent billions buying all of the compute.
— Humanity has been on the decline. People are hateful towards each other, striking their fellow man and poisoning the environment. Despots eventually launched nukes which killed everyone but the cockroaches.
— And yet the world still turned.
For example the likes of Poland, China, India and Vietnam were and still are growing like crazy when you compare to the likes of UK or Germany.
You seem confused. Allocating more memory than optimal levels is not a measure of quality. Similarly, a web page is not suddenly lower quality if an image asset is 50kb instead of 25kb. And how much complexity and engineering effort and bugs are you willing to tolerate to halve your memory allocations?
You are conflating quality with mindless minimization, not even knowing or caring that are the tradeoffs. The blog post you're commenting on starts by presenting the case for celebrating small improvements, even 1% improvements at a time. A similar 1% improvement in a mobile app is at like 1MB. Do you ever notice it? How many hours of engineering effort are you hoping to spend on this nonsense? And you prefer to spend it on this or in actually fixing a bug or implementing a feature?
This puerile conflation of minimization with quality suggests your personal notion of quality has no bearing on what quality actually is.
You seem to be confused about this relationship. It's usually exactly the opposite.
The wasteful applications are generally not well reasoned about and half assed implementations. That's why they're guzzling resources
There is ofc a middle ground, because targeting eg incredibly resource constrained embedded systems will naturally increase complexity, but that's something entirely different to the scenario this discussion was about up to this point.
But with software and a install base of some millions 1% optimization is seen as wasteful, its like software slop is acceptable since forever because hardware gets faster, and electricity is "green" anyway.
Ultimately it is an expression of anti-fragility: some minimal challenge must be met and conquered on many different axes of a production in order to help the full production itself mature to the best possible quality.
It is the same reason we exercise, the same reason that cars and toilets and so many products greatly increase in quality after emissions and usage and waste-related regulations get applied from above, etc.
The appreciation of the refining force of outside limitations is not about min-maxing, but it is at least partly about curbing the min-maxing of other concerns such as "ship the fastest garbage possible to move on to the next opportunity to repeat that process".
What if they want memory efficiency
The OP, GGP and GP are about reducing memory usage by software
Pretending the parent question is about a different subject is unconvincing. The context does not support it
So this hypothetical is wrong.
You can get a new laptop to run software A or competing software B.
You can pay $1000 for the laptop with 4GB of RAM, and software A either can't run on that or will act like pulling teeth, so that software requires you to upgrade to 16GB of RAM which will put you back another $2000.
Or you can run software B which will hum along perfectly smoothly on the original $1000 laptop.
(All of that of course simplifying right past "everyone runs every app 24/7 with five trillion browser tabs open")
Are you honestly suggesting that the end user doesn't care about having to throw exponentially more money at stuffing RAM into their PC just to make the game all their friends are playing online function?
That's speculative to the point of being conspiratorial.
> Are you honestly suggesting that the end user doesn't care about having to throw exponentially more money at stuffing RAM into their PC just to make the game all their friends are playing online function?
I'm extremely clearly not suggesting that. Don't make up things and pretend that other people said them. Engage honestly or not at all.
Isn't this mainly Cloudflare's scale though? That's literally also what's in the introduction written as the reason why they are doing the optimization
Tried out the first 1000 words in Pangram, and it seemed happy it was human written. Not surprised either, it has been some of the better writing I've seen out of Cloudflare recently.
Do you know it was actually AI? I've seen humans imitating AI "it's not X — it's Y" to try to fool it that it correctly guessed was still human. Humans did use those patterns before AI so they're not enough on their own to flag.
I don’t know if I’m just a lot more sensitive, but I find it really hard to write up even basic communication with Claude. Super overwrought.
You use the first N bits of your key hash to pick the server partition so it’s a reasonable number (eg 128 servers per partition). Then use high quality precomputed hashes (first 64 bits of sha256) for the server name as N in H(K + N). Use wymum from wyhash as the H so that you do o(n) integer multiplications while retaining a result that’s still a good hash statistically.
Now you’re using a tournament hash, the small N means O(N) vs O(N log N) doesn’t matter, and also this O(N) is also going to be much less CPU than computing 160 hashes per key as they do now, so much less latency added per request.
That in effect boils down to consistently selecting server S with probability P, where P is function of weight and total number of servers?
Surely there must be better way to select server with a given probability without storing a massive lookup table of hashes? Randevouz hashing of some sorts
I also expect as AI becomes more cost sensitive once the quality plateaus (there's only so many ways to get an answer to 100% right), the data centers are going to chase where the cheap power is, and this long term is likely to be in high-solar locations. So lower latitudes. Doesn't rule out places like Texas of course, but places like India, Mexico, Brazil, Israel or Saudi Arabia will have home field advantages.
One wants to turn on an indicator on a remote device. A simple Boolean value. But we need networking, TLS, authentication plugins, certificate validation, distributed logging, containers, orchestration, HTTP client/server, interprocess communication, daemon dependency management, …
Sure, one can say each of these layers and abstractions has an important and justifiable purpose. But one can also step back and start wondering - what the hell are we really doing???
At some level, it seems like each layer of abstraction has to manage others, only simply because they exist.
Imagine the simplicity of 1800s telegraph signaling - no software!
Too often we build systems with Fortune-50 style hierarchies when a 5-person team could do the whole job.
An 1800s telegraph system doesnt work in the modem world, there is far too much communication and the system would just collapse into molten slag.
All those things you've listed are because we live in an adversarial world and I'd steal all your money off the telegraph wire if you tried it.
But Oracle probably deleted then so you'll have to find them on archive.org.
Whereas for somewhere like Burger King, the costs are all rent and staff, so they wouldn’t care how much RAM their website uses. But you would see similar optimisation in their supply chains for their ingredients, and their rotas to reduce staffing.
Are you trying to succinctly say AL'S spaghetti code outweighs the benefits of what it produces quickly?
If you're not saying it outweighs it, what are you saying?
Around the year 2015.
The team that owns it needs to understand it. Everyone else can just use it.
I found the motivation pretty lackluster: nowhere does it actually explain why you use consistent hashing (dividing the item space naively/regularly would actually cause much more than 1/n items to move, which is unintuitive) and how you actually use it.
That said, it got me to spend a few minutes studying this and got me to understand the key bit I was missing.
It is purpose-designed for exactly this type of proxy/cache load-balancing scenario!
I also really appreciate the fact that this is human-written and not just AI slop. It’s refreshing to actually read English instead of Claudelish.
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...)
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
The part I missed is that the load balancers don't have a consistent view of the set of servers. There is no magical synchronization scheme that creates that consistent view. You want load balancers with slightly different ideas of what servers are available to mostly make the same choices for the servers they do agree on.
Doh! I should have been able to infer that from the original solution.
Indeed the statistical model described in the article does not model the distribution over server hash allocations you'd get if you allow them to be inconsistent across load balancer hosts, so the model actually models (and thus implies) a single global source of truth that they probably don't have in practice.
I can't do the math to prove it, but their solution still seems wrong to me. Rather than generating and storing and searching so many hashes, it seems like you should get partway there with a different sampling procedure that doesn't do quite as well with the inconsistent sets of servers, and then only use duplication to limit the consistency loss.
Simple example: use their scheme but instead of choosing the first server to the left of the probe, grab the first two and flip a coin to decide which one to use. That already spreads the bucket variance out a bit, without using any extra space. It does have a penalty in that if one balancer has a server that the other doesn't, then it spreads out the range of probes that could get a disagreement. But I don't know how to quantify that; if the balancers disagree on the set of servers available, you have to produce different results part of the time, and I haven't thought through how to characterize when that disagreement is "bad".
Then you could extend that to looking at the previous 8 servers. Or the previous k tickets, if you give each server a ticket for each weight unit.
The math works out easier if you sample regions of probe space rather than server counts: hash the incoming task, map that to a range of space on the number line, and all servers within that range are your candidate set. Choose from that set, making the candidates be either equally weighted, weighted proportionally to their weight (size/capacity/whatever), or weighted by how much they got shafted by the random distribution of the server hashes.
I get EBRAINTOOSMALL when I try to work out the statistics, especially when I try to figure out what the inconsistency cost is, but intuitively it still seems better than recording a bajillion hashes for each server. (With the latter sampling mechanism, you'd need to deal with the possibility of probing a window with no server in it, either by double hashing the task and trying again, or expanding the probed region. Details schmetails.)
In practice, I'd probably simulate it and look at the distributions. Or nerd snipe a math geek.
You could solve that by storing the new-query flip result, but the goal was reducing storage…