02 Feet in the Cloud, 03 Serverless Development

Not all that Glitters is Gold: Limitations & Refactoring

Reading Time: 5 minutes

Functions are amazing and we’ve been through a lot of tradeoffs to try and figure out when it’s better to use Functions over Containers. Alas, there are some tasks that are just impossible to do with Functions either because the function model inherently prohibits you or due to technical limitations. This will provide more insights into the difference between Functions and Containers. Lastly, this series of articles would finish with a few more general development and refactoring related concerns and tradeoffs.

Limitations

Image Size

Functions need to be deployed and scaled extremely fast to meet the on-demand invocation requirements. When a new micro container instance is required, it may not exist on the underlying server or may not be up to date. Part of the provisioning process is binary copying the micro container image from its repository to the underlying server. As the transfer speed is high but still limited, the micro container image size / the function code is limited to 250mb total. That includes its runtime, layers and dependencies/packages.

This limit is barely a concern for a Container, but still image size greatly affects the provisioning time. Higher provisioning time would make scaling up longer than needed. This would make it harder to catch up with traffic surges. This is why it is still a best practice to keep image size to a minimum. Fargate limits image size to 10GB and Container Registries are limited to 10GB-20GB.

Storage Persistency

Both Lambda and Fargate do not provide a long living 100% persistent storage whatsoever. They do differ in their storage lifetime:

  • A Function’s storage would be deleted when an instance is terminated.
  • A Container’s storage on Fargate would be deleted only when the task is stopped.

If you need persistent storage, Serverless is not the way to go [more on this in The Filing Cabinets: Persistent Storage].

Storage size and sharing

Lambda’s storage size is limited to 512MB and can not be shared between function instances, thus should not be counted on between invocations/requests. The best practices suggests to use this storage to “cache static assets locally”, preferably during bootstrap so it can be reused between invocations and speed up each invocation processing. 

With Fargate you can mount a volume of up to 20GB that can withstand a container restart. This opens up an entire world of possibilities, for example:

  • On bootstrap/run, cache the entire static contents that your web application requires which can’t be added during the image build
  • A caching layer with a TTL (Redis with persistence, e.g.)
  • Temporary session storage [see Irresilient Sessions: Stateless Applications previously in this series]
  • Temporary storage for async operations (instead of uploading an image to S3 for further processing, store it locally and have another thread/actor of the application) process it later

Resources

Lambda allocates vCPU in a linear proportion to memory. Memory has a minimum allocation of 128MB (0.07 vCPU) and a maximum of 3GB (1.67 vCPU), with 64MB increments. If you require heavy duty processing with multiple CPUs, Functions will not do. Go with Containers.

Fargate can allocate up to 4 vCPU and up to 30GB of memory per composition (called Task in Fargate terminology), divided between the composed containers. If you need more than that, Serverless is not the way to go more on that in the next series of articles].

Processing Duration

Lambda has a timeout of 15 minutes. Functions are not intended for long running processing, so it depends on the processing time mean and variance and predictability.

Think about a processing of a 100GB video, a resize of each frame independently of another. If it would take an hour Lambda just won’t do. In order to make it applicable to Functions, you’d need to somehow split the file into multiple segments for concurrent processing. You’d need to ensure that each one takes less than 15 minutes. It can be done, but you’d need to make sure it’s worth the effort of splitting up and putting it back together.

Supported Languages

Node.JS, Python, Java, Ruby, C#, .NET Core and PowerShell are supported and maintained by AWS. You can implement your own Runtime and support any programming language, as long as you won’t cross the image size limit. I wouldn’t rush into doing that, unless your entire company needs support for that and agree that it would require continuous maintenance and update.

Supported OS and processors

Lambda and Fargate both work with Linux only. ECS and EKS both support Windows Container. Do notice that a container engine (e.g. Docker Engine) can work with almost any Linux based OS, but a Container needs to be built per processor architecture (x32/x64/ARM v6/ARM v7, etc).

It’s currently irrelevant, but AWS has recently (2019/2020) started pushing their own ARM based servers, for cheaper prices. It’s only a matter of time before Lambda/Fargate/ECS would support ARM based Containers.

Refactoring

I’ve mostly talked and considered either entire new applications or ones that can already run both in a Container and as a Function. There is a third kind of application, one that requires refactoring to move from a Container to a Function.

Is it worth the time spent?

Let’s presume that all the tradeoffs lead into “it should have run as a Function” as it could have saved the company a big annual sum. Forgive me for stating the obvious but do not forget that refactoring could take days and weeks. Refactoring can harm a system’s resilience for a while.

I really wouldn’t like to be the one to extract entire business flows from an Apache based web application where you don’t know where it starts and where it ends. It may not be ready for concurrently running instances and would need adjustments to distributed computing. How much effort you’d need to invest to make the application stateless? Not to mention if you’d have to replace the dependent database thus stepping into migration. Avoid migration which could take forever.

Are you sure that all that time spent on refactoring the code would be worth the future costs saved? If it’s already in a Container and working fine – just let it be.

Does it scale?

I would consider a refactor if an application running in a Container does not meet scaling and availability requirements and you do not have the know how of design for high availability. If you do not know how to take a single Container instance and scale it up to multiple instances running in multiple availability zones and if you do not know how to change the code accordingly, I would consider skipping refactoring for multiple Container instances and jump straight to refactoring or entirely re-coding it as a Function. 

Even if you know how to design and change for high availability, the problem could be that Containers would not scale as fast or as resilient as required. Consider concurrency control as the major tradeoff.

The Middle Way

Consider following a strategy called “strangler application”. Extract only a portion of the code, the one that does not scale, and run it and only it as a Function. You’d be able to synchronously invoke it from within the Containerized application using the Invoke API or with an HTTP request to another service. That would be a safe and a minimal refactor.

Leave a Reply