etcd для самых маленьких: гайд по хранилищу kubernetes

от автора

Разбираемся, что за зверь хранит весь ваш Kubernetes, учимся читать его ошибки и честно отвечаем на вопрос, можно ли ему доверять

Что это вообще

etcd — распределённое key‑value хранилище со строгой консистентностью. Если на пальцах: это место, где Kubernetes хранит авторитетное состояние кластера. Деплойменты, поды, секреты, конфигмапы — всё это записи в etcd. kube‑apiserver — по сути толстый REST‑фасад над ним, а scheduler, controller‑manager и kubelet не являются источниками истины для состояния kubernetes; они получают его через apiserver и имеют собственное локальное состояние

Отсюда первое следствие, которое стоит выжечь на подкорках:

Умер etcd == умерла память кластера. Поды продолжат бежать (kubelet автономен), но задеплоить, отскейлить или изменить что‑либо уже нельзя. Грубо говоря — экспонаты работают, трогать запрещено.

Raft на пальцах и магия чисел

etcd переживает падение узлов благодаря протоколу консенсуса Raft. Упрощённо он работает так:

  1. Узлы выбирают лидера. Только лидер проводит записи через Raft; клиенту при этом необязательно знать, кто сейчас лидер. follower может автоматически перенаправить consensus‑запрос

  2. Каждая запись это proposal: лидер пишет её в свой журнал (WAL) и рассылает фолловерам.

  3. Когда кворум — большинство узлов (2 из 3, 3 из 5) — подтвердил запись на диске, она считается committed и применяется к состоянию.

  4. Пропал лидер → новые выборы, счетчик term увеличивается, жизнь продолжается.

Отсюда магия чисел: кластер из 3 узлов переживает потерю 1, из 5 потерю 2. А кластер из 2 узлов хуже, чем из одного: кворум = 2, потеря любого узла = потеря кворума. Вы заплатили за два сервера и построили распределённую единую точку отказа.

Как безопасно расширять кластер: Learner‑узлы

Классическая проблема: у вас 3 узла, вы хотите добавить 4й чтоб потом вывести старый. В момент добавления пустого узла кворум становится 3 из 4. Если пустой узел долго синхронизирует базу (а сеть или диск подтупливают) и в этот момент падает один из старых узлов то кластер встаёт колом. Вы потеряли кворум в попытке сделать безопаснее.

Решение ставшее индустриальным стандартом — Learner nodes. Добавляйте новый узел флагом --learner.

  1. Ученик синхронизирует журнал (Raft log) от лидера.

  2. Ученик не участвует в голосованиях и не влияет на расчет кворума (кворум остается 2 из 3).

  3. Только когда метрика синхронизации догоняет лидера, ученик переводится в полноправные участники командой member promote.

Внутренности: WAL, bbolt, MVCC

Три кита, из которых растут 90% ошибок в логах:

WAL (write‑ahead log) — журнал в member/wal/, туда попадают proposals и состояние Raft, необходимое для восстановления. Это гарантия durability: что подтверждено — то переживёт ребут. Латентность fsync WAL — метрика № 1 здоровья etcd: etcd_disk_wal_fsync_duration_seconds, официальный ориентир — p99 ниже 10 мс.

bbolt — встраиваемая B+tree база (member/snap/db), собственно хранилище. Важная особенность: bbolt не отдаёт место ОС после удаления данных, файл только растёт.

MVCC (multi‑version concurrency control) — etcd не перезаписывает ключи, а хранит все версии, каждая с глобальным монотонным номером — revision. Когда вы делаете kubectl get pods --watch, вы буквально подписываетесь на поток ревизий. Старые версии нужно чистить операциями compaction (забыть старое) и defrag.

Над всем этим — квота --quota-backend-bytes, по умолчанию 2 GiB (рекомендованный потолок 8 GiB). Пробили квоту — etcd поднимает аларм NOSPACE и переводит в read‑only весь кластер.

Анатомия дисковой подсистемы: разделяй и властвуй

Чтобы понять, почему диски так важны, посмотрим на критический путь записи

Поскольку клиент ждет только WAL, нужно понимать профиль нагрузки на SSD:

  1. WAL — преимущественно последовательная append‑запись. Важна низкая латентность и желательно наличие на SSD конденсаторов PLP (Power Loss Protection), чтобы контроллер моментально подтверждал fsync.

  2. bbolt — случайное чтение/запись (Random I/O).

