1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

MongoDB UniversityのM103を修めた

記事一覧はこちら

Table of Contents

背景・モチベーション

8月1日から中途入社して働いている会社ではRDBだけでなく、いわゆるNoSQL(Not Only SQL)であるMongoDBを利用しています。そのうえshardingも利用しているということで、データ指向アプリケーションデザインを7月に読んでいたこともあり、非常に興味を持ちました。

同じチームの方から、MongoDB Universityが良いよというアドバイスを頂き、早速受講してみたのでまとめたメモをブログに残しておこうと思います。

自分が受講したコースは、レクチャー動画を観て、Quizを解き、更にインタラクティブなターミナルをブラウザ上で動かして実際に構築しているという流れで進んでいきます。動画を観るだけではないので、講義内容が右から左に抜けていきづらくなっていると思います。動画は英語ですが、Transcriptがダウンロード出来るようになっているので、最悪DeepLにお願いすることでなんとかなります。

講師の方の中にスターウォーズが好きな人が居て、時々画面の前の僕らに向かってYoung Padawanと語りかけてくるのが面白いです。

M103

f:id:jrywm121:20210830010153p:plain

Chapter 1: The Mongod

What is mongod

mongodはmongodbのメインのdaemon processです。
It is the core server of the database, handling connections, requests, and most importantly, persisting your data.

mongodはReplicaSetやSharded Clusterなど複数サーバでの構成において、各サーバで動作する

Default Configuration

設定ファイルや起動引数で設定を渡すことになるが、渡さない場合にはデフォルトの設定で動く

  • The port mongod listens on will default to 27017.
  • The default dbpath is /data/db.
    • the data files representing your databases, collections, and indexes are stored so that your data persists after mongod stops running
    • The dbpath also stores journaling information (crash logs…)
  • mongod binds to localhost
    • mongodに接続できるクライアントはlocalhostに存在するものだけ
    • リモートクライアントを受け入れるには設定を変更する必要がある
  • Authentication is turned off

当たり前っちゃ当たり前だけど、リモートからのアクセスを許可するときには、authtrue にしましょうね

Configuration file

コマンドラインでオプション渡す事もできるが、設定ファイルを利用することも可能

設定ファイルの形式はYAMLとなる 階層化されてるので、コマンドオプションより格段readabilityが増して分かりやすいぞ

mongod -f "/etc/mongod.conf"

Configuration File Options — MongoDB Manual

File Structure

MongoDB standalone server のときのファイル構造

root@2a9d0458fdcc:/data/db# tree -L 2 .
.
|-- WiredTiger
|-- WiredTiger.lock
|-- WiredTiger.turtle
|-- WiredTiger.wt
|-- WiredTigerHS.wt
|-- _mdb_catalog.wt
|-- collection-0-7411902203291987629.wt
|-- collection-2-7411902203291987629.wt
|-- collection-4-7411902203291987629.wt
|-- diagnostic.data
|   |-- metrics.2021-08-22T08-52-18Z-00000
|   `-- metrics.interim
|-- index-1-7411902203291987629.wt
|-- index-3-7411902203291987629.wt
|-- index-5-7411902203291987629.wt
|-- index-6-7411902203291987629.wt
|-- journal
|   |-- WiredTigerLog.0000000001
|   |-- WiredTigerPreplog.0000000001
|   `-- WiredTigerPreplog.0000000002
|-- mongod.lock
|-- sizeStorer.wt
`-- storage.bson

WiredTiger (storage engine) がクラスタメタデータやWiredTiger固有の設定などの情報をtrackする用途で使われているファイル群が上記

The WiredTiger.lock file acts as a safety. 2つ目のMongoDBプロセスを同時に実行し、このフォルダを指定した場合、ロックファイルはその2つ目のMongoDBプロセスが起動するのを防いでくれます。 mongod.lock も同じような役割を果たす。

ホストマシンが落ちたりcrashしたときには、このlockファイルのせいでmongodが起動できないことがある。

.wt の拡張子を持つファイルは、collectionとindexのデータ。WiredTigerでは、それぞれ別の構造として保存される。

diagnostic.data ディレクトリはMondoDBサポートエンジニアが診断する目的のみのために利用されるもの。 この診断データは、Full Time Data Capture (FTDC) モジュールによって取得される。

journal ディレクトリ内のファイルはそれぞれWiredTigerのjournaling systemの一部。

WiredTigerでは、書き込み操作はメモリにバッファリングされる。60sごとにflashされてデータのチェックポイントが作成される。

また、WiredTigerでは、ディスク上のjournal fileへの書き込みにWAL(Write Ahead Logging)を採用している。journal entryははまずメモリ上にバッファリングされ、その後WiredTigerは50ミリ秒ごとにjournalをディスクに同期します。Each journal file is limited to 100 megabytes of size.

WiredTigerは、データのディスクへの同期にファイルローテーション方式を採用しています。障害発生時には、WiredTigerはjournalを使用してチェックポイント間に発生したデータを回復することができます。

※ 基本的にMongoDBのdata direcrotyは直接編集を行わないこと!

root@2a9d0458fdcc:/tmp# tree .
.
`-- mongodb-27017.sock

