Learning Platform
Глоссарий Troubleshooting
Урок 03.03 · 22 мин
Продвинутый
etcdRaftMVCCQuorumEncryption at restLease

etcd: распределённое key-value хранилище

etcd — это сердце Kubernetes. Не api-server, не scheduler, не controller-manager — а именно etcd, потому что в нём живёт state кластера. Если упадёт api-server — поднимется заново, прочитает state из etcd, продолжит работу. Если потеряется etcd без бэкапа — кластер мёртв навсегда, потому что нет источника правды.

В этом уроке разбираем etcd как distributed system: Raft consensus, MVCC, watch-стримы, lease, encryption at rest. Это знание формально входит в CKA scope, но для CKAD понимать etcd важно — все performance и consistency гарантии apiserver наследуются именно от него.


etcd как KV store

etcd — это distributed key-value store, написанный в CoreOS (теперь под CNCF), с одной главной фичей: strong consistency через Raft consensus. Это значит, что после успешного write все последующие read любым клиентом любого узла увидят это значение (или более новое).

Ключи — произвольные строки (но Kubernetes использует префиксы /registry/...). Значения — произвольные байты (Kubernetes хранит protobuf-сериализованные API объекты).

# Прямой доступ через etcdctl (только на control plane узлах кластера)
ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  get /registry/pods/default/nginx --print-value-only | od -c | head

Этот вывод покажет бинарные данные — protobuf encoding API объекта Pod. Сериализуется он схемой из k8s.io/api/core/v1.

NOTE

В Kubernetes используется etcd v3 API (не v2 — v2 deprecated). Различие важное: v3 хранит ключи в lexicographic-упорядоченной BTREE-структуре (а не в иерархической как в v2), и поддерживает range queries — etcdctl get --prefix /registry/pods/ возвращает все Pod-ы кластера за один запрос.


Синхронизация: race conditions, mutex, atomic — основа distributed consistency

Raft consensus: как etcd достигает согласия

etcd — кластер из 3, 5, 7 узлов (нечётное число). Один из них — leader, остальные — followers. Все writes идут через leader. Followers только реплицируют.

Raft в etcd-кластере из 3 узлов
Client writeКлиент (kube-apiserver) делает PUT на любой etcd узел. Если это не leader — узел проксирует запрос на leader.
LeaderПринимает write, добавляет в свой Raft log (append-only), отправляет AppendEntries RPC всем followers. Не коммитит запись пока quorum не подтвердил.
AppendEntries RPC
FollowerПринимает AppendEntries, проверяет term + предыдущий log entry, аппендит к своему log, отвечает ACK leader-у. Не применяет к state machine пока не получит сигнал commit.
FollowerТо же, что у второй ноды. Параллельно с node-2.
majority ACK получен (2 из 3)
Commit + applyLeader увидел majority ACK (включая себя), commit-ит entry, применяет к local state machine (MVCC BTree + boltdb storage). Возвращает success клиенту. Followers в следующем heartbeat узнают commit index и тоже применяют.

Quorum и почему нечётное число

Write quorum = N/2 + 1. Это минимальное число узлов, которое должно подтвердить запись, чтобы она считалась успешной.

Total nodesWrite quorumFailure tolerance
110 (нет HA)
321
431 (то же, что 3)
532
642 (то же, что 5)
743

Чётное число узлов — анти-паттерн: добавление одного узла не даёт прироста tolerance, но даёт прирост latency (больше пиров для репликации в hot path). Production-стандарт — 3 узла (минимальный HA) или 5 (можно сделать maintenance одной ноды без потери HA).

Leader election

Если followers не получают heartbeat от leader-а за election timeout (по умолчанию 1 сек), начинается election: один из followers объявляет себя candidate, увеличивает term, голосует за себя, рассылает RequestVote всем остальным. Кто получает majority голосов — становится новым leader-ом.

Это объясняет, почему etcd чувствителен к network latency. Если RTT между узлами > election timeout — постоянные re-elections, write latency взрывается, кластер деградирует. Для multi-DC топологии нужно делать timeout больше (--election-timeout 5000ms).

WARNING

Никогда не растягивайте etcd кластер через слабые сетевые линки (между регионами, через VPN). Используйте multi-cluster federation вместо этого. etcd best practice — все members в одном DC с RTT < 5ms, лучше < 1ms.


MVCC: версионность всего

etcd v3 — это Multi-Version Concurrency Control store. Это значит, что каждое изменение не перезаписывает старое значение — оно создаёт новую версию с новым revision.

revision — глобальный монотонно растущий счётчик. Каждый успешный write увеличивает его на 1. Любой read возвращает значение на указанном revision (или на самом свежем, если revision не указан).

# Поток изменений
Put /registry/pods/default/nginx  → revision 100, value V1
Put /registry/pods/default/nginx  → revision 105, value V2
Put /registry/pods/default/web    → revision 106, value W1
Delete /registry/pods/default/nginx → revision 110

# Чтение на разных revision
Get /registry/pods/default/nginx --rev=100 → V1
Get /registry/pods/default/nginx --rev=105 → V2
Get /registry/pods/default/nginx --rev=110 → not found
Get /registry/pods/default/nginx          → not found (latest)

