⚙️ What is etcd?
etcd is a distributed, strongly consistent key-value store that serves as Kubernetes' sole persistent backing store. Every cluster object — Pods, Deployments, Secrets, ConfigMaps, RBAC rules — lives in etcd. If etcd is lost without a backup, the cluster state is irrecoverable.
etcd uses the Raft consensus algorithm to guarantee that all members agree on every write before it is committed — even in the face of network partitions and node failures.
Leader Election
A leader is elected via Raft when the cluster starts or the current leader times out. Only the leader accepts writes.
Log Replication
Every write is appended to the leader's log and replicated via AppendEntries RPCs. Committed once a quorum ACKs.
Quorum
A cluster of n nodes tolerates (n-1)/2 failures. 3-node = 1 failure. 5-node = 2 failures. Always use odd counts.
Strong Consistency
Every committed read reflects all prior writes — no stale reads from leader (linearizable). Followers serve stale reads by default.
🗄️ Data Model — Keys, MVCC & Revisions
Key Namespace Structure
Kubernetes stores all objects under the /registry prefix. The path follows the pattern /registry/{group}/{resource}/{namespace}/{name} — or without namespace for cluster-scoped resources.
/registry/apps/deployments/default/nginx/registry/core/pods/kube-system/coredns-5d/registry/core/secrets/my-app/db-password/registry/rbac.authorization.k8s.io/clusterroles/admin/registry/apiregistration.k8s.io/apiservices/v1.apps/registry/events/default/nginx.event-abcMVCC — Multi-Version Concurrency Control
etcd never overwrites values in place. Every write appends a new version keyed by the global revision (a monotonically increasing integer). Old versions are retained until compaction removes them. This is how the API server serves watch history without re-reading etcd.
# Every key has:
# - mod_revision: etcd revision when last modified
# - create_revision: etcd revision when first created
# - version: number of times this key has been modified
ETCDCTL_API=3 etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/apiserver-etcd-client.crt \
--key=/etc/kubernetes/pki/apiserver-etcd-client.key \
get /registry/apps/deployments/default/nginx \
--print-value-only | auger decode
# List all keys under a prefix
etcdctl get /registry/ --prefix --keys-only | head -30
# Get a key at a specific historical revision
etcdctl get /registry/apps/deployments/default/nginx \
--rev=12345 --print-value-only | auger decode
# Watch for changes in real time
etcdctl watch /registry/apps/deployments/ --prefix
kubectl get --raw to decode. Raw etcdctl get output will appear as binary.
Revision vs resourceVersion
| Concept | Where | Meaning |
|---|---|---|
revision | etcd internal | Global monotonic counter incremented on every write to any key |
resourceVersion | Kubernetes object metadata | The etcd revision at which this object was last written — used for optimistic locking & watch |
generation | Kubernetes object metadata | Incremented only when spec changes (not status) — used by controllers to detect spec changes |
Transactions & Optimistic Locking
The API server uses etcd transactions with a compare-and-swap on mod_revision to prevent lost updates. If two clients try to update the same object concurrently, one gets a 409 Conflict — the loser must re-fetch and retry.
# etcd transaction: only update if revision matches (CAS)
etcdctl txn <<EOF
compares:
mod("my-key") = "42"
success:
put my-key new-value
failure:
get my-key
EOF
get my-key
EOF
🛠️ Operations — Backup, Compaction & Defrag
Snapshot Backup
etcd snapshots capture the entire keyspace at a point in time. Take them regularly — this is your only recovery path if the cluster state is lost.
# Take a snapshot (run from a control-plane node)
ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-$(date +%Y%m%d-%H%M%S).db \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/apiserver-etcd-client.crt \
--key=/etc/kubernetes/pki/apiserver-etcd-client.key
# Verify the snapshot
etcdctl snapshot status /backup/etcd-20240101-020000.db --write-out=table
# +----------+----------+------------+------------+
# | HASH | REVISION | TOTAL KEYS | TOTAL SIZE |
# +----------+----------+------------+------------+
# | abc12345 | 189423 | 3847 | 6.5 MB |
# +----------+----------+------------+------------+
Restore from Snapshot
# Stop kube-apiserver first (remove its static pod manifest or stop kubelet)
# Then restore on each member with a unique --name and --initial-advertise-peer-urls
ETCDCTL_API=3 etcdctl snapshot restore /backup/etcd-20240101-020000.db \
--name=master-1 \
--initial-cluster="master-1=https://10.0.0.1:2380,master-2=https://10.0.0.2:2380,master-3=https://10.0.0.3:2380" \
--initial-cluster-token=etcd-cluster-1 \
--initial-advertise-peer-urls=https://10.0.0.1:2380 \
--data-dir=/var/lib/etcd-restore
# Update etcd's --data-dir to point to the restored directory, then restart
Compaction
Because MVCC retains all historical revisions, etcd grows unboundedly without compaction. Compaction discards all revisions older than a given revision number, freeing storage. The API server auto-compacts by default every 5 minutes.
# Get current revision
REV=$(etcdctl endpoint status --write-out=json | jq '.[0].Status.header.revision')
# Compact up to current revision (removes old MVCC versions)
etcdctl compact $REV
# Check remaining size
etcdctl endpoint status --write-out=table
# ENDPOINT DB SIZE ...
# https://127.0.0.1:2379 42 MB
Defragmentation
Compaction frees logical space but leaves fragmentation in the bbolt database file on disk. Defragmentation rewrites the file to reclaim physical disk space. It temporarily locks etcd — do it one member at a time.
# Defrag one member at a time (takes the member offline briefly)
etcdctl defrag --endpoints=https://10.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/apiserver-etcd-client.crt \
--key=/etc/kubernetes/pki/apiserver-etcd-client.key
# Then defrag the next member
etcdctl defrag --endpoints=https://10.0.0.2:2379 ...
# Verify size reduced
etcdctl endpoint status --write-out=table
Health Checks
# Check health of all cluster members
etcdctl endpoint health --cluster \
--endpoints=https://10.0.0.1:2379,https://10.0.0.2:2379,https://10.0.0.3:2379 \
--cacert=... --cert=... --key=...
# Check who is the leader
etcdctl endpoint status --cluster --write-out=table
# ENDPOINT ID IS LEADER IS LEARNER RAFT TERM ...
# https://10.0.0.1:2379 abc123 false false 14 ...
# https://10.0.0.2:2379 def456 true false 14 ...
# https://10.0.0.3:2379 ghi789 false false 14 ...
# Check member list
etcdctl member list --write-out=table
etcdctl member list --write-out=table
📐 Sizing, Tuning & Production Tips
Hardware Recommendations
| Cluster size | etcd members | CPU | RAM | Disk | Network |
|---|---|---|---|---|---|
| Small (<50 nodes) | 3 | 2 cores | 8 GB | 50 GB SSD | 1 Gbps |
| Medium (50–250) | 3–5 | 4 cores | 16 GB | 100 GB SSD | 1 Gbps |
| Large (250–1000) | 5 | 8 cores | 32 GB | 200 GB NVMe | 10 Gbps |
fsync the write-ahead log to disk before committing. Network disk (NFS, cloud HDD) with >10ms fsync latency causes leader timeouts and cluster instability. Use local NVMe or SSD. Check with fio --rw=write --ioengine=sync --fdatasync=1.
Key etcd Flags
# Election / heartbeat tuning
--heartbeat-interval=100 # ms between leader heartbeats (default 100)
--election-timeout=1000 # ms before follower starts election (default 1000)
# Rule: election-timeout should be 5-10x heartbeat-interval
# For high-latency networks (cross-region), increase both
# Storage
--quota-backend-bytes=8589934592 # 8 GB max DB size (default 2 GB)
--auto-compaction-mode=periodic
--auto-compaction-retention=1h # compact revisions older than 1h
# Snapshot
--snapshot-count=10000 # entries between snapshots (default 10000)
# Security
--cert-file=/etc/etcd/etcd.crt
--key-file=/etc/etcd/etcd.key
--peer-cert-file=/etc/etcd/peer.crt
--peer-trusted-ca-file=/etc/etcd/ca.crt
--client-cert-auth=true
--peer-client-cert-auth=true
Production Best Practices
Dedicated Nodes
Run etcd on dedicated nodes or at minimum dedicated disks. Never share etcd storage with the OS or workload volumes.
Automated Backups
Schedule hourly snapshots via a CronJob or systemd timer. Store snapshots in object storage (S3/GCS) with versioning and cross-region replication.
Monitor DB Size
Alert when etcd_mvcc_db_total_size_in_bytes exceeds 70% of --quota-backend-bytes. A full etcd becomes read-only — emergency compaction required.
Separate etcd per Cluster
Don't share one etcd cluster across multiple Kubernetes clusters. Each cluster should have its own dedicated etcd ring.
Critical etcd Metrics
| Metric | Alert threshold | Meaning |
|---|---|---|
etcd_server_leader_changes_seen_total | >3/hour | Frequent leader elections — network or disk issue |
etcd_disk_wal_fsync_duration_seconds p99 | >10ms | Disk too slow for Raft WAL writes |
etcd_disk_backend_commit_duration_seconds p99 | >25ms | bbolt page commit latency — defrag needed |
etcd_mvcc_db_total_size_in_bytes | >70% quota | Growing toward quota limit — compact & defrag |
etcd_network_peer_round_trip_time_seconds | >150ms | Peer latency too high — may cause election timeouts |
📝 Knowledge Check
etcdctl compact, but the quota is 8 GB. The file hasn't shrunk. Why?compact marks old MVCC revisions as free in the bbolt B-tree but doesn't rewrite the file. defrag rewrites the bbolt database file from scratch, reclaiming the freed pages as actual disk space.