Перейти к содержанию

Файлы постоянного хранилища etcd

Справочник по формату и файлам постоянного хранилища

В этом документе описан формат постоянного хранилища etcd: именование, содержимое и инструменты, позволяющие разработчикам исследовать файлы. В дальнейшем документ следует дополнять по мере изменений модели хранения. Он предназначен для разработчиков etcd и помогает при восстановлении данных.

Предварительные сведения

Для понимания документа полезны следующие вводные материалы:

Обзор

Долгоживущие файлы

Имя файлаВысокоуровневое назначение
./member/snap/db
b+tree bbolt, хранящее все применённые данные, сведения об авторизации состава кластера и метаданные. Оно знает последний применённый индекс журнала WAL ("consistent_index").
./member/snap/0000000000000002-0000000000049425.snap
./member/snap/0000000000000002-0000000000061ace.snap

Периодические снимки устаревшего хранилища v2, содержащие:

  • основные сведения о составе кластера
  • версию etcd

Начиная с etcd v3 их содержимое дублирует содержимое файлов /snap/db.

Периодически (каждые 30s) эти файлы удаляются, при этом сохраняются последние --max-snapshots=5.

/member/snap/000000000007a178.snap.db

Полный загруженный снимок bbolt с лидера etcd, если реплика отставала слишком сильно.

Содержит данные того же типа, что и файл (./member/snap/db).

Файл используется в 2 сценариях:

  • В ответ на запрос лидера восстановиться из снимка.
  • Во время запуска сервера, если найден последний снимок (файл .snap.db) и его индекс новее consistent_index в текущем файле snap.db.
Примечание: периодические снимки, создаваемые на каждой реплике, записываются только как файлы *.snap, а не snap.db. Поэтому наличие файла *.snap.db для самого нового снимка в журнале WAL не гарантируется. Однако в этом случае бэкенд (snap/db) должен быть новее снимка.

Файл не удаляется после завершения восстановления, когда всё его содержимое перенесено в ./member/snap/db. Периодически (каждые 30s) файлы удаляются. Здесь также сохраняются последние --max-snapshots=5. Поскольку размер таких файлов может иметь порядок O(GBs), возникает риск исчерпания дискового пространства.

./member/wal/000000000000000f-00000000000b38c7.wal
./member/wal/000000000000000e-00000000000a7fe3.wal
./member/wal/000000000000000d-000000000009c70c.wal

Журналы предзаписи Raft, содержащие недавние транзакции, принятые Raft, периодические снимки или записи CRC.

Сохраняются последние --max-wals=5 файлов. Размер каждого составляет ~64*10^6 байтов. Файл обрезается после превышения этого жёстко заданного размера, поэтому файлы могут быть немного больше (и предварительно выделенный 0.tmp не обеспечивает полной защиты от переполнения диска).

Если снимки создаются слишком редко, файлов может быть больше --max-wals=5, поскольку блокировки на уровне файловой системы защищают их от слишком раннего удаления.

./member/wal/0.tmp (or .../1.tmp)
Предварительно выделенное пространство для следующего файла журнала предзаписи. Позволяет не допустить остановки Raft из-за нехватки места для журналов WAL без возможности активировать аварийный сигнал.

Временные файлы

Во время внутренней обработки etcd могут встречаться несколько краткоживущих файлов:

ФайлВысокоуровневое назначение
./member/snap/0000000000000002-000000000007a178.snap.broken

Файлы снимков переименовываются в ‘broken’, если их невозможно загрузить.

Попытка загрузить самый новый файл выполняется при запуске etcd.

Либо при выполнении команд backup/migrate в etcdctl.

./member/snap/tmp071677638 (random suffix)

Временный файл bbolt, создаваемый на репликах в ответ на запрос лидера msgSnap, то есть на требование лидера восстановить хранилище из заданного снимка.

После успешного полного получения содержимого файл переименовывается в /member/snap/[SNAPSHOT-INDEX].snap.db. Если сервер отказывает или принудительно завершается во время загрузки файлов, они остаются на диске и никогда не очищаются автоматически. Их размер может быть значительным (GBs).

См. etcd/issues/12837. Исправлено в etcd 3.5.

/member/snap/db.tmp.071677638 (random suffix)

