📌

ひとりMongoDB University / M201 MongoDB Performance(6)

2021/10/28に公開

この記録は、アドベントカレンダー形式ではじめた、MongoDB Universityの学習コースの記録の続きになります!

ただいまのコース

このコースでは、開発者 / オペレーション担当者双方向けの中級レベルの内容とのことです。
前回の記事は、ひとりMongoDB University / M201 MongoDB Performance(5) でした。

Chapter 3: Index Operations

Building Indexes (動画)

MongoDB 4.2 から利用できるようになった、ハイブリッドインデックスについて。

  • 従来のインデックス作成 (createIndex(...)) では、デフォルトではフォアグラウンドでインデックスを作成

  • この時、ドキュメントにロックがかかるため、最新の値の読み取りや更新ができない

  • インデックス作成時に、バックグラウンドでの作成の指定をすると、ロックはかからないが生成が完了するまで時間がかかっていた

  • Ref. https://docs.mongodb.com/manual/core/index-creation/

    • Index Builds on Populated Collections
    • MongoDB 4.2 からはインデックス作成時のパフォーマンスが向上
    • インデックス作成時にバックグラウンドで動くといった設定は必要ない
    • インデックス作成とロックに対して最適化された形でインデックスが作成される

※ MongoDB 4.4 からは、レプリカセット上のインデックス生成にもパフォーマンス向上が図られている。

Check: Building Indexes

MongoDB 4.2 以後について:

  • 以前のバージョンでは、インデックス作成時にオプション指定があった
    • インデックス作成にロックがかかるため
    • フォアグラウンドでの作成
    • バックグラウンドでの作成

Ref. https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/#options-for-all-index-types

For feature compatibility version (fcv) "4.2", all index builds use an optimized build process that holds the exclusive lock only at the beginning and end of the build process. The rest of the build process yields to interleaving read and write operations. MongoDB ignores the background option if specified.

4.2以降はこのオプションが指定されていても無視される。ロックはインデックス生成の最初と、最後のプロセスのみ。

Query Plans (動画)

  • Query Plans ってなに?

    • クエリに従って、いくつかのスキャンのプランを算出する
    • その中で、最も効率がいいものを採用する
    • クエリで指定したフィールドに対して、複数のパターンのインデックスの利用が考えられる場合には、それらがクエリプランとして用意される
    • explain() でクエリプランと採用されるプランが取得できる
  • Query planは同じ条件のクエリが渡された時に再利用できるようにキャッシュされる

    • mongod / mmongos の再起動や、インデックスの再構築、キャッシュの明示的なクリアによってキャッシュが削除される

Check: Query Plans

  • Query plans are cached so that plans do not need to be generated and compared against each other every time a query is executed.
  • クエリプランはキャッシュされるよ!

Forcing Indexes with Hint (動画)

Call this method on a query to override MongoDB's default index selection and query optimization process. Use db.collection.getIndexes() to return the list of current indexes on a collection.
このメソッドで、MongoDB が選択するデフォルトのインデックスをオーバーライドできる


# hint() のパラメーターとして、インデックスのフィールドを渡す
# { age: 1 } もしくは { "age_1" } という渡し方 (後者はキーパターン)

# 以下は複合インデックスを使うように設定
db.people.find({ "first_name": "James", "address.zip": "70945-6616" })
  .hint("first_name_1_address.street_-1_address.city_-1_ssn_1")

制約はいくつかあるみたい。

  • Index filter が指定されている時は hint() は無視される
  • $text クエリを含む場合は無視される
  • hidden index を使う場合はこの指定は無視される

通常はMongoDBのオプティマイザが返すクエリプランに従う方が良い。
hint() を使うようなケースは、コレクションにたくさんインデックスが設定されているようなケース。

Check: Forcing Indexes with Hint

クエリの実行計画(plan) にヒントを指定したい場合は?

  • hint() を使いましょう

Resource Allocation for Indexes Part 1 (動画)

  • インデックス作成前後の効果をチェック
    • explain('executionStats') で確認
  • インデックスでの効果は期待できるが、考慮すべき点がいくつかある
    • インデックスのサイズ
    • インデックスデータの割り当て先(Allocation)

インデックスサイズ

Returns statistics that reflect the use state of a single database.
The db.stats() method is a wrapper around the dbStats database command.


# m201 データーベースの例
show collections
foreign_text
people
products
restaurants
textExample

# m201 でインデックスデータを確認
db.stats()
{
  db: 'm201',
  collections: 5,   # show collections で表示された件数と一致
  views: 0,
  objects: Long("341482"),
  avgObjSize: 211.94357242841497,
  dataSize: Long("72374915"),
  storageSize: Long("33611776"),
  totalFreeStorageSize: Long("0"),
  numExtents: Long("0"),
  indexes: 11,    # 11個生成されている
  indexSize: Long("10604544"),
  fileSize: Long("0"),
  nsSizeMB: 0,
  ok: 1
}

