Virtualisation
Hardware used to dictate what software could do. Virtualisation inverted that relationship. Since IBM CP-40 (1967), the story has been to “abstract the machine away” $\to$ “slice it thinner” $\to$ “pack more workloads onto fewer boxes”. Began with hypervisors that virtualise entire computers, then containers that isolate without duplicating the kernel, and now orchestrators that manage thousands of both.
I
1.1. Virtual Machine
Physical servers historically ran one application each at 10-15% capacity. Virtualisation solved it by executing guest code natively on the same ISA, intercepting only sensitive operations, allowing dozens of isolated workloads per machine. Hence, a virtual machine (VM) is a software abstraction of a physical computer with its own virtualised CPU, RAM, disk, and NIC, where a full guest OS boots and runs unmodified. While virtualisation is distinct from emulation (§602#3.1), which translates a foreign ISA entirely in software (e.g. QEMU in TCG mode running an ARM guest on an x86 host), it gave rise to cloud computing via server consolidation and multi-tenancy.
A hypervisor (aka. VM monitor) creates and manages VMs. Type 1 hypervisors (bare-metal) run directly on host hardware without an underlying OS, e.g. VMware ESXi, MS Hyper-V (Azure), and KVM (AWS, GCP). Type 2 hypervisors (hosted), e.g. VirtualBox and VMware Workstation, run as applications on a conventional OS, trading performance and isolation for convenience. KVM blurs the boundary: it is a kernel module that turns Linux itself into a Type 1 hypervisor, reusing Linux’s scheduler, memory allocator, and device drivers rather than implementing its own.
Popek and Goldberg (1974) formalised when this interception can be done purely by hardware privilege. An instruction is i) sensitive if it alters machine configuration or behaves differently depending on it, and ii) privileged if it traps when executed outside the highest privilege level. If sensitive $\subseteq$ privileged, every sensitive operation traps automatically when the guest runs at reduced privilege, and trap-and-emulate suffices. IBM mainframes satisfied this condition, and virtualisation thrived there for decades. x86 did not, as certain sensitive instructions executed silently in user mode instead of trapping, forcing the software workarounds that §1.2 traces.
1.2. CPU Virtualisation
On x86, instructions such as POPF and SGDT are sensitive but not privileged, so they execute silently in user mode instead of trapping. Two software workarounds emerged. VMware (1999) introduced binary translation, scanning the guest instruction stream at runtime and rewriting sensitive instructions into safe sequences that trap or emulate correctly. Xen (2003) took the opposite path with paravirtualisation, modifying the guest kernel to replace sensitive instructions with explicit hypercalls to the hypervisor.
Intel VT-x (2005) and AMD-V (2006) eliminated both workarounds in hardware. A new non-root execution mode and a VM control structure (VMCS/VMCB) cause sensitive instructions to VM exit to the hypervisor regardless of privilege level, satisfying P-G by hardware extension. The hypervisor handles the exit, adjusts guest state, and executes VMRESUME to return control. KVM (2007) exposes this hardware to user space through /dev/kvm, while QEMU emulates the remaining devices (virtual disks, NICs, display) and manages the guest lifecycle.
1.3. Resource Virtualisation
Just as an OS multiplexes processes onto shared hardware, a hypervisor multiplexes VMs one level below: abstract, isolate, overcommit. The hypervisor presents each VM with virtual CPUs (vCPUs) scheduled onto physical cores. Overcommitment allows a host with 64 cores to run 200 vCPUs (~3:1 for general workloads) under the assumption that not all VMs demand full CPU simultaneously, but introduces scheduling complexity: the guest OS scheduler is unaware that its vCPUs are themselves being preempted. Lock-holder preemption is when a guest thread holding a spinlock is descheduled, and other guest threads spin wastefully waiting for a lock whose holder is not running.
Each VM sees its own physical address space, but the hypervisor must translate guest-physical to host-physical addresses. Without hardware support, shadow page tables merged these two mappings, trapping on every guest page table update. Extended page tables (EPT on Intel, NPT on AMD) add a second level of translation in hardware, eliminating the need to trap on guest page-table updates. Each guest page-table access itself requires a full EPT walk, so a TLB miss on 4-level paging can cost up to 24 memory references, but this nested overhead is far cheaper than the constant trap-and-update cost of shadow tables. Memory can also be overcommitted: memory ballooning reclaims pages from underutilising VMs by inflating a balloon driver that forces the guest to surrender physical frames back to the host, and kernel same-page merging (KSM) deduplicates identical pages across VMs via copy-on-write.
Storage follows the same pattern of indirection: virtual disks are files (VMDK, QCOW2, VHD) on the host file system. Thin provisioning allocates physical storage only as the guest writes data rather than reserving the full virtual disk size upfront, so a 100 GB virtual disk might occupy only 20 GB on the host. Snapshots capture the disk state at a point in time by redirecting subsequent writes to a new differencing layer, enabling instant rollback.
Each VM also needs network access. The hypervisor assigns one or more virtual NICs connected through a virtual switch (e.g. Open vSwitch) running on the host. The virtual switch forwards frames between VMs on the same host at memory speed and routes external traffic through the physical NIC. Most cloud VMs use VirtIO devices, a standardised paravirtual interface where the guest cooperates with the hypervisor through shared memory ring buffers, avoiding the overhead of emulating real hardware. For workloads that need bare-metal network performance, SR-IOV (Single Root I/O Virtualisation) allows a single physical NIC to present multiple lightweight virtual functions directly assignable to a VM, bypassing the virtual switch entirely, while an IOMMU such as Intel VT-d isolates each function’s DMA to the correct VM’s memory.
Because a VM is ultimately CPU/memory state plus virtual disk files, live migration can move a running VM between physical hosts by iteratively copying memory pages while the VM continues executing, then briefly pausing (typically under 100 ms) to transfer the final dirty pages and switch execution to the destination.
II
2.1. Container
OS-level virtualisation (aka. containerisation) eliminates the per-VM OS overhead by sharing a single host kernel, reducing startup to sub-seconds and memory to megabytes per instance. The tradeoff is weaker isolation because all containers share the same kernel, so a kernel-level escape can compromise the host and every container on it. FreeBSD jails (2000) and Solaris Zones (2005) pioneered this model on their own kernel mechanisms, while LXC (2008) and Docker (2013) brought it to Linux (§603#1.3). Specifically, a container is not a kernel primitive but a user-space abstraction built from two Linux kernel features, i) namespaces that restrict the process’s view of the system; and ii) cgroups that limit the hardware resources available.
Both are documented in the Linux man-pages project. A namespace (kernel/nsproxy.c) wraps a global resource so processes inside see their own isolated instance. Linux provides eight types: pid gives each container a PID tree rooted at 1, net gives it a private network stack, mnt with pivot_root() swaps the visible root filesystem, and user maps UID 0 inside to an unprivileged host UID for rootless containers. From the host, a container’s PID 1 is just another process in the default namespace, assembled by calling clone() with the desired namespace flags.
A cgroup (kernel/cgroup/) organises processes into hierarchical groups and caps their hardware resources. Without cgroups a single container could exhaust host memory or monopolise CPU, so the kernel enforces limits on CPU shares, memory (with an OOM killer scoped to the cgroup), I/O bandwidth, and device access. Driven by Google’s experience running Borg on shared machines, cgroups were merged in 2008 (Linux 2.6.24). Cgroups v2 unified the fragmented v1 hierarchies into a single tree and added per-cgroup pressure stall information (PSI) for observability. Note that cgroups also underpin Kubernetes resource requests and limits. Container isolation is further strengthened by Linux capabilities, seccomp filters, and security modules such as AppArmor or SELinux, which restrict the system calls and privileges available to processes inside the container.
2.2. Docker
Namespaces and cgroups isolate a process, but shipping it with its exact dependencies remained manual. Docker built on Linux container primitives but introduced a declarative, layered image model where a Dockerfile builds the rootfs incrementally (FROM, RUN, COPY), each step producing a content-addressed read-only layer that is shared on disk and skipped during pulls. The open container initiative (OCI) standardised the image format and runtime specification, making images portable across any compliant runtime, while registries such as Docker Hub and Elastic Container Registry (ECR) handle distribution.
Docker turns an image into a running container. The Docker daemon exposes a REST API over a Unix socket (local) or TCP socket (remote) through which the CLI builds, pushes, pulls, and runs containers. On docker run, the daemon delegates via containerd to runc (i.e. the OCI reference runtime). runc mounts the image layers as the container’s rootfs via OverlayFS, then isolates and starts the process using clone() and cgroup limits. The container lives as long as its PID 1 runs, and so its storage is ephemeral. Notice that on non-Linux hosts (e.g. macOS, Windows), Docker Desktop runs a hidden Linux VM for these underlying kernel features.
Writes inside a container go to the thin OverlayFS writable layer, a directory on the host that is deleted when the container is removed, and so Docker provides three mount types to outlive the ephemeral writable layer: i) volumes mount a Docker-managed directory (e.g. /var/lib/docker/volumes/) at the VFS layer, bypassing OverlayFS entirely; ii) Bind mounts map arbitrary host paths into the container’s mount namespace; and iii) tmpfs mounts store files entirely in memory rather than on persistent storage. Volumes suit databases and persistent state, bind mounts enable live code reloading during development, and tmpfs protects secrets that should never touch disk.
2.3. Docker Networking
Docker connects each container, isolated in its own network namespace, to a bridge (i.e. docker0) via a virtual ethernet (veth) pair and assigns a private IP (e.g. 172.17.0.0/16). Outbound traffic is NATed through iptables, and port mapping (-p 8080:80) adds DNAT rules for inbound. Whereas, containers on the same bridge communicate via the bridge’s L2 switch (i.e. L2 forwarding). In fact, user-defined bridge networks add their own subnet (e.g. 172.18.0.0/16) with an embedded DNS server (127.0.0.11) for name resolution. Containers on different bridges are fully isolated, and –network host bypasses namespace isolation entirely for direct host-level access.
Docker Compose builds on user-defined bridges to orchestrate multi-service applications from a YAML file, creating containers in dependency order with name-based service discovery. However, this only connects containers on the same host because a bridge operates at L2. Overlay networks tunnel container traffic between hosts via VXLAN, so containers on different machines communicate as if co-located. It is where single-host tooling reaches its limit, as managing containers across multiple hosts requires automated scheduling, health checking, and service discovery. §III extends these primitives to cluster-wide orchestration.
III
3.1. Container Orchestration
Running containers on a single host suffices for development, but production deployments must schedule workloads across a cluster (a set of networked machines), restart failed instances, balance load, and roll out updates without downtime. Container orchestration platforms solve this by treating a pool of machines as a single logical compute surface. The orchestrator maintains a desired state (e.g. “run 5 replicas of this container with 2 CPUs and 4 GB each”) and continuously reconciles reality to match it through a control loop that observes the current state, computes the difference, and takes corrective action. This declarative model distinguishes orchestration from imperative commands.
Kubernetes (K8s ), developed at Google building on Borg and open-sourced in 2014, separates a cluster into a control plane and worker nodes. The control plane runs i) an API server as the single RESTful entry point; ii) etcd as a distributed key-value store with Raft consensus; iii) a scheduler that filters infeasible nodes then scores the rest; and iv) a controller manager that runs one control loop per resource type. Each worker node runs a kubelet that watches the API server for pod assignments and delegates container creation via the container runtime interface (CRI) to a runtime such as containerd or CRI-O. Managed services (e.g. AWS: EKS, GCP: GKE) host the control plane so that users only manage worker nodes and workloads.
A pod is the fundamental scheduling unit, a group of one or more containers that share a network namespace, storage volumes, and a lifecycle. Most pods run a single container, but the abstraction allows co-locating tightly coupled containers as sidecars (e.g. a web server alongside a log collector or service-mesh proxy) that share localhost and are scheduled together. Pods are ephemeral by design, so applications must either be stateless or externalise their state, a constraint that naturally encourages scaling and resilience to node failures.
3.2. K8s Workloads
A pod by itself has no self-healing: if the node it runs on fails, the pod is gone. Deployments solve this for stateless applications by declaring a desired replica count and a pod template. The Deployment finds its pods via label selectors that query labels, key-value pairs (e.g. app: nginx) attached to K8s resources to express ownership and relationships. The Deployment controller creates a ReplicaSet that ensures the specified number of identical pods are running at all times, automatically replacing any that fail. On an update (e.g. a new container image version), the Deployment performs a rolling update by gradually creating pods with the new template and terminating old ones, and supports rollback to the previous ReplicaSet if the new version fails. The Horizontal Pod Autoscaler (HPA) adjusts the replica count based on CPU, memory, or custom metrics, while the Cluster Autoscaler adds or removes nodes when pods cannot be scheduled.
Not all workloads are stateless and interchangeable. StatefulSets manage workloads that require stable network identities (pod-0, pod-1, …), ordered startup and shutdown, and persistent storage that survives pod rescheduling, making them suited for databases and distributed systems such as Kafka or ZooKeeper. DaemonSets ensure exactly one copy of a pod runs on every eligible node, commonly used for logging agents, monitoring daemons, or device plugins. Jobs run a pod to completion rather than indefinitely, suited for batch processing, and CronJobs schedule Jobs on a cron expression for periodic tasks such as backups or report generation.
Pods also need configuration and persistent storage decoupled from the image. ConfigMaps and Secrets inject environment variables or mounted files, so the same image runs unchanged across development and production. PersistentVolumeClaims (PVCs) request storage from the cluster, and StorageClasses enable dynamic provisioning so that creating a PVC automatically allocates the underlying volume (e.g. an EBS volume on AWS or a GCE persistent disk), keeping workload definitions portable across clusters and cloud providers. K8s namespaces (distinct from Linux namespaces) partition a cluster into logical units (e.g. dev, staging, prod), scoping resource names and access policies. All resources are declared as YAML manifests and applied via kubectl, the primary CLI.
3.3. K8s Networking
Docker’s bridge network is host-local, so containers on different hosts cannot route to each other without additional overlays or port mapping. Kubernetes replaces this with a flat network model: every pod receives a cluster-wide routable IP and pods communicate directly without NAT, preserving the source IP end-to-end. The cluster delegates implementation to CNI (container network interface) plugins such as Flannel (VXLAN overlay), Calico (BGP-based routing with network policy), or Cilium (eBPF-native with L7 visibility).
Pod IPs change on every restart. Services provide a stable virtual IP (ClusterIP) and DNS name that load-balances traffic across pods matching a label selector. On each node, kube-proxy programs iptables or nftables rules to forward packets destined for the ClusterIP to a healthy backend pod. To expose services externally, NodePort opens a static port on every node, LoadBalancer provisions a cloud load balancer that routes to NodePorts, and Ingress (or its successor Gateway API) defines HTTP-level routing (hostname and path matching, TLS termination) implemented by controllers such as Nginx Ingress or Traefik.
From hypervisors that virtualise entire machines, to containers that share a kernel, to orchestrators that schedule across clusters, each layer trades isolation for density and abstracts the one below it. The unit of deployment has moved from a physical server to a VM to a container to a pod, but the underlying goal remains the same: pack more workloads onto fewer boxes.
(C:)
I gathered words solely for my own purposes without any intention to break the rigour of the subjects.
I also prefer eating corn in spiral .