/tmp ディレクトリには、socketファイルがありプロセス間通信に利用される。 このファイルは起動時に作成され、MongoDBサーバーにこのポートを所有させる。

Basic Commands

Shell Helperを利用することでメソッドLikeに操作することができる

  • db - database
    • db.<collection>.createIndex 等…
    • db.runCommand() をwrapしてusabilityを高めたもの
  • rs - replicaset
  • sh - sharded

db.commnadHelp(<Command>) でヘルプが見れる

Logging

db.getLogComponents() でlogLevelを確認できる commandやindexなどのcomponentごとにLoglevelが設定できる

-1 は親の指定を引き継ぐこと。下記でいうと、一番上のレベルで”verbosity”: 0 が宣言されていているので0 になる 0 - 5 まで設定することでき、数字が高いほどverboseとなる。0はinfoレベルのみで、1からdebugレベルの出力を行う。

{
    "verbosity" : 0,
    "accessControl" : {
        "verbosity" : -1
    },
    "command" : {
        "verbosity" : -1
    },
    "control" : {
        "verbosity" : -1
    },
    "//": "omitted..."
}

ログレベルの設定

mongo admin --eval '
  db.setLogLevel(0, "index")
'

ログを確認する手段は2つ

# via Mongo Shell
db.adminCommand({ "getLog": "global" })

# via command line
tail -f /data/db/mongod.log

5 Severity Levels Logエントリの2つ目のフィールド

F - Fatal
E - Error
W - Warning
I - Information(Verbosity Level 0)
D - Debug(Verbosity Level 1-5)

Profilers

ログにはコマンドに関するデータも含まれていますが、クエリの最適化を開始するのに十分なデータはありません。

execution statsやクエリで使用されるインデックス、rejected planなどの情報を取得し、遅いoperationをデバッグするためには、ログではなくProfilerを利用する。

Profilerが取得するデータは下記の3つ

  • CRUD operations
  • Administrative operations
  • Configuration operations

データベースレベルでProfilerを有効にできる。 有効にすると、system.profile というcollectionに、CRUD operationのprofiling dataが格納される。

profilingLevelには3つの段階がある

  • 0: profilingがオフになっている
  • 1: slow operationのみprofilingする
  • 2: 全てをprofilingする

デフォルトでは、100ms 以上の操作を「遅い」と判断するが、自分で設定することも可能

> db.getProfilingLevel()
0
> db.setProfilingLevel( 1, { slowms: 50 } )
{ "was": 0, "slowms": 50, "sampleRate": 1, "ok": 1 }
> db.system.profile.find().pretty()

Security

Authentication

  • SCRAM (default)
  • X.509

MongoDB Enterprise Only

Authorization