# 同じ結果か確認してみます
db.runCommand( {
   dbStats: 1
})
{
  db: 'm201',
  collections: 5,
  views: 0,
  objects: Long("341482"),
  avgObjSize: 211.94357242841497,
  dataSize: Long("72374915"),
  storageSize: Long("33611776"),
  totalFreeStorageSize: Long("0"),
  numExtents: Long("0"),
  indexes: 11,
  indexSize: Long("10604544"),
  fileSize: Long("0"),
  nsSizeMB: 0,
  ok: 1
}

# コレクション単位でも stats() が実行可能
db.people.stats()
{
  ns: 'm201.people',
  size: 19574079,
  count: 50474,
  avgObjSize: 387,
  storageSize: 9887744,
  freeStorageSize: 20480,
  capped: false,
  nindexes: 2,  # インデックスは2indexBuilds: [],
  totalIndexSize: 4059136,
  totalSize: 13946880,
  indexSizes: {
    _id_: 790528,
    'first_name_1_address.street_-1_address.city_-1_ssn_1': 3268608
  },
  scaleFactor: 1,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1635690526, i: 22 }),
    signature: {
      hash: Binary(Buffer.from("66ee1171d53b6046baa0ab24ff187b19c481ab0c", "hex"), 0),
      keyId: Long("6959643930757431297")
    }
  },
  operationTime: Timestamp({ t: 1635690526, i: 22 })
}
  • インデックスとコレクションのデータは異なる領域、ディスク上に配置できる
    • その分、ディスクの領域不足には気を付けましょう

Resource Allocation for Indexes Part 2 (動画)

  • インデックスを利用する場合、インデックスのデータはメモリ上にロードされて利用される
  • 利用できるメモリが十分でない場合は、インデックスから読み取りを分割して実施するのでページの in/out が発生する
    • free コマンド(OSコマンド)などで空きメモリを確認しつつインデックスがどれだけキャッシュされているかもチェックすること
  • stats()でのデータ確認の際に、オプションを指定すると、インデックスの詳細情報がチェックできる
    • { indexDetails: true } を指定してみる

db.people.stats({ indexDetails: true })

# 以下、出力
db.people.stats({ indexDetails: true })
{
  ns: 'm201.people',
  size: 19574079,
  count: 50474,
  avgObjSize: 387,
  storageSize: 9887744,
  freeStorageSize: 20480,
  capped: false,
  nindexes: 2,
  indexDetails: {
    _id_: {
      metadata: [Object],
      creationString: 'access_pattern_hint=none,allocation_size=4KB,app_metadata=(formatVersion=8),assert=(commit_timestamp=none,durable_timestamp=none,read_timestamp=none,write_timestamp=off),block_allocation=best,block_compressor=,cache_resident=false,checksum=on,colgroups=,collator=,columns=,dictionary=0,encryption=(keyid=,name=),exclusive=false,extractor=,format=btree,huffman_key=,huffman_value=,ignore_in_memory_cache_size=false,immutable=false,import=(enabled=false,file_metadata=,repair=false),internal_item_max=0,internal_key_max=0,internal_key_truncate=true,internal_page_max=16k,key_format=u,key_gap=10,leaf_item_max=0,leaf_key_max=0,leaf_page_max=16k,leaf_value_max=0,log=(enabled=false),lsm=(auto_throttle=true,bloom=true,bloom_bit_count=16,bloom_config=,bloom_hash_count=8,bloom_oldest=false,chunk_count_limit=0,chunk_max=5GB,chunk_size=10MB,merge_custom=(prefix=,start_generation=0,suffix=),merge_max=15,merge_min=0),memory_page_image_max=0,memory_page_max=5MB,os_cache_dirty_max=0,os_cache_max=0,prefix_compression=true,prefix_compression_min=4,readonly=false,source=,split_deepen_min_child=0,split_deepen_per_child=0,split_pct=90,tiered_object=false,tiered_storage=(auth_token=,bucket=,bucket_prefix=,local_retention=300,name=,object_target_size=10M),type=file,value_format=u,verbose=[],write_timestamp_usage=none',
      type: 'file',
      uri: 'statistics:table:index-6603-569468269065890606',
      LSM: [Object],
      'block-manager': [Object],
      btree: [Object],
      cache: [Object],
      cache_walk: [Object],
      'checkpoint-cleanup': [Object],
      compression: [Object],
      cursor: [Object],
      reconciliation: [Object],
      session: [Object],
      transaction: [Object]
    },
    'first_name_1_address.street_-1_address.city_-1_ssn_1': {
      metadata: [Object],
      creationString: 'access_pattern_hint=none,allocation_size=4KB,app_metadata=(formatVersion=8),assert=(commit_timestamp=none,durable_timestamp=none,read_timestamp=none,write_timestamp=off),block_allocation=best,block_compressor=,cache_resident=false,checksum=on,colgroups=,collator=,columns=,dictionary=0,encryption=(keyid=,name=),exclusive=false,extractor=,format=btree,huffman_key=,huffman_value=,ignore_in_memory_cache_size=false,immutable=false,import=(enabled=false,file_metadata=,repair=false),internal_item_max=0,internal_key_max=0,internal_key_truncate=true,internal_page_max=16k,key_format=u,key_gap=10,leaf_item_max=0,leaf_key_max=0,leaf_page_max=16k,leaf_value_max=0,log=(enabled=false),lsm=(auto_throttle=true,bloom=true,bloom_bit_count=16,bloom_config=,bloom_hash_count=8,bloom_oldest=false,chunk_count_limit=0,chunk_max=5GB,chunk_size=10MB,merge_custom=(prefix=,start_generation=0,suffix=),merge_max=15,merge_min=0),memory_page_image_max=0,memory_page_max=5MB,os_cache_dirty_max=0,os_cache_max=0,prefix_compression=true,prefix_compression_min=4,readonly=false,source=,split_deepen_min_child=0,split_deepen_per_child=0,split_pct=90,tiered_object=false,tiered_storage=(auth_token=,bucket=,bucket_prefix=,cache_directory=,local_retention=300,name=,object_target_size=10M),type=file,value_format=u,verbose=[],write_timestamp_usage=none',
      type: 'file',
      uri: 'statistics:table:index-7056-5475919732054582565',
      LSM: [Object],
      'block-manager': [Object],
      btree: [Object],
      cache: [Object],
      cache_walk: [Object],
      'checkpoint-cleanup': [Object],
      compression: [Object],
      cursor: [Object],
      reconciliation: [Object],
      session: [Object],
      transaction: [Object]
    }
  },
  indexBuilds: [],
  totalIndexSize: 4059136,
  totalSize: 13946880,
  indexSizes: {
    _id_: 790528,
    'first_name_1_address.street_-1_address.city_-1_ssn_1': 3268608
  },
  scaleFactor: 1,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1635775902, i: 12 }),
    signature: {
      hash: Binary(Buffer.from("998cb318776d4db9dd73b745da7aebbf353f68a2", "hex"), 0),
      keyId: Long("6959643930757431297")
    }
  },
  operationTime: Timestamp({ t: 1635775902, i: 12 })
}

