Файли постійного сховища etcd

Довідка про формат постійного сховища та файли

Цей документ пояснює формат постійного сховища etcd: іменування, вміст та інструменти, які дозволяють розробникам перевіряти їх. У майбутньому документ буде доповнюватися змінами в моделі зберігання даних. Цей документ призначений для розробників etcd, щоб допомогти їм у відновленні даних.

Передумови

Наступні статті надають корисну інформацію для цього документа:

Огляд

Довготривалі файли

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

Періодичні знімки старого сховища v2, що містять:

  • основну інформацію про членство
  • версію etcd

З версії etcd v3, вміст дублюється у файлах /snap/db.

Періодично (30с) ці файли видаляються, і зберігаються останні --max-snapshots=5.

/member/snap/000000000007a178.snap.db

Повний знімок bbolt, завантажений з лідера etcd, якщо репліка відставала занадто сильно.

Має такий самий тип вмісту, як і файл (./member/snap/db).

Файл використовується у 2 сценаріях:

  • У відповідь на запит лідера для відновлення зі знімка.
  • Під час запуску сервера, коли останній знімок (.snap.db файл) виявляється новішим за consistent_index у поточному файлі snap.db.
Примітка: Періодичні знімки, створені на кожній репліці, випускаються лише у формі *.snap файлу (не snap.db файлу). Тому немає гарантії, що найновіший знімок (у журналі WAL) має *.snap.db файл. Але в такому випадку бекенд (snap/db) очікується новішим за знімок.

Файл не видаляється після завершення відновлення (тому весь вміст переноситься у файл ./member/snap/db). Періодично (30с) файли видаляються. Тут також зберігаються --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 (або .../1.tmp)
Попередньо виділений простір для наступного файлу журналу попереднього запису. Використовується для запобігання зупинці Raft через відсутність можливості журналів WAL без можливості підняти тривогу.

Тимчасові файли

Під час внутрішньої обробки etcd можливо, що можуть зʼявитися кілька короткотривалих файлів:

ФайлОсновна мета
./member/snap/0000000000000002-000000000007a178.snap.broken

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

Спроба завантажити найновіший файл відбувається під час запуску etcd.

Або під час команд резервного копіювання/міграції etcdctl.

./member/snap/tmp071677638 (випадковий суфікс)

Тимчасовий (bbolt) файл, створений на репліках у відповідь на запит msgSnap лідера, тобто на вимогу лідера відновити сховище зі знімка.

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

Див. etcd/issues/12837. Виправлено у версії etcd 3.5.

/member/snap/db.tmp.071677638 (випадковий суфікс)

Тимчасовий файл, що містить копію вмісту бекенду (/member/snap/db), під час процесу дефрагментації. Після успішного процесу файл перейменовується на /member/snap/db, замінюючи оригінальний бекенд.

Під час запуску сервера etcd ці файли видаляються.

bbolt b+tree: member/snap/db

Цей файл містить основний вміст etcd, застосований до певної точки журналу Raft (див. consistent_index).

Фізична організація