RBAC

  • Each user has one or more Roles
  • Each Role has one or more Privilleges
  • A Previlleges represents a group of Action and the Resources those actions apply to

Localhost Exception

mongodでauthenticationを有効にしても、デフォルトではMongoDBはユーザを作成しないので手詰まりになってしまいます。

そのため、最初にユーザを作成するまではlocalhostからなら認証なしで操作できるというLocalhost Exceptionが存在します。

必ず最初に管理者権限のあるユーザーを作っておき、Localhost Exceptionがclose後に、ユーザを作成できる状況にしておく必要があります。

$ mongo --host 127.0.0.1:27017
> use admin
> db.createUser({
  user: "root",
  pwd: "root123",
  roles : [ "root" ]
})

Role

Role Structure

Role is composed of

  • Set of Privilleges
    • Actions → Resources
  • Network Authentication Restrictions
    • clientSource
    • serverAddress

Resources

  • Database
  • Collection
  • Set of Collections
  • Cluster
    • ReplicaSet
    • Sharded Cluster

Built-In Roles

Each Database or All Database (two scope)

Only one scope

> use admin
> db.createUser(
  { user: "m103-application-user",
    pwd: "m103-application-pass",
    roles: [ { db: "applicationData", role: "readWrite" } ]
  }
)

Server Tools

root@2a9d0458fdcc:/tmp# find /usr/bin/ -name "mongo*"
/usr/bin/mongodump
/usr/bin/mongod
/usr/bin/mongo
/usr/bin/mongos
/usr/bin/mongoexport
/usr/bin/mongotop
/usr/bin/mongosh
/usr/bin/mongoimport
/usr/bin/mongostat
/usr/bin/mongofiles
/usr/bin/mongorestore

mongostat

mongostat — MongoDB Database Tools

CRUD Operationやメモリ、ネットワークの統計を確認することができる

mongodump / mongorestore

mongodumpはBSONで保存されているCollectionをBSONのまま出力する。 データの変換がないために高速。ディレクトリを指定しないと、カレントディレクトリ配下にdump ディレクトリが作成されてその中に吐かれる。metadataはJSON形式で出力される。

mongorestoreコマンドでcollectionをdumpから作成できる。

mongoexport / mongoimport

単一のファイルにJSONファイルとして吐ける(デフォルトでは標準出力に)。 BSONでなくJSONなのでそんなに速くない。また、metadataのファイルを作らないので、database名やcollection名を指定してあげる必要がある。

# mongoimport --port 27000 --file /dataset/products.json \
-d applicationData -c products -u m103-application-user \
-p m103-application-pass --authenticationDatabase=admin

Chapter 2: Replication

What is ReplicaSet?

同じデータセットを持つmongod プロセスのグループ

ReplicaSetには最大1つのPrimaryが存在する。もしPrimaryがunavailableとなった場合には、新しいPrimaryがSecondaryからvoteのプロセスを経て選出されてサービスを継続する(fail over)。 Replica Set Elections に詳細がある。

全てのメンバがRead operatoinを受け入れられるが、Write operatoinはPrimaryのみ。Secondaryは非同期にPrimaryのデータ更新を受け取り同期する。

Replicationのプロトコルには異なるバージョンがあるが、デフォルトはProtocol Vesion1でRAFTをベースにしたもの。

Rplicationメカニズムの中心となるのが、oplog になる。Primaryノードへの書き込みが成功するたびにoplog がidempotentな形式で記録される。

ReplicaSetのメンバには、Primary/Secondaryの他にarbiterという役割も設定できる。arbiterはデータセットを持たない。なのでPrimaryにもなれない。electionのプロセスで頭数を揃えるためにある役割。分散データシステムの一貫性に大きな問題を引き起こすので利用をあまりおすすめしない。

Failoverには過半数のノードが利用可能であることを必要とする。 ReplicaSetは奇数のノードが必要(最低でも3ノード)

ReplicaSetは最大50メンバーまで利用可能だが、voting memberは7まで。7を超えると時間がかかりすぎてしまうため。

