Software at Scale: Practice Like You Play
I recently learned an interesting lesson in developing software that needs to scale to large capacities.
I can't get into the specifics of what I was working on, but the essential facts are more or less as follows:
- It includes a cache algorithm
- I tested it with as much load as my workstation could generate
- In tests, it performed well as far as I could stretch it
- We deployed this system and it started falling over in difficult-to-understand ways
The frustrating thing about this process was that I followed good testing practices to the letter - or so I thought. Make sure you have good coverage, test for edge cases, think about how real use patterns will affect behavior of the system, and so on. I even set up my tests to deliberately try and thrash the cache, hoping that it would reveal any weaknesses in that algorithm.
I plotted some data from the pre-launch test runs to see how it scaled. It looked pretty much linear, and so I figured it would work fine under heavier loads, because the coefficients were small enough that linear would be well under hardware capacity even at full demand. Testing showed no flaws at small scale, and I extrapolated from the test numbers that it would scale perfectly well to the load demanded of it.
Except it didn't.
The crucial lesson here is simple: when you are designing a system that must scale to high capacity, never extrapolate your test data.
My mistake was in assuming that the linear-looking behavior would stay linear as the cache size grew and the number of active clients increased by a couple orders of magnitude; in reality, that innocuous linear-looking curve turned out to be substantially sub-linear. The actual big-O analysis of the algorithm is too intricate and boring to go into, but suffice it to say that the behavior as the cache filled degraded very quickly.
The system then fell into a state where it would take longer to satisfy requests than it took for clients to generate new requests, meaning that the backlog of load snowballed until the whole thing just ground to a halt and barely served any requests at all.
We discovered that, paradoxically, flushing the cache would result in better performance almost immediately once this happened; it only took a few seconds of profiling data collected via XPerf on a test machine to figure out why.
I rewrote the cache algorithm to eliminate several of the algorithmic complexity issues (and several practical problems that Big-O alone never would have revealed) and now throughput has jumped substantially. At this point, it takes longer to send the response of a request over the network than it does to compute that response to begin with - precisely the kind of scaling behavior we wanted originally.
TL;DR: practice like you play. If you don't test at scale, you are not ready to deploy at scale. Extrapolations from small-scale tests will be misleading, and can give you a false sense of confidence. If you can't test at scale, analyze things rigorously and relentlessly to make sure you don't have hidden sub-linear factors in your system.