Временный файл, содержащий копию содержимого бэкенда (/member/snap/db) во время дефрагментации. После её успешного завершения файл переименовывается в /member/snap/db и заменяет исходный бэкенд.

При запуске сервера etcd эти файлы удаляются.

b+tree bbolt: member/snap/db

Этот файл содержит основное содержимое etcd, применённое до определённой точки журнала Raft (см. consistent_index ).

Физическая организация

Физически хранилище better bolt организовано как b+tree . Физические страницы b-tree никогда не изменяются на месте1. Вместо этого содержимое копируется на новую страницу, полученную из списка свободных, а старая страница добавляется в список свободных, как только не остаётся открытых транзакций, способных к ней обратиться. Благодаря этому открытая транзакция RO видит согласованное историческое состояние хранилища. Транзакция RW является исключительной и блокирует все остальные транзакции RW.
Большие значения хранятся на нескольких непрерывных страницах. Освобождение страниц в сочетании с необходимостью выделять непрерывные области страниц разного размера может усиливать фрагментацию хранилища bbolt.

Файл bbolt сам по себе никогда не уменьшается. Только при дефрагментации его можно переписать в новый файл с некоторым запасом свободных страниц в конце и усечённым размером.

Логическая организация

Хранилище bbolt разделено на сегменты. В каждом сегменте ключи (пары byte[]->value byte[]) хранятся в лексикографическом порядке. Ниже перечислены сегменты, используемые etcd по состоянию на версию 3.5, и задействованные ключи.

СегментКлючПример значенияОписание
alarmrpcpb.Alarm: {MemberID, Alarm: NONE|NOSPACE|CORRUPT}nilУказывает, что у одного из участников диагностированы проблемы.
auth"authRevision""" (empty) or BigEndian.PutUint64

Любое изменение ролей или пользователей увеличивает это поле при фиксации транзакции.

Значение используется только для оптимистической блокировки во время авторизации.

authRoles[roleName] в виде строкисериализованный authpb.Role
authUsers[userName] в виде строкисериализованный authpb.User
cluster"clusterVersion""3.5.0" (string)Дополнительная версия общей версии хранилища, согласованной консенсусом.
"downgrade"JSON:
{
  "target-version": "3.4.0"
  "enabled": true/false
}

Сохраняет намерение, заданное последним запросом Downgrade RPC.

Начиная с v3.5

key

[revisionId], закодированный с помощью bytesToRev{main,sub}

Удаления пар «ключ — значение» сериализуются с 't' в конце (как "Tombstone")

сериализованный proto mvccpb.KeyValue (key, create_rev, mod_rev, version, value, lease id)
leaseсериализованный proto leasepb.Lease (ID, TTL, RemainingTTL)

Примечание: LeaseCheckpoint продлевает только RemainingTTL. TTL берётся из исходного Grant.

Примечание 2: TTL сохраняются в секундах (от неопределённого 'now'). Сервер в цикле аварийных перезапусков не освобождает аренды!!!

members[memberId] в шестнадцатеричном виде как строка: "8e9e05c52164694d"Структура Member, сериализованная в JSON как строка:
{
  "id":10276657743932975437,
  "peerURLs":[
  "http://localhost:2380"],
  "name":"default",
  "clientURLs": ["http://localhost:2379"]
}
Согласованные сведения о составе кластера.
members_removed[memberId] в шестнадцатеричном виде как строка: "8e9e05c52164694d"[]byte("removed")

Идентификаторы всех удалённых участников. Используются для проверки, что удалённый участник никогда не добавляется снова с тем же идентификатором.

Сейчас (3.4) поле читается из хранилища V2 и никогда из V3. См. https://github.com/etcd-io/etcd/pull/12820