Secondary には特定のプロパティが存在する

  • Hidden Node
    • アプリケーションから隠されたデータのコピーを持つこと
    • レプリケーションプロセスの遅延も設定できる(= Delayed Node)
  • Delayed Node
    • アプリケーションレベルの破損に対して、コールドバックアップファイルに頼らずに回復できるようにする目的

Initiate ReplicaSet

Replicationするための設定 replicationを設定すると、clientのauthenticationを行われるようになる

openssl rand -base64 741 > /var/mongodb/pki/m103-keyfile
chmod 400 /var/mongodb/pki/m103-keyfile
# add lines for replications
security:
  keyFile: /var/mongodb/pki/m103-keyfile
replication:
  replSetName: m103-example

Replicationを開始するためのコマンド

rs.initiate() # このコマンドを発行したノードがPrimaryになる
rs.isMaster() # どれが Primary になっているか確認

rs.stepDown() # 意図的にSecondaryをPrimaryに昇格させる

Secondaryを参加させるには、rs.add() を利用する必要がある

mongo --port 27003 -u m103-admin -p m103-pass —-authenticationDatabase admin
rs.add("localhost:27001")
rs.add("localhost:27002")
rs.status() # 確認

Replication Configuration

  • JSON Objectで表現される設定
  • mongo shellから手作業で設定することも可能
    • helper method: rs.initiate(), rs.add(), etc…

Replica Set Configuration — MongoDB Manual

{
  "_id": "replicaSetName",
  "version": X,
  "members": [
    {
      "_id": 1,
      "host": "mongo.example.com:28017",
      "arbiterOnly": false,
      "hidden": false,
      "priority": 1,
      "secondaryDelaySecs": 3600,
    }
  ]
}

Replication Command

  • rs.status()
    • ReplicaSetの総合的な情報を出力してくれる
    • Heartbeatも確認できる
  • rs.hello()
    • ノードのRoleを表示する
    • rs.status() よりシンプル
    • 以前は、rs.isMaster() という名称だったがdeprecatedになっている
  • db.serverStatus()[‘repl’]
    • rs.isMaster() に含まれていないrbidというのが出る
  • rs.printReplicationInfo()
    • oplogに関する情報

Local DB

ReplicaSetの構成を組んでいる場合に local DBの中身は下記のように複数ある standaloneの場合には、startup.logのみ存在する local DBに直接書いたデータはReplicationされない

殆どはサーバ内部で利用している情報 oplog.rsは、レプリケーションカニズムの中心で、レプリケートされているすべてのステートメントを追跡するoplogコレクション

oplog.rsコレクションには、知っておくべきことが幾つか

  • capped collection
    • サイズが制限されているコレクションのこと
  • デフォルトではoplog.rsコレクションは、空きディスクの5%を占める(デカい)、もちろん設定で指定することも出来る
  • oplogは短時間ですぐ増大する(Fear not, young Padawan.)
    • oplogのサイズが埋まったら、古いログから上書きされていく
  • oplogのサイズはどう影響するか?
    • 例えば、Secondaryのノードの接続が途切れてしまった場合、そのノードは Recoveryモードになり同期できてた操作から直近までの操作を一気に書き込んで追いつきます。しかしながら、既に同期できてた操作がoplogに残っていなかった場合に追いつけずエラーになってしまう
> use local
> show collections

me
oplog.rs
replset.election
replset.minvalid
startup.log
system.replset
system.rollback.id

Reconfiguraion Replicaset

例えば、4ノードになっていたのでvote出来るnodeを奇数にしつつ、hiddenにしてしまうのケースの場合には…

これはDBを止めることなく反映することが可能

cfg = rs.conf()
cfg.members[3].votes = 0
cfg.members[3].hidden = true
cfg.members[3].priority = 0
rs.reconfig(cfg)  // updating

rs.conf() // confirm

Reads and Writes on ReplicaSet

