Choosing a consumer service type for message queue processing: deployment or job or serverless?

Search for a command to run...

No comments yet. Be the first to comment.
Do you have your security engineer running behind you to remove secrets off git? Or keep getting alerts in gitleaks or in your CI security scanning tool that someone inadvertently committed a secret in git? Developers might forget to sanitize the app...

It is not quite straight forward to integrate DataDog with your apps, simply because of, surprise, too much documentation. There are so many options and ways to install the agent and tracer and configure them, that you can end up with the wrong confi...

As you build new projects for various teams, you realize that every time, you have to start from scratch, because there is no single solution or pattern that works in all cases. It's tricky because you can very easily bring in successes from your pas...

Here goes some techno-philosophy. As a software engineer, you learn a lot in your career. Each day produces something wonderful, something new to learn, or sometimes a new perspective on things you've been doing for years. You just need to pause and ...

I architect and partly manage a modular monolith application (~25 services) that runs on Kubernetes on cloud (Azure). I recently got a requirement for a workload with the following characteristics:
As you can see, that's a highly specific requirement, which warrants a highly specific solution.
What are some of the possible approaches we can take to solve this?
The simplest option is to just use kubernetes deployments. It is simple, native, most of the other services would already be deployments. The downside, in this very specific requirement, is the high cost to keep deployments running as each pod would need 4 cores of CPU request at least. We would have to run at least 10 replicas to support concurrency and performance.
If we knew the approximate time window of these workloads, we could schedule scale out of the VMs, get them ready up front. As we don't have a very narrow window, we need to rely on the autoscalers. Horizontal pod autoscaler does not work very well, because the avg CPU utilization of the pod takes a bit of time to hit the levels for scaling out. As the pod is idle most of the time, if concurrently multiple workloads come in, they might get assigned to the same pods, thus degrading their performance. Even if that can be resolved somehow, if there aren't enough resources on the VM, the node autoscaler starts to spin up a new VM, and then new replica gets created on that VM and then the workload has to run there. Significant delay (~2-3 minutes, but significant in our context nonetheless).
Another point to note, if we choose this route, is regarding CPU throttling. Because these are CPU intensive workloads, we need to be aware of this issue. Even if you don't completely remove limits, you need to set the limit to 2 cores higher at least than what is required.
So, it would be great to "reserve" all 4 cores for just one request. And that brings us to...
Use one job for processing one request. Jobs start fast (in the context of a 2-3 minute workload), do one thing, and free up the resources once done.
But how do you dynamically create a job? The only controller for jobs that we get from k8s is the cronjob, which is not what we want here. It would be great to trigger a job based on a request, or a message. How do you do this? You can use KEDA to trigger a Kubernetes job! Have the request to run the workload come in to a queue, use KEDA to monitor the queue and create job on each message.
While running as a job solves the problem of using up resources when not in use, it still doesn't handle the case of scaling out when the VM runs out of resources. For example, if the 16 core VM already has 3 jobs running (each say requested 5 cores), what happens when the 4th job is triggered? Node autoscaler has to start a new VM, and that causes a delay. More than the delay, it causes unpredictability in the performance of the workload. One solution to this is to reduce the CPU requests to fit in some extra pods into the existing VMs, assuming your nodes aren't peaking already. (I would have been great to have a node autoscaler in Azure that works based on cpu / memory usage %, rather than pending pods..)
Let's also try a non k8s approach to see if there are alternatives...
Use virtual kubelet to schedule the pod on a "virtual" node type, and a pod scheduled on this node is created in the ACI.
Ideally, this should have worked, however, ACI also faces the problem of having to provision the underlying infrastructure if there isn't any. I think of ACI as being placed somewhere between AKS and Azure Functions. (Aren't there enough Leaky Abstractions already?)
So, ACI also has the cold start problem and the start up times are not guaranteed (took anywhere between 0.5-5 minutes for me). It can give you up to 4 cores CPU, so if the workload could have tolerated up to a 5 min startup delay, or the unpredictability, this would have been fine.
Would Azure Functions work? The consumption plan has a 1 CPU limit, so that's not useful. Wait, AWS Lambda now has a 6 core limit in its pay-as-you-go plan! And it does work. But connecting Azure and AWS (considering everything else I had was on Azure), setting up VPCs, opening up database accesses only to the lambda, making sure the lambda is considered in the Terraform scripts... its a nightmare, and not worth the benefits.
But what about Azure Functions in the Premium plan? That works, allowing up to 4 cores. You can use an Always Ready instance, and some burstable instances (which need to be paid on a consumption basis. I believe you will still get some cold start issue even if you use Always Ready instance, I haven't actually implemented this one. But when I tried out the math in the Azure pricing calculator, it turned to be way more expensive than simply running the VMs (with 3 year reservations).
Final words:
So what did I choose?
I'm pretty sure there could be a few more possible solutions for this scenario. Even a slight change in requirement would tilt the suitability of one solution over the other.
Photo by Pichara Bann on Unsplash [and modified by me ;) ]