meta"consistent_index"байты uint64 (BigEndian)Представляет смещение последней записи WAL, применённой к хранилищу БД bolt.
"scheduledCompactRev"закодированный bytesToRev{main,sub}. (16 байтов)Используется для повторной инициализации компактизации, если после её запроса произошёл сбой.
"finishedCompactRev"закодированный bytesToRev{main,sub}. (16 байтов)Ревизия, на которой недавно успешно компактизировано хранилище (https://github.com/etcd-io/etcd/blob/ae7862e8bc8007eb396099db4e0e04ac026c8df5/server/mvcc/kvstore_compaction.go#L54)
"confState"Начиная с etcd 3.5
"term"Начиная с etcd 3.5
"storage-version"

Инструменты

bbolt

bbolt предоставляет инструмент командной строки для исследования содержимого файла.

Примеры использования:

Перечисление всех сегментов заданного файла bbolt:
% go run go.etcd.io/bbolt/cmd/bbolt buckets ./default.etcd/member/snap/db
Чтение определённой пары «ключ — значение»:
% go run go.etcd.io/bbolt/cmd/bbolt get ./default.etcd/member/snap/db cluster clusterVersion

etcd-dump-db

etcd-dump-db позволяет перечислить содержимое бэкенда etcd v3 (bbolt).

% go run go.etcd.io/etcd/v3/tools/etcd-dump-db  list-bucket default.etcd
alarm
auth
...

Дополнительные примеры: https://github.com/etcd-io/etcd/tree/master/tools/etcd-dump-db

WAL: журнал предзаписи

Журнал предзаписи — постоянное хранилище Raft для предложений. Сначала лидер сохраняет предложение в своём журнале, а затем параллельно реплицирует его последователям по протоколу Raft. Каждый последователь сохраняет предложение в своём WAL, прежде чем подтвердить репликацию лидеру.

Используемый в etcd журнал WAL отличается от канонической модели Raft по 2 признакам:

  • Он сохраняет не только индексированные записи, но также облегчённые снимки Raft и hard-state. Поэтому полное состояние Raft участника можно восстановить только из журнала WAL.
  • Он доступен только для добавления. Записи не перезаписываются на месте: добавленная позднее в файл запись с тем же индексом замещает предыдущую.

Имена файлов

Файлы журнала WAL именуются по следующему шаблону:

"%016x-%016x.wal", seq, index

Пример: ./member/wal/0000000000000010-00000000000bf1e6.wal

Таким образом, имя файла содержит в шестнадцатеричной кодировке:

  • Порядковый номер файла журнала WAL
  • Индекс первой записи или снимка в файле. В частности, первый файл “0000000000000000-0000000000000000.wal” содержит запись исходного снимка с index=0.

Физическое содержимое

Файл журнала WAL содержит последовательность «кадров ». Каждый кадр содержит:

  1. Закодированное в LittleEndian 2 значение uint64, содержащее длину сериализованной walpb.Record (3).
  2. Заполнение: некоторое количество байтов 0, обеспечивающее выравнивание размера всего кадра (mod 8)
  3. Сериализованные данные walpb.Record :
    1. type — перечисление в кодировке int, определяющее интерпретацию поля data ниже
    2. data — в зависимости от типа, обычно сериализованный proto
    3. crc — контрольная сумма RC-32 совокупности всех полей “data” без type во всех записях журнала этой реплики с момента создания WAL. Обратите внимание: CRC учитывает ВСЕ записи, даже если Raft их не зафиксировал.

Файлы «обрезаются» (начинается новый файл), когда текущий превышает 64*10^6 байтов.

Логическое содержимое

На логическом уровне файлы журнала предзаписи содержат:

  • Raftpb.Entry: недавние предложения, реплицированные лидером Raft. Некоторые из них считаются зафиксированными, а остальные могут быть логически замещены.
  • Raftpb.HardState(term,commit,vote): периодические и очень частые сведения об индексе зафиксированной записи журнала, то есть реплицированной на большинство серверов, гарантированно не подлежащей изменению или замещению и применимой к бэкендам (v2, v3). Также содержит “term”, указывающий на изменения, связанные с выборами, и vote — участника, за которого текущая реплика проголосовала в текущем сроке полномочий.
  • walpb.Snapshot(term, index): периодические снимки состояния Raft без содержимого БД, только индекс журнала снимка и срок полномочий Raft
    • Содержимое хранилища V2 хранится в отдельных файлах *.store.
    • Содержимое хранилища V3 находится в файле bbolt и становится неявным снимком сразу после применения записей.
  • запись контрольной суммы crc32 в начале каждого файла, позволяющая продолжить проверку CRC для остальной части файла.
  • etcdserverpb.Metadata(node_id, cluster_id) — идентификаторы кластера и реплики, представленных журналом.

Каждый файл журнала WAL состоит из следующих элементов по порядку:

  1. Кадр CRC-32 (накопленная crc всех предыдущих файлов, 0 для первого файла).

  2. Кадр метаданных (идентификаторы кластера и реплики)

  3. Только для исходного файла WAL:

    • Пустой кадр Snapshot (Index:0, Term: 0). Он поддерживает инвариант, согласно которому всем записям «предшествует» снимок.

    Для не исходного файла WAL (2nd+):

    • Кадр HardState.
  4. Смесь записей entry, hard-state и snapshot

Журнал WAL может содержать несколько записей с одним индексом. Такая ситуация возможна в случаях, описанных на рисунке 7 статьи о Raft . Журнал WAL etcd доступен только для добавления, поэтому запись замещается добавлением новой записи с тем же индексом.

В частности, при чтении WAL логика замещает старые записи новыми . Поэтому окончательной можно считать только последнюю версию записей с entry.index <= HardState.commit. Записи с index > HardState.commit могут изменяться.

Значения “terms” в журнале WAL должны быть монотонными.

Значения “indexes” в журнале WAL должны:

  1. начинаться с некоторого снимка
  2. последовательно расти после снимка, пока остаются в том же ‘term’
  3. при изменении term индекс может уменьшиться, но только до нового значения, превышающего последний HardState.commit.
  4. новый снимок может появиться с любым index >= HardState.commit, открывая новую последовательность индексов.
etcd persistent storage files

Инструменты

etcd-dump-logs

Журналы WAL etcd можно читать инструментом etcd-dump-logs :

% go install go.etcd.io/etcd/v3/tools/etcd-dump-logs@latest

% go run go.etcd.io/etcd/v3/tools/etcd-dump-logs --start-index=0 aname.etcd

Учитывайте следующее:

  • Инструмент показывает только Entries, а не все записи WAL (Snapshots, HardStates) из файлов журнала.
  • Инструмент автоматически применяет «замещения» записей. Если запись замещена более новой с тем же индексом, выводится только окончательное значение.
  • Инструмент также выводит незафиксированные записи из хвоста LOG без сведений о HardState.commitIndex, поэтому неизвестно, являются ли они окончательными.

Снимки (хранилища V2): member/snap/{term}-{index}.snap

Имена файлов:

member/snap/{term}-{index}.snap

Имена файлов создаются здесь по шаблону ("%016x-%016x.snap") и содержат 2 компонента в шестнадцатеричной кодировке:

  • term -> срок полномочий Raft (период между выборами) на момент создания снимка
  • index -> индекс последнего применённого предложения на момент создания снимка

Создание

Файлы *.snap создаются методом Snapshotter.SaveSnap .

Создание этих файлов управляется 2 триггерами:

  • Новый файл создаётся примерно каждые –snapshotCount=(по умолчанию 100'000) применённых предложений. Значение приблизительно, поскольку предложения могут поступать пакетами, создание снимка рассматривается только в конце пакета, а сам процесс планируется асинхронно. Имя флага (–snapshotCount) не вполне точно: он определяет разницу значений индекса между индексом последнего снимка и индексом последнего применённого предложения.
  • Raft требует, чтобы реплика восстановилась из снимка. Получая снимок по сети в сообщении msgSnap, реплика также создаёт его облегчённую контрольную точку в журнале WAL. Это гарантирует, что в хвосте журналов WAL всегда находится действительный снимок с последующими записями, и предотвращает возможный разрыв непрерывности журналов.

Сейчас файлы приблизительно3 соответствуют записям Snapshot журналов WAL в отношении 1-1. После вывода хранилища v2 из эксплуатации запись этих файлов должна полностью прекратиться (необязательно в 3.5.x, обязательно в 3.6.x).

Содержимое

Файл содержит сериализованный proto snapdb.snapshot (uint32 crc, bytes data),

в поле ‘data’ которого находится Raftpb.Snapshot :

(bytes data, SnapshotMetadata{index, term, conf } metadata),

Наконец, вложенные данные содержат сериализованное в JSON содержимое хранилища v2 .

В частности, присутствуют:

  • Term
  • Index
  • Данные о составе кластера:
    • /0/members/8e9e05c52164694d/attributes -> {"name":"default","clientURLs":["http://localhost:2379"]}
    • /0/members/8e9e05c52164694d/RaftAttributes -> "{"peerURLs":["http://localhost:2380"]}"
  • Версия хранилища: /0/version-> 3.5.0

Инструменты

protoc

Следующая команда позволяет просмотреть содержимое файла при выполнении из корневого каталога etcd:

cat default.etcd/member/snap/0000000000000002-0000000000049425.snap |
  protoc --decode=snappb.snapshot \
    server/etcdserver/api/snap/snappb/snap.proto \
    -I $(go list -f '{{.Dir}}' github.com/gogo/protobuf/proto)/.. \
    -I . \
    -I $(go list -m -f '{{.Dir}}' github.com/gogo/protobuf)/protobuf

Аналогично можно извлечь поле ‘data’ и декодировать его как ‘Raftpb.Snapshot '

Пример сериализованного в JSON содержимого хранилища v2 в файлах *.snap etcd 3.4:

{
  "Root":{
    "Path":"/",
    "CreatedIndex":0,
    "ModifiedIndex":0,
    "ExpireTime":"0001-01-01T00:00:00Z",
    "Value":"",
    "Children":{
      "0":{
        "Path":"/0",
        "CreatedIndex":0,
        "ModifiedIndex":0,
        "ExpireTime":"0001-01-01T00:00:00Z",
        "Value":"",
        "Children":{
          "members":{
            "Path":"/0/members",
            "CreatedIndex":1,
            "ModifiedIndex":1,
            "ExpireTime":"0001-01-01T00:00:00Z",
            "Value":"",
            "Children":{
              "8e9e05c52164694d":{
                "Path":"/0/members/8e9e05c52164694d",
                "CreatedIndex":1,
                "ModifiedIndex":1,
                "ExpireTime":"0001-01-01T00:00:00Z",
                "Value":"",
                "Children":{
                  "attributes":{
                    "Path":"/0/members/8e9e05c52164694d/attributes",
                    "CreatedIndex":2,
                    "ModifiedIndex":2,
                    "ExpireTime":"0001-01-01T00:00:00Z",
                    "Value":"{\"name\":\"default\",\"clientURLs\":[\"http://localhost:2379\"]}",
                    "Children":null
                  },
                  "RaftAttributes":{
                    "Path":"/0/members/8e9e05c52164694d/RaftAttributes",
                    "CreatedIndex":1,
                    "ModifiedIndex":1,
                    "ExpireTime":"0001-01-01T00:00:00Z",
                    "Value":"{\"peerURLs\":[\"http://localhost:2380\"]}",
                    "Children":null
                  }
                }
              }
            }
          },
          "version":{
            "Path":"/0/version",
            "CreatedIndex":3,
            "ModifiedIndex":3,
            "ExpireTime":"0001-01-01T00:00:00Z",
            "Value":"3.5.0",
            "Children":null
          }
        }
      },
      "1":{
        "Path":"/1",
        "CreatedIndex":0,
        "ModifiedIndex":0,
        "ExpireTime":"0001-01-01T00:00:00Z",
        "Value":"",
        "Children":{


        }
      }
    }
  },
  "WatcherHub":{
    "EventHistory":{
      "Queue":{
        "Events":[
          {
            "action":"create",
            "node":{
              "key":"/0/members/8e9e05c52164694d/RaftAttributes",
              "value":"{\"peerURLs\":[\"http://localhost:2380\"]}",
              "modifiedIndex":1,
              "createdIndex":1
            }
          },
          {
            "action":"set",
            "node":{
              "key":"/0/members/8e9e05c52164694d/attributes",
              "value":"{\"name\":\"default\",\"clientURLs\":[\"http://localhost:2379\"]}",
              "modifiedIndex":2,
              "createdIndex":2
            }
          },
          {
            "action":"set",
            "node":{
              "key":"/0/version",
              "value":"3.5.0",
              "modifiedIndex":3,
              "createdIndex":3
            }
          }
        ]
      }
    }
  }
}

Изменения

Этот раздел предназначен для описания изменений форматов файлов между различными версиями etcd.


  1. Страницы метаданных в начале файла bbolt изменяются на месте. ↩︎

  2. Это непоследовательно, поскольку большинство uint записываются в bigendian ↩︎

  3. Исходный снимок (index:0) в начале журнала WAL не связан с файлом *.snap. Кроме того, старые файлы *.snap или журналы WAL могут удаляться. ↩︎