Secondaryのノードに対してmongo shellを起動してデータを読み込もうとしてみます。ちなみに ReplicaSetの名称を指定すると自動的にPrimaryにつなぎにいくので注意が必要になる。

Secondaryのノードではこのままだとコマンドを実行できません。 MongoDBはconsistencyを重視しているため、Secondaryから読み込む場合には明示的に伝える必要がある。それが、rs.secondaryOk になる。

consistencyを担保するために書き込みはPrimaryにしか出来ないようになっている。 ReplicaSetが過半数のノードに到達できなくなると、レプリカセットの残りのノードはすべてSecondaryになる。たとえ、残ったのがPrimaryのノードだったとしても。

Failover and Election

Primaryが利用できなくなる理由

一般的にはメンテナンスがそう 例えばローリングアップグレード(e.g. v3.4→ v3.6)

  • SecondaryのMongoDBプロセスを停止し、新しいDBのバージョンで戻ってきます(1台ずつ)
  • 最後に Primary でrs.stepDown() で利用して安全にelectionを開始します
  • electionが完了すると最後の古いバージョンのMongoDBプロセスはSecondaryになる
  • そして同じようにプロセスを新しいバージョンで起動すれば全て完了!かつ可用性も損なわない

Electionの仕組み どのSecondaryがPrimaryに立候補するかという点に関してはロジックが存在する

  • Priorityが全ノード同じ値
    • その場合は最新データを持っているノードが立候補する
    • そのノード自身が自分に投票を行う
    • 2ノードが名乗り出たとしても、投票権を持っているのが奇数ノードであれば問題ないが、偶数ノードだった場合には同点となる可能性があり、もし同点になった場合はelectionをやり直すことになる(=処理がストップする)
  • Priorityがノードごとに違う場合
    • Priorityが高いほど、Primaryになる可能性が高くなる
    • ノードをPrimaryにしたくない場合には、Priorityを0にする
      • Primaryになる資格のないノードのことをpassive nodeと言うらしい

前述したが、ReplicaSetの過半数のノードがダウンしたときには、たとえPrimaryだとしても疎通できなくなる

Write concern

write concern はdeveloperが、write operationに追加できるAckknowledgement(確認)の仕組み ACKのレベルが高いほどDurability(耐久性)が増す

書き込みが成功したことを確認するReplicaSetのメンバが多ければ多いほど、障害が発生したときに永続化が継続する可能性が高い Durabilityを高めようとすると、各ノードから書き込み確認の応答を貰う必要があるので待ちが必要になります。

Write Concern level

  • 0: クライアントは確認応答を待たないので、ノードの接続に成功したかどうかを確認するだけ
  • 1: デフォルトの値。クライアントはPrimaryからの確認応答を待つ
  • => 2 : Primaryと1つ以上のSecondary、例えばlevelが3のときには1つのPrimaryと2つのSecondaryから確認応答を待つ

majority というキーワードを利用することができる。これはReplicaSetのサイズが変わってもいちいち変えなくても良い。levelはメンバの数を2で割って切り上げた値となる。

Write Concernはsharded clusterも対応している。

MongoDBには更に2つのWrite Concernオプションがある。

  • wtimeout: クライアントが操作に失敗したと判断するまでの最大時間。重要なのは書き込みが無かったことになるわけではなく要求した耐久性を満たさなかったということ
  • j: journalの意。各ReplicaSetのメンバが書き込みを受け取ってジャーナルファイルにコミットしないと確認応答を返せない。これをtrue にすることでディスクに書き込まれることまで保証できる。false のときにはメモリに保存する所までの確認。

MongoDB 3.2.6以降は、Write concernが過半数になるとデフォルトでj がtrueになる。

Write concernはクライアントのアプリケーションから指定するっぽい。

Read Concern

起きうる不味いシチュエーション

  • クライアントアプリケーションがdocumentをinsertする
  • そのdocumentがSecondaryにReplicateされる前に、クライアントアプリケーションがReadする
  • 突然Primaryが壊れる
  • ReadしたdocumentはまだSecondaryにReplicateされてない
  • 古いPrimaryがオンラインに戻ったとき、同期プロセスの中でそのデータはRollbackして存在していないことになります
    • 📝ここで古いと行っているのはFailoverが起こるため、復帰したときにはSecondaryになっているからと思います

