Submind YouTube summaries
Thumbnail for How Deliveroo cut ECS compute costs with spot instances

How Deliveroo cut ECS compute costs with spot instances

Watch on YouTube

Video summary

Deliveroo addresses the challenge of optimizing cloud costs while maintaining high availability by leveraging AWS Spot Instances to handle traffic spikes during peak lunch and dinner hours. Unlike homogeneous workloads with steady usage, Deliveroo's business model experiences significant fluctuations, meaning their baseline is covered by Savings Plans but they rely heavily on On-Demand instances for peaks. To reduce expenses without sacrificing reliability, the company adopted a strategy of running resilient, stateless workloads on Spot Instances, which offer up to 90% discounts compared to standard pricing despite carrying risks like unexpected termination and limited availability during high-demand periods such as Black Friday. To mitigate these inherent risks, Deliveroo established strict eligibility criteria for services eligible to run on Spot instances. A key rule is that any service running on Spot must have at least two replicas; this ensures that if a Spot instance is terminated, another replica remains active on an On-Demand node to handle traffic with only degraded performance rather than complete downtime. Additionally, the company prohibits stateful applications and services using local storage from running on Spot instances because losing data upon termination would be catastrophic. They also explicitly exclude Kafka workers from this strategy to avoid expensive rebalancing costs if a worker is disrupted, ensuring that all eligible workloads can shut down quickly within the two-minute notice period AWS provides before terminating an instance. The implementation involves sophisticated automation using the AWS Spot Placement Score API and custom Lambda functions to proactively manage capacity decisions. Because checking availability for every scaling configuration hits API limits, Deliveroo uses a percentile-based approach on their Auto Scaling Group maximum size to estimate likelihood scores efficiently. They developed logic that automatically rolls back Spot usage if the score drops below a specific threshold or during critical events like product launches, and they implemented a mechanism to deregister instances from load balancers before termination occurs. This prevents 502 errors caused by traffic being routed to containers that have already been killed but whose underlying instance hasn't fully terminated yet, prioritizing user experience over strict theoretical availability windows. Ultimately, this approach allowed Deliveroo to gradually increase their Spot usage percentage without impacting system reliability or underutilizing their Savings Plans. By starting with a small percentage and incrementally scaling up based on performance data, they achieved meaningful cost savings while maintaining robustness against interruptions. The solution is highly adaptable; although built for AWS ECS, the decision-making engine can be extended to other platforms like Kubernetes by adjusting node affinity configurations rather than relying solely on cloud provider-specific tools. This workload-aware automation ensures that fine-tuning happens at the application level, allowing Deliveroo to safely navigate market volatility and infrastructure constraints while keeping their food delivery platform running efficiently for millions of users.
Read the full video transcript
Okay, cool. Hi guys, I'm Akash. I work for Deliveroo. I manage the platform space at Deliveroo. So, today I'll be speaking about cost optimization at scale. And how do we do that without causing a hit on availability or reliability on the system. So, that's what I'll be speaking about today. So, when we speak about cost optimization, any uh Oh, good to Cool. So, yeah. Whenever we talk about cost optimization or reducing our cloud cloud cost, the very first thing any enterprise does is they buy cost saving plans, which sort of brings down your uh AWS spend. But, in our case, uh we are a food delivery company. So, we don't have a homogeneous traffic throughout the day. And given the nature of our business, we see a lot of spike during lunch and dinner time, which also represents in this graph. This is a AI-generated image, which demonstrate how our cost look like. So, our baseline usage is covered by savings plan, but we do run a lot of on-demand during lunch and dinner peaks. And this is where uh we wanted to save money. So, yeah. As I said, our baseline usage is covered, but we still have a lot of on-demand spikes uh throughout the day, which we wanted to manage. So, to uh save on the uh spiky usages, we opted for a spot. Spot So, uh there is no demand. AWS just gives it off at a very cheaper price with a lot of caveats. So, theoretically, it offers up to 90% discounts over on-demand pricing. But, yeah, given uh it's cheap, it comes with a lot of limitations. So, for example, uh any spot instance, it can be terminated at any given time. So, for example, a company demanded 1,000 instances for on-demand. So, the moment they get a uh request for 1,000 on-demand instances, they'll immediately take back all the spot instances. So, that being said, any spot instance can go off at any given time. And availability of an uh spot instance also fluctuates based on demand. So, if AWS has a lot of demand, you'll see less uh spot instances available. If it's a lot of hardware is free, they give spot instances. For example, during Black Fridays or any critical sales, you'll see the spot market is not that great. And uh another limitation is it gives you 2-minute notice before it starts terminating your instance. So, if it wants to take back the capacity, it gives you a 2-minute notice. You can clean up or do whatever you want. And if you don't do any of that in like 2 minutes, it will forcefully terminate your uh instance. So, these are the limitations with spot instance. So, now to adopt uh spot, the workloads must be resilient, stateless, and capable of shutting down quickly. Given now we understand what are the limitations, so what we did was we came up with a criteria to select what all workloads can actually uh run on spot. So, uh to ensure a workload can actually handle spot instance, the criteria that we came up with that the instance should be uh sorry, the service or the workload, it should be able to shut down within 2 minutes cuz that's the max time uh AWS will give you. Wait a minute. Yeah. Another thing we did was we ensured that any service which is running at least two replicas run on spot. This is another eligibility criteria that we have uh added. So, our uh thesis behind this was that we never run more than 50% spot. So, that being said, if a service is running more than two replicas, one of its replica will always be on on demand and even if spot goes completely off, there's something running and managing your traffic instead of a complete downtime, you have uh degraded service, but at least it is running. So, that's another criteria that we have defined that any service should be running more than two replicas to be eligible to be running on spot. And another thing that we do is uh any service which should not have any stateful thing running on it or shouldn't be using any local storage on it. That's another criteria that we have defined. And there's one more criteria which isn't on the slide, but we ensure that none of the Kafka workers run on uh spot instances cuz if Kafka workers get disrupted, the entire rebalancing thing is pretty expensive. So, we ensure that none of the Kafka workers run on spot instances. So, at this point we know what are the limitations and how we can get around them. So, we have built uh this criteria and any service that fulfills this criteria, only those services are eligible to run uh on spot. This is our eligibility criteria. So, now by now we we know that what exactly is spot and what all workloads we should run on spot. Next thing that we wanted to ensure is uh we wanted a way to predict if a spot instance is going to be available or not. This is to ensure that we don't end up getting high interruptions. So, Wait a minute. Yeah. Sorry, I'm just struggling which slide is there. Yeah. Uh So, to get this, we came across AWS's spot placement score API. So, this is a API that AWS exposes. What it does is it takes a configuration and tells you what are the likelihood of getting a spot place spot instance of the configuration. So, when I say configuration, it essentially means uh the number of instances I want to run. So, for example, I say I want to run 250 instances. And then I give what kind of instances I want to run. So, based on that configuration, AWS will give me back a score between 0 to 10. Where 0 means there's less likelihood of getting an instance. And 10 means that you'll get a spot instance for 250 instances. So, what we did was we wrapped this API and made a metric out of it. So, we run this API, trigger this, and emit a metric based on our requirements and get a spot placement score. And based on that, we take all the decision part. But, uh this obviously have some limitations. So, when we started using this API and emitting metric, the moment we went live, we saw that the metric isn't coming in after an hour. And what happened is uh spot placement score has a constraint that you can only check SPS for limited number of configurations in a day. What that essentially means is so in our the cluster is always scaling up and down. So, if I want to check SPS for uh 250 instances, that's one configuration. And if I want to check SPS for 250 260 instances, that's another configuration. So, as a cluster scales up and down, uh for SPS, it's a completely different uh configuration altogether. So, that being said, it started throttling us that you can't uh check SPS for so many configurations. And initially, we wanted to check for SPS for whatever number of instances we are running at that given point in time. But, given this constraint, we couldn't do that. So, to overcome this uh limitation, we came up with a strategy that broadened our SPS visibility without hitting these API limits. I'll take help of a diagram to explain this. So, what we did was, we use ASG's max size as our uh metric. So, basically, let's say my ASG has uh max size of 1,000 instances. We break it down into multiple chunks, percentile-based capacity. So, for example, P10 is 100 instance, P20 is 20 200 instance, and so on till P100. That way, now, instead of checking uh multiple configurations, we are only checking for uh 10 configurations at a time, and AWS is fine with that. So, at any given point, our Lambda is emitting metric, what's the SPS score for running 200 instance, 300 instance, and so on. And now, what we do is uh we check how many instances we are running. So, for example, we are running 300 instances. We check what's the closest SPS score for that. So, in this scenario, for 300 instances 310 instances, P30 is the closest one. We check, okay, what's the SPS score for P30? It's 8.5. And accordingly, we will take the decision if we want to run spot or not. So, uh when I say threshold, this is something we had to do a lot of uh hit and try. So, there's no one rule that this works for everyone. We started off with a value of eight, but we realized we are rolling back spot a lot and paging people for no reason. After running spot for some 6-7 months, we learned that SPS of six is good for us. That's the threshold that we use. If SPS goes below six, we roll back spot. If it's above six, it's fine. So, this is a value that we have uh come up with after running spot for 6-7 months. So, at this point, uh we know what spot is, what all workloads we should be running on spot, and how to take the decision if we should be running spot at this point or not. So, now we needed some automation where we can take all this logic and without any hurdle roll out spot or roll back spot or do anything like that. So, what we did was we built an automation around this SPS score. And this allows us to basically uh proactively roll out spot or let's say SPS is trending downwards, so we can roll back spot and there are some critical periods where we don't want to run spot. For example, there's any product launch or something of those sorts. So, it helps us to uh roll it back instantly. And whenever the conditions are favorable or the SPS score is good, we can roll back uh roll out spot again without any uh lot of operational overhead. So, that's the idea behind coming up with this automation. So, I'll uh walk you through the key components of this automation. So, the very first thing that we have is our data miner function. What it does is, it takes a service and analyzes where it should be running. Whether it should be so we defined the eligibility criteria. It runs that eligibility criteria against that service and sees if it is compatible to run on spot or not. And once it's done that, it also checks if it is running on spot or not. So for example, service A should be running on spot and it is not running in spot. So it will emit that service out that service A, it should be running here. Next component we have is an updated lambda. It is also a AWS lambda. What it does is >> errors. And when we started looking into those errors, most of those errors were coinciding with spot uh interruptions. And upon looking into them, we realized all of these uh fiber two errors actually are caused by uh spot interruptions. So, let me walk you through a diagram. So, what exactly happens whenever there's a spot interruption notice, ECS will mark that node for drain. Let's say this is our node running three tasks. And when the node is marked as draining, it will start moving tasks to other healthy nodes, but it will only kill the running task when the new tasks are up and running. So, assume there are these uh three tasks running. Task A and B, the replacement tasks are not up and running yet and the two-minute window is over. So, what happens is this instance gets terminated and as a part of getting terminated, it first kills all of its processes running on that instance and then the instance is killed. And between container getting killed and instance getting killed, there's a brief period of 4 to 5 seconds where this instance is still attached as a target on the load balancer and load balancer can still send traffic. So, what happens is when this instance gets traffic, the container is dead and it gets back 502. So, if you're a small small company, it might not matter to you, but at our scale, it was a big problem and we had to fix it. So, to fix this we came up with another lambda. And what we does did is whenever there's a spot interruption notice, it's a event on AWS, so you can do whatever you want. It triggers a deregister lambda. Deregister lambda is nothing but what it does is identifies what's which node is getting interrupted and goes on that node, figures out what all tasks are running on that node, and what is the corresponding service, and what is the corresponding ALB for that service. And it waits for 90 seconds for ECS to do the clean, uh you know, removing and deregistering and everything. But for example, the in this scenario, task C was dead in 2 minutes, but after 90 seconds, task A and task B was still running. So, it will figure out all those things and make a deregistration call to the ALBs and deregister this instance from all the ALBs. So, what happens here is after 90 seconds, all the tasks will not get any traffic and as a result, the users will not get any 502 errors. So, the catch here is we take a hit on sort of availability. I mean, our systems can be loaded, but the fact that these instances would have been killed in another 30 seconds and it's better to take a little bit more load than serving 502s. So, that's our rational. That's a risk that we have took that it's fine to send traffic to other healthy nodes. And another thing is while we kill this, the new replacement tasks are already in the process of coming up. So, maybe not in 30 seconds, they might come up in 50 seconds, but they will be up and take the traffic. There's one thing I want to talk about. I talked about in the eligibility criteria that we look for any service that can shut down in 2 minutes, but we missed the part that if a service is not starting in 2 minutes, it will still get killed and impact. And that's the reason we had to come up with this whole business. But the problem was when you talk about stop time, you can configure that, but when you talk about startup time, there are a lot of variables that come into picture. So, your app can start potentially in 2 seconds or 10 seconds, but then when you talk about a container getting started, you need to ensure that the new underlying infrastructure is ready. So, if the underlying host is not ready, if the networking for that is taking more time, It all adds up to task startup time. So, that being said, there's no easy way to add this part in eligibility criteria that the service to start in 2 minutes or 30 seconds or whatsoever cuz there too too many factors in play when it comes to task startup time. So, this is the lambda we came up with and handle the scenario. There are some other scenarios as well which we are working right now on figuring out how we can manage them or handle them. So, one being There are times when we see a lot of high interruption rate within a short span of time. So, for example, 30% of our capacity goes out within 10 minutes. So, this is one thing that we observe or monitor. Right now, we don't have automation around it. We are still trying to figure out what's the right thresholds when we should kick in the automation. So, if this happens that 30% or 40% of our capacity goes off within say 30 minutes. Right now, we page someone and then someone comes online and looks if this is something where they should roll back or not. But, this is our future plan where we will come up with some sort of automation once we figure out what should be the right threshold for this. Another thing that we have observed is at times what happens is whenever there's an interruption AG is unable to spin up the replacement instances. So, what happens is let's say 30 instances are killed. AG will spin up new 30 instances, but there are times when it fails to do so for 20 minutes or something like this. So, right now, we are paging for it, but we are trying to figure out what's the right threshold where we can say, you know, if AG is unable to get the replacement instances for 20 minutes, just roll back from spot. So, we are trying to figure out what's the right threshold. Also, sometimes it's just one-off instance and where ASG is unable to replace, but then it's fine. So, these are the two things which is sort of manual in not manual, where the decision-making is manual, but rolling back and everything is automated. Uh coming to what we see after running uh all the spot. Again, this is a AI-generated image. So, after running spot, what we've started seeing is a lot of our peaks are now run by uh spot instances. And on-demand usage is going down. And this is after a conservative rollout. We are not yet at a point where we are running 50% spot. So, we want to ensure that our rollouts are uh gradual and there's no hit on the availability of the system. So, that's why we are doing gradual rollouts. And uh with this, this is where we see that we are not underutilizing our savings plan as well. And uh we are not spending too much on on-demand. So, with this, we have achieved meaningful savings without impacting our availability of the system or reliability of the systems. And given we have this automation in place, it helps us to do safe and gradual rollouts. We don't have to, you know, say run everything on spot. We can do 5% spot, 10% spot, 30% spot, and so on. And we know it's safe cuz any service that runs on spot goes through that eligibility criteria. And the biggest advantage we've got is that whatever uh fine-tuning we are doing, it's on the workload level and not on the infra level. This is our biggest advantage. So, with all this automation in place, even though this right now seems like very ECS specific, but these principles can be very easily applied on any platform, say Kubernetes. So, in ECS world, we are updating capacity provider configuration, but say tomorrow we want to move to Kubernetes, all we got to do is just extend our tool, do all the decision making, and instead of updating capacity provider configuration, we can just update the node affinity part of Kubernetes deployment, and then Carpenter take care of it. So, in ECS world, there isn't a lot of options to use from open space world or anything. It's very cloud provider tight, but Kubernetes world has a lot of tools available, but when we were looking at it, there's nothing that can take decision make do the decision making part for it. So, for example, when people use Kubernetes, Carpenter is a one of the tool that everyone uses. It can run your workload, it can decide when to run on on demand and when to run on spot based on different parameters, memory, CPU, or architecture, but it cannot take workload aware decisions that is this workload eligible to run on spot or not. So, this is what our engine does. It takes the decision, and then it tells the orchestrator that you run it on spot or not. So, irrespective of the platform, tomorrow we can do the same thing on Kubernetes. Take the do the decision making and let Carpenter take care of rolling out stuff and running the underlying hardware. So, that's my time, guys. Thank you. Uh Open for questions. Sure. >> So yes, the question is how do we ensure that some of the replicas are running not on spot? So in ECS world for any service, you define the capacity provider configuration. What it essentially is Are you Are you comfortable with Kubernetes? So okay, in Kubernetes world, think of it like node affinity configuration where we say two node groups and give weights. So for example, we say uh 40% on spot. So in Carpenter world, you give node pool. And there we we give weighted things. So similarly in ECS, you have cap provider configuration where you can say which capacity provider it should use. So for any service, we say run 40% on spot and 60% on demand. So that being said, and our criteria is we never want to go more than 50% for spot. So at any given point, if a service is running two workloads, one of the workload will be on on demand. Yep, yep, yep. So every workload runs on two node groups. One is on demand and one is spot. We just change the percentages. So uh it could be 0% spot and 100% on demand or it could be 50% spot, 50% on demand. Any more questions? Yeah. Yep. So that's our percentage. Uh what we did, we started doing a gradual rollout. So we first started with 5% spot cuz there's no one rule that works for everyone in spot. You have to run, see what works, and accordingly adapt. We started off with 5% spot and see that okay, everything is working fine, there's no problem, and if there are any problems, we catch on early. So, for example, the problem I said about 502s. So, if we were doing 50% spot, it would be a massive problem, but given it was just 5%, we got it early, worked on it, fixed it, and then gradually rolled it out to 10%, 20%, 30%. Since we do gradual rollouts and see how spot does, how our services are responding to it. Are there any issues or are there any errors? Accordingly, we decide, and we have come up with the criteria that we will never go above 50. Given if we do that, there's a risk that if spot goes off 100%, which is rare, but does happen, we don't want to be in a problem like that, that we don't have underlying compute to run our workloads. So, to answer your question, it's us who decide that percentage. It's a human thinking that 5, 10. Exactly. So, for example, during Black Friday sale, Black Friday is the time where you don't have any spot availability. So, we saw that okay, spot is doing really bad. We are seeing high interruptions. So, first we tried reducing the spot percentage, and then during Black Friday, we completely rolled back spot. We set it to zero. And once the Black Friday was gone, we saw that okay, our SPS score is looking good. We started rolling it out again, but instead of going directly to 50 or 45, we realized okay, 30 is a very good starting point for us. Instead of 5%, now we started at 30, and every week we just added 5, 10%, and aim to reach 45% or 50%, wherever we see that okay, everything is healthy, and our systems are reliable, and there's no hit on availability of the system. Yeah. >> I'm sorry, can you When you say data service, you mean any service which has uh state involved? No, so uh while doing the technical design of this, this was very clear to us that we do not want to run anything which is stateful cuz if the instance is gone, the state is also gone. So, that's the reason we consciously make the decision that we do do not want to run any stateful application on spot instances. Same goes with Kafka cuz if the Kafka worker is moved to a different node or shut down, it has to do all the rebalancing and all that which is expensive. So, during the design phase, while coming up with the criteria, we came up with the criteria that if you these are the limitations and that's the reason we don't want to run these services on spot. So, before working on any of this, uh we spent some time doing our research and writing a coming up with a technical design and how it should be implemented. So, during that phase, we figured out, "Okay, this is not the right thing." Any other questions? Multi-region? So, when you say about multi-region, uh the way we handle multi-region is very different. So, it might not be relevant to this. So, we don't really run service uh in a way that >> Mhm.