Elasticsearchのスナップショットについて復習

この記事は Elastic Stack (Elasticsearch) Advent Calendar22日目の記事です。

今回はElasticsearchのスナップショットについて復習していきます。

背景と目的

ある日仕事で「Searchable Snapshotなにもわからない。」となりまして、「いや、そもそもSearchable Snapshotというかスナップショット自体、使っているわりに詳しく知らないわ」ということでスナップショットの復習していきたいと思います。

スナップショットといっても復元や削除も含めると範囲が広いため、今回は作成だけにスコープをしぼります。

Elasticsearchについて簡単な復習

「そもそもこんな記事を開いて、さらにこの先を読もうとしている人は知ってるだろ...」とは思いつつ、前提になるElasticsearchの物理概念についてざっくり復習します。

ノード

Elasticsearchが動くサーバーのことをノードと呼びます。

ノードにはいくつか役割があります。ひとつのノードがひとつの役割を持つこともあれば、複数の役割を持つこともあります。

正確には他にも種類や役割がありますが、ここでは割愛して後の説明で出てくる役割だけ紹介します。

  • masterノード
    • のちほど紹介するクラスタを管理する役割を担います。クラスタには1台のmasterノードが必要です。
  • Dataノード
    • データの格納、クエリの応答などを行う役割を持ちます。

それ以外の役割についてはドキュメントや本を読んでください。

クラスタ

Elasticsearchはノードを起動すると、互いにメッセージを送信しあい自律的にノードグループを形成することができます。 これをクラスタとよびます。

インデックス

Elasticsearchでドキュメントを保存する場所をインデックスと呼びます。RDBでいうとテーブルのようなものです。

シャード

インデックスに保存されるデータは、一定数(最低一つ)のシャードと呼ばれる単位に分割されてノードに分散されて格納されます。 これにより検索性能をスケールさせることができます。

シャードには種類が2つあります。

オリジナルのシャードであるプライマリシャードと、プライマリシャードをコピーしたレプリカシャードです。 レプリカシャードに複製することで、インデックスの可用性を担保します。また検索処理はレプリカシャードでも行われるため検索性能が向上するメリットもあります。

セグメント

名前がややこしいのですが、Elasticsearchのシャードは実態はLuceneインデックスです。 このLuceneインデックスはセグメントとよばれる単位でデータを格納しています。

シャード(Luceneインデックス)にドキュメントが書き込まれると、一定の間隔でLuceneはデータをセグメントに書き込みます。 このセグメントは一度作成されると中身を変更されることのないファイルになります。

実際はもっと複雑ですが、今回の趣旨から外れてしまうため、詳しくは以下のドキュメントを参照してください。

Inside a Shard | Elasticsearch: The Definitive Guide [2.x] | Elastic

スナップショット

概要

やっと本題に入りいます。

スナップショットは起動しているクラスタやインデックスのバックアップをとる機能です。

バックアップは、S3やGCSのようなオブジェクトストレージをリポジトリとして使えます。ノードのローカルストレージもリポジトリとして使用可能です。

実行の仕方

具体的な使い方の説明は割愛するので、ドキュメントを見てください。

Snapshot and restore | Elasticsearch Guide [8.5] | Elastic

スナップショットの仕組み

スナップショットには、クラスタの状態やデータストリームやインデックスが含まれます。

インデックスをバックアップする際に、インデックスのセグメントをコピーしてリポジトリに格納します。 セグメントは不変なので、コピーされるのは前回のバックアップからの差分のみです。

バックアップする際にオリジナルのコピーであるレプリカシャードは不要なため、コピーされるセグメントはプライマリシャードのみです。 したがって、インデックスのスナップショットの処理はプライマリシャードをもつデータノードのみで行われます。

一方で、クラスタの状態をリポジトリに書き込む処理はマスターノードで行われます。