上記の結果、キャッシュにどれだけインデックスのデータが載っているのかもチェックできる。

Resource Allocation for Indexes Part 3 (動画)

エッジケースについて。理解が追いつかないのでここは省略....

Check: Resource Allocation for Indexes Part 3

  • For the fastest processing, we should ensure that our indexes fit entirely in RAM
    • 処理を高速化させるためには、インデックスがRAM上に収まるようにすること
  • Indexes are not required to be entirely placed in RAM, however performance will be affected by constant disk access to retrieve index information.
    • 必ずしもRAMに全てが収まる必要はないが、インデックス情報を取得するためにディスクアクセスが発生すると、その分パフォーマンスに影響する

Basic Benchmarking Part 1 (動画)

  • パフォーマンスベンチマークのタイプ

    • Public Test Suite (公開されているツールによるテストなど)
    • Specific or Private Testing Environment (システム、アプリケーションに特化したルール、ワークロードを考慮したテストなど)
  • Low Levelのベンチマーク

    • ファイルI/Oのパフォーマンス
    • スケジュールに従ったパフォーマンス(定期的な処理、ワークロードに対して)
    • メモリの割り当てや転送スピード
    • スレッドパフォーマンス
    • データベースServerのパフォーマンス
    • トランザクションの分離レベルなど
    • etc....
  • ベンチマークツールの例

ファイルI/Oやメモリ、CPU、スレッドのパフォーマンス測定にはこれらのベンチマークツールが利用できる。

  • データベースサーバーのベンチマーク

    • 項目がきめ細かくなる
    • データセットのロード
    • 秒あたりの書き込み、秒あたりの読み込み
    • ワークロードのバランス
    • Read/Write ratio
  • データーベース向けのベンチマークツール

  • TPC のベンチマークも考慮

ただし、これらのツール、基準はRDBMSがベースになるため、MongoDBの特性にあった計測、結果の読み取りにはならないかもしれない。

  • Ditributed Systems Benchmarking (分散システム向けのベンチマーク)
    • これらの点の考慮が必要
      • Linearization
      • Serialization
      • Fault tolerance
    • ノードも複数
  • ビッグデータ、分散システム用のベンチマークツール
  • Jepsen (Distributed Systems Safety Research)
    • https://jepsen.io
    • それそれのデーターベースについてのレポートを提供してくれている

Basic Benchmarking Part 2 (動画)

  • 公開されているベンチマークツールの多くは、RDBMSに主眼を置いたものであるということに注意
    • RDBSに寄せたスキーマ(モデル)は、実際にはMongoDBの特性を活かせない場合がある
  • MongoDBのベンチマークについてのアンチパターン
    • Mongo Shellでのデータの読み書きや計測は参考値なので、実際のアプリケーション(API)を通すこと
    • パラーメーターに注意すること
    • クライアントの制限にも数字が左右されます(ラップトップでのテスト実行では処理がそもそも数を出せない)

などなど。

Check: Basic Benchmarking Part 2

  • Publicly available tools, including correct database variations
    • 公開されているツールを使う場合は、データーベースの特性にあったものを選ぶ、特性を考慮して数値を見ること

Discussion