Разбираемся, что за зверь хранит весь ваш 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 начинает опаздывать, а при достаточной деградации возникают лишние выборы лидера.

Никогда не запускайте defrag одновременно на всех мемберах кластера. Во время live defrag member блокирует обработку операций на время перестройки backend. Если одновременно положить на defrag весь кластер, можно временно потерять доступность control plane.

Если ETCD_ENDPOINTS содержит все endpoints кластера, не передавайте его вслепую в команду defrag. Выбирайте один member, дождитесь его завершения и переходите к следующему.

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

Fio и проверка storage

Для etcd недостаточно посмотреть на паспортную скорость SSD. Красивые 5000 MB/s в fio --rw=read --bs=1M мало что говорят о том, насколько хорошо storage подходит для etcd.

Для WAL гораздо важнее latency синхронных записей. etcd последовательно пишет данные в WAL и периодически выполняет fdatasync, чтобы гарантировать durability. Поэтому storage с огромным throughput, но плохой latency на sync-write может оказаться для etcd гораздо хуже, чем менее быстрый, но предсказуемый SSD.

Для грубой проверки можно использовать fio.

Например:

[global]
time_based=1
group_reporting=1
directory=/mnt/wal
loops=1
ramp_time=5s

[etcd]
ioengine=sync
fdatasync=1
direct=0
bs=2300
iodepth=1
rw=write
nrfiles=1000
filesize=64MiB
fallocate=truncate

Этот профиль пытается приблизить одну из важных характеристик WAL - последовательную запись с fdatasync и малой глубиной очереди. Это не полный эмулятор workload etcd.

Не используйте runtime= с заранее ограниченным набором файлов, если хотите измерить именно append-поведение. После исчерпания пространства тест начнёт повторно использовать уже записанные области, и профиль нагрузки перестанет соответствовать первоначальной модели.

Гонять такой тест нужно на тестовом сторе или на отдельном mount. Не надо запускать fio с таким workload прямо на /var/lib/etcd, пока там живой production etcd, иначе получится прекрасный эксперимент по проверке вашей способности объяснять инцидент руководству.

По поводу подобных тестов для bbolt - WAL и backend имеют разные I/O-профили, поэтому хороший результат synthetic WAL benchmark не означает автоматически хороший результат для bbolt backend.

Часть 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 < 10ms
etcd_disk_backend_commit_duration_seconds
etcd_server_proposals_failed_total          # должно быть 0
etcd_server_leader_changes_seen_total       # рост = флаппинг лидера
etcd_mvcc_db_total_size_in_bytes
etcd_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).

На почитать

