Once your app is running in Kubernetes or preferably even before you start, you may need to rethink parts of your application’s architecture. With good design both your application and your development team can scale and meet the demands of customers without any infrastructure or security issues that can develop with growing Kubernetes usage and improper planning.
In this part 1 of this two part blog, we discuss some common container design and optimization patterns that are used in containerized distributed applications today.
A note about Pods
A Pod is the basic building block in Kubernetes and is the smallest deployable unit that typically represents a running process on your cluster. Pods encapsulate an application’s container storage resources, a unique network IP and the configuration options on how the container should run. All containers that are part of a Pod run on the same Kubernetes node.
Kubernetes is responsible for health checks, deployments, restarts and other tasks on a Pod without the need to handle all individual containers that are a part of the Pod. Pods can include multiple containers, but if you choose to do so, all processes must be tightly coupled for greater efficiency.
For example, running both the front-end server and the backend server for your service in a single Pod with two containers would not be recommended, and instead should be run as separate Pods. Since all containers running on the same Pod are always physically co-located, running many containers as a part of a single Pod can negatively impact the performance of your Kubernetes workers.
In the above diagram (from Kubernetes docs), one container is a web server for files kept in a shared volume. A sidecar container updates the files from a remote source. The two processes are tightly coupled and share both network and storage and are therefore suited to being placed within a single Pod.
Container Design Patterns in Kubernetes
In general, design patterns are implemented to solve and reuse common well thought out architectures. Design patterns also introduce efficiency into your application and for your developers, reducing overhead and providing you with a way to reuse containers across your applications. There are several ways to group or to enhance containers inside of Kubernetes Pods. These patterns can be categorized as single node, multi-container patterns or you can use the more advanced multi-node applications patterns.
Single node, multiple container patterns
In these single node patterns, all containers are co-scheduled on a single node or machine with events occurring between containers on a Pod.
The sidecar container extends and works with the primary container. This pattern is best used when there is a clear difference between a primary container and any secondary tasks that need to be done for it.
For example, a web server container (a primary application) that needs to have its logs parsed and forwarded to log storage (a secondary task) may use a sidecar container that takes care of the log forwarding. This same sidecar container can also be used in other places in the stack to forward logs for other web servers or even other applications.
(An example of a sidecar container augmenting an application with log saving. Link )
The ambassador pattern is another way to run additional services together with your main application container but it does so through a proxy. The primary goal of an ambassador container is to simplify the access of external services for the main application where the ambassador container acts as a service discovery layer. All configuration for the external service lives within the ambassador container with the main application using the service on localhost. The ambassador container takes care of connecting to a service to keeping the connection open, re-connecting when something unexpected happens, and updating the configuration.
With this pattern developers only need to think about their app connecting to a single server on the localhost. This pattern is unique to containers since all Pods running on the same machine will share the localhost network interface.
(An example of the ambassador pattern applied to proxying to different memcache shards. Link)
The adapter container pattern generally transforms the output of the primary container into the output that fits the standards across your applications. For example, an adapter container could expose a standardized monitoring interface to your application even though the application does not implement it in a standard way. The adapter container takes care of converting the output into what is acceptable at the cluster level.
(An example of the adapter pattern applied to normalizing the monitoring interface. Link )
Multi-node application patterns
In a multi-node pattern, containers are not on a single machine or node, instead these more advanced patterns coordinate communications across multiple nodes. According to Brendan Burns, “modular containers make it easier to build coordinated multi-node distributed applications.”
Leader election pattern
A common problem with distributed systems that have replicated processes is the ability to elect a leader. Replication is commonly used to share the load among identical instances of a component, for example, and applications may need to distinguish one replica from a set as the “leader”. If the election fails, another set must move in to take its place. You may also have multiple leaders that need to be elected in parallel across shards.
There are libraries that can handle such types of elections for you, but they are limited to a particular language and can also be complex to implement. A pattern is to link a leader election library to your application through an election leader container. You can then deploy a set of leader-election containers, each one co-scheduled with an instance of the application that needs the leader election. A simplified HTTP API can then be used over the localhost network to perform the election when needed.
The idea behind this pattern is that the leader election containers can be built once and reused across your application.
Work Queue pattern
This is another common problem in distributed computing. Like leader elections it also benefits from containerization. There are frameworks available to solve the problem but again are limited to a single language environment. Instead, a generic work queue container can be created and reused whenever this capability is required. The developer then for example will then only need to create another container that can take input data and transform it the required output. The generic work queue container in this case does the heavy lifting to coordinate the queue.
(A generic work queue. Reusable framework containers shown in dark gray. Developer containers in light gray. Link)
In this pattern, which is common in search engines, an external client sends an initial request to a “root” or to a “parent” node. The root scatters the request out to a group of servers to perform a set of tasks in parallel and where each shard returns partial data. The root is responsible for gathering the data into a single response for the original request.
Like the other two patterns discussed above, there is a lot of generic code that can be put into containers to do most of the work: one container to implement the leaf node computation for example, and a second container that returns the corresponding result.
(A reusable root container (dark gray) implements client interactions and request ‘fan-out’ to developer supplied leaf containers and merge results container: Link )
In this part 1, we had a look at Pod design, and also the different containers patterns you can implement to reuse code or to make generic reusable containers that can be used in your Kubernetes applications. Next week in Part 2 we’ll take a look at Kubernetes health checks and readiness probes.
All diagrams in this post are from: “Design patterns for container-based distributed systems”