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
A virtual machine (VM) is a software abstraction of a physical computer (e.g. CPU, RAM, SSD, NIC) that an unmodified guest OS boots. Such a system VM virtualises the full hardware, while a process VM (e.g. JVM, PVM, §602#3.1) does only bytecode ISA for a single program. Virtualisation executes guest code natively on the host ISA and intercepts only sensitive operations, admitting many isolated workloads on a machine that historically ran merely one application at 10-15% utilisation. It is distinct from emulation, that does not execute guest code on the host CPU but translates a foreign ISA entirely in software, as when QEMU in TCG mode runs an ARM guest on an x86 host.
The component that performs this interception is the hypervisor (aka. VM monitor), which creates, schedules, and manages VMs. Type 1 hypervisors run directly on host hardware without an underlying OS (e.g. VMware ESXi, MS Hyper-V: {Azure}). Type 2 hypervisors run as applications on a conventional OS (e.g. VirtualBox, VMware Workstation), trading performance and isolation for convenience. KVM: {AWS, GCP} sits between the two, a kernel module that turns Linux itself into a Type 1 hypervisor while reusing Linux features, such as scheduler, memory allocator, and device drivers, rather than implementing its own.
Popek and Goldberg (1974) formalised the condition under which such interception can rest on hardware privilege alone. Specifically, an instruction is i) sensitive if it alters the machine’s configuration or behaves differently according to it, and ii) privileged if it traps when executed outside the highest privilege level. Where sensitive $\subseteq$ privileged, deprivileging the guest makes every sensitive operation trap of its own accord, and trap-and-emulate suffices. IBM mainframes satisfied the inclusion and virtualised cleanly for decades. x86 instead executes a number of sensitive instructions in user mode without trapping and forces software workarounds.
-
Type 1 owns the hardware, type 2 sits on a host OS.
1.2. CPU Virtualisation
On x86 the hypervisor claims the highest privilege level, so a guest kernel runs deprivileged while still expecting an authority the hardware no longer grants it. Robin and Irvine (2000) counted seventeen Pentium instructions that are sensitive but not privileged, and rather than trapping they execute with the wrong semantics. POPF restores the flags register yet silently discards the interrupt-flag bit when the caller is unprivileged, so a guest that disables interrupts merely believes it has, and SGDT leaks the host’s descriptor-table register into guest memory. The hypervisor observes neither, so trap-and-emulate has nothing to intercept.
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. It stayed tractable since only kernel-mode code required translation while user-mode code ran directly on the CPU, and cached translated blocks amortised the cost. Xen (2003) took the opposite path with paravirtualisation, modifying the guest kernel to replace sensitive instructions with hypercalls to the hypervisor, which outruns translation but demands the kernel source, so an unmodified guest such as Windows cannot boot.
Intel VT-x (2005) and AMD-V (2006) eliminated both in hardware. A new non-root execution mode and a VM control structure (VMCS/VMCB) make sensitive instructions VM exit regardless of privilege level, so the P-G inclusion holds again, after which the hypervisor adjusts guest state and VMRESUME returns control. The first generation nonetheless lost to binary translation on exit-heavy workloads, as a round trip cost roughly a thousand cycles against a cached block’s none, and hardware prevailed only as exit latency fell and the CPU absorbed page-table shadowing too. KVM (2007) exposes it to user space through /dev/kvm, while QEMU emulates the remaining devices.
-
Trap-and-emulate on the left, a hypercall from a modified kernel on the right.
1.3. Resource Virtualisation
Just as an OS multiplexes processes onto shared hardware, a hypervisor multiplexes VMs one level below, repeating the same three moves per resource, abstraction, isolation, and overcommitment. 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) since VMs rarely demand full CPU at once, but blinds the guest scheduler, which is unaware that its vCPUs are themselves preempted. Lock-holder preemption follows, where a descheduled guest thread still holds a spinlock and its peers spin on a lock whose holder is not running.
Each VM sees its own physical address space, so translation composes two partial functions, the guest’s $\pi_g$ (guest-virtual $\rightharpoonup$ guest-physical, §603#3.2) and the hypervisor’s $\pi_h$ (guest-physical $\rightharpoonup$ host-physical). Shadow page tables materialised the composition $\pi_h \circ \pi_g$ at the cost of a trap on every guest update. Extended page tables (Intel EPT, AMD NPT) instead evaluate the composition lazily in hardware. Each access of the guest’s walk then requires its own EPT walk, so a TLB miss on 4-level paging can cost up to $(4{+}1) \times (4{+}1) - 1 = 24$ memory references, yet cheaper than the shadow tables’ traps.
Memory is overcommitted as well as translated. Memory ballooning reclaims pages by inflating a balloon driver inside the guest until it surrenders physical frames, so reclamation depends on guest cooperation and a driverless guest leaves the host nothing but blind swapping, which may page out frames the guest already considers free. Kernel same-page merging (KSM) instead deduplicates identical pages across VMs via copy-on-write, trading a background scan for density.
A virtual disk is an ordinary host file (VMDK, QCOW2, VHD), which makes storage the cheapest resource to overcommit. Thin provisioning allocates physical storage only as the guest writes rather than reserving the virtual size upfront, so a 100 GB disk might occupy 20 GB. Snapshots freeze the disk state by redirecting later writes to a new differencing layer, so rollback is instant. Copy-on-write here operates at the cluster (64 KB in QCOW2) rather than the file, so a long snapshot chain pays read amplification, since each lookup walks the backing chain, while thin provisioning leaves the host to exhaust its storage once guests fill the disks they were promised.
The hypervisor connects each VM’s virtual NIC to a virtual switch, which forwards frames among co-resident VMs at memory speed and routes the rest through the physical NIC. Most cloud VMs use VirtIO, a standardised paravirtual interface whose shared-memory rings spare the hypervisor from emulating real hardware. For bare-metal performance, SR-IOV discards the software layer altogether, as a single physical NIC presents lightweight virtual functions assignable directly to VMs, while an IOMMU (Intel VT-d) confines each function’s DMA to its VM’s memory. A virtual function is PCIe state that cannot be reconstructed elsewhere, so migratable instances stay on VirtIO.
Since a VM is ultimately CPU/memory state plus virtual disk files, live migration moves a running VM between hosts by copying memory pages in rounds while the VM keeps executing, then pausing briefly (typically under 100 ms) to transfer the final dirty pages and switch execution. The iteration converges only while pages move faster than the guest dirties them, so a write-heavy VM on a narrow link forces the hypervisor to stop the guest outright, whereas post-copy migration inverts the order by resuming on the destination first and faulting pages across on demand. Server consolidation and multi-tenancy on this basis gave rise to cloud computing.
II
2.1. Container
OS-level virtualisation (aka. containerisation) shares a single host kernel rather than booting one per instance. It reduces startup to sub-seconds and footprint to megabytes at the cost of weaker isolation, in that a kernel-level escape would compromise the host and every container it runs. Specifically, a container is not a kernel primitive but a user-space abstraction built from two Linux kernel features, namespaces and cgroups, to restrict a process’s view of the system and bound the hardware resources available to that process. They were originated in FreeBSD jails (2000) and Solaris Zones (2005), then reached Linux through LXC (2008) and Docker (2013, §603#1.3).
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 fs, and user maps UID 0 inside to an unprivileged host UID for rootless containers, while uts, ipc, cgroup, and time isolate the hostname, IPC objects, cgroup root, and boot clock. From the host, a container’s PID 1 is just another process in the default namespace, assembled by clone() with the desired flags. A namespace, however, bounds what a process sees rather than what it consumes.
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, cgroups were merged in 2008 (Linux 2.6.24). Cgroups v2 unified v1’s fragmented hierarchies into one tree and added per-cgroup pressure stall information (PSI) for observability. The first two fail differently, as a container over CPU quota is throttled whereas one over its memory limit is killed.
What a process may ask the kernel to do is, however, bounded by neither feature but by three further mechanisms. i) Linux capabilities: root’s authority decomposed into roughly forty independent privileges; ii) Seccomp: a Berkeley packet filter (BPF) program screens all system calls; and iii) security modules: mandatory policy on file, socket, and capability access under AppArmor or SELinux. Each lets a container bind a low port without also being able to load kernel modules, cuts the reachable surface to the calls actually needed, and enforces policy its own root cannot alter, respectively.
-
Kernel features underlying containers (5 of 8 namespace types shown).
2.2. Docker
Isolation alone did not make a workload shippable while dependency packaging remained manual. Docker answered with a declarative, layered image model, where an image on disk is an immutable filesystem (fs) template, built once to serve many workloads. One uses a Dockerfile to build the image’s rootfs through FROM, RUN, and COPY steps, yet identical layers are stored once and skipped on pulls, and each step instead yields a content-addressed read-only layer (i.e. fs diff). The open container initiative (OCI) standardised image and runtime specifications, letting an image run on any compliant engine, while registries such as Docker hub distribute the images.
At build time, layer immutability decides what is recomputed. Each instruction’s cache key derives from its parent layer’s digest and the instruction itself, with a checksum of the copied files for COPY. A change at one step gives its layer a new digest, every later key inherits it through its parent, and the cache misses from that point down. This is also why Dockerfile order is structural rather than stylistic, as placing dependency manifests before application source confines a rebuild to the final steps. A RUN key is the command string rather than its effect, however, leaving RUN apt-get update to reuse a stale layer until an earlier step changes or –no-cache forces re-execution.
Immutability decides not only what is rebuilt but also what the image ships. A layer records the filesystem state a step leaves behind rather than the operations it performs. Deleting a file in a later step therefore merely masks it with a whiteout marker while the bytes remain, which is why cleanup belongs inside the instruction that creates the artefact (e.g. RUN apt-get update && apt-get install -y … && rm -rf /var/lib/apt/lists/*). Multi-stage builds answer the same problem structurally, where a FROM … AS builder stage compiles and a later stage copies only the finished artefact via COPY –from, thus the toolchain never enters the shipped image.
-
The client only talks to the daemon, which pulls from the registry and runs containers.
At runtime, Docker turns an image into a container, a running isolated process with one writable layer. The CLI sends build, push, and run requests to the Docker daemon through its REST API (§605#4.2), usually over a local Unix socket (§603#3.1), but also over TCP. On docker run, the daemon delegates containerd to prepare the rootfs, where its snapshotter stacks the writable layer over the image layers with OverlayFS (§603#3.3). It then invokes runc, the OCI reference runtime, which creates the namespaces with clone(), applies cgroup limits, and starts the image’s entrypoint as PID 1. Docker desktop runs hidden Linux VM for non-Linux hosts such as macOS and Windows.
The writable layer is what keeps the image immutable, but it fails persistent state in both permanence and performance. For instance, a container lives only while its PID 1 does, docker rm deletes the stopped container with its layer, and thus the next redeploy erases any library installed into the writable layer via docker exec app apt-get install curl. Performance instead fails when a container modifies a file held in a read-only layer. That is, the file cannot change in place, OverlayFS performs copy-up (i.e. copies the whole file up into the writable layer and edits it). One INSERT into a multi-gigabyte SQLite file shipped in the image therefore begins by copying gigabytes.
Mounts escape both problems by placing data outside the writable layer. Docker offers three. i) volumes: Docker-managed directories (/var/lib/docker/volumes/) for databases and persistent application data, ii) bind mounts: a chosen host path mapped into the container for live code reloading in development, and iii) tmpfs: files held in memory alone for short-lived secrets (e.g. TLS keys, API tokens). In every case the mount shadows the image content at its destination path, where Docker seeds a new empty volume from that content whereas a bind mount simply hides it.
-
One host directory mounted into two containers at once.
2.3. Docker Networking
A network namespace begins with nothing but a loopback interface, and a container therefore has no path off the host until one is built for it. Docker places one end of a virtual ethernet (veth) pair inside the namespace as eth0 and enslaves the other to a software bridge (the default docker0). The bridge’s address (172.17.0.1, private per RFC 1918, §605#2.1) then serves every container as its default gateway. The host thereby acts as an L2 switch among its containers (one broadcast domain, §605#1.3) and as an L3 router beyond them. Since no host elsewhere routes 172.17.0.0/16, an outbound packet from, for example, 172.17.0.2 leaves masqueraded behind the host’s address.
Inbound traffic must instead be published since an external client cannot name a private IP address. For instance, -p 8080:80 publishes via a DNAT rule rewriting the destination (host:8080 to 172.17.0.2:80), while EXPOSE merely records intent. Docker also writes rules of its own via iptables into the host’s firewall, the rule list against which the kernel admits or drops every packet by its tuple $($address, port, protocol$)$. The kernel consults Docker’s entries before those a tool such as UFW administers, thus a published port remains open to the LAN even after a deny, and the deny holds only from the DOCKER-USER chain, which Docker consults first.
Reaching other containers is a separate matter. The default docker0 affords L2 forwarding but no name resolution. That is, a container reaches another by IP address, while restarts may reassign the address. Docker instead provides user-defined bridge networks, carrying their own subnet (172.18.0.0/16, §605#2.1) with an embedded DNS server (127.0.0.11), which resolves container and alias names to current addresses. In practice, Docker compose automatically creates one such network per project from a YAML ain’t markup language (YAML) file and starts containers in dependency order. Containers on separate bridges remain isolated, as no rule forwards between them.
The isolation hardens across machines. An L2 bridge is confined to its host, thus the docker0 on two hosts each issue 172.17.0.0/16, and neither has a path to the other. An overlay network supplies one by wrapping container frames in UDP packets between hosts (VXLAN, §605#1.2). Wrapping costs 50 header bytes, hence the MTU of 1450. Paths that filter ICMP break path MTU discovery (§605#1.3) and full-size packets vanish, thus overlay faults surface as hangs on large responses rather than refused connections. Reachability is nonetheless the smaller half. The larger half, scheduling and repairing workloads across machines, falls to orchestration.
-
Default docker0 and a user-defined bridge, each an isolated subnet behind the host NIC.
III
3.1. Container Orchestration
A single host suffices for development, but production must schedule workloads across a cluster (a set of networked machines), restart failures, balance load, and roll out updates without downtime. Container orchestration treats the pool as a single logical compute surface and maintains a desired state (e.g. “run 5 replicas with 2 CPUs and 4 GB each”) which a control loop restores by correcting observed drift. The desired state is thus a fixed point of the reconcile map, toward which the loop drives the system anew after every disturbance, and declaring the fixed point rather than the path to it is what distinguishes orchestration from imperative commands.
Kubernetes (K8s ), built at Google on Borg, open-sourced in 2014, separates a cluster into a control plane and worker nodes. Cluster state lives in etcd, a Raft-replicated key-value store that only the API server reads or writes, so every other component watches that entry point (§605#4.2), never the store. A scheduler places pods by filtering infeasible nodes and scoring the rest. A controller manager runs one control loop per resource type, each reconciling its slice. A kubelet on each node starts its assigned pods through the container runtime interface (CRI). Managed offerings (EKS, GKE) host the control plane, typically leaving users 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, as a rescheduled pod is a new pod with a new IP and a fresh filesystem rebuilt from the image rather than the old one relocated, so whatever an application keeps locally is lost at that moment. Stateless workloads absorb this, and stateful ones do not.
-
Every arrow ends at the API server.
3.2. K8s Workloads
A pod alone has no self-healing, so a failed node erases its pods. Deployments close this gap for stateless applications with a replica count and a pod template, and find them by label selector over labels such as app: nginx. A Deployment acts only through a ReplicaSet, which holds one template, drives the matching pod count toward it, and replaces failures. That indirection makes updates reversible, as a rolling update shifts replicas to a fresh ReplicaSet for the new template, while the old survives at zero so rollback shifts them back. The Horizontal Pod Autoscaler resizes replicas on metrics, whereas the Cluster Autoscaler adds nodes when pods fit on none.
Not every workload tolerates interchangeable replicas. A Raft or Kafka quorum addresses members by identity and expects each to return with its log, which a ReplicaSet cannot give since its pods are anonymous and their storage dies with them. StatefulSets supply it, as each pod receives a stable ordinal name (pod-0, pod-1) and DNS record, a volume that survives rescheduling, and ordered startup and shutdown, so a rolling update replaces one member at a time. The remaining controllers vary the loop over other targets, where DaemonSets run one pod per eligible node for node agents and device plugins, Jobs run a pod to completion, and CronJobs schedule them.
Pods also need configuration and storage decoupled from the image. ConfigMaps and Secrets inject environment variables or mounted files so the same image runs unchanged across environments, though a Secret is base64-encoded rather than encrypted and is guarded only by etcd access control and RBAC until encryption at rest is configured. PersistentVolumeClaims (PVCs) request storage from the cluster and StorageClasses provision it dynamically (e.g. an EBS volume), which keeps manifests portable but binds the claim to the volume’s failure domain, so a pod whose zonal volume has no schedulable node in its zone stays Pending.
K8s namespaces (distinct from Linux namespaces) partition a cluster into logical units (e.g. dev, staging, prod) that scope resource names and access policies. A ResourceQuota caps the aggregate CPU and memory a namespace may claim, so one team’s workloads cannot starve another’s, and RBAC roles bind permissions at the same boundary, so a user or service account holds rights within its namespace and nothing beyond. Every resource is declared as a YAML manifest applied via kubectl, where kubectl apply merges the declaration into the desired state held by the API server rather than issuing imperative commands, the same fixed point the control loops then maintain.
-
Deployment drives replica scaling (3 to 5) and rolling updates.
3.3. K8s Networking
Kubernetes replaces the host-local bridge with a flat network where every pod holds a cluster-routable address and reaches any other without NAT, so a service sees its caller’s real address. The cluster must now assign cluster-unique addresses and route them everywhere, the problem CNI (container network interface) plugins solve. They divide on defaults, where Flannel encapsulates pod frames in VXLAN and pays the header cost to run anywhere, Calico advertises pod routes over BGP to travel unencapsulated wherever the underlay carries them, and Cilium programs the datapath in eBPF, which extends policy to L7 where Calico stops at L3/L4 and Flannel omits it.
Pod IPs change on every restart, so Services typically provide a stable virtual IP (ClusterIP) and DNS name that load-balance across whichever pods currently match a label selector. On each node kube-proxy programs that mapping into iptables rules, whose count grows with services and endpoints, so newer clusters move to its nftables mode or an eBPF datapath that hashes to a backend in constant time.
Reaching a Service from outside the cluster is a separate ladder, where NodePort opens the same port on every node from a default 30000-32767 range yet offers no single address to publish, LoadBalancer puts a cloud load balancer in front of those ports and so supplies the address at the cost of one balancer per Service, and Ingress (or its successor the Gateway API) terminates TLS and routes on hostname and path so that many Services share one balancer, 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. MicroVM runtimes (e.g. AWS Firecracker, which underpins Lambda and Fargate) and sandboxed runtimes (e.g. gVisor’s user-space kernel) occupy the middle ground, and restore per-workload hardware isolation at near-container startup cost. The unit of deployment has moved from a physical server to a VM to a container to a pod, but the underlying goal is unchanged, to 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 .