このシチュエーションを許容できない要件のアプリケーションの場合に困ります。 そんなときに役に立つのがRead Concernである。

Read Concernで指定された数のReplicaSetメンバに書き込まれたと認められたデータのみが返されるようになる。

Read Concern Level

  • Local: Primaryを読み取るときのデフォルトの設定。クラスタ内の最新のデータを返す。Failover時のデータの安全性が保証されない。
  • Available: Secondaryを読み取るときのデフォルトの設定。ReplicaSetのときはLocalと同じで、Sharded Clusterのときに挙動が変わる
  • Majority: 過半数のReplicaSetメンバに書き込まれたことが確認されたデータのみを返す。Durabilityと Fastの中間だが古いデータを返すこともある
  • Linearizeable: read your own writeを提供する。常に新しいデータを返すが、読み取り操作が遅かったり、制限がある。

どのようなRead Concern Levelにするかは、”Fast”, “Safe”, “Latest” の観点で考えると良さそう LocalやAvailableはSecondaryの読み取りに関しては最新が返ってくるとは限らない

Read Preference

読み込みのoperationをルーティングする設定

  • primary(default): Primaryのノードからしか読み取らない
  • primaryPreffered: PrimaryがUnavailableになったときにはSecondaryから読み取る
  • secondry: Secondaryのノードからしか読み取らない
  • secondaryPreffered: Secondaryが全てがUnavailableになったときにはPrimaryから読み取る
  • nearest: 地理的に一番近いところ

Chapter 3: Sharding

What is Sharding?

https://docs.mongodb.com/manual/images/sharded-cluster-production-architecture.bakedsvg.svg

画像引用元

MongoDB sharded clustershard , mongos , config servers で構成されている。

  • shard: 物理的に分散されたcollectionを保存する。replica set としてデプロイされる。
  • mongos: shardへクエリをルーティングする。
  • config servers: shardのmetadataを保存する。

When to sharding?

いつShardingが必要になるのかを考えてみましょう

まず最初にVerticcal scaleが経済的に可能かを確認する。特定されたボトルネックにリソースを増やすことでダウンタイムなしにパフォーマンスが向上する。

しかしながら、経済的な理由や非常に困難なポイントにぶち当たりスケールアップが難しくなります。

もう一つ考慮すべき点は、運用業務への影響です。 データセットの量が10テラバイト級になるとバックアップやリストアに時間がかかる。ディスクのサイズを増やすとインデックスのサイズも増えることになり、多くのRAMが必要になる。

一般的には、個々のサーバーには2~5テラバイトのデータを格納することが望ましいとされている それ以上になると時間がかかりすぎてしまう。

最後に、下記のようなケースでもshardingは有用

  • 並列化可能なSingle Thread Operation
  • 地理的に分散したデータ → zone sharding
    • 特定の地域に保存する必要があるデータ(M121でしっかり理解出来るやつ)
    • ↑のようなデータを取得するクライアント

Sharding Architecuture

https://youtu.be/6cCL4-3gF8o

動画がとても分かりやすかった

mongosconfig server からクエリされたデータがどのshardにあるかを取得し、ルーティングする。各shardに含まれる情報は時間とともに変化する可能性があるのでとても重要。mongos は頻繁にconfig server にアクセスする。

なぜ変化するかというと、各シャードのデータ量が均等になるように分散させるため。

sharded clusterはprimary shardという概念も存在する。各データベースにはprimary shardが割り当てられ、そのデータベースのshard化されていないcollectionは全てそのshardに残る。 primary shardには他にも幾つか役割がある。一つは、aggregation commandのMERGE操作によるもの。