Комментарии (6)


  1. ArtemDudich
    20.08.2026 15:52

    Отличный разбор, особенно про разнесение WAL и bbolt по разным дискам — это редко где так внятно объясняют. Вопрос: в managed-кубах (EKS/GKE/AKS) мы обычно не видим ни --wal-dir, ни метрик etcd напрямую. Есть ли у вас практики, как в таких условиях хотя бы косвенно ловить «диск не тянет fsync» — по apiserver-метрикам, request latency, чему-то ещё?


    1. metalw4rrior Автор
      20.08.2026 15:52

      Манагед кубы снимают с тебя ответственность за диски, в этом их плюс. Если смотреть универсально - единственное что торчит наружу из control plane это сам apiserver, он же точка входа для kubectl. Придется навешивать Prometheus. Можно скрапить обычный /metrics apiserver и он сам скажет сколько времени у него ушло на каждый поход в etcd (etcd_request_duration_seconds). Если она растёт именно на записи, но на чтении всё ровно, можно сделать предположение что под etcd тупит именно диск. Может послужить косвенным сигналом о здоровье етсд. еще есть счётчик объектов по каждому ресурсу. Если он только растёт и не падает, значит где-то плодятся кривые объекты. etcd может упереться в квоту и присесть в read-only. Больше ничего облако не покажет, только эти два сигнала через apiserver могут послужить маячками. Даже если etcd там крякнет остается только идти с тикетом в поддержку облака


  1. makurus
    20.08.2026 15:52

    Какие еще best practice по etcd можно добавить:

    • НИКОГДА не выполнять команду defrag одновременно на всех нодах etcd кластера. Это, гарантировано, на нагруженном кластере, приведет к отказу в обслуживании; да, скорее всего временно, но тем не менее, может стать триггером к более серьезным проблемам. Почему такое может случиться? Каждый раз перечислять все узлы кластера в аргументе --endpoints= для etcdctl контрпродуктивно, поэтому, как правило, используется env переменная ETCD_ENDPOINTS, где прописаны все участники кластера

    • Если у вас etcd до версии v3.6.x может быть неожиданностью, что auto compaction mode работает не так как ожидается. В предыдущих версиях много неразберихи: в v3.2.x, например, параметр --auto-compaction-mode отсутствует, в v3.3/4/5.x разные дефолты для cli-флага --auto-compaction-mode и в функции NewConfig() при запуске через --config-file. С версии v3.6.x это все устранили

    Можно еще для синтетики прогнать fio:

    [global]
    time_based=1
    group_reporting=1
    directory=/mnt/wal
    loops=1
    ramp_time=5s
    
    [etcd]
    ioengine=sync
    fdatasync=1
    direct=0
    bs=2300
    iodepth=1
    rw=write
    nrfiles=1000
    filesize=64MiB
    fallocate=truncate
    

    Это близкое к тому как пишется WAL: etcd преаллоцирует 64MiB файл, последовательно в него пишет, после заполнения текущего открывает новый файл и так далее.

    Тут есть нюанс: fio не умеет преаллоцировать файлы во время работы и после заполнения удалять их, поэтому все файлы нужно создать заранее.

    Задавайте число файлов (nrfiles) достаточно большим, чтобы тест не завершился за считанные секунды на SSD/NVMe.

    Длительность теста по времени (runtime=) нет смысла проводить, потому как после заполнения всех преаллоцированных файлов fio начнет писать в уже заполненные файлы, что провоцирует паттерн readModifyWrite и iops падают на порядок. У меня было так 250k -> 20k.

    Block size (bs) не кратен 4k, чтобы было честно, так как etcd при записи в wal никакие границы блока не выравнивает и запись может быть любого размера (разве что ограничена --max-request-bytes).

    Можно добавить fio сценарий для bbolt (он, кстати, совершенно другой), но основной паттерн для etcd, все таки WAL


  1. metalw4rrior Автор
    20.08.2026 15:52

    спасибо! обязательно дополню статью!


  1. SlavikF
    20.08.2026 15:52

    Я экперементировал дома с k3s на трёх нодах. etcd кластер на трёх нодах.

    Каждая нода - 2 TB NVMe диск, быстрый.

    Но так как там вместе и поды и etcd, то постоянно в логах жалуется на тормоза диска. Но работает.

    Я вот думаю поменять настройка дискового кэша, чтобы игнорировались fsync для etcd. Хотя везде пишут, что это прям плохо. А я вот думаю: ну если рассчитывать на backup даже каждый час, то всё равно будут какие-то потери. А если потеряется что-то из-за кэша, то это всё равно на так много, как потери при восстановлении из backup.

    Как ещё можно оптимизировать etcd? kine ставить не хочу.


    1. metalw4rrior Автор
      20.08.2026 15:52

      Не трогайте лучше fsync, идея плохая. Может возникнуть мысля мол “потеряю немного, как при откате к бэкапу”, но это вообще не так. Если щелкнет свет, а все три ноды словят рассинхрон одновременно придётся сносить кластер и поднимать заново. Это хуже чем бэкап. Я бы проверила точно ли дело в диске, может гадит какой-то из процессов (частый виновник в домашних кластерах эт какой нибудь Traefik/cert-manager со своим статус-чурном). Можно погонять fio с fdatasync на директорию etcd и посмотреть p99, он если укладывается в 10ms то тормозят не диск, а может логи от конкуренции с подами за очередь. Стоит еще глянуть есть ли у дисков PLP (nvme id-ctrl флаг VWC). Если нет то у диска нет полной гарантии durability, еще один повод не трогать fsync руками. Как вариант подкрутить heartbeat-interval и election-timeout, может уменьшить лишние перевыборы лидера из логов без всякого риска. Прежде чем тюнить дальше не лишним будет посмотреть кто вообще генерит основную запись, иногда там режется большая часть нагрузки одной правкой конфига.