Если они лежат на одном диске, тяжелый defrag или снятие снапшота bbolt забивает очередь (Queue Depth) контроллера SSD. Последовательная запись в WAL встаёт в очередь за жирными блоками bbolt, latency WAL растёт, heartbeat начинает опаздывать, а при достаточной деградации возникают лишние выборы лидера

Best practice для highload: Разносить их физически. Флаг --wal-dir позволяет вынести журнал на отдельный сверхбыстрый NVMe‑накопитель, оставив /var/lib/etcd на диске попроще.

Часть 2. Где обычно болит

Конфигурация: типовые грабли

Квота на дефолте. 2 GiB на живом кластере с шумными CRD и операторами, пишущими статусы каждые 5 секунд, выедаются за недели. Дальше по учебнику: NOSPACE, read‑only.

Не задан --auto-compaction-retention. В самом etcd auto‑compaction по умолчанию отключён (0). Если история MVCC не компактируется, она постепенно съедает quota. В кубах способ и параметры compaction зависят от того, как развёрнут control plane, поэтому не стоит считать его магически настроенным просто потому, что это кубы.

Чётное число членов. Обсудили: минус доступность.

Ручные правки static pod‑а. /etc/kubernetes/manifests/etcd.yaml правится руками при каждом «а давайте подкрутим». Опечатка в имени флага — kubelet рестартует под в CrashLoopBackOff ‑→ у вас нет apiserver, чтобы это увидеть. Диагностика только через crictl и journalctl кубелета

История одного факапа: Как 1900 Evicted‑подов убили кластер

Подойдем к суровой практике. Как‑то раз я напоролась на кластер, вставший колом. Поды крутятся, приложения отдают трафик, но задеплоить ничего нового нельзя. kubectl get pods работает, а вот apply или delete зависают и отваливаются по таймауту.

kubbectl get po ‑A = кладбище 1900 подов в статусе Evicted.

Что произошло под капотом?

  1. На одной из рабочих нод кончилось место на диске. Автономный kubelet запаниковал и начал спасать ноду, вытесняя поды.

  2. Контроллеры (ReplicaSet/Deployment) тут же пытались пересоздать эти поды, они снова падали на проблемные ноды и снова получали статус Evicted.

  3. etcd: Каждое изменение статуса пода заставляет kubelet слать update в apiserver.

  4. Apiserver покорно пишет это в etcd. Но мы же помним про MVCC! etcd не перезаписывает статус, он создает новую ревизию ключа. 1900 подов, которые агрессивно флапают= десятки тысяч новых ревизий за очень короткое время.

  5. Файл базы bbolt стремительно раздувается

  6. etcd понимает, что писать больше некуда и чтобы избежать коррапта поднимает тревогу NOSPACE

    Попала я в классический капкан: не могу удалить поды через kubectl, потому что удаление в кубах аналогично операции записи (установка deletionTimestamp), а база данных в режиме RO.

Как из этого выбираться?

Подключаемся напрямую к etcd (через crictl exec в под etcd или локально с сертификатами). Берем текущую ревизию кластера (etcdctl endpoint status).

  1. etcdctl compact <revision>, приказываем забыть старые ревизии MVCC.

  2. etcdctl defrag = самая тяжелая, блокирующая операция. Возвращает пустые страницы из bbolt обратно операционной системе. Файл базы физически уменьшается.

  3. etcdctl alarm disarm = снимаем тревогу NOSPACE.

Мораль: Именно поэтому auto‑compaction должен быть настроен всегда, дефолтную квоту для крупных кластеров стоит увеличивать до 8 GiB, а директорию /var/lib/etcd ОБЯЗАТЕЛЬНО выносить на отдельный раздел или диск, чтобы взбесившийся kubelet или переполненный /var/log на мастере не нагадили в панамку.

Учимся читать

Строка в логе

Что означает

Что делать

wal: sync duration of 1.5s, expected less than 1s

Диск не тянет fsync. Корень большинства бед.

Отдельный SSD/NVMe под WAL, найти соседей по IO.

waiting for ReadIndex response took too long

Лидер перегружен или сеть/диск деградировали.

Смотреть fsync + сеть.

failed to send out heartbeat on time

Лидер не успел за 100 мс, обычно диск, не сеть.

То же + поднять --heartbeat-interval / --election-timeout.

etcdserver: request timed out

Proposal не собрал кворум за ожидаемое время

Искать, какой узел/диск тормоз.