shard key以外を条件にしたクエリの場合、例えば動画の例だとサッカー選手の名前でshardを分けているが、年齢に関するクエリを受け取った場合には全てのshardにクエリを送信する mongos もしくはcluster内でランダムに選択されたshardで、それぞれの結果を収集しソートなどを行います。これをSHARD_MERGE ステージと呼ぶ。

Setting Up a Sharded Cluster

config server の実態はMongoDBのReplicaSetです ただし、.sharding.clusterRole: configsvr を設定ファイルに追加する必要がある

.security.keyFile をレクチャーでは利用しているが、本番環境においてはX509証明書を利用することになる

既存の単一のReplicaSetをsharding構成にRolling Upgradeするには下記の手順を取る

  • CSRS (= Config Server Replica Set) を起動させる
  • mongos を起動させる
    • データを保存する必要が無いため設定ファイルでdbpath プロパティが無い
    • mongosconfig server で作成したユーザを継承する
    • config server の向き先を設定する必要がある
  • 既存のReplicaSetのnodeの設定を変更して再起動する
    • .sharding.clusterRole: shardsvrを設定にいれる
    • Secondaryをそれぞれshutdownする
    • PrimaryでstepDownを行い、他のnodeにPrimaryの役割を引き渡した後にshutdownする
  • mongos からshardを追加する
    • sh.addShard( "rs1/mongodb0.example.net:27018" )
    • ReplicaSetの中の1つのノードを指定するだけでPrimaryを認識可能

参考:

Configuration File Options — MongoDB Manual

ConfigDB

MongoDBが内部的に使うものなので、ユーザ側でデータの書き込みは行わないが有用な情報を読み取ることが出来る

use config

# databaseが幾つにpartitioningされているか
db.databases.find().pretty()

# collectionのshardkeyに関する情報
db.collections.find().pretty()

# shardとして利用しているReplicaSetの情報
db.shards.find().pretty()

# chunkがshardkeyのどの範囲を持っているかとどのshardに属するか
db.chunks.find().pretty()

# mongosの情報
db.mongos.find().pretty()

Shard Keys

sharded collectionのデータを分割して、cluster内のshardに分割するために使用する、indexed field(s) をshard keyと言う

chunkはshard keyを使ってdocumentを分けた論理的なグループのこと shard keyとして選択したフィールドの値によって、各chunkのinclusive lower boundとexclusive upper boundが決まる(e.g. 1 <= x < 6)

新しいdocumentをcollectionに書き込むたびに、mongosルータはどのshardにそのdocumentのkey valueに対応するchunkがあるかを確認し、そのshardのみにdocumentを送る。 つまり、挿入されるdocument全てにshard keyの項目が必要になる。

shardingを行う手順

  • databaseのshardingを有効に: sh.enableSharding("m103")
  • indexを作成: db.products.createIndex( { "sku": 1 } )
  • shard keyを選択: sh.shardCollection( "m103.products", { "sku": 1 } )
    • 📝 shard keyはimmutableとLecture動画で言われているが、shard fieldがimmutableな_id で無い限り、refineCollectionShardKey で更新することができる

Picking a Good Shard Key

What makes a Good Shard Key?

  • High Cardinality
    • Cardinalityが高ければChunkが増えshardの数も増えるのでクラスタの成長を妨げることがない
    • 例えばbool値をkeyにした場合には、上限が2chunkになりshardも2つまでになってしまいます
  • Low Frequency
    • frequencyはデータの中でuniqueな値が発生する頻度を表す
    • 例えばアメリカの州をshard keyにしたとして、90%の確率で「New York」のdocumentが挿入される場合、書き込みの90%以上が1つのシャードに行くことになる = HotSpot!
  • Non-Monotonically Change
    • 単調に変化する値(e.g. タイムスタンプやID)は、chunkが下限と上限を持つ性質上、相性が悪い
    • Monotonicなshard keyを分散させるためには後述のhashed shard key を利用する必要がある

shard keyの選定でもう一つ重要なことはread isolation です。よく実行するクエリに対応しているかどうかを検討する必要がある