Это даёт два важных свойства:

  1. Time-travel queries — можно прочитать state на любом historical revision (до compaction).
  2. Точная consistency для watch — клиент может попросить «шли мне все изменения после revision X», и сервер пошлёт ровно их, в правильном порядке, без пропусков и дубликатов.

Связь revision и Kubernetes resourceVersion

Помните resourceVersion из урока про apiserver? Это и есть etcd revision. Когда apiserver делает Put в etcd и получает новый revision — он кладёт его в .metadata.resourceVersion объекта при возврате клиенту. Так что resourceVersion — не просто counter, а конкретный revision в etcd.

TIP

ResourceVersion — это per-cluster, а не per-object. Когда вы видите Pod с resourceVersion=874523, это значит что глобальный revision на момент этого изменения был 874523. Следующий Put в любой объект кластера получит 874524.


Watch на уровне etcd

etcd v3 поддерживает long-lived watch на key range, начиная с указанного revision. Это native gRPC streaming, поверх которого работает Kubernetes watch.

# Подписаться на все изменения под /registry/pods/, начиная с revision 100
etcdctl watch --prefix /registry/pods/ --rev=100

Сервер шлёт events стримом:

PUT
/registry/pods/default/nginx
<protobuf bytes>

DELETE
/registry/pods/default/nginx

Каждый event имеет kv.mod_revision — на каком revision произошло это изменение. Если клиент потерял соединение, он переподключается с --rev=<last seen mod_revision + 1>.

Compaction: чистка истории

MVCC хранит все revisions, что значит history растёт бесконечно. Чтобы это не убило диск, etcd делает compaction — удаляет revisions старше указанного.

# Compact всё старше revision 5000
etcdctl compact 5000

После этого etcdctl get --rev=4000 вернёт ошибку — данные стёрты. Kubernetes apiserver автоматически делает compaction каждые 5 минут (compact-ит revision на 5 минут моложе самого старого живого watch).

Если клиент watch отстал больше чем на compacted-окно — он получает ErrCompacted и должен сделать LIST + новый WATCH. Этот сценарий ловится apiserver-ом и транслируется как 410 Gone для kubectl/SDK клиентов.


Lease: TTL для key

Lease — это механизм автоматического удаления ключа по TTL. Lease создаётся отдельно, имеет ID и duration. Дальше можно привязать к нему один или несколько ключей — когда lease expires (если не renewed), все привязанные ключи удаляются.

# Создать lease на 60 секунд
etcdctl lease grant 60
# → lease 32695410dcc0ca06 granted with TTL(60s)

# Привязать ключ к lease
etcdctl put /test/key1 "value" --lease=32695410dcc0ca06

# Через 60 секунд ключ автоматически удалится — если не сделать renew
etcdctl lease keep-alive 32695410dcc0ca06
# → стрим renewals, продлевает lease каждые ~20 сек

Зачем это Kubernetes

Два главных юзкейса:

  1. Node heartbeat / node lease — каждый kubelet раз в 10 секунд обновляет свой Lease объект в kube-node-lease namespace. Если NodeController не видит renewal в течение --node-monitor-grace-period (default 50 сек в v1.32+, исторически 40 сек) — нода считается NotReady. Это легковесная замена старого механизма «обновлять весь Node объект» (которое создавало много write traffic).

  2. Leader election — controller-manager, scheduler, и другие компоненты, которые работают в HA, используют Lease как distributed lock. Один из инстансов держит lease (renew-ит периодически), он же leader. Если renewals прекращаются — lease expires, другой инстанс claim-ит, становится новым лидером.

apiVersion: coordination.k8s.io/v1
kind: Lease
metadata:
  name: kube-controller-manager
  namespace: kube-system
spec:
  holderIdentity: ip-10-0-1-12_a1b2c3d4
  leaseDurationSeconds: 15
  acquireTime: "2026-05-13T09:00:00Z"
  renewTime: "2026-05-13T09:42:30Z"
  leaseTransitions: 3

Это объект в API server, но под капотом — обычный etcd ключ, обычно без attached etcd-lease (поскольку renew логика тут в apiserver-е).


Encryption at rest: Secrets — это не secrets

По умолчанию etcd хранит данные в открытом виде на диске. Файл /var/lib/etcd/member/snap/db — это boltdb, который можно скачать и прочитать. Secrets в нём — это base64-encoded, не encrypted.

Чтобы Secrets были реально зашифрованы, нужен конфиг encryption at rest в apiserver:

# EncryptionConfiguration в файле, путь передаётся apiserver-у через --encryption-provider-config=/etc/kubernetes/encryption.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
  - secrets
  - configmaps                  # опционально — обычно только secrets
  providers:
  - aescbc:                     # шифрование AES-CBC с 32-byte key
      keys:
      - name: key1
        secret: <base64-encoded-32-byte-key>
  - identity: {}                # fallback — read без шифрования (для миграции)

После применения этой конфигурации apiserver шифрует Secret-ы при записи в etcd. Существующие Secrets остаются незашифрованными до тех пор, пока их не пересохранить — обычно через kubectl get secrets --all-namespaces -o json | kubectl replace -f -.