また、プライマリシャードのセグメントをコピーするため、プライマリシャードが使えない状態であるときはスナップショットに失敗します。 もしプライマリシャードが開始中・再配置中の場合はプロセスが終わるの待ってからコピーが始まります。

生成されるファイルを確認する

なんとなくスナップショットの仕組みがわかったところで、実際にどういうファイルが生成されるのかを確認します。

今回は手元にノードを立ち上げてローカルファイルストレージをリポジトリとして登録します。

環境は以下を使います。

github.com

dockerでElasticsearchを立ち上げて、type: "fs"リポジトリのlocationを登録します。 この際ni

url="http://localhost:9200"
index_name="test_index"

# register a snapshot repository
curl -XPUT -H 'Content-Type: application/json' ${url}/_snapshot/fs_backup -d @- <<EOS
{
  "type": "fs",
  "settings": {
    "location": "/usr/share/elasticsearch/backup"
  }
}
EOS

完了したら適当なインデックスを作って、いくつかドキュメントを追加します。

# init index & data
curl -XDELETE ${url}/${index_name}
curl -XPUT ${url}/${index_name}

curl -XPOST -H 'Content-Type: application/json' ${url}/${index_name}/_doc/1 -d '{"name": "宇宙兄弟"}'
curl -XPOST -H 'Content-Type: application/json' ${url}/${index_name}/_doc/2 -d '{"name": "カオスゲーム"}'
curl -XPOST -H 'Content-Type: application/json' ${url}/${index_name}/_doc/3 -d '{"name": "ダンジョンの中の人"}'
curl -XPOST -H 'Content-Type: application/json' ${url}/${index_name}/_doc/4 -d '{"name": "ガクサン"}'
curl -XPOST -H 'Content-Type: application/json' ${url}/${index_name}/_doc/5 -d '{"name": "トリリオンゲーム"}'
curl -XPOST -H 'Content-Type: application/json' ${url}/${index_name}/_doc/6 -d '{"name": "ワールドトリガー"}'
curl -XPOST -H 'Content-Type: application/json' ${url}/${index_name}/_doc/7 -d '{"name": "暗殺教室"}'

最後に作ったインデックスを対象にスナップショット作成します。 エンドポイントのmy_snapshot_1は独自につけることができ、これが今回作成するスナップショットを識別する名前になります。

今回は生成されるファイルを確認しやすくするため、インデックス (test_index)をひとつだけ指定して、my_snapshot_1という名前のスナップショットを作成します。

url="http://localhost:9200"
index_name="test_index"

curl -XPUT -H 'Content-Type: application/json' ${url}/_snapshot/fs_backup/my_snapshot_1 -d @- <<EOS
{
    "indices": "${index_name}",
    "include_global_state": false
}

buckupに指定したディレクトリを見ると以下のようなファイル郡が作成されていました。

indices/
    LyrD9CRASh2-hs2o2L3Wjg/
        0/
            __BLqNUJH1QjSLsZyiW_zO9Q
            __nlDaU-UySNuvtFu8zHZ2Pw
            index-DCx8RmXXQeq5IOIJQ0spwQ
            snap-k4KAjMGQRAqtC4eysZ1l6g.dat
        meta-IGxlKoUBCn8g1JRSN441.dat
index-0
index.latest
meta-k4KAjMGQRAqtC4eysZ1l6g.dat
snap-k4KAjMGQRAqtC4eysZ1l6g.dat

生成されたファイルの解説

生成されたファイルがなにか簡単に説明していきます。正直ここは理解が怪しいところです。間違っている可能性が大いにあります。

