How Do You Benchmark and Monitor etcd Health in Production?: Architecture & Implementation
Monitor etcd with its native Prometheus metrics endpoint (`:2381/metrics`), alert on `etcd_disk_wal_fsync_duration_seconds` p99 > 10ms, `etcd_server_leader_c...
Quick Answer:
Monitor etcd with its native Prometheus metrics endpoint (:2381/metrics), alert on etcd_disk_wal_fsync_duration_seconds p99 > 10ms, etcd_server_leader_changes_seen_total > 0 in 5 minutes, and etcd_mvcc_db_total_size_in_bytes approaching quota.
Detailed Answer:
etcd exposes a rich Prometheus metrics endpoint on its client port. The four critical signal categories are: disk health (WAL fsync latency), Raft health (leader changes, proposal failures), DB health (size vs. quota, compaction lag), and network health (peer round-trip time). Leader changes are the most operationally significant metric — every leader change causes a brief write unavailability and triggers all API server connections to reconnect. In a healthy cluster, leader changes should be rare (fewer than 1 per day in steady state).
Deep Dive:
Essential Prometheus alert rules:
# prometheus-etcd-alerts.yaml
groups:
- name: etcd.critical
interval: 30s
rules:
# Disk: WAL fsync too slow → writes will back up
- alert: EtcdWALFsyncLatencyHigh
expr: |
histogram_quantile(0.99,
rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m])
) > 0.010
for: 5m
labels:
severity: critical
annotations:
summary: "etcd WAL fsync p99 > 10ms on {{ $labels.instance }}"
description: |
Current value: {{ $value | humanizeDuration }}
Cause: Disk I/O contention or slow underlying storage.
Action: Check disk scheduler, iostat, and consider NVMe upgrade.
# Raft: Leader change detected
- alert: EtcdLeaderChange
expr: |
increase(etcd_server_leader_changes_seen_total[10m]) > 0
for: 1m
labels:
severity: warning
annotations:
summary: "etcd leader change detected on {{ $labels.instance }}"
description: |
Leader changes cause write unavailability and API server reconnects.
Check network between control plane nodes and disk latency.
# Raft: Proposals failing (writes being rejected)
- alert: EtcdHighNumberOfFailedProposals
expr: |
rate(etcd_server_proposals_failed_total[5m]) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "etcd proposals failing at {{ $value }}/s"
description: "Quorum may be lost or members unreachable."
# DB: Size approaching quota (default 2GB, you should set 6GB+)
- alert: EtcdDatabaseSizeApproachingQuota
expr: |
(etcd_mvcc_db_total_size_in_bytes /
etcd_server_quota_backend_bytes) > 0.80
for: 10m
labels:
severity: warning
annotations:
summary: "etcd DB is {{ $value | humanizePercentage }} of quota"
description: "Run compaction and defrag, or increase quota-backend-bytes."
# DB: Quota exceeded → etcd is read-only
- alert: EtcdDatabaseQuotaExceeded
expr: etcd_server_quota_backend_bytes - etcd_mvcc_db_total_size_in_bytes < 0
for: 1m
labels:
severity: critical
annotations:
summary: "etcd quota exceeded — cluster is READ ONLY"
description: |
Immediately run: etcdctl compact && etcdctl defrag
Then raise --quota-backend-bytes in etcd config.
# Network: Peer RTT too high → risk of spurious elections
- alert: EtcdPeerNetworkLatencyHigh
expr: |
histogram_quantile(0.99,
rate(etcd_network_peer_round_trip_time_seconds_bucket[5m])
) > 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "etcd peer RTT p99 > 100ms on {{ $labels.instance }}"
# Member: etcd member down
- alert: EtcdMemberDown
expr: up{job="etcd"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "etcd member {{ $labels.instance }} is down"
Grafana dashboard key panels — query reference:
# Panel 1: WAL fsync latency (p50, p99, p999)
histogram_quantile(0.99,
sum by (le, instance) (
rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m])
)
)
# Panel 2: DB backend commit latency
histogram_quantile(0.99,
rate(etcd_disk_backend_commit_duration_seconds_bucket[5m])
)
# Panel 3: DB size vs. quota (all members)
etcd_mvcc_db_total_size_in_bytes
etcd_server_quota_backend_bytes
# Panel 4: Proposal rate and failure rate
rate(etcd_server_proposals_committed_total[1m])
rate(etcd_server_proposals_failed_total[1m])
rate(etcd_server_proposals_pending[1m])
# Panel 5: Leader changes (should be near-zero)
increase(etcd_server_leader_changes_seen_total[1h])
# Panel 6: Active connections from kube-apiserver
etcd_grpc_server_started_total
etcd_grpc_server_handled_total
# Panel 7: Key space size (total stored keys)
etcd_debugging_mvcc_keys_total
# Panel 8: Compaction lag
etcd_mvcc_db_total_size_in_bytes - etcd_mvcc_db_total_size_in_use_in_bytes
# Large gap = fragmented space waiting for defrag
Operational runbook — etcd health check script:
#!/bin/bash
# etcd-healthcheck.sh — run daily or include in on-call runbook
ETCDCTL="etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/peer.crt \
--key=/etc/kubernetes/pki/etcd/peer.key"
echo "=== etcd Cluster Health ==="
$ETCDCTL endpoint health --cluster
echo ""
echo "=== Member List ==="
$ETCDCTL member list --write-out=table
echo ""
echo "=== Endpoint Status ==="
$ETCDCTL endpoint status --cluster --write-out=table
# Key columns: DB SIZE, IS LEADER, RAFT TERM, RAFT INDEX
# All members should have same RAFT TERM and nearly same RAFT INDEX
echo ""
echo "=== DB Size vs Quota ==="
$ETCDCTL endpoint status --cluster --write-out=json | jq -r '
.[] | {
endpoint: .Endpoint,
db_size_mb: (.Status.dbSize / 1024 / 1024 | round),
db_size_in_use_mb: (.Status.dbSizeInUse / 1024 / 1024 | round),
fragmented_mb: ((.Status.dbSize - .Status.dbSizeInUse) / 1024 / 1024 | round)
}
'
echo ""
echo "=== WAL fsync latency (last 60s via metrics) ==="
curl -s http://127.0.0.1:2381/metrics | \
grep etcd_disk_wal_fsync_duration_seconds | \
grep -v "^#"
echo ""
echo "=== Recent Leader Changes ==="
curl -s http://127.0.0.1:2381/metrics | \
grep etcd_server_leader_changes_seen_total
etcd health signal summary:
| Signal | Healthy | Warning | Critical | |---|---|---|---| | WAL fsync p99 | < 10ms | 10–50ms | > 50ms | | DB size / quota | < 60% | 60–80% | > 80% | | Leader changes / day | 0 | 1–3 | > 3 | | Proposal failure rate | 0 | < 0.1/s | > 0.1/s | | Peer RTT p99 | < 10ms | 10–100ms | > 100ms | | Fragmented space | < 100MB | 100MB–1GB | > 1GB |
Production lesson: The most commonly missed metric in on-call runbooks is
etcd_mvcc_db_total_size_in_bytes - etcd_mvcc_db_total_size_in_use_in_bytes— the fragmented space gap. A cluster can report "healthy" with 4GB DB size but only 800MB in actual use, meaning 3.2GB is fragmented garbage waiting to be reclaimed. Defrag will instantly bring the DB from 4GB back to 800MB.
Common mistake: Only monitoring the etcd endpoint that kube-apiserver connects to. Monitor all members — a follower with a slow disk creates a bottleneck because the leader must wait for quorum acknowledgment from all followers before committing a write.
Self-assessment: Can you write the three most important Prometheus alert rules for etcd from memory? Can you interpret
etcdctl endpoint statusoutput and identify a lagging member? Can you explain why fragmented space doesn't
Finished reading?
Mark as complete to claim your +25 XP and track progress.