WARNING

Provider aescbc discouraged с v1.28 — для in-cluster шифрования рекомендуется aesgcm (AES-GCM, 32-byte key, лучше performance). Production-уровень — KMS v2 (GA с v1.29): внешний KMS (AWS KMS, GCP KMS, Vault) хранит KEK, apiserver запрашивает DEK через KMS API. Это даёт key rotation без перешифровывания всего etcd, full audit и hardware-bound keys.

Что хранится в etcd кроме API объектов

Помимо /registry/<resource>/..., etcd хранит:

  • /registry/services/specs/... — Service objects
  • /registry/services/endpoints/... — Endpoints objects (legacy, заменены EndpointSlices)
  • /registry/leases/... — Lease objects для leader election и node heartbeats
  • /registry/events/... — Events (с TTL — по умолчанию 1 час)
  • /registry/namespaces/... — Namespace objects
  • /registry/csidrivers/..., /registry/csinodes/... — CSI driver state

Один типичный production-кластер хранит несколько GB данных в etcd. Самая частая причина distended etcd — много Events или массовая утечка resourceVersion-ов из-за неправильного использования watch.


Backup и restore

Хотя backup — формально CKA scope, концептуально важно понять. etcd snapshot — это консистентное состояние на конкретном revision:

# Снять snapshot
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  snapshot save /backup/etcd-$(date +%Y%m%d-%H%M%S).db

# Восстановить из snapshot
ETCDCTL_API=3 etcdctl snapshot restore /backup/etcd-20260513.db \
  --data-dir=/var/lib/etcd-new \
  --initial-cluster=master-1=https://10.0.1.10:2380 \
  --initial-advertise-peer-urls=https://10.0.1.10:2380 \
  --name=master-1

Это однопоточный snapshot всего boltdb на конкретном revision. Backup нужно делать регулярно (cron каждые 30 минут — типичная частота для production). Без backup потеря большинства etcd узлов = полная потеря кластера.


Killer-момент: что лежит для одного Pod-а

Чтобы окончательно понять, посмотрим что etcd хранит для одного-единственного Pod-а после kubectl run nginx --image=nginx.

# Ключ
/registry/pods/default/nginx

# Decoded protobuf — упрощённо
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  namespace: default
  uid: 7c8b9d10-1234-...
  resourceVersion: "874523"
  creationTimestamp: "2026-05-13T09:00:00Z"
  generation: 1
spec:
  containers:
  - name: nginx
    image: nginx
    ports: [...]
    resources: {}
    terminationMessagePath: /dev/termination-log
    imagePullPolicy: Always
  restartPolicy: Always
  terminationGracePeriodSeconds: 30
  dnsPolicy: ClusterFirst
  serviceAccountName: default
  serviceAccount: default
  nodeName: worker-3                       # ← scheduler записал это
  schedulerName: default-scheduler
  ...
status:
  phase: Running
  conditions:
  - type: Ready
    status: "True"
    ...
  hostIP: 10.0.2.13
  podIP: 10.244.3.7
  startTime: "2026-05-13T09:00:02Z"
  containerStatuses:
  - name: nginx
    state:
      running:
        startedAt: "2026-05-13T09:00:05Z"
    ready: true
    restartCount: 0
    image: nginx:latest
    imageID: docker.io/library/nginx@sha256:abc...
    containerID: containerd://def456...    # ← kubelet записал это

Это один объект, но он наполняется разными компонентами:

  • spec (без nodeName) — записан apiserver-ом при создании
  • spec.nodeName — записан scheduler-ом через binding
  • status.* — постоянно обновляется kubelet-ом (heartbeat-ы статуса)
  • Каждое такое обновление — это новый revision в etcd, новый resourceVersion объекта

Объект живёт в etcd с момента создания до удаления Pod-а. При удалении он не сразу стирается — сначала помечается deletionTimestamp, kubelet видит это, останавливает контейнеры, удаляет финализаторы — и только тогда apiserver реально делает delete в etcd.


Проверка знанийKnowledge check
Что произойдёт с Kubernetes-кластером, если quorum etcd-узлов потерян (например, 2 из 3 узлов недоступны)?
ОтветAnswer
Кластер переходит в read-only режим относительно state. Конкретно: kube-apiserver продолжит работать и обслуживать GET/LIST/WATCH запросы из watch-cache (in-memory снапшот), но любая попытка write (POST/PUT/PATCH/DELETE) вернёт 500 или timeout, потому что apiserver не может закоммитить запись в etcd без quorum. Существующие Pod-ы продолжат работать — kubelet и контейнеры на нодах не зависят от etcd напрямую, они работают с локальным state. Но controllers замирают: ReplicaSetController не может создать новые Pod-ы взамен упавших, scheduler не может bind unscheduled pods, DeploymentController не может ничего обновить. Восстановление quorum — единственный путь обратно. Если узлы потеряны навсегда — нужен etcd restore из backup.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. В кластере etcd из 5 узлов отказали 2 узла. Что произойдёт с кластером Kubernetes?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6