file or directory 説明 中身
indices/ インデックスの全データを含むディレクト
indices/LyrD9CRASh2-hs2o2L3Wjg/ test_indexのデータを含むディレクトリ。indicesディレクトリ内の一意のIDが名前
indices/LyrD9CRASh2-hs2o2L3Wjg/0/ test_indexの初回スナップショットのデータを含むディレクトリ。2回目のスナップショットを行うと1が作成される。
indices/LyrD9CRASh2-hs2o2L3Wjg/0/__BLqNUJH1QjSLsZyiW_zO9Q セグメントファイル 前回スナップショットとの差分のみのセグメントファイル
indices/LyrD9CRASh2-hs2o2L3Wjg/0/__nlDaU-UySNuvtFu8zHZ2Pw セグメントファイル 前回スナップショットとの差分のみのセグメントファイル
indices/LyrD9CRASh2-hs2o2L3Wjg/0/index-DCx8RmXXQeq5IOIJQ0spwQ test_indexのシャードのすべてのスナップショットに関する情報 SMILE形式にシリアライズされたorg.elasticsearch.index.snapshots.blobstore.BlobStoreIndexShardSnapshots
indices/LyrD9CRASh2-hs2o2L3Wjg/0/snap-k4KAjMGQRAqtC4eysZ1l6g.dat スナップショットmy_snapshot_1が参照するすべてのファイルのリストとスナップショットに関するメタデータ SMILE形式にシリアライズされたorg.elasticsearch.index.snapshots.blobstore.BlobStoreIndexShardSnapshot
indices/LyrD9CRASh2-hs2o2L3Wjg/meta-IGxlKoUBCn8g1JRSN441.dat test_indexのメタデータ SMILE形式にシリアライズされたorg.elasticsearch.cluster.metadata.IndexMetadata
index-0 すべてのスナップショットidと各スナップショットのインデックスの情報, 数字は世代 json
index.latest 最新のスナップショットの世代を持つファイル numeric value
meta-k4KAjMGQRAqtC4eysZ1l6g.dat スナップショットmy_snapshot_1のクラスタメタデータ SMILE形式にシリアライズされたorg.elasticsearch.cluster.metadata.Metadata
snap-k4KAjMGQRAqtC4eysZ1l6g.dat スナップショットmy_snapshot_1に関する情報 SMILE形式にシリアライズされたorg.elasticsearch.snapshots.SnapshotInfo

ディレクトリやファイル名はuuidになっていて読みにくいですが、インデックス名とスナップショット名の対応がindex-0に書かれています。

{
    "min_version": "7.12.0",
    "uuid": "AGqsvostTMKOAIvPvI2unQ",
    "cluster_id": "LRMOnF-tQc-0u33BqcAseQ",
    "snapshots": [
        {
            "name": "my_snapshot_1",
            "uuid": "k4KAjMGQRAqtC4eysZ1l6g",
            "state": 1,
            "index_metadata_lookup": {
                "LyrD9CRASh2-hs2o2L3Wjg": "2Um7BIJBT6CFF-HZj-7sdw-_na_-1-2-1"
            },
            "version": "8.5.3",
            "start_time_millis": 1671453554078,
            "end_time_millis": 1671453554278,
            "slm_policy": ""
        }
    ],
    "indices": {
        "test_index": {
            "id": "LyrD9CRASh2-hs2o2L3Wjg",
            "snapshots": [
                "k4KAjMGQRAqtC4eysZ1l6g"
            ],
            "shard_generations": [
                "DCx8RmXXQeq5IOIJQ0spwQ"
            ]
        }
    },
    "index_metadata_identifiers": {
        "2Um7BIJBT6CFF-HZj-7sdw-_na_-1-2-1": "IGxlKoUBCn8g1JRSN441"
    }
}

セグメントファイル以外のほとんどのファイルがElasticsearchのクラスがもとになったバイナリファイルのようです。

正直なところ、この説明だけをみても中身や用途がわかりません。 このあたりを読み解くには実際にコードを追っていく必要があります。

スナップショット生成時に行われる処理

先程みたファイル郡を生成する処理は、BlobStoreRepositoryクラスにベースとなる実装があります。

が、APIのリクエストから生成処理の説明はかなり長くなるので、また後日ということにしたいと思います。