apply entries took too long

Записи committed, но применяются медленно.

Диск, defrag, размер значений, CPU starvation

mvcc: database space exceeded (+ NOSPACE)

Квота пробита, кластер read‑only.

compact, defrag, disarm.

request is too large

Значение больше --max-request-bytes (1.5 MiB по дефолту).

Не хранить огромные configmap/блобы в etcd.

Мониторинг жив/мёртв для etcd бесполезен, если apply‑петля захлёбывается. Караулить здоровье etcd можно хотя бы этой шестеркой метрик:

etcd_disk_wal_fsync_duration_seconds        # p99 < 10msetcd_disk_backend_commit_duration_secondsetcd_server_proposals_failed_total          # должно быть 0etcd_server_leader_changes_seen_total       # рост = флаппинг лидераetcd_mvcc_db_total_size_in_bytesetcd_mvcc_db_total_size_in_use_in_bytes

Часть 3. А можно ли вообще доверять etcd?

Аргумент за: В анализе etcd 3.4.3 Jepsen пришёл к выводу, что KV‑операции выдержали проверку на strict serializability (строжайшая модель согласованности) под крашами процессов и сбоями сети. Это комплимент века. Библиотека etcd/raft стала индустриальным стандартом (используется в CockroachDB, TiKV).

Аргументы против и древние скелеты в шкафу:

  1. В ранних версиях ветки 3.5 существовал баг, из‑за которого после OOMKill состояние backend могло расходиться с кластером, но Raft этого не замечал.

    • UPD: в etcd 3.6 появился отдельный Robustness Testing framework, который прогоняет кластер через различные нагрузки и отказовые сценарии с последующей проверкой linearizability. При этом встроенная corruption detection существует отдельно и не является включённой по умолчанию в качестве GA‑механизма: в актуальных версиях она управляется feature gates.

  2. Локи = не совсем локи. Lease сам по себе не является гарантией взаимного исключения: клиент может считать lease действующим, тогда как сервер уже его отозвал. Встроенный lock использует проверки revision/version для защиты от этого сценария. Для внешних ресурсов нужен собственный механизм fencing/version validation.

  3. Raft не защищает от полуживого железа. Частичный отказ свитча может вызвать выборочную видимость узлов и карусель перевыборов лидера. Формально база цела, фактически control plane лежит.

География: etcd в Multi‑AZ

Многие размазывают 3 узла etcd по трем разным зонам доступности (AZ). Грубо говоря, commit latency упирается в RTT между членами и latency записи на диск до достижения кворума. Multi‑AZ — норм сценарий. Multi‑Region технически возможен, но WAN RTT напрямую увеличивает commit latency и делает control plane значительно медленнее. Для etcd это обычно плохой компромисс


Часть 4. Чеклист паровозика, который выжил

  1. Диски. Отдельное блочное устройство под etcd. Для highload — разнесение WAL (--wal-dir) на сверхбыстрый NVMe с PLP, база (/var/lib/etcd) на отдельном SSD. В облаке избегайте дешевых сетевых дисков.

  2. --quota-backend-bytes=8589934592, --auto-compaction-mode=periodic,--auto-compaction-retention=1h (вне k8s обязательно).

  3. Расширение кластера только через Learner nodes. Сначала --learner, потом member promote. Нечетное число участников (3 или 5).

  4. Алерты на шестерку метрик + например, alert при db_size / quota > 0.8+ свободное место на диске < 20%.

  5. etcdctl snapshot save кроном + периодическая проверка раскатки бэкапа

    1. с версии 3.6

      • snapshot save — etcdctl

      • snapshot restore/status — etcdutl

  6. настройте очистку завершённых джобов (ttlSecondsAfterFinished), ограничьте lifetime событий (--event-ttl) и не складывайте большие блобы в configmap.

    С etcd 3.6 не путайте инструменты: etcdctl работает с живым кластером, а etcdutl предназначен для offline‑операций над данными и snapshot restore

Вместо заключения

etcd — база,пережившая Jepsen, публично признала свои баги и внедрила CI‑проверки на их исключение в будущем. Можно доверять, но доверие это контрактное: с вас будут диски, мониторинг, апдейты, а если нарушите свою часть, то никакой консенсус не спасёт. Если же etcd для вас избыточен, то смотрите в сторону kine (k3s поверх PostgreSQL/SQLite).

На почитать

ссылка на оригинал статьи https://habr.com/ru/articles/1072628/