shard keyを条件にしたクエリであれば、多くの場合は1つのshardにアクセスするだけで済むが、そうでなければ全てのshardにアクセスするscatter gather な操作になってしまい時間がかかる

shard keyを選ぶときに注意する点

  • 1度shardingしたcollectionはunsharding出来ない
  • 1度shardingしたcollectionはshard keyを更新できない(条件によっては可能)
  • shard keyのvalueは更新できない(これも可能と全セクションで言及があった)

Hashed Shard Keys

hashed shard keyを利用する場合、MongoDBはshard keyに指定しているField Valueのhash値を使ってどのchunkにdocumentを置くか決めることになる。

実際に保管するデータがhash値になるわけではなく、shard keyのベースとなるindex自体がhash値で保存される。データをより均等に分散させることができる。 前述したようにMonotonicな値でもちゃんと分散されるようになる。

hashed shard key の欠点

  • shard key fieldに対する範囲のクエリは単一のshardではなく複数shardに投げられることになる
  • データを地理的なグループに分離できない
  • 利用できるのは単一のField Shard Keyのみで、配列や複合インデックスはサポートしていないし、hash化されているのでsortも速くない

Chunks

Chunkはdocumentsの論理的なグループである config server が保持する最も重要な情報の1つはchunkとshardのマッピングである

chunkが作成されてからの流れは下記のような形

  • collectionをshardingした瞬間に、1つの初期chunkを定義する
    • この初期chunkは$minKeyから$maxKeyまでの範囲にある
  • 時間とともに初期chunkを複数のchunkに分割してshard間でデータが均等になるようにする

shard内のchunk数を決める要素はshard keyのcardinalityの他にchunk sizeがある デフォルトのChunk Sizeは64MB 設定によって1MB <= ChunkSize <= 1024MB の幅で変更することが可能 Chunk Sizeは稼働中に変更することが可能だが、新しいデータを入ってこないとmongosはアクションを起こさないのですぐにはchunk数に反映されない可能性がある

Jumbo chunkという概念がある 新しいdocumentの90%が同じshard keyを持っていたりすると、定義されたchunk sizeよりも大きくなる可能性が高い。そうなった場合は、Jumbo chunkとしてマークされる。Jumbo chunkは大きくて動かせないという判断をされる。 これを避けるためにも、Shard KeyのFrequencyの考慮は大事だ

Balancing

config server のReplicaSetのPrimaryで実行されているbalancer プロセスがshard間でchunkを移動させてデータを均等にしている

balancer は1ラウンドで複数のMigrationを並行に行うことができるが、1つのノードが複数のMigrationプロセスに一度に参加することは出来ない

balancerはデータの移動だけではなく、必要に応じてchunkの分割を行う

balancer は特にユーザの入力や指示を必要としないが、開始停止や設定の変更など行うこともできる

Targeted Queries vs Scatter Gather

Targeted Queryは全てのshardにクエリを飛ばさなくて済むクエリ

combound indexを利用したshard keyを設定することで、scatter gatherなクエリを避けることができる

# Shard Key
{ "sku": 1, "type": 1, "name": 1 }

# Targetable Queries
db.products.find( { "sku": … } )
db.products.find( { "sku": …, "type": … } )
db.products.find( { "sku": …, "type": …, "name": … } )
db.products.find( { "type": …, "name": …, "sku": … } )

# Scatter Gather 
db.products.find( "type": … )
db.products.find( "name": … )

db.products.find({"sku" : 1000000749 }).explain() のようにexplainでクエリがtargetedになっているかを確認できる

  • winningPlan.stage の値がSINGLE_SHARD になっているか
  • inputStage の値がIXSCAN (index scan)になっているか

Targeted Queryにはshard keyが必須になることが大事!

まとめ

ちゃんとモチベーションを維持しつつ進められるような仕組みになっていた。
次はM201のPerformanceをやりたいところだけど、Datadogも触りたいので一旦後回しにしちゃいそう。
この記事なんと2万字になっててビビる。