Architecture
Big picture
Longhorn splits cleanly into a control plane and a data plane. The control plane is longhorn-manager, a single Go binary that runs as a DaemonSet, one pod per node. It owns the CRDs and the controllers that reconcile them. The data plane is the per-volume engine and replica processes, started inside longhorn-instance-manager pods on each node, with the actual block I/O handled by longhorn-engine. The manager never serves I/O itself; it declares desired state and drives the data plane toward it.
Components
CLI and daemon entrypoint (main.go, app/)
The binary uses urfave/cli to bundle several subcommands: daemon, recurring-job, csi, pre-upgrade, post-upgrade, uninstall, and more (main.go:63-74). The manager's resident mode is app.DaemonCmd(). startManager (app/daemon.go:250) wires clients and calls controller.StartControllers (app/daemon.go:332).
CRD types (k8s/pkg/apis/longhorn/v1beta2/)
Every Longhorn object is a CRD: Volume, Engine, Replica, Node, InstanceManager, snapshot and backup types, ShareManager, backing image types, EngineImage, RecurringJob, Orphan, and others. register.go registers them into the scheme.
Controllers (controller/)
The core. There is one controller per CRD. controller/controller_manager.go:43 constructs 30+ controllers, and each is launched with its own go ...Run() loop (for example controller/controller_manager.go:191). Controllers do not call the Kubernetes API directly; they go through the datastore package.
Datastore (datastore/)
A thin wrapper over informer caches and typed clients. Controllers read and write objects through c.ds.*, which keeps cache consistency in one place and makes the controllers mockable in tests.
Scheduler (scheduler/)
scheduler/replica_scheduler.go holds the pure placement logic that decides which node and disk a replica lands on, evaluating anti-affinity, zones, node selectors, and disk capacity.
Engine API (engineapi/)
The client boundary to the data plane. engineapi/instance_manager.go:16 imports longhorn/longhorn-instance-manager/pkg/client, and the client holds an InstanceServiceClient and a ProcessManagerClient (engineapi/instance_manager.go:55-56). This is how the manager asks a node's instance manager to start or stop engine and replica processes over gRPC.
CSI, webhook, upgrade (csi/, webhook/, upgrade/)
csi/ implements the CSI driver for dynamic provisioning, snapshots, and expansion. webhook/ holds admission and conversion webhooks. upgrade/ handles migration between versions.
How a request flows
Creating a volume from a PersistentVolumeClaim runs end to end like this:
- The CSI driver creates a
VolumeCR (k8s/pkg/apis/longhorn/v1beta2/volume.go:454).VolumeController.processNextWorkItem(controller/volume_controller.go:252) dequeues it intosyncVolume(controller/volume_controller.go:307). syncVolumefetches the volume withc.ds.GetVolume(controller/volume_controller.go:320), then checksisResponsibleFor(controller/volume_controller.go:334). If this manager is not the owner it returns immediately. Otherwise it claims ownership by writing its node intoStatus.OwnerID(controller/volume_controller.go:342) and lists the engines, replicas, and frontends (controller/volume_controller.go:355).- The reconcile body (
controller/volume_controller.go:603-659) callshandleVolumeAttachmentCreation, thenReconcileEngineReplicaState(controller/volume_controller.go:607), thenReconcileVolumeState(controller/volume_controller.go:655), thencleanupReplicas(controller/volume_controller.go:659). A deferred block (controller/volume_controller.go:550) flushes spec and status changes and requeues on conflict. - Replica top-up runs in
replenishReplicas(controller/volume_controller.go:3066). It first tries to reuse a failed replica viaCheckAndReuseFailedReplica(controller/volume_controller.go:3118); failing that it goes throughRequireNewReplica(controller/volume_controller.go:3142) and creates a newReplicaCR withnewReplicaCR(controller/volume_controller.go:3143). - Scheduling happens in
ScheduleReplica(scheduler/replica_scheduler.go:66), which callsFindDiskCandidates(scheduler/replica_scheduler.go:138),getNodeCandidates(scheduler/replica_scheduler.go:213),getDiskCandidates(scheduler/replica_scheduler.go:301), and finallyscheduleReplicaToDisk(scheduler/replica_scheduler.go:673) to fill inReplica.Spec.NodeIDandDiskID. ReplicaControllerandEngineControllerpick up their CRs and both delegate to the sharedInstanceHandler.ReconcileInstanceState(controller/instance_handler.go:324); the engine path iscontroller/engine_controller.go:373.- To create the real process,
InstanceHandler.createInstance(controller/instance_handler.go:544) calls the controller'sCreateInstance(controller/engine_controller.go:630), builds a client withengineapi.NewInstanceManagerClient(controller/engine_controller.go:655), and issuesc.EngineInstanceCreate(controller/engine_controller.go:718). This gRPC call crosses from control plane into data plane and starts the process on the target node.
Key design decisions
- Microservice per volume. One volume equals one engine plus N replicas, each an independent process, rather than a shared pool behind one controller. This keeps the blast radius to a single volume and lets each volume be scheduled and upgraded on its own. The cost is process count and overhead that scale with the number of volumes.
- CRDs as a state machine.
VolumeSpecis the desired state,VolumeStatusis the observed state, and the controller reconciles the gap. Real I/O is the engine's job; the manager only orchestrates it declaratively. - Distributed ownership instead of a single leader. Because the manager runs on every node, Longhorn does not elect one active controller. Each CR carries
Status.OwnerID, and a manager reconciles only the CRs it owns.syncVolumebails out early whenisResponsibleFor(controller/volume_controller.go:5965) returns false. Ownership is pinned to the volume's attach node, which keeps the manager controlling a volume on the same node as its engine. - Datastore abstraction. Controllers never hold a raw Kubernetes client; they go through
datastore.DataStore, balancing cache consistency against testability.
Extension points
- CSI driver (
csi/): the standard integration surface for Kubernetes provisioning, snapshots, and expansion. - Admission and conversion webhooks (
webhook/): validate and convert CRDs across API versions. - CRDs themselves:
Volume,Node,RecurringJob,BackingImage, and the rest are the public API that tooling builds on. - Backup targets: S3 and NFS endpoints for backups, configured through settings and CRDs.