Краще сховище 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""" (порожній) або BigEndian.PutUint64

Будь-яка зміна ролей або користувачів збільшує це поле під час коміту транзакції.

Значення використовується лише для оптимістичного блокування під час процесу авторизації.

authRoles[roleName] як stringauthpb.Role серіалізований
authUsers[userName] як stringauthpb.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")

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

Примітка: LeaseCheckpoint розширює лише RemainingTTL. Просто TTL з оригінального Grant.

Примітка2: Ми зберігаємо TTL у секундах (з невизначеного 'зараз'). Сервер, що зазнає краху, не звільняє оренди!!!

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

Ідентифікатори всіх видалених членів. Використовується для перевірки, що видалений член ніколи не додається знову під тим самим ідентифікатором.

Поле наразі (3.4) читається лише зі сховища V2 і ніколи з V3. Див. https://github.com/etcd-io/etcd/pull/12820

meta"consistent_index"uint64 байти (BigEndian)Представляє зміщення останнього застосованого запису журналу WAL до сховища bolt DB.
"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 можна використовувати для переліку вмісту бекенду v3 etcd (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: Журнал попереднього запису

Журнал попереднього запису (Write ahead log) є постійним сховищем Raft, яке використовується для зберігання пропозицій. Спочатку лідер зберігає пропозицію у своєму журналі, а потім (одночасно) реплікує її за допомогою протоколу Raft до послідовників. Кожен послідовник зберігає пропозицію у своєму журналі WAL перед підтвердженням реплікації лідеру.

Журнал WAL, що використовується в etcd, відрізняється від канонічної моделі Raft двома способами:

  • Він зберігає не тільки індексовані записи, але й знімки Raft (легкі) та жорсткий стан. Таким чином, весь стан Raft члена можна відновити лише з журналу WAL.
  • Він призначений лише для додавання. Записи не перевизначаються на місці, але запис, доданий пізніше у файлі (з тим самим індексом), витісняє попередній.

Імена файлів

Файли журналу WAL називаються за наступним шаблоном:

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

Приклад: ./member/wal/0000000000000010-00000000000bf1e6.wal

Отже, імена файлів містять шістнадцяткові коди:

  • Послідовний номер файлу журналу WAL
  • Індекс першого запису або знімка у файлі. Зокрема, перший файл “0000000000000000-0000000000000000.wal” має початковий запис знімка з індексом=0.

Фізичний вміст

Файл журналу WAL містить послідовність “Фреймів”. Кожен фрейм містить:

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

Файли “розрізаються” (починається новий файл), коли поточний файл перевищує 64*10^6 байтів.

Логічний вміст

Файли журналу попереднього запису на логічному рівні містять:

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

Кожен файл журналу WAL складається з (у порядку):

  1. Фрейм CRC-32 (поточний crc з усіх попередніх файлів, 0 для першого файлу).

  2. Фрейм метаданих (ідентифікатори кластера та репліки)

  3. Для початкового файлу WAL:

    • Порожній фрейм знімка (Index:0, Term: 0). Мета цього фрейму — підтримувати інваріант, що всі записи «передують» знімку.

    Для не початкового (2-го+) файлу WAL:

    • Фрейм жорсткого стану.
  4. Змішування записів, жорсткого стану та знімків

Журнал WAL може містити кілька записів для одного індексу. Така ситуація може статися у випадках, описаних на малюнку 7. у статті про Raft. Журнал etcd WAL лише доповнюється, тому записи перевизначаються шляхом додавання нового запису з тим самим індексом.

Зокрема, під час читання журналу WAL, логіка перезаписує старі записи новими записами. Таким чином, лише остання версія записів з entry.index <= HardState.commit може вважатися остаточною. Записи з індексом > HardState.commit можуть змінюватися.

“term” у журналі WAL очікуються монотонними.

“indexe” у журналі WAL очікуються:

  1. починаються з якогось знімка
  2. послідовно зростають після цього знімка, поки вони залишаються у тому ж ‘term’
  3. якщо term змінюється, індекс може зменшуватися, але до нового значення, яке є вищим за останній HardState.commit.
  4. новий знімок може статися з будь-яким індексом >= HardState.commit, що відкриває нову послідовність для індексів.

Інструменти

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

Зверніть увагу, що:

  • Інструмент показує лише записи, а не всі записи WAL (Snapshots, HardStates), які є у файлах журналу WAL.
  • Інструмент автоматично застосовує «перезаписи» до записів. Якщо запис був перезаписаний (новішим записом з тим самим індексом), інструмент покаже лише остаточне значення.
  • Інструмент також показує непідтверджені записи (з кінця журналу), без інформації про HardState.commitIndex, тому невідомо, чи записи є остаточними чи ні.

Знімки (Store 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 завжди є дійсний знімок, за яким слідують записи. Таким чином, це пригнічує потенційну відсутність безперервності у журналах WAL.

Наразі файли приблизно3 асоціюються 1-1 із записами знімків журналу WAL. З виведенням з експлуатації сховища v2 ми очікуємо, що файли перестануть записуватися взагалі (опціонально: 3.5.x, обовʼязково 3.6.x).

Вміст

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

  • у полі ‘data’ містить Raftpb.Snapshot:
  • (байти даних, SnapshotMetadata{index, term, conf} метадані),

Нарешті, вкладені дані містять серіалізований JSON вміст сховища v2.

Зокрема, є:

  • Term
  • Index
  • Дані про членство:
    • /0/members/8e9e05c52164694d/attributes -> {"name":"default","clientURLs":["[http://localhost:2379](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'

Зразковий вміст сховища v2 у форматі JSON у файлах *.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) можуть бути видалені. ↩︎