本文へ移動

etcdの永続ストレージファイル

永続ストレージの形式とファイルのリファレンス

このドキュメントでは、etcdの永続ストレージの形式、つまり命名、内容、開発者が内容を調べるためのツールを説明します。今後、ストレージモデルの変更に合わせて内容を拡充することを想定しています。etcd開発者のデータリカバリーに役立つことを目的としています。

前提知識

次の記事は、このドキュメントを読むための背景知識として役立ちます。

概要

長期間保持されるファイル

ファイル名 主な用途
./member/snap/db
bboltのb+treeです。適用済みのすべてのデータ、メンバーシップの認可情報、メタデータを保存します。最後に適用したWALログのインデックス("consistent_index")を把握しています。
./member/snap/0000000000000002-0000000000049425.snap
./member/snap/0000000000000002-0000000000061ace.snap

定期的に作成する旧v2ストアのスナップショットで、次を含みます。

  • 基本的なメンバーシップ情報
  • etcd-version

etcd v3以降、その内容は/snap/dbファイルの内容と重複しています。

定期的(30s)にファイルを削除し、直近の--max-snapshots=5個を保持します。

/member/snap/000000000007a178.snap.db

レプリカが大きく遅れた場合に、etcdリーダーからダウンロードする完全なbboltスナップショットです。

内容の種類は、./member/snap/dbファイルと同じです。

このファイルは、次の2つの場合に使用します。

  • スナップショットからのリカバリーを求める、リーダーの要求に応じる場合。
  • サーバーの起動時に、最後のスナップショット(.snap.dbファイル)が見つかり、そのインデックスが現在のsnap.dbファイルのconsistent_indexより新しいと分かった場合。
注意:各レプリカが定期的に生成するスナップショットは、*.snapファイルとしてだけ出力され、snap.dbファイルにはなりません。そのため、WALログ内の最新のスナップショットに、対応する*.snap.dbファイルがあるとは限りません。ただし、その場合はバックエンド(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)
次の先行書き込みログファイルのために事前に割り当てる領域です。 WALログの容量不足により、アラームを発報することもできずにRaftが停止するのを防ぎます。

一時ファイル

etcdの内部処理中には、短時間だけ存在する次のようなファイルが見られることがあります。

ファイル 主な用途
./member/snap/0000000000000002-000000000007a178.snap.broken

スナップショットファイルを読み込めない場合、ファイル名を‘broken’に変更します。

最新のファイルの読み込みは、etcdの起動時に試みます。

または、etcdctlのbackup/migrateコマンド実行時にも行います。

./member/snap/tmp071677638 (random suffix)

リーダーのmsgSnapリクエスト、つまり指定したスナップショットからストレージをリカバリーする要求に応じて、レプリカ上に作成する一時的なbboltファイルです。

内容の取得が正常に完了すると、ファイルをリネームし、/member/snap/[SNAPSHOT-INDEX].snap.dbにします。ファイルのダウンロード中にサーバーが停止したり強制終了されたりすると、ファイルはディスク上に残り、自動的には削除されません。サイズはGB単位になることがあります。

etcd/issues/12837を参照してください。etcd 3.5で修正されています。

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

デフラグメンテーション処理中に、バックエンドの内容(/member/snap/db)のコピーを保持する一時ファイルです。処理が正常に完了すると、/member/snap/dbにリネームし、元のバックエンドを置き換えます。

etcdサーバーの起動時に、これらのファイルを削除します。

bbolt b+tree: member/snap/db

このファイルは、Raftログの特定の時点まで適用された、etcdの主要な内容を保持します(consistent_index を参照)。

物理的な構成

bboltストレージは、物理的にはb+tree として構成されます。b-treeの物理ページをその場で変更することはありません1。代わりに、空きページ一覧から回収した新しいページへ内容をコピーし、古いページにアクセスする可能性のある未終了のトランザクションがなくなった時点で、そのページを空きページ一覧に追加します。この処理により、未終了のROトランザクションは、一貫した過去のストレージ状態を参照できます。RWトランザクションは排他的であり、他のすべてのRWトランザクションをブロックします。
大きな値は、複数の連続したページに保存します。ページの回収処理と、異なる大きさの連続したページ領域を割り当てる必要性が組み合わさり、bboltストレージの断片化が進むことがあります。

bboltファイルが自動で縮小することはありません。デフラグメンテーション時にだけ、末尾に一定の空きページ領域を確保した、サイズを切り詰めた新しいファイルに書き換えられます。

論理的な構成

bboltストレージはバケットに分かれています。各バケットには、キー(byte[]->value byte[]のペア)を辞書順に保存します。以下は、etcd(バージョン3.5時点)で使用するバケットとキーの一覧です。

バケット キー 値の例 説明
alarm rpcpb.Alarm: {MemberID, Alarm: NONE|NOSPACE|CORRUPT} nil いずれかのメンバーで問題が検出されたことを示します。
auth "authRevision" ""(空)またはBigEndian.PutUint64

RolesまたはUsersを変更すると、トランザクションのコミット時にこのフィールドが増加します。

この値は、認可処理中の楽観的ロックにだけ使用します。

authRoles 文字列の[roleName] authpb.Roleをマーシャリングした値
authUsers 文字列の[userName] authpb.Userをマーシャリングした値
cluster "clusterVersion" "3.5.0"(文字列) コンセンサスで合意した共通ストレージバージョンのマイナーバージョンです。
"downgrade" JSON:
{
  "target-version": "3.4.0"
  "enabled": true/false
}

直近のDowngrade RPCリクエストで設定された意図を永続化します。

v3.5以降

key

[revisionId]をbytesToRev{main,sub}でエンコードしたものです。

キー・バリューの削除は、末尾に't'を付けてマーシャリングします(「トゥームストーン」として)。

mvccpb.KeyValueをマーシャリングしたproto(key, create_rev, mod_rev, version, value, lease id)
lease leasepb.Leaseをマーシャリングしたproto(ID, TTL, RemainingTTL)

注意:LeaseCheckpointが延長するのはRemainingTTLだけです。TTLは元のGrantの値です。

注意2:TTLは、未定義の「現在」からの秒数として永続化します。クラッシュを繰り返すサーバーは、リースを解放しません!!!

members [memberId]を16進数の文字列にしたもの:"8e9e05c52164694d" JSON文字列にシリアライズしたMember構造体:
{
  "id":10276657743932975437,
  "peerURLs":[
  "http://localhost:2380"],
  "name":"default",
  "clientURLs": ["http://localhost:2379"]
}
合意済みのクラスターのメンバーシップ情報です。
members_removed [memberId]を16進数の文字列にしたもの:"8e9e05c52164694d" []byte("removed")

削除されたすべてのメンバーのIDです。削除済みのメンバーが、同じIDで再び追加されないことを確認するために使います。

このフィールドは現在(3.4)、V2ストアから読み取り、V3からは読み取りません。https://github.com/etcd-io/etcd/pull/12820を参照してください。

meta "consistent_index" uint64のバイト列(BigEndian) bolt DBストレージに最後に適用したWALエントリーのオフセットを表します。
"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も永続化します。そのため、WALログだけで、そのメンバーのRaft状態全体をリカバリーできます。
  • 追記専用です。エントリーをその場で上書きせず、ファイルに後から追記した同じインデックスのエントリーが、以前のエントリーを置き換えます。

ファイル名

WALログファイルは、次のパターンで命名します。

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

例:./member/wal/0000000000000010-00000000000bf1e6.wal

つまり、ファイル名には次の情報が16進数で含まれます。

  • WALログファイルの連番
  • ファイル内の最初のエントリーまたはスナップショットのインデックス 特に、最初のファイル“0000000000000000-0000000000000000.wal”には、index=0の初期スナップショットレコードが含まれます。

物理的な内容

WALログファイルは、一連の「フレーム 」を含みます。各フレームには、次のものが含まれます。

  1. マーシャリングしたwalpb.Record (3)の長さを格納する、LittleEndian 2でエンコードしたuint64。
  2. パディング:フレーム全体のサイズを整列(mod 8)させるための、必要な数の0バイト。
  3. マーシャリングしたwalpb.Record のデータ:
    1. type - 下記のdataフィールドの解釈を決める、intでエンコードされた列挙値。
    2. data - typeによって異なり、通常はマーシャリングしたproto。
    3. crc - WALログの作成以降、このレプリカ上のすべてのログレコードの“data”フィールドを連結したもの(typeは含まない)のCRC-32チェックサム。CRCは、Raftでコミットされなかったものも含め、すべてのレコードを対象とすることに注意してください。

現在のファイルが64*10^6バイトを超えると、ファイルを「分割」し、新しいファイルを開始します。

論理的な内容

先行書き込みログファイルは、論理的には次を含みます。

  • Raftpb.Entry: Raftリーダーが複製した最近の提案です。一部は「コミット済み」と見なされ、残りは論理的に上書きされる可能性があります。
  • Raftpb.HardState(term,commit,vote): 「コミット済み」、つまり過半数のサーバーに複製されたログエントリーのインデックスに関する、定期的で非常に頻繁な情報です。このエントリーは、変更・上書きされないことが保証され、バックエンド(v2、v3)に適用できます。また、選挙に関する変更があったかを示す“term”と、現在の任期でこのレプリカが投票したメンバーを表すvoteも含みます。
  • 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. Metadataフレーム(クラスターとレプリカのID)

  3. 最初のWALファイルの場合だけ:

    • 空のSnapshotフレーム(Index:0, Term: 0)。 すべてのエントリーに「先行する」スナップショットがある、という不変条件を維持するためのフレームです。

    最初以外のWALファイル(2番目以降)の場合:

    • HardStateフレーム。
  4. entry、hard-state、snapshotレコードの混在

WALログは、同じインデックスのエントリーを複数含むことがあります。これは、Raftの論文 の図7で説明される状況で発生します。etcdのWALログは追記専用なので、同じインデックスの新しいエントリーを追記することで、以前のエントリーを上書きします。

特に、WALの読み取り時には、古いエントリーを新しいエントリーで上書きする処理 が行われます。そのため、entry.index <= HardState.commitを満たすエントリーの最後のバージョンだけが確定済みと見なせます。index > HardState.commitのエントリーは変更される可能性があります。

WALログの“term”は、単調であることが想定されています。

WALログのインデックスには、次の性質が想定されています。

  1. いずれかのスナップショットから始まる。
  2. 同じ‘term’に属する間は、そのスナップショット以降、順に増加する。
  3. 任期が変わるとインデックスが減少することもあるが、新しい値は、最新のHardState.commitより大きい。
  4. index >= HardState.commitを満たす任意のインデックスで新しいスナップショットが作られることがあり、そこからインデックスの新しい列が始まる。
etcdの永続ストレージファイル

ツール

etcd-dump-logs

etcdのWALログは、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)を表示するわけではありません。
  • エントリーの「上書き」は自動的に適用されます。同じインデックスの新しいエントリーで上書きされた場合、このツールは最後の値だけを表示します。
  • このツールは、ログ末尾の未コミットのエントリーも表示します。HardState.commitIndexの情報は表示しないため、エントリーが確定済みかどうかは分かりません。

ストアV2のスナップショット:member/snap/{term}-{index}.snap

ファイル名

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

ファイル名はここ で、("%016x-%016x.snap") の形式で生成されます。16進数でエンコードした次の2つの要素を使います。

  • term -> スナップショット出力時のRaftの任期(選挙間の期間)
  • index -> スナップショット出力時に最後に適用した提案のインデックス

作成

*.snapファイルは、Snapshotter.SaveSnap メソッドによって作成されます。

このファイルの作成には、次の2つの契機があります。

  • およそ–snapshot-count=(v3.7.0のデフォルトは10,000)件の提案を適用するたびに、新しいファイルを作成します。提案をバッチで受け取り、バッチの最後でのみスナップショットの作成を検討し、最終的な作成処理も非同期でスケジュールするため、件数は概数です。 –snapshot-countというフラグ名は、かなり誤解を招きやすいものです。制御するのは、最後のスナップショットのインデックスと、最後に適用した提案のインデックスとの差 だからです。
  • Raftがレプリカに、スナップショットからのリストアを要求した場合です。レプリカはネットワークでスナップショット(msgSnapメッセージ)を受信すると、WALログにも軽量なチェックポイントとして記録します。これにより、WALログの末尾には、常に有効なスナップショットがあり、その後にエントリーが続くことが保証されます。これによって、WALログに連続性の欠落が生じるのを防ぎます。

現在、このファイルはWALログのSnapshotエントリーと、おおむね31-1で対応しています。v2ストアの廃止に伴い、ファイルの書き込みは完全に停止される見込みです(3.5.xでは任意、3.6.xでは必須)。

内容

ファイルは、マーシャリングしたsnapdb.snapshot proto (uint32 crc, bytes data)を含みます。

その’data’フィールドには、Raftpb.Snapshot が格納されます。

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

最後に、入れ子になったdataには、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 としてデコードできます。

etcd 3.4の*.snapファイルにある、JSONにシリアライズしたv2ストアの内容の例

{
  "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の大半はビッグエンディアンで書き込まれるため、一貫していません。 ↩︎

  3. WALログの先頭にある初期スナップショット(index:0)には、対応する*.snapファイルがありません。また、古い*.snapファイルやWALログは削除されることがあります。 ↩︎