「数学的ゲームデザイン」というアプローチ

前回の議論をより一般化した話です。数式も少なめ。実ビジネスにおいて数学がどこまで貢献できるのかというところを理解してもらい,少なからず関心を持って頂ければ幸いです。ただしあくまで読み物として捉え,実世界ビジネスにおける違法性など指摘をするのはやめて下さい。

 

目次

1. 『コンプガチャの数理 -コンプに必要な期待回数の計算方法について-』

2. 『「数学的ゲームデザイン」というアプローチ』

3. 『コンプガチャの数理 -ガイドラインに基づいたゲームデザイン その1-』

4. 『コンプガチャの数理 -ガイドラインに基づいたゲームデザイン その2-』

 

定義

「数学的ゲームデザイン」とは,とある数学モデルのレールに沿ったゲームをデザインすることである。それによって,その背景にある種々の数学的性質を活用して優位な戦略を立てることが可能になる。

コンプガチャは,「The Coupon Collector's Problem」という数学モデルに基づいた数学的ゲームデザインの好例である。この例から学んだことは,

  1. 高々10種類のアイテムをコンプするだけでも,コンプまでに10万円を要するような確率設定を行うことができる。
  2. 「具体的な売上数値目標」を,「アイテム収集枚数ごとのユーザー数を調節する」ことで達成可能にするといった,戦略を実行可能なアクションスキームにまで落とし込むことができる。

であった。より一般的に言えば,数学的ゲームデザインを行うことは実世界のビジネス文脈において以下の様な性質を持つ:

  1. 人間の感覚的見積もりと真値の大きな乖離(錯覚と呼ばれたりする)を利用して,非合理的な行動に誘導する
  2. 調節可能なパラメータを提供側が初めから握っておくことで常に有利な戦略を立てることが可能になる

ところで,このように書くとこういったゲームはフェアで無い,ずるいゲームという印象を持つ人もいると思う。ここでは実世界におけるそういった評価は置いておき,以下の様にゲームの「公平性」を定義しておく:

本文脈において「フェアなゲームである」とは,ユーザー側に予めゲームのルールと必要なパラメータ値が明示されていることである。

コンプガチャの文脈において,このゲームがフェアであると言えるのは「アイテムの種類数と,各アイテムごとの出現確率が公開されている」場合である。なぜならこの場合においては,ユーザー側も「次に新しいアイテムが得られる確率」「あと何回でコンプできるかの期待回数」といった,現在の状況を把握し合理的に判断できる,提供側と同じ情報を得ることができるからである。

※ ただ実際はフェアなゲームであっても,ルールをきちんと把握しかつ状況を把握するための数学的手法を心得ていない人の方が多いし,提供側はそれを親切に教えてくれない。つまりほとんどの人においてはこのようなフェアなゲームでも初めからやや不利な状況に置かれている事になる。そこにさらに射幸心を仰ぐ戦略をぶつけてこられるのである。「リテラシーを高める」とは,情報と数学を駆使してそのような提供側の攻撃から防衛する手段を身につけることである。

※ あくまでも本文の話は数学の世界の話なので,以上の設定におけるこれ以上の深いつっこみは無しと言うことで。

 

Case 1: ルーレット

最も有名でわかりやすい数学的ゲームデザインはカジノにおけるルーレットである。ルーレットは 38 区分(1 - 36,0,00)の数字の中から1つが一様ランダムに選ばれる,最もベーシックな確率論に基づいたフェアなゲームである。故にある特定の数字・色・偶奇が出る確率も明確であり,また,0と00(色は緑)の存在によってどのように賭けてもその期待値の見積もりができ,それがユーザー側が不利であること(このことと本文脈での"フェア"とは関係ない)がすぐにわかる。

ルーレットの面白いところはこのルール設定に加えて,いわゆるユーザーの錯覚を誘う巧妙(?)な罠を仕込んでおり,ユーザーの選択肢を制御し,提供側がさらにゲームを有利に進めていることである。

多くのカジノには過去20回の試行の結果が派手に大きく掲示されており,これは多くの人に「ギャンブラーの誤謬」と呼ばれる錯覚を与えている。

  • 直近の10回の試行が全て赤であったので,「次こそは」と黒に賭ける
  • 過去20回で出現した数字は次は出にくいはずなので,それらを除外した数字のみを考慮範囲する

※ [2012/05/11] 以下,このような状況でも「次の出玉確率自身が変わることは無い」ので,「不利な選択=期待値が変わる」というようにとれる元の文脈を修正しました。

これらの行為は,実はユーザー自らが勝つ可能性を狭めていることになっている。毎回の試行は「独立」に行われているので過去の試行に依存することは無い。提供側はあえて過去の結果を見せることでユーザー側に「試行の独立」という概念をぼやけさせ,「過去との関連」を考慮した不利な選択を行わせているのである。 

これらの行為は,実際は毎回の試行が独立であるはずなのに,過去の試行と関連しているように思わせている(バイアスを誘導する)。依然として赤黒共に出現確率は変わらないのに,ユーザー側では黒の方に偏った(条件付き)確率を想定してしまっている。

 

Case 2: ブラックジャック

一方,ブラックジャックも複雑であるが(おそらく)数学モデルに沿ったゲームデザインである。ルールも明白でフェアなゲームである。ここでブラックジャックを取り上げた理由は,ブラックジャックは他のカジノとは異質なケースであるからだ。他のカジノと同じように一見提供側に有利に作られたと思われるブラックジャックルールは,天才数学者 エドワード・O・ソープ によってその必勝方法(カードカウンティング)を見いだされ,ユーザー側にも有利なケースが存在してしまっていることである。フェアなゲームの上ではユーザー側もまた,ゲームを有利に進められる可能性がある。

さらにブラックジャックというゲームは「シャッフル」の難しさを利用することもできる(こちらは厳密に評価はできないが)。もし前回のカードの並びを記憶しており,今回のプレイにおいても十分なシャッフルが行われないまま開始されたとすると,前のカードの並びを受けて,次に出るカードがある程度予測できることになるのだ。

それでは,十分なシャッフルを達成するには何回カードをシャッフルしないといけないのだろうか?この解析は変動距離の概念と「マルコフ連鎖のカップリング」という手法を用いて評価される。結果は

n ln (n/ε) 回シャッフルした後では,シャッフルが十分でない確率は ε まで小さくなっている

ということである。具体的に 52枚 (n=52) のカードで 1% (ε=0.01) のシャッフル失敗に抑える設定の元では必要なシャッフル回数は 445回 も必要になることになる。

(現代のブラックジャックゲームはおそらく以上の事は全て考慮に入れられた上のルールの下で行われているような気がするので簡単には有利に進めさせてくれないと思われる)

 

※ 残りはベーシックな数学モデルの紹介と,基本的な解析結果である。より詳細な解析およびより具体的なゲームデザインについては触れていない。

 

Case 3. Birthday Paradox

非常に有名な問題である。ここでは閏年と双子の可能性を排除している。

問:「クラスに30人の生徒がいる。この時,(1) ある2人の誕生日が同じである,(2) どの2人も誕生日が同じで無い,どちらの方がより起こりやすいだろうか?」

答:(1)。約 70% の確率である2人の誕生日が同じである。また23人いれば,ある2人の誕生日が同じ事象が50%で起こりうる。

この問題は,365バリエーションもある誕生日においても,たった23人いれば誕生日が重複する可能性の方が高くなることを述べている。人間の感覚値と真値が乖離しているを表したわかりやすい例である。

一般に m 人,n 誕生日の設定の元では50%の閾値は概ね \sqrt{2nlog(2)} と見積もれる。例えば10万パターンのアバターを用意しても,372人のパーティーを作ってみるとそこにはアバターが重複しているメンバーが出る確率の方が大きい。

 

Case 4. Balls and Bins Problem

問:「n 個のボールと n 本のビンがある。ボールは各々のビンに投げ入れられるとし,投げ入れられるビンは n 本から独立かつ一様ランダムに選ばれる。この時,ビンの中の最多ボール数(最大内容量)はいくらくらいになるか?」

答:1に非常に近い確率で \Omega(log(n)/log log(n)) となる。

また非常に大きな n では,最大容量が 3 ln n / ln ln n より大きくなるのは 1 / n 程度でしか無いことも言える。

今(大きな n では無いが),20 回のぬいぐるみ投げゲームにおいて,(20のいずれかの)箱に入っているぬいぐるみの数が 8.2 個以上であるケースは 5% に過ぎない。どれかの箱に 9 個入っていればユーザーの勝ちであるとするならば,ほとんどのユーザーはこれを達成することはできない。

※ ビンとボールの問題は,前述の「Birthday Paradox」および「Coupon Collector's Problem」を包含する様々なパターンが考えられている問題である。また,解析手法も Poisson近似,マルコフ連鎖のカップリング,マルチンゲールなど多岐に及ぶ。

 

Case 5. Balls and Bins Problem (revised)

問:「n 個のボールと n 本のビンがある。ボールは各々のビンに投げ入れられるとし,今回の場合は投げ入れられるビンはランダムに 2 本選ばれることにする。そして選ばれたビンの内,容量が少ない方にボールが入れられる。つまりビン同士のボールの数をより"均質にする"という仕組みを導入する。この時,ビンの中の最多ボール数(最大内容量)はいくらくらいになるか?もしより一般に d 本選べるとしたらどうか?」

答:ビンを 2 本選ぶ場合は ln ln n / ln 2 + O(1),d>2 では ln ln n / ln d + O(1) となる。

この結果は次の様な興味深い考察を与えている。

Case 4 の場合(d=1)に比べて,d=2 の場合はその結果が指数的に減少している。つまりビンを2つ選んで少ない方に入れるという余地を与えることで最大容量を大幅に減らす結果になっている。しかしながら,d=2 から d=3 への変更はその減少は定数倍にとどまっている。d>3 の場合も高々定数倍の減少でしかない。

元の問題から一段階難易度が下がった時の劇的な変化を体感した後では,ユーザーは次の段階でも同じような変化量を期待してしまうが,実際はそれほど変化しないという一種の乖離を生んでしまう可能性を含んでいる。


Case 6. Coupon Collector's Problem

問:「全部で n 種類の応募シールがある。どのシリアルの箱にもどれか1種類の応募シールがある確率で入っていて,n 種類の応募シールを集めると懸賞をもらうことができる。いったい何箱のシリアルを買えば懸賞がもらえるのだろうか?」

答:シールが全て等確率ならば nH(n) 箱,シールが異なる確率であるなら,

\Sigma_{i=0}^{n-1}(-1)^{n-1-i} \Sigma_{|J|=i}\frac{1}{1-P_j},\qquad\qquad\qquad\left(P_j=\Sigma_{j \in J}p_j\right)

 

Case 7. Coupon Collector's Problem (revised)

問:「全部で n 種類のシリアルが有り,それぞれには点数付きのシールが与えられている。ただし,ここではシリアルを購入しないと点数がわからないとする。集めたシールの点数の合計が全部で20点になると懸賞がもらえる。点数のバリエーションが 1点,1.5点,2点あるとして,懸賞をもらうのに必要なシリアルの購入数はいくらだろうか?」

答:しゅくだい

コンプガチャの数理 -コンプに必要な期待回数の計算方法について-

目次

1. 『コンプガチャの数理 -コンプに必要な期待回数の計算方法について-』

2. 『「数学的ゲームデザイン」というアプローチ』

3. 『コンプガチャの数理 -ガイドラインに基づいたゲームデザイン その1-』

4. 『コンプガチャの数理 -ガイドラインに基づいたゲームデザイン その2-』

 

目的

コンプガチャのコンプに必要な回数を求める問題は「The Coupon Collector's Problem」と呼ばれる数学モデルの枠組みに沿った美しい問題である事を述べ,いくつかの有用な結果を示す。

※ あくまで個人研究のつもりで書いたので,色々不備があるかもしれません。その際は一言頂けると助かります。

 

定義

コンプガチャ問題を Coupon Collector's Problem に準じた形で書くと以下の様になる:

「全部で n 種類のアイテムがあって,1つのガチャの中にアイテムが1つ入っているけれども,ガチャを空けるまではその中身はわからない。今,アイテム集めに関しては他の人と協力しない(トレードや売買をしない)という前提の元で,n 種類のアイテムを全部コンプリートするのにいったいいくつのガチャを買わないといけないだろうか?」

※ 例えば n 種類の中の特定の m 種類(レアガチャ)を集めれば良いといった問題は今回は対象としていない。

 

結果

1. 全アイテムの出現確率が等確率の場合

全アイテムの出現確率が等確率,つまり 1/n の場合は大学学部レベルの数学でこの問題を解くことができる。

今, X_{i} を新たなガチャが出るまでに買わないといけない数であるとする。n 種類のガチャを全て得るための回数を Xとすると,本問題におけるゴールは X の期待値 E[X] を求める事に等しい。

X=\Sigma_{i=1}^{n} X_{i} であるから期待値の線形性より E[X]=\Sigma_{i=1}^{n} E[X_{i}] であるので,E[X] を求めるには任意の i において E[X_i] を計算できれば良いことになる。さて,E[X_i] の計算は任意の i で  X_i が幾何分布に従っているという事実から簡単に求めることができる。

※ 幾何分布とは「確率 p で表が出るコイン投げを行ったとき,n 回目に初めて表が出る確率」を表現するものであり,

p(X=n)=(1-p)^{n-1}p

と表現される。p は幾何分布のパラメータと呼ばれる。この期待値 E[X]=1/p である。これは例えば p=1/2 のコイン投げにおいて、初めて表が出る期待回数は 2 回という事を示している。

今,既に i-1 種類のガチャを集めているとする。この時新しいアイテムが得られる確率は 1 から現在持っている i-1 種類のガチャが選ばれる確率を引けば良いので,

p_{i}=1-\frac{i-1}{n} = -\frac{1}{n}i + \frac{n+1}{n}

である。当たり前だが,この確率の示すところは,i (既に集めているアイテムの数)が大きくなれば p_i (新しいアイテムが得られる確率)が小さくなっていくことである。

ところで、X_i はパラメータ p_i を持った幾何分布であるので,

E[X_i]=\frac{1}{p_i} = \frac{n}{n-i+1}

である。よって

E[X]=\Sigma_{i=1}^{n} E[X_i]=\Sigma_{i=1}^{n}\frac{n}{n-i+1}= n\Sigma_{i=1}^{n}\frac{1}{i}

となる。ここで \Sigma\frac{1}{i} は Harmonic Number: H(n) と呼ばれる。

∴ n 種類全てのアイテムをコンプするために必要な期待回数は nH(n) 回である。

ln(n) \leq H(n) \leq ln(n) + 1

であるので,期待回数はまた  n\ln(n)+\Theta(n) とも書ける。

e=2.72,\quad e^{2}=7.39,\quad e^{3}=20.09,\quad e^{4}=54.60,\quad e^{5}=148.41

であるので,n=3 で 2n+α,n=8 で 3n+α,n=20 で 4n+α,n=150 で 5n+α n=3 で n+α,n=8 で 2n+α,n=20 で 3n+α,n=55 で 4n+α,n=150 で 5n+α 程度の回数を必要とする。 

AKB48「ポスター44種類コンプでイベント招待」企画,「独禁法違反」のおそれで中止

解: 今44種類のポスターを全てコンプするのに必要な期待回数は 44*H(44) である。

Rでは H(n) を以下の様に定義することができる:

 

つまり44枚のポスターを全部コンプするのに必要なCDは192枚ということになる。CDは1250円であったらしいので、全部コンプするのに 1250*192 = 240,000(24万円!)かかることになる。

また、n を1から50まで動かした時の期待回数は以下の通り。

今求めているのはあくまで期待値であるので実際はこの期待値まわりの値にばらつく。

そのばらつき具合は Y がパラメータ p を持つ幾何分布であるとき,その分散が

Var[X]=\frac{1-p}{p^2}\lt\frac{1}{p^2}

であることを利用して、

Var[X]=\Sigma_{i=1}^{n}Var[X_i]\leq\Sigma_{i=1}^{n}\left(\frac{n}{n-i+1}\right)^2= n^2\Sigma_{i=1}^{n}\left(\frac{1}{i}\right)^2\leq\frac{\pi^{2}n^{2}}{6}

となる。この結果に Chebychev の不等式を適用することで(導出省略)以下のさらに強い結果を言うことができる:

「2nln(n) 回ガチャを回してもまだアイテムをコンプできていない確率は 1/n 以下である」

先ほどのAKB48の例では「CDを 2*44*ln(44)=333 枚(416,250円)買ったとしてもまだポスターをコンプできない人は 2.3% 程度に過ぎない」,つまり50万円を持っておけば準備はほぼ万全と言える。

(Q. 以上の結果を踏まえると,ポスターのコンプリートセットを販売するとするなら,いくらの値段設定が妥当であろうか?例えば10万円なら本気でコンプリートしたい人には安い設定となる)

 

2. 出現確率が互いに異なる確率の場合

ここで考える問題は,全 3 種類のコンプガチャゲームがあったとき,その3種類の出現確率が (p_1,p_2,p_3) = (1/6, 1/3, 1/2) と異なって設定されている場合である。

一般にはこちらの状況の方が多いが,式の導出はなかなかのハイレベルなので結果のみを示すことにする。詳細は http://algo.inria.fr/flajolet/Publications/FlGaTh92.pdf を参照。

∴n 種類のアイテムをコンプするのに必要な期待回数は,

E[X]=\Sigma_{i=0}^{n-1}(-1)^{n-1-i} \Sigma_{|J|=i}\frac{1}{1-P_j},\qquad\qquad\qquad\left(P_j=\Sigma_{j \in J}p_j\right)

である。

(ただし \Sigma_{i=1}^{n}p_i=1 )この結果より,以下の事がわかる:

  1. 等確率の設定よりもコンプに必要な回数は遙かに多くなる
  2. 出現確率のばらつきによってもコンプに必要な回数は変動する
  3. 300円ガチャであれば10種類のアイテム数でもコンプまでに10万円以上かかる設定が可能である

以下が主な数値結果。

# 3種類

  1. (1/3,1/3,1/3) -> 5.5回
  2. (1/6,1/3,1/2) -> 7.3回
  3. (1/10,4/10,5/10) -> 10.7回

# 20種類(括弧内は確率ではなくそれぞれのアイテムの重み)

  1. (1,1,...,1) [等確率] -> 72回,21,586円
  2. (1,1,1,1,1,10,...,10) [5個のレアイテムと15個のアイテム] -> 354回,106,195円

# 10種類(括弧内は確率ではなくそれぞれのアイテムの重み)

  1. (1,1,...,1) [等確率] -> 29回,8,787円
  2. (1,1,1,30,...,30) [3個の超レアイテムと7個のアイテム] -> 390回, 117,152円

R コードは本文の最後に記載。

 

感想

ソーシャルゲームのコンプガチャやAKBのポスター購入戦略のすごいところは,数学モデルのレールに沿った仕組みを実世界で提供することにより,物事を有利に進められる点である。例えばあるイベントにおいては本イベント期間での売上目標を x 円と定めたとすると,この目標は以下の具体的なアクションスキームまで落とし込むことができる:

  • アイテムを n 種類用意する
  • 確率テーブルを (p_1, p_2,...) に設定する
  • 以下の様な見込みセグメントにおいて,ユーザー数 a, b,...人と期待金額 x_1, x_2,...円に設定する
  1. コンプするユーザー a 人         --(計算)--> x_1 円
  2. 3/4種類集めるユーザー b 人   --(計算)--> x_2 円
  3. 半分種類集めるユーザー c 人 --(計算)--> x_3 円
  4. ...

(以上のパラメータは全て事前に定める事ができる。)

後はイベントにおいて,各セグメントを達成するユーザー数が a, b, c,... となるように工夫をしていけば良い。

データマイニングがデータ(結果)から数学モデルを見いだし,調節可能なパラメータを見いだしそれを変更することで改善を図る戦略なのに対し,上記の戦略は数学理論に沿ったデザインを初めから提供することで調節可能なパラメータを初めから握っておくという対照的なものであると言える。

また,今後コンプガチャに代わるデザインを考える場合にも数学モデルに基づいたものを検討するのは有効な戦略と考える。「Coupon Collector's Problem」にも色々な派生があり,またより大きな視点で見れば Coupon Collector's Problem は「Balls and Bins Problem」の一部であると考えることができる。つまりこれらのモデルをうまくデザインすることができれば,それはまたコンプガチャと同じように結果が予測可能で具体的なアクションに落とし込むことができるゲームを作ることができるかもしれない。以下の問題と結果は,次のデザインを生み出すヒントになるかもしれない。

a. Birthday Paradox

「全 n 種類のアイテムの出現確率の等しいガチャがある。ゲームのルールを同じアイテムが 2 個揃った(つまり重複した)時点でユーザーにご褒美がもらえるとする。このとき,\Omega(\sqrt{n}) 回で初めて重複する確率が 1/2 以上になる」

b. Balls and Bins Problem

「全 n 種類のアイテムの出現確率の等しいガチャがある。今ユーザーは n 回ガチャを回すチャンスがあり,どれかのアイテムが k 個以上重複していれば(これを最大内容量と呼ぶ)ユーザーにご褒美がもらえるとする。このとき最大容量 k が 3ln(n) / ln(ln(n)) より大きくなるのは 1/n でしか無い。」

c. Coupon Collector's Problem 

「より優しいガチャコンプ問題を考える。1つのガチャからは1種類のアイテムが得られるのでは無く,ランダムに選ばれた d 種類のアイテムが掲載されたカタログが得られ,ユーザーはそこからどれか 1 つのアイテムを選択することができる。ユーザーは合理的な選択,つまり自分が今持っていないアイテムを選択するという過程のもとでは,コンプまでの期待回数は

\Sigma_{i=0}^{n-1}\frac{1}{1- {i \choose d}/{n \choose d}}

である。十分大きい n で \frac{n}{d}\log(n) と書ける。この結果は本文の結果よりも遙かに少ない。」

 

参考文献

  1. Probability and Computing: Randomized Algorithms and Probabilistic Analysis
  2. Randomized Algorithms (Cambridge International Series on Parallel Computation) 
  3. The Generalized Coupon Collector Problem
  4. SOME NEW ASPECTS OF THE COUPON COLLECTOR’S PROBLEM
  5. Birthday  paradox,  coupon collectors,  caching  algorithms  and self-organizing  search 

※ 数式に不備があれば教えて下さい。R のコードはより効率的な記述ができると思っているで,良いコードがあれば教えて下さい。

 

MongoDB JPの一年を振り返り、そして今後についてお話しします

MongoDB JPが発足してちょうど一年が経とうとしています。発足当時まだ学生だった頃の話です。初めは小さなコミュニティだったMongoDB JPも、現在では600名近いのGoogleグループのメンバーを抱え、毎回の勉強会でも100名以上の参加者に集まって頂くような規模にまで成長してきました。
この一年でMongoDBを実運用で使われているWeb企業さんもだいぶ増えてきました。多くの方の支えと協力があっての今もなお成長し続けている事に関しては感謝の気持ちでいっぱいです。

今回は技術の話しでは無く、以下の内容でMongoDB JPのこれまでの活動と現状を振り返り、そして今後について述べていきたいと思います。

アジェンダ

  1. MongoDB JPのこれまでの活動
    • MongoDBと共に成長した一年間を簡単に振り返ります。
  2. MongoDB JPの現状の問題
    • 金銭的な問題・ドキュメント整備の問題などについて。
  3. MongoDB JPのこれからについて
    • 10genと共に今後は日本での本格展開に向けて動いていきます。
  4. 今後のMongoDBイベント
    • 今年から来年にかけて、Mongoイベントは盛りだくさんです。

1. MongoDB JPのこれまでの活動

MongoDB JPはこれまでに6回の定例勉強会(最近の開催風景第5回第6回)と、10gen主催の公式MongoDB Conferenceを1回、そしてソースコードリーディング分科会やその他の勉強会に招かれての発表など、たくさんの活動を行ってきました。僕がこれまでに書いたMongoDB関連の資料も20近くになりました。これまでの経緯を少し振り返っていきます。

第1回MongoDB勉強会

記念すべき12月の第1回勉強会CouchDB JPの方と合同で開催しました。初めての主催側の勉強会という事で段取りなどに非常に苦労したことを覚えています。リラックスなCouchな皆さんには非常に助けられました。スタッフを募り・発表資料を作り・当日はあれこれ裏方で動く…当時はそんな大変な勉強会をこれから続けていけるのかと不安でした。

MongoDB Conference in Japan

3月には10gen主催のMongoDB Conference in Japan(通称 mongotokyo)を開催しました。10genの方と密にやり取りしながら、英文メールの書き方や簡単な会話のしかたを学びました。楽天さんに会場をお借りし、技評さんに告知をお願いし、スタッフと発表者を募集し…、これらも非常に大変でしたが、世界公式のカンファレンスを日本で行える嬉しさと誇りはその苦労を遙かに上回るものでした。例え個人であっても、OSSを通じて世界のトップエンジニアとつながれるチャンスはいくらでもあるんだなぁとその時強く感じました。今思えば蒼々たる方々に発表して頂け、カンファレンス自身も成功に終わったことはMongoDBの今後の可能性を確信させてくれ、大きな自信となるものでした。
そしてまた、この時に当時スタッフとして参加してくださっていた @ さんに出会えたことは、以後のMongoDB JPの安定かつ継続的な活動ができているという意味で非常に大きな事だったと思います。

MongoDB 勉強会@フューチャーアーキテクト

それ以降の勉強会は @ さんの協力のおかげでフューチャーアーキテクトさんで開催できることになりました。常に10名近くのフューチャー社内・そして今では社外の多くのスタッフの方が勉強会の進行を支えてくれることになり、僕は運営や金銭管理のオペレーションを全て任せてしまって発表と会の進行に専念することができるようになりました。会場誘導をはじめUstream、そして懇親会の集金や準備まで全てが彼らの完璧なオペレーションの元で遂行されています。僕自身、勉強会で発表することで誰よりも自分自身が勉強になっていますので、僕は当日の司会と発表をするだけで良いというのは本当にありがたい状況です。いつもMongoDB勉強会をさせてくださっているスタッフの皆さん、本当にありがとうございます。そしてそれからも宜しくお願いします。

2. MongoDB JPの現状の問題

さて、皆さんに支えられて成長してきたMongoDB JPですが、もちろん色々と問題を抱える事にもなります。

運営資金の問題

基本的にオープンなコミュニティでありますので、勉強会などは必要な最低限の経費のみ参加者から頂くことにしています。主におやつ代と懇親会費ですね。ただ100名を超える規模になりますと当日キャンセルの方も多く、予定していた懇親会参加人数を10-20名下回るということもありました。そういった場合は数万円の赤字となってしまい、コミュニティで負担することになります。現状で僕たちは5万円近くの累積赤字を抱えています。金銭管理をうまくやれていないことに関してはきちんと反省してアクションを起こしていかないと日々反省するばかりです。

会場の問題

いつもフューチャーさんのおかげで100名を超える規模で安定してやらせてもらっていますが、フューチャーさんの会場が借りれなくなった時点で色々と問題が発生します。100名を超える人数を収容する会場を見つける事、その会場をお借りするのに必要なお金、そしてそのお金をどう捻出していくのか、など色々と大変になってしまいます。
特に来年1月に開催する第2回MongoDBカンファレンスでは、多数の参加者を見込めることから300名以上を収容できる会場が良いわけですが、まともに借りれば百万円はかかりますし、それを実現するためにたくさんのスポンサーを集めないといけません。そのために日々の仕事の合間を縫ってスポンサーを探す営業活動などをしていかないといけない、そういった所に時間をとられる懸念がありました。今回は前回に引き続き、楽天さんのご厚意によって無料で大きな会場を貸して頂く事ができました。本当にありがとうございます。他にも同時通訳を採用するのか、もしそうならその費用をどう捻出するかなどなど、コミュニティがこれだけ大きくなるとお金と時間が個人レベルではまかなえないくらいたくさんかかってしまい、もはや今までのままでは存続維持ができない状況になりつつあります。

日本語ドキュメント整備の問題

これも非常にしんどい問題です。本家のドキュメントの更新が非常に早くて翻訳が追いつかない、常に古い情報しか提供できていないという状況にあります。また、現状でドキュメント翻訳に参加している人もほぼいない状況ですので現在はほとんど行われていない状況にあります。僕も最近ほとんど訳していません…すいません…
これはきちんと呼びかけを行っていないという僕のさぼりの問題なのですが、前述した更新の速さもあってなかなかしんどいです。元々英語のドキュメントを読んでもらうようにしてもらえばありがたいというのが本音だったりします(すいません…)。

3. MongoDB JPのこれからについて

本の窓口・エンタープライズ

さて、このようにコミュニティが大きく活発で、またそれ故の問題点を多数かかえるMongoDB JPですが、現在一つの転換期を迎えようとしています。10genによるMongoDBの本格的な日本展開です。具体的には日本の中でMongoDBをサポートする窓口を設けること、また強力なパートナー企業さんを見つけ、スポンサーやエンタープライズへのMongoDBの普及を後押ししてもらう、などなどです。
現在はそのファーストステップであり、それらを実現するための活動を10genと一緒に積極的に行っていく、といった状況です。特にエンタープライズ向けへのMongoDB本格導入のために動いてくれ、将来的にMongoDBの日本の窓口の中心なってくれるパートナーさんとも手を組んでいます。彼らが10gen側と密にやりとりをしてくれ、既にエンタープライズ方面で活発に動いてくれています。僕も今後はエンタープライズの方々向けに資料を用意し、導入メリットや技術的な話しを紹介しに行くといった活動を増やしていこうと思っています。
なんにせよ、10genというアメリカのテクノロジーベンチャーが世界の中の日本という国に可能性を見いだしてくれたことはまずは非常に嬉しいことです。

ですので今後MongoDBの導入を検討されている企業さんがいらっしゃいましたら是非とも声をおかけ下さい。どのようなシーンでMongoDBが活用できるのか、そもそもMongoDBで何ができるのか、色々ご提案させて頂きます。僕としても新しいMongoDB活用事例に出会えることに関して非常にわくわくしています。

MongoDBとオープンコミュニティ

ただ僕自身、エンタープライズの領域やサポート窓口の部分は今の所は深く介入するつもりはなく、これからもMongoDBを愛する1ユーザーとして、オープンなコミュニティ活動を中心となって勢力的に行っていこうと考えています。
オープンソースの恩恵を享受している身として、その恩返しのためにもこの活動を継続的に行っていくことは僕たちエンジニアに課せられた使命でもあります。また今まで一緒にコミュニティを育ててきたMongoな面々、 @ さん、 @ さん、 @ さんを代表するフューチャーのMongoスタッフの皆さん、ソースレベルでMongoDBを語れるPFIの @ さん・ @ さん、ソースコードリーディング主催者の @ さん、そして人生の師である @ 師、またMongoDB立ち上げを後押ししてくれ、コミュニティにいつも参加してくださっている @ さん、宴会部長 @ さん、その他多数のメンバー(書ききれなくてすいません…)と共に今後ともMongoDBを通じた交流と発信を行っていくつもりです。今後とも宜しくお願いします。

4. 今後のMongoDBイベント

今後もMongoDBイベント盛りだくさんです。特に来年1月18日のmongotokyo2012は10gen側からPresidentを迎え、300名を超える規模で開催したいと考えております。ふるってご参加下さい!

mongotokyo2012について

既に募集の方が始まっています。開催一ヶ月前まではEarly Birdという通常より安い参加費で登録できます。一ヶ月を切りますと、General Admissionの価格になりますのでご注意下さい。また楽天の社員の方・発表者およびスタッフの方の参加日は無料です。ひとまずここで登録せずにお待ち下さい。本カンファレンスでは懇親会の費用をすべてこちらで負担します、カンファレンス後の懇親会まで、是非ともご参加下さい!

日付 タイトル
2011/11/15(火) 「第7回 MongoDB 勉強会 in Tokyo」〜今年最後のMongoイベント〜
2011/11/19(土)-20(日) オープンソースカンファレンス2011
2011/11/19(土) 楽天テクノロジーカンファレンス2011
2011/12/7-9 MongoDB Leader Summit & MongoSV *海外に行ってきます!
2012/1/18(水) Mongo Tokyo 2012
2012/1/27(金) エンジニアサポート新年会

今後ともMongoDB JP をどうぞ宜しくお願いします!

MongoDBの新機能:ジャーナリングについて詳しく

v1.8でMongoDBはジャーナリングと呼ばれる機能が新たに加わりました。今日はMongoDBのジャーナリングについて、実際にどのような処理が行われているのかを確認しながら、丁寧に見ていくことにしましょう。※なお、ジャーナリングという言葉自身、Mongoにこの機能が実装されるまで深く意識するようなことはありませんでした。解釈の部分で誤りなどがあるかもしれません、その際はご指摘していただけると幸いです。

ジャーナリングによってデータの堅牢性が格段に高まった

v1.8でジャーナリング機能が追加されたことによって、シングルサーバーにおけるデータの堅牢性がさらに高まりました。ジャーナリングという言葉は主にファイルシステムの分野においてかなり前から議論され、改善が進められてきた機能です。この意味におけるジャーナリングの目的はファイルシステム全体を保護することであり、そのためにメタデータの整合性を保持するために処理が施されるのが一般的なものです。一方、データベースで用いられるジャーナリングの目的は主にデータの保全であり、サーバーの障害からデータの損失をいかに最小限に抑えられるか、という目的で処理が施されます。もちろんMongoDBにおけるジャーナリングの目的も後者に該当します。

また、サーバーが完全に再起不能な状態に陥ってしまう状況に備えたデータ保全対策も重要で、その場合の対策は物理的に異なるサーバーにReplica Setを(またはMaster/Slave)構成するといった事が必要です。MongoDBでは従来のReplica Setによるデータ保全に加えて、ジャーナリング機能が今回新たに加わった形でシングルサーバーに対しての強力なデータ保全機能が備わりました。これによりサーバーダウンによるデータの損失を最小限にとどめることができ、またスムーズなリカバリが可能になりました。

実はジャーナリングが導入される前のバージョンまでは、シングルサーバーに対してはそれほど強力なデータの保全機能を備えているわけではありませんでした。次の章では、まずジャーナリングが追加される前のv1.6が抱えていた問題点を指摘し、それがジャーナリングによってどのように改善されるのかを示すことにします。

insertなどのデータ変更のディスクへの反映は60秒に1回

MongoDBを使用する多くの場合、各種言語ドライバを介してMongoDBへデータ処理を行うはずです。その際に注意して欲しいのは、多くのドライバのデータのinsertは"fire-and-forget"と呼ばれ、毎回のinsert実行後、それが実際にディスクに書き込まれたかを確認せずに次の処理を次々に実行します。基本的にはupdate等のクエリに対しても同じです、これらのオペレーション併せて「データ変更処理」と呼ぶことにします。
この理由の1つに、MongoDBではinsert処理実行後すぐにデータがディスクに書き込まれるわけではないというのが挙げられます。まずはメモリに書き込まれ、(デフォルトでは)60秒に1回、メモリ上の全てのデータがディスクに書き込まれるという処理が行われています。これによって大量のデータを効率良くかつ高速に書き込めるようになっているのですが、これはメモリからディスクへの書き込みが行われる前にサーバーのダウンなどの何らかの問題が生じた場合にはまだディスクに書き込まれていなかったデータは完全に消失することになってしまうというデメリットも持っています。つまりデフォルトでは最悪60秒の間にinsertされたデータが丸々失われてしまうリスクを背負っていることになります。
ただ、この問題への対応策は既に2点用意されています。
1つは起動時のコマンドラインオプション"--syncdelay"によってディスクへのフラッシュ間隔を任意の秒数に変更することです。ただしこの設定を短い秒数に変更することで、Replica SetsやMaster/Slave環境を構成している場合はメンバーとの同期のための通信量が増えてしまい、大きな負荷となりえます。シングルサーバーで稼働している場合以外にはこの値を減少させることは推奨されていません。
2つ目は毎回のinsertの実行結果を確認するオプションがドライバに備わっているはず("safe"モード、または "set write concern"と呼ばれているコマンドです。)ですのでそれを使用する事です。このオプションを使用する事で、毎回のinsert()コマンドの後にgetLastError()コマンドを実行するようになります。このgetLastError()コマンドは、直前のオペレーションが成功したか否かを確認するコマンドです。またこのコマンドを実行することでデータに変更が加えられるオペレーションに対しては即座にディスクに書き込まれるようになります。ただしこのモードを使用すると(当たり前ですが)明らかに処理速度が低下します。MongoDBを利用する際には"critical"なデータと"non-critical"なデータという区別を持ち続け、それに併せた適切なデータ変更処理を行うようにしてください。

以上をまとめますと、MongoDBのデフォルトの振る舞いは"non-critical"なデータを想定したパフォーマンス重視に触れた設定になっています。安全なデータ変更処理を行う場合は、パフォーマンスを多少犠牲にしてでも上に述べた2つの施策(特に後者)を必ず行うようにして下さい。

ジャーナリングがデータの保全性を格段に高める第3の手段を与えてくれた

そしてv1.8で第3の手段となるジャーナリング機能によって、先ほど述べたデータ損失のリスクを大幅に低減されることになりました。
ジャーナリング機能を有効にして起動されたMongoDBは、実際にオペレーションを実行してデータに変更が加えられる前にこのオペレーション自身を追記専用の先行書き込みログ、ジャーナルファイルという特殊なファイルに書き込みます。より厳密に言えば100ミリ秒に1回、グループコミットという手法を用いてジャーナルファイルにオペレーションを書き込んでいきます。
このジャーナルファイルへの先行書き込みによって、インデックス作成などのデータの変更処理の実行途中、あるいはいくつかのデータがディスクにフラッシュされていない状況でサーバーがダウンしてしまったとしても、再起動時にそのオペレーションを再実行することでデータのリカバリができるようになります。ジャーナル機能をオンにしたMongoDBでは再起動時にクライアントから処理を受け付ける状態に入る前に、ジャーナルファイルに記録されたオペレーションを再実行してデータのリカバリが行うようになります。これによって今まで最悪60秒間のデータが全て失われてしまう可能性のあったものが、100ミリ秒間のデータの損失のみで済むようになりました。また、今まで必要だったmongod.lockの削除と、起動後のdb.repairDatabase()の実行も不要になりました。
このようにジャーナリングはデータの保全性に対して非常に大きな改善をもたらしてくれました。ただし、毎回データに変更を与えるオペレーションが先行してログに書き込まれる事になるので、それなりにパフォーマンスが落ちてしまう事はさけられません。記事、MongoDB Journaling Performance - Single Server Durabilityでは、ジャーナリングによって30%程度パフォーマンスが落ちてしまったという報告がされています。まだこの機能が登場して間もないことやこのようなパフォーマンスの問題もあり、v1.8においてはジャーナリングはデフォルトの機能にはなりませんでした。jounalingを使用するかどうかは、MongoDBをどのような目的や環境で利用しているかによって判断すべきでしょう。ただし今後ジャーナリングのグループコミットを行う間隔の改善や種々のパフォーマンスの改善がある程度進んだ段階でデフォルトの機能になる事でしょう。

従来のダウンからの復帰に必要な手順

さて、実際にジャーナリングの振る舞いを確認する前に、先ほど触れた言葉、mongod.lock、db.repairDatabase()に関する従来のサーバーダウンから復帰の手順についてお話ししておきます。
mongodが正常に終了されなかった場合、手動で再起動して正常な状態に復帰させるには以下の2つの手順が必要になってきます:

起動前に mongod.lock ファイルを削除する

mongodの起動時にはデータディレクトリの中にはmongod.lockというファイルが作成されます。また、正常終了時にはこのファイルは削除されます。mongod.lockの主な役割は正常終了されたか否かをファイルの存在によって確認することと、1つのデータディレクトリに1つのmongodしかアクセスできないようにブロックする役割です。異常終了の場合はこのファイルが残ることになり、再起動する場合には警告が出てこのファイルを削除しない限り起動することができません。この手順の詳細は後ほど具体的に説明しますのでここでは割愛させて頂きます。

起動後、db.repairDatabase()を実行して全データが壊れていないかのチェックを行う

また、いくつかのデータが破損している可能性がありますので起動後はまずdb.repairDatabase()というコマンドを実行して全データのスキャンを行う必要があります。このdb.repairDatabase()コマンドは全データが壊れていないかを確認するとともにデフラグのような役割も担います。ただ、このコマンドはデータベースの規模が大きいと(ロックはしないものの)パフォーマンスの大幅な低下と、非常に時間がかかってしまう事には注意しないといけません。また、実行のためにテンポラリファイルをdbpath内に新たに作成することを知っておかないといけません。

実際にジャーナリングの動作を確認してみる

それではここからは実際に手を動かし、mondodのサーバーログを確認しながらジャーナリングの動作に対する理解を深めることにしましょう。

journalサブディレクトリが作成され、ジャーナルファイルが作成されていることを確認する

mongodの起動時に--journalオプションをつけて起動してみましょう、ここではdbpathはdataディレクトリを指定しています。そして早速mongodのサーバーログを確認してみましょう。[1]で"journal dir=data/journal" として、なにやら新しいディレクトリがdbpath以下に作成されたのがわかります。[2]では"recover : no journal files present, no recovery needed"と書かれていますね。"recover"を行おうとしたけれども、ジャーナルファイルが見つからなかったのでその必要がない事が書かれています。その後、[3]でmongodはクライアントからの接続待ちの状態になります。(今回は必要な部分しか抜粋していません。)

$ mongod --journal --dbpath data
Sun Jun  5 22:33:13 [initandlisten] MongoDB starting : pid=50557 port=27017 dbpath=data 64-bit
Sun Jun  5 22:33:13 [initandlisten] db version v1.8.0, pdfile version 4.5
[1]Sun Jun  5 22:33:13 [initandlisten] journal dir=data/journal
[2]Sun Jun  5 22:33:13 [initandlisten] recover : no journal files present, no recovery needed
[3]Sun Jun  5 22:33:13 [initandlisten] waiting for connections on port 27017
Sun Jun  5 22:33:13 [websvr] web admin interface listening on port 28017
...

まず[1]で実際にdbpathにjournalサブディレクトリが作成された事を確認してみましょう:

$ ls data   
journal    mongod.lock    test.0    test.1    test.ns

確かに作成されていることが確認できました。さらにjournalサブディレクトリの中を確認してみます:

$ ls -lh data/journal/

total 24
-rw-------  1 doryokujin  staff   8.0K  6  5 22:55 j._0
-rw-------  1 doryokujin  staff    88B  6  5 22:56 lsn

j._0 というファイルとlsnというファイルが作成されているのがわかります。どちらもバイナリファイルですので中身を確認することはできません。それではここでtestデータベースのmycollコレクションに対して100,000件の挿入を行ってみます:

$ mongo localhost:27017/test  #<host>:<port>/<dbname>

> for(var n=0; n<100000; n++){ db.mycoll.insert( { "n": n } ); }

ここでもう一度journalサブディレクトリを確認してみます:

$ ls -lh data/journal/

total 23696
-rw-------  1 doryokujin  staff    12M  6  5 23:19 j._0
-rw-------  1 doryokujin  staff    88B  6  5 23:19 lsn

j._0ファイルサイズが増えているのが確認できました。この j._0ファイルは前述したジャーナルファイルと呼ばれるもので、データに対して更新が行われた際のオペレーションが全て記録されています。インデックスの作成もジャーナルファイルに記録されます:

> db.mycoll.ensureIndex({"n":1})

このオペレーションに対してもジャーナルファイルに記録されているのが確認できます:

$ ls -lh data/journal/

total 30280
-rw-------  1 doryokujin  staff    15M  6  5 23:29 j._0
-rw-------  1 doryokujin  staff    88B  6  5 23:29 lsn

ジャーナルファイルはそのファイルサイズが1GBに達した時点でローテートされj._0, j._1,...という名前で保存されていきます。また、古くなったジャーナルファイルは自然に削除されるため、通常運用においては2つから3つのジャーナルファイルが存在している状況になります。ただし、念のためにジャーナルファイル用に10GB程度の空き容量を確保するようにしておいて下さい。また、データの読み取りに関するオペレーションは何回行ってもジャーナルファイルには記述されません:

> var ary=[]; 
> for(var n=0; n<1000; n++){ ary[n]=db.mycoll.findOne( { "n": n } ); }
> ary

[
        {
                "_id" : ObjectId("4deb904e79d55a3e071f21a9"),
                "n" : 0
        },
        {
                "_id" : ObjectId("4deb904e79d55a3e071f21aa"),
                "n" : 1
        },
...
$ ls -lh data/journal/

total 30280
-rw-------  1 doryokujin  staff    15M  6  5 23:29 j._0
-rw-------  1 doryokujin  staff    88B  6  5 23:37 lsn
正常終了時から再起動までの動作を確認する

ここからはmongodの正常終了・異常終了の際のジャーナリング機能周辺の挙動を確認してみることにしましょう。まずは正常終了させるために、起動している27017のmondodに対してCTRL+Cを押します。するとmongodのサーバーログは正常に終了処理を開始します。[1]に"shutdown: journalCleanup..."という表示が出ていますね、ここでジャーナルファイルのクリーンアップが行われているようです。[2]でジャーナルファイルが削除されているのが確認できます。[3]でmongod.lockファイルが削除され、[4]で正常な終了処理が完了したという通知とともに、mongodサーバーが終了します。

...
Sun Jun  5 22:45:30 [interruptThread] shutdown: closing all files...
Sun Jun  5 22:45:30 closeAllFiles() finished
[1]Sun Jun  5 22:45:30 [interruptThread] shutdown: journalCleanup...
[2]Sun Jun  5 22:45:30 [interruptThread] removeJournalFiles
[3]Sun Jun  5 22:45:30 [interruptThread] shutdown: removing fs lock...
[4]Sun Jun  5 22:45:30 dbexit: really exiting now

このように、正常な終了処理が行われるとjournalサブディレクト内のジャーナルファイルは全て削除されます。正常に終了したのですから、終了前の全てのデータ変更オペレーションはディスクに反映されていることが保証されていますので、次の起動の際にジャーナルファイルを保存しておく必要もないわけです。

異常終了時から再起動までの動作を確認する(ジャーナリング機能を有効にしていない場合)

それではここでmongodを意図的に異常終了(ダウン)させてみますが、ここではさらにジャーナリング機能を有効にした場合とそうでない場合でその挙動の違いを確認することにします。まずは--journalオプションなしで起動し、newcollコレクションに対して100,000件のデータ挿入してみましょう。

$ mongod --dbpath data

> for(var n=0; n<100000; n++){ db.newcoll.insert( { "n": n } ); }

この操作の後にmongodを強制的にシャットダウンさせます。

$  ps aux | grep mongod | grep -v grep
doryokujin 35401  16.4  0.5  2675212  22240 s015  S+   11:54PM   0:00.61 mongod --dbpath data

$  sudo kill -KILL 35401

強制終了されたmongodに対して再起動しようとすると前述したとおり、以下の様な警告が出て起動できません。

mongod --dbpath data
Sun Jun  5 23:56:48 [initandlisten] MongoDB starting : pid=42647 port=27017 dbpath=data 64-bit 
Sun Jun  5 23:56:48 [initandlisten] db version v1.8.0, pdfile version 4.5
Sun Jun  5 23:56:48 [initandlisten] git version: 9c28b1d608df0ed6ebe791f63682370082da41c0
Sun Jun  5 23:56:48 [initandlisten] build sys info: Darwin erh2.10gen.cc 9.6.0 Darwin Kernel Version 9.6.0: Mon Nov 24 17:37:00 PST 2008; root:xnu-1228.9.59~1/RELEASE_I386 i386 BOOST_LIB_VERSION=1_40
************** 
[1]old lock file: data/mongod.lock.  probably means unclean shutdown
[2]recommend removing file and running --repair
see: http://dochub.mongodb.org/core/repair for more information
*************
Sun Jun  5 23:56:48 [initandlisten] exception in initAndListen std::exception: old lock file, terminating
Sun Jun  5 23:56:48 dbexit: 
Sun Jun  5 23:56:48 [initandlisten] shutdown: going to close listening sockets...
Sun Jun  5 23:56:48 [initandlisten] shutdown: going to flush diaglog...
Sun Jun  5 23:56:48 [initandlisten] shutdown: going to close sockets...
Sun Jun  5 23:56:48 [initandlisten] shutdown: waiting for fs preallocator...
Sun Jun  5 23:56:48 [initandlisten] shutdown: closing all files...
Sun Jun  5 23:56:48 closeAllFiles() finished
Sun Jun  5 23:56:48 dbexit: really exiting now

[1]で正常終了時には削除されるはずのdata/mongod.lock が残っているとの警告が出されていますね。そして[2]でこのファイルを削除して再起動し、reparコマンドを実行する事を勧めてきています。先ほどは記述しませんでしたが、repairコマンドは、起動後にdb.repairDatabase()を実行する以外に、起動の際に--repair コマンドラインオプションをつけることでも同じことが行えます。また、mongod.lockの削除の際はくれぐれも他のファイルやディレクトリを削除しないように注意して下さい。

$ rm data/mongod.lock 
$ mongod --repair --dbpath data

このように--journalオプションをつけずに復帰した場合は強制終了するまでにディスクに書き込まれていたデータだけが反映されており、実行途中のもの、実行されたもののまだディスクにフラッシュされていないデータはディスクに反映しないまま消失してしまいます。

異常終了時から再起動までの動作を確認する(ジャーナリング機能を有効にしている場合)

それでは次に--journalオプションをつけて起動し、前回使用したnewcollコレクションを一度ドロップしてから同じ処理を行ってみます。

$ mongod --journal --dbpath data

> db.newcoll.drop()
true
> for(var n=0; n<100000; n++){ db.newcoll.insert( { "n": n } ); }
$ ps aux | grep mongod
doryokujin 71504  17.2  0.2  2889368   6684 s015  R+   12:05AM   0:01.34 mongod --journal --dbpath data
$ sudo kill -KILL 71504

それでは--journalオプションをつけたままmongodを再起動してみましょう。そうすると今度は警告も出ずに再起動することができます。サーバーログを見てみましょう:

$ mongod --journal --dbpath data
Mon Jun  6 00:08:24 [initandlisten] MongoDB starting : pid=83165 port=27017 dbpath=data 64-bit 
Mon Jun  6 00:08:24 [initandlisten] db version v1.8.0, pdfile version 4.5
Mon Jun  6 00:08:24 [initandlisten] git version: 9c28b1d608df0ed6ebe791f63682370082da41c0
Mon Jun  6 00:08:24 [initandlisten] build sys info: Darwin erh2.10gen.cc 9.6.0 Darwin Kernel Version 9.6.0: Mon Nov 24 17:37:00 PST 2008; root:xnu-1228.9.59~1/RELEASE_I386 i386 BOOST_LIB_VERSION=1_40
[1]Mon Jun  6 00:08:24 [initandlisten] journal dir=data/journal
[2]Mon Jun  6 00:08:24 [initandlisten] recover begin
[3]Mon Jun  6 00:08:24 [initandlisten] recover lsn: 0
[4]Mon Jun  6 00:08:24 [initandlisten] recover data/journal/j._0
[5]Mon Jun  6 00:08:26 [initandlisten] recover cleaning up
[6]Mon Jun  6 00:08:26 [initandlisten] removeJournalFiles
[7]Mon Jun  6 00:08:26 [initandlisten] recover done
Mon Jun  6 00:08:26 [dur] lsn set 0
Mon Jun  6 00:08:26 [initandlisten] waiting for connections on port 27017
Mon Jun  6 00:08:26 [websvr] web admin interface listening on port 28017

[1]でジャーナルディレクトリの存在を確認し、[2]でrecover(リカバリ)を開始したことを述べています。[3]でlsnファイルから必要なジャーナルファイルを確認し、[4]でジャーナルファイル: j._0 に記述されたオペレーションを実行してリカバリを行っています。これにより、実行されたものの、ディスクに書き込まれていないデータをDBに反映することができます。これにより実際に実行されたオペレーションに対しては、リカバリ時に再実行されることによってデータの変更が保証されることになります。そして[5]で正常にリカバリが行われ、[6]でジャーナルファイルを削除し、[7]でリカバリが終了したことが述べられています。このように、サーバーログを見ればジャーナリング有効時の挙動を理解することが出来ます。

まとめ

さて、いかがでしたでしょうか?途中でも触れましたが、ジャーナリングはまだ導入されたばかりの機能であり、今後はグループコミットのタイミングの短縮化など、より精錬され、デフォルトの機能になることは間違いありませんので、その仕組みがどんなものでどれだけ重要な機能であるかを理解しておくことは非常に重要です。この記事によってその助けになれば幸いです。それではまた。

Sharding を使いこなすための5つのTips

@です。今日も相変わらずMongoDBの、そしてShardingに関する記事を書こうと思います。

…と、その前にお知らせです!6月は2つのMongoDB勉強会を予定しております、是非ご参加下さい!

・2011年6月11日(土) 「第4回 MongoDB 勉強会 in Tokyo」@フューチャーアーキテクト 
・2011年6月28日(火)「第1回 MongoDB ソースコードリーディング」@PFI

さて、それでは本題に入りたいと思います。
MongoDBのShardingといえば、

・初期設定やShardの追加・削除といった導入の容易さ
・Shardの面倒をMongo側がずっと見てくれるという管理の容易さ

を備えていると言うことで興味を持っておられる方も多数いると思います。
しかしその一方で実際にSharding環境を導入している方々の中の多くは、遭遇する様々な不都合や不整合に頭を悩まし、初期設定のミスで取り戻しのつかない状況に陥っていたりと、想定していた以上の苦労を強いられているように思われます。確かに現バージョン(v1.8)のMongoDBのShardingはまだまだ扱いが難しく、出来る限り「Shardingを使わない」方向で検討を進める方が得策である場合も多くあるような気がします。しかしきちんと対処法を身につけておきさえすれば、そういった起こりうるShardingの種々の問題に立ち向かうことができるはずです。
今日は少しマニアックな内容ですがShardingについて、知っておくと将来きっと役立つであろう5つのTipsを紹介したいと思います。
MongoDBのShardingはその余りある欠点を補うほどに強力です。大規模なSharding環境では、TBを超えるデータをあたかもシングルサーバーのMongoDBを相手にしているように簡単に扱え(若干言い過ぎですが…)、Replicationと連携した「大規模データストレージ」としての役割と、Mongo MapReduceやRedisやHadoopとの連携した「大規模データ解析基盤」としての役割を高い次元で両立させることができます。
この事はログ解析の観点で言えばScribeやFlume等で多数のサーバーに分散された大規模ログを定常的にMongoDBに集約させつつ、Mongo ShellからMap Reduceをさらっと記述して実行して、非バッチな処理やごく一部の領域にしか存在しない対象の解析でもインデックスなどを活用して高速かつ手軽に実行することができるようになる事を意味しています。Webアプリケーションの観点で言えば、あらゆるユーザーデータをMongoDBに格納し、一方バックグラウンドではリアルタイムに集計やクラスタリングアルゴリズムなどを走らせ、ユーザー単位でのパラメータの最適化やデータに基づいたアクションを即座にアプリケーションに反映することができる事を意味しています。

Sharding 5 Tips

0. 準備 〜自動Shardingと自動Balancing機能、そしてChunkという概念を知る〜
1. Primary Shardを活用する 〜Primary Shardの存在を知る〜
2. Chunkの事前分割 〜運用開始からデータを分散させる〜
3. Chunkの手動マイグレーション 〜安全に効率的にChunkを移動する〜
4. Chunkの手動分割&マイグレーション 〜動かないChunkを移動する〜
5. Shardの状態を監視する 〜有用な2つのコマンド〜

0. 準備 〜自動Shardingと自動Balancing機能、そしてChunkという概念を知ろう〜

ここではMongo Shardingの基本的な概念について簡単に解説したいと思います。「自動Sharding」とは挿入されるデータに対して、そのデータがどのShardに入るべきかを決定して振り分けてくれる機能です。より具体的に言えば、Sharding環境においてMongoDBはInsertされたドキュメントを(最初に設定した)ShardKeyの値に基づいて振り分けます。Sharding環境ではドキュメントの集合である"Chunk"という単位を持ち、このChunk単位で分割や移動が行われます。それぞれのChunkは他と重複しないShardKeyのレンジを持っています。例えばChunkAは [ "a", "k" ), ChunkBが [ "k", "{" ) のレンジを持っていたとすると、ShardKeyのイニシャルが"a"から"j"までの値を持つドキュメントはChunkAに、"k"から"z"までを持つドキュメントはChunkBに属します。"{" は "z" の次の順序を持つ値です。例えば"inoue" の値を持つドキュメントは ChunkA に、"takahiro" を持つドキュメントは ChunkB に属する事になります。繰り返しますが、このChunkがShard間を移動したり分割されたりします。1つ1つのドキュメント単位でそれらが行われることが無いことにはくれぐれも注意して下さい。

各Chunkはそのレンジが重複することは無く、またカバーしていないレンジもありません。先ほどの例では実は最低でもさらに後2つのChunkが存在します。それは [ -∞, "a" ) と [ "{", ∞ ] です。そして1つのChunkがデフォルトで200MB(Sharding開始時は64MB)以上のサイズに膨れあがると、Chunkはそれを等分割するキーによって分割されます。例えば ChunkA は [ "a", "g" ) と [ "g", "k" ) に分割されたりします。データ流入量が増え、Chunkの分割があちこちで行われるようになると、Shard間でChunkの数にばらつきが起こってきます。ドキュメントのShardKeyが特定のレンジ、つまり特定のChunkに集中する場合はそのShard内でたくさん分割が行われる事になります。これはある程度避けられない事であり、またデータローカリティを強く意識するならこの現象は必ずしも悪いものとも言い切れません。
MongoDBでは現在アクセスされているShardのChunkの数とクラスタ内で最小のChunk数を持つShardとの数の差を計算しており、その差が10以上になったときに、「Shard間に偏りが起こっている」と認識され、Chunk数が大のところから小のところへChunkの移動(マイグレーション)が行われます。これを自動Balancing機能と呼び、バックグラウンドでShard間のデータサイズを均質に保ち続けようとしてくれる親切な機能です。

このように、MongoDBのShardingは自動Sharding機能と自動Balancing(とReplica Sets の自動フェイルオーバー機能)によって容易にスケールさせる事が可能です。その他のShardingの機能についてや、ここで取り上げなかったSharding環境で起こる様々な問題については過去の資料をご覧下さい。

1. Primary Shardを活用する 〜Primary Shardの存在を知ろう〜

MongoDBには"Primary Shard"という概念があります。これは運用開始直後の数個しかないChunkが保存される初期Shardであり、MapReduceのOutputCollectionが作成される場所であり、その他様々なケースで優先的に使われるShardです。このPrimary Shardの存在を知り、適切な設定を行っておく事は以下の意味で非常に有用です。

・メモリやCPUの優秀なマシンを割り降ることで初期段階のデータの局所集中に耐える
・Primary ShardをSSD上に配置し、Map Reduceなどの出力を高速に行う

この2つの説明に入る前に、まずはShardingの構成について簡単にお話しておきます。

Sharding の初期構成

※ここでは簡単なShardingを用意する時間が無かったので、現在使用しているSharding環境を流用させて頂きます。

# 25のShard ( With Replica Sets ) を追加(24HDD + 1SSD)
# HDD
> db.adminCommand( { addshard: "shard00/delta1:2400,delta2:2400", name: "shard00" } )
> db.adminCommand( { addshard: "shard01/delta1:2401,delta2:2401", name: "shard01" } )
...
> db.adminCommand( { addshard: "shard22/delta6:2422,delta5:2422", name: "shard22" } )
> db.adminCommand( { addshard: "shard23/delta6:2423,delta5:2423", name: "shard23" } )
# SSD
> db.adminCommand( { addshard: "delta5:2424",name: "shard24" } )  
# Enable Sharding
> db.adminCommand( { enablesharding : "mydb" } ) 
> db.adminCommand( { shardcollection : "mydb.log", key : { hour : 1 } } )

"addshard" コマンドによってshardを構成していきます。"name"キーはそのshardの名前を決定し、"addshard"キーでshardの所在をhost名とport名により指定します。上記の例はReplica Setsを各Shardで構成しているためにReplica Setsメンバー全員の所在とSet名を指定しています。例えば"shard00"は"shard00"というSet名を持ったReplica Setsメンバー、delta1:2400とdelta2:2400(と後1つのarbiter server)を指定しています。またこのような24台のHDD上のReplica Setsを持つShardを構成するのに加えて、"shard0024"というReplicationを行っていない、かつSSD上に配置されたShardを登録しています。これは他のShardとは別の目的で用意しています。そして"enablesharding"コマンドによってデータベース"mydb"をSharding対象に加え、"mydb.log"コレクションをShard Key: "hour"で分割することを指定しています。実際に挿入されるレコード例を挙げておきます(一部加工済):

{
        "_id" : ObjectId("4de3d6f573a4bc264f002619"),
        "protocol" : "HTTP/1.1",
        "hour" : 23,
        "referer" : "-",
        "ipaddr" : "000.00.000.000",
        "responseBodySize" : 6052,
        "userId" : "777777",
        "timeMilliSec" : 83396000,
        "options" : {

        },
        "userAgent" : "DoCoMo/2.0 F706i(c100;TB;W24H17)",
        "params" : {
                "count" : "10",
                "opensocial_viewer_id" : "777777",
                "material" : "1",
                "item" : "61",
                "opensocial_owner_id" : "777777",
                "opensocial_app_id" : "10"
        },
        "time" : "23:09:56",
        "date" : 20110530,
        "path" : "/hoge/foo/bar",
        "method" : "GET",
        "statusCode" : "200"
}
メモリやCPUの優秀なマシンを割り降ることで初期段階のデータの局所集中に耐える

Sharding開始直後は1つのChunkからスタートします。これは1つのShardにデータが集中することを意味しています。特に初動から多数のアクセスが見込まれる場合、またはShard間でサーバースペックが異なる場合は、高性能マシンをPrimary Shardに割り当て、高い負荷に備える準備が必要です。Primary Shardの設定は上記のコマンドに続いて以下の様に行います。

> db.adminCommand( { moveprimary :     "mydb", to : "shard24" } );
   { "_id" : "mydb", "partitioned" : true, "primary" : "shard24" }

これによりPrimary Shardを意図的に"shard0024"に移動することができます。この設定を行わない場合は、自動でどこかのShardがPrimaryに設定されています。Primary Shardの設定はデータの挿入後にも行う事ができますが、エラーが出る可能性があること、事前に行うことに多くの意義を持つことに注意しておいて下さい。

Primary ShardはSSD上に配置し、Map Reduceなどの出力を高速に行う

今回の例の場合では、Primary Shard: shard24をこちらの目的で活用しています。shard24は解析専用サーバーのSSD上に配置されています。また、意図的な設定によって、このshard上には普通のデータは入って来ないようにしており、Map Reduceを使用した際の出力先のコレクションとしてのみ活用することにしています。複雑な集計作業においては、複数のMap Reduceの結果を用いてさらにMap Reduceを行うような場合がありますので、今回はその目的で高速な出力と、SSD上で多段MapReduceを高速に行うためのShardとして活用しています。例えば以下の様なUUを求めるMapReduceの結果はshard24上の"mr"コレクションに格納されます:

> m = function() { emit(this.userId, 1); }
> r = function(k,vals) { return 1; }
> res = db.log.mapReduce(m, r, { out : {replace:'mr'} });
{        "result" : "mr",
        "shardCounts" : {
                "shard00/delta1:2400,delta2:2400" : {
                        "input" : 11349174, 
                         "emit" : 11349174,
                         "output" : 167265 },
                "shard01/delta1:2401,delta2:2401" : {
                        "input" : 7582207,
                        "emit" : 7582207,
                        "output" : 139164 },
                ...
                "shard23/delta5:2423,delta6:2423" : {
                        "input" : 7508581,
                        "emit" : 7508581,
                        "output" : 144319
                }
        },
        "counts" : {
                "emit" : NumberLong(174721895),
                "input" : NumberLong(174721895),
                "output" : NumberLong(3317670)
        },
        "ok" : 1,
        "timeMillis" : 681401,
        "timing" : {
                "shards" : 653462,
                "final" : 27938
        },
}

以上の結果の統計情報を見ても"shard24"はデータが存在せずに、集計対象から外れていることがわかります。それではmongosからではなく、shard24上のmongodに直接アクセスして、そこに"mr"コレクションがあるか覗いてみます:

$ mongo delta5:2424/mydb
  MongoDB shell version: 1.8.1
  connecting to: delta5:2424/mydb
> show collections
log
mr                     # mr コレクションが確かにshard24に存在
system.indexes
> db.mr.count()
500000
> db.mr.find() 
{ "_id" : "u100001", "value" : 1 }
{ "_id" : "u100002", "value" : 1 }
{ "_id" : "u100003", "value" : 1 }
has more

大量の出力がある場合や保存されたコレクションからさらに何らかの集計を行う場合には、Primary Shardを利用して特定の用途だけのShardを作るような方法は有効な手段となることでしょう。

2. Chunkの事前分割 〜運用開始からデータを分散させる〜

Primary Shardの話の中で、「初期段階では1つのShardにデータが集中する」問題を挙げましたが、これに対してはより効果的な対策を施す事が出来ます。設定したShardKeyに対してデータを挿入する前にChunkの分割を行ってしまおうというものです。これは事前にShardKeyの分布が予測できている場合には始めから均質なデータの分散を可能にすることができます。今回の例では(そこまで均質性を求める事はできませんが、)入ってくるデータ(ログレコード)が記録された時間(0〜23)をshardKeyにすることで、初動からデータを分散させるように意図しています。この作業はデータが挿入される前に行わなければならないことに注意して下さい。

db.adminCommand( { split : "mydb.log" , middle : { hour:   0 } )
db.adminCommand( { split : "mydb.log" , middle : { hour:   1 } )
...
db.adminCommand( { split : "mydb.log" , middle : { hour:   23 } )

このsplitコマンドを実行する事によって、"middle"で定めたShardKeyの値を中心にしてChunkの分割を行ってくれます。今回はこれを0,..23のmiddle値でsplitコマンドを実行する事によって異なる時間のログレコードは異なるchunkに入るように設定しています。このコマンドを実行した結果を確認してみます:

> db.printShardingStatus()
        { "_id" : "mydb", "partitioned" : true, "primary" : "shard24" }
                mydb.log chunks:
                                shard24       25
                        { "hour" : { $minKey : 1 } } -->> { "hour" : 0 } on : shard0025 { "t" : 1000, "i" : 1 }
                        { "hour" : 0 } -->> { "hour" : 1 } on : shard24 { "t" : 1000, "i" : 3 }
                        { "hour" : 1 } -->> { "hour" : 2 } on : shard24 { "t" : 1000, "i" : 5 }
                        { "hour" : 2 } -->> { "hour" : 3 } on : shard24 { "t" : 1000, "i" : 7 }
                        { "hour" : 3 } -->> { "hour" : 4 } on : shard24 { "t" : 1000, "i" : 9 }
                        ...
                        { "hour" : 22 } -->> { "hour" : 23 } on : shard24 { "t" : 1000, "i" : 47 }
                        { "hour" : 23 } -->> { "hour" : { $maxKey : 1 } } on : shard24 { "t" : 1000, "i" : 48 }

この結果より、Chunkのレンジが (-∞, 0 ), [0, 1 ), [1, 2 ),..., [22, 23 ), [23, ∞ ) に分割されていることが確認できます。ここまででChunkの事前分割は実現しましたが、また異なるshardに分散させるところまでは実現していません。全ての行で"on : shard24"とあるように、全てのChunkは自動Balancing機能が働くまで、依然としてshard24上に集中して存在してしまうことになります。

3. Chunkの手動マイグレーション 〜安全に効率的にChunkを移動する〜

そこで次にChunkを意図的に移動(マイグレーション)させることにします。これを手動で行う事によって初動からデータを分散させることができ、かつChunkを置きたい場所に自由に移動させておくことで、データの意図的なルーティングも可能になります。

db.adminCommand({moveChunk : "mydb.log", find : {hour :  0}, to : "shard00"})
db.adminCommand({moveChunk : "mydb.log", find : {hour :  1}, to : "shard01"})
db.adminCommand({moveChunk : "mydb.log", find : {hour :  2}, to : "shard02"})
...
db.adminCommand({moveChunk : "mydb.log", find : {hour :  22}, to : "shard22"})
db.adminCommand({moveChunk : "mydb.log", find : {hour :  23}, to : "shard23"})

この"moveChunk"コマンドは、findキーで指定したShardKey条件に該当するドキュメントを含むChunkをtoキーで指定したShardに移動させます。今回の例では、Chunk [ 0, 1 ) (つまりhour=0のデータ)をshard00に移動させています。この結果を見てみますと今度は、各Chunkが異なるShardに存在している事が確認できます。

> db.printShardingStatus()
        { "_id" : "mydb", "partitioned" : true, "primary" : "shard0025" }
                mydb.log chunks:
                                shard24       1
                                shard00       1
                                shard01       1
                                shard02       1
                                ...
                                shard22       1
                                shard23       1
                        { "hour" : { $minKey : 1 } } -->> { "hour" : 0 } on : shard24 { "t" : 26000, "i" : 1 }
                        { "hour" : 0 } -->> { "hour" : 1 } on : shard00 { "t" : 3000, "i" : 0 }
                        { "hour" : 1 } -->> { "hour" : 2 } on : shard01 { "t" : 4000, "i" : 0 }
                        { "hour" : 2 } -->> { "hour" : 3 } on : shard02 { "t" : 5000, "i" : 0 }
                        ...
                        { "hour" : 22 } -->> { "hour" : 23 } on : shard22 { "t" : 25000, "i" : 0 }
                        { "hour" : 23 } -->> { "hour" : { $maxKey : 1 } } on : shard23 { "t" : 26000, "i" : 0 }

この設定の元でデータの挿入を行った場合、データのhour値によってそれぞれのShardに始めから分散させて挿入することができます。実際に挿入して確かめてみましょう:

$ mongo delta1:2600/mydb #connect to mongos
> for( var n=0; n<24; n++ ){
...   db.log.insert({"hour": n})
... }

$ mongo delta1:2400/mydb
> db.log.distinct("hour")
[ 0 ]

$ mongo delta1:2401/mydb
> db.log.distinct("hour")
[ 1 ]
...
$ mongo delta1:2423/mydb
> db.log.distinct("hour")
[ 23 ]

意図したとおりの結果が得られている事が確認できました。この設定は非常に効果的ですので、一度検討してみて下さい。

4. Chunkの手動分割&マイグレーション 〜動かないChunkを移動する〜

すでにSharding環境を構築して運用されている場合でも、2.と3.でお話したことは実践が可能です。つまりあるShard内の、既にデータが詰まっているChunkを意図的に分割して軽くし、特定のShardへ移動させることが可能です。これらの作業は次の意味で非常に有用です:

・Shard間の偏りを自力で補正する
・類似のChunkを同じShardへ集合させることでデータローカリティを実現する

shard間の偏りを自力で補正する

MongoDBの自動Balancing機能ではShard間の偏りがどうしても生じてきてしまいます。実際のChunkのマイグレーションには、そのChunkサイズと同じ以上のメモリ領域と、configサーバーへの大きな負担をかけるというリスクによって、必ずしも成功するとは限りません。メモリが足りなければChunkの移動が必要な状況でも決して起こりません。これらの状況は避けることができませんので、手動でのChunkの移動を定期的に行うようにします。また、Chunkサイズが大きかったりメモリが足りなくて移動に失敗する場合は、Chunkの手動分割によってより細かいサイズまで分割してから移動させることにします。まずはShardごとのデータサイズを確認して、Chunkの手動分割・移動を行ってみます:

> printShardingSizes()
        ...
        { "_id" : "mydb", "partitioned" : true, "primary" : "shard24" }
                mydb.log chunks:
                        { "hour" : { $minKey : 1 } } -->> { "hour" : 0 } on : shard24 { "size" : 0, "numObjects" : 0 }
                        { "hour" : 0 } -->> { "hour" : 1 } on : shard00 { "estimate" : false, "size" : 89590388, "numObjects" : 198518  }
                        { "hour" : 1 } -->> { "hour" : 2 } on : shard01 { "estimate" : false, "size" : 75265412, "numObjects" : 165859 }
                        { "hour" : 2 } -->> { "hour" : 3 } on : shard02 { "estimate" : false, "size" : 163306000, "numObjects" : 360413 }
                        ...
                        { "hour" : 22 } -->> { "hour" : 23 } on : shard22 {"estimate" : false, "size" : 254483548, "numObjects" : 576126 }
                        { "hour" : 23 } -->> { "hour" : { $maxKey : 1 } } on : shard23 { "estimate" : false, "size" : 247971184, "numObjects" : 561015 }

このコマンドによって各Shardの各データベース/コレクションのデータサイズ(の推定値)とオブジェクト数を確認することができます。他にも各Shard情報を確認するコマンドがあります。実際の分割・移動は前回までにお話したことと同じですが、分割に関しては、既にデータが存在しているので、対象のChunkをfindキーで特定して等分割を行う事にします(前回はmiddleキーだったことに注意して下さい)。今までの例はhourという24種類しかないキーをShardKeyに設定してしまっているため、分割などをしにくい状態になってしまっています。ここでは00:00:00を0としたtimeMilliSecをShardKeyとしている前提で分割することにします。

> db.adminCommand( { split : "mydb.log" , find : { timeMilliSec : 60000 } } ) 

このコマンドによってtimeMilliSecキー: 60000 を含むChunkが等分割になるようなキーで分割が行われます。Chunkの移動の方は前回お話したコマンドと全く同じです:

db.adminCommand({moveChunk : "mydb.log", find : {timeMillSec :  30000}, to : "shard10"})

これによって分割した片方の、timeMillSec=30000を含む方のChunkをsahrd10に移動させる事ができました。

類似のchunkを同じShardへ集合させることでデータローカリティを実現する

後の解析を見据えた場合のデータストレージにおいては、データの分散を意識しつつ、データローカリティ、つまり類似のデータは同じShardに存在させておいて検索や集計の効率性を上げることも意識しておく必要があります。完全にデータローカリティに特化したストレージを行うには、類似のChunkを特定のShardに集合させるようにします。この場合はShardの偏りはあまり気にしないでしょう。

5. Shardの状態を監視する 〜有用な2つのコマンド〜

さて、最後になりましたが、ここまでに登場したSharding環境で頻繁に使うことになるコマンドを紹介しておきます。

printShardingStatus(undefined,true)

機能的には db.printShardingStatus() と同じなのですが、 db.printShardingStatus() で肝心のChunkの分割情報を表示してくれない状況が発生してきます:

> db.printShardingStatus()
                                ...
                                shard0022       1
                                shard0023       1
                        too many chunksn to print, use verbose if you want to force print #肝心な情報が見れない…orz

その場合に printShardingStatus(undefined,true) を使います:

> printShardingStatus(undefined,true)
                                ...
                                shard0022       1
                                shard0023       1
                        { "hour" : { $minKey : 1 } } -->> { "hour" : 0 } on : shard0025 { "t" : 26000, "i" : 1 }
                        { "hour" : 0 } -->> { "hour" : 1 } on : shard0000 { "t" : 3000, "i" : 0 }
                        { "hour" : 1 } -->> { "hour" : 2 } on : shard0001 { "t" : 4000, "i" : 0 } 
                        ...

また、Chunkの情報はconfigデータベースからも確認することができます:

>use config
> db.chunks.find().forEach(printjson)
...
{
        "_id" : "mydb.log-hour_22.0",
        "lastmod" : {
                "t" : 25000,
                "i" : 0
        },
        "ns" : "mydb.log",
        "min" : {
                "hour" : 22
        },
        "max" : {
                "hour" : 23
        },
        "shard" : "shard22"
}
{
        "_id" : "mydb.log-hour_23.0",
        "lastmod" : {
                "t" : 26000,
                "i" : 0
        },
        "ns" : "mydb.log",
        "min" : {
                "hour" : 23
        },
        "max" : {
                "hour" : { $maxKey : 1 }
        },
        "shard" : "shard23"
}

configデータベースにはその他Shardingに必要なメタ情報を管理していますのでここから様々な情報を取得することが可能です。ただし、このメタ情報をupdateしたりremoveしたりすると大変な事になるので注意して下さい。configサーバーで手を加えて良いのは自動Balancing機能をオフにする場合だけです:

use config
db.settings.update( { _id: "balancer" }, { $set : { stopped: true } } , true )
printShardingSizes()

こちらも既出ですが、各Shardのデータサイズとオブジェクト数をコレクションごとに表示してくれる便利なコマンドです。ただし、このコマンドがデータ数が多くなると返ってくるのが非常に遅くなってしまいます。その場合には他のコマンドで確認するようにしてください。

> printShardingSizes()
Wed Jun  1 00:30:46 updated set (shard00) to: shard00/delta1:2400,delta2:2400
Wed Jun  1 00:30:46 [ReplicaSetMonitorWatcher] starting
...
--- Sharding Status ---
  sharding version: { "_id" : 1, "version" : 3 }
  shards:
      { "_id" : "shard0000", "host" : "shard00/delta1:2400,delta2:2400" }
      { "_id" : "shard0001", "host" : "shard01/delta1:2401,delta2:2401" }
      { "_id" : "shard0002", "host" : "shard02/delta1:2402,delta2:2402" }
      ...
               playshop-gree-access.log chunks:
                        { "hour" : { $minKey : 1 }, "date" : { $minKey : 1 } } -->> { "hour" : 0, "date" : { "$minKey" : 1 } } on : shard0012 { "estimate" : false, "size" : 1854702464, "numObjects" : 3765808 }
                        { "hour" : 0, "date" : { "$minKey" : 1 } } -->> { "hour" : 1, "date" : { "$minKey" : 1 } } on : shard0013 { "estimate" : false, "size" : 1384094100, "numObjects" : 2806727 }
                        { "hour" : 1, "date" : { "$minKey" : 1 } } -->> { "hour" : 2, "date" : { "$minKey" : 1 } } on : shard0014 { "estimate" : false, "size" : 882130544, "numObjects" : 1780488 }
                        { "hour" : 2, "date" : { "$minKey" : 1 } } -->> { "hour" : 3, "date" : { "$minKey" : 1 } } on : shard0015 { "estimate" : false, "size" : 539366856, "numObjects" : 1089989 }
                        { "hour" : 3, "date" : { "$minKey" : 1 } } -->> { "hour" : 4, "date" : { "$minKey" : 1 } } on : shard0016 { "estimate" : false, "size" : 419811240, "numObjects" : 848770 }
                        { "hour" : 4, "date" : { "$minKey" : 1 } } -->> { "hour" : 5, "date" : { "$minKey" : 1 } } on : shard0017 { "estimate" : false, "size" : 726579776, "numObjects" : 1471823 }
                        { "hour" : 5, "date" : { "$minKey" : 1 } } -->> { "hour" : 6, "date" : { "$minKey" : 1 } } on : shard0018 { "estimate" : false, "size" : 1152177872, "numObjects" : 2347418 }
                        { "hour" : 6, "date" : { "$minKey" : 1 } } -->> { "hour" : 7, "date" : { "$minKey" : 1 } } on : shard0019 { "estimate" : false, "size" : 2630525884, "numObjects" : 5485197 }
                        { "hour" : 7, "date" : { "$minKey" : 1 } } -->> { "hour" : 8, "date" : { "$minKey" : 1 } } on : shard0020 { "estimate" : false, "size" : 5387886924, "numObjects" : 11132989 }
                        { "hour" : 8, "date" : { "$minKey" : 1 } } -->> { "hour" : 9, "date" : { "$minKey" : 1 } } on : shard0021 { "estimate" : false, "size" : 2379915544, "numObjects" : 4840048 }
                        { "hour" : 9, "date" : { "$minKey" : 1 } } -->> { "hour" : 10, "date" : { "$minKey" : 1 } } on : shard0022 { "estimate" : false, "size" : 2308302612, "numObjects" : 4689784 }
                        { "hour" : 10, "date" : { "$minKey" : 1 } } -->> { "hour" : 11, "date" : { "$minKey" : 1 } } on : shard0023 { "estimate" : false, "size" : 2558705640, "numObjects" : 5189979 }
                        { "hour" : 11, "date" : { "$minKey" : 1 } } -->> { "hour" : 12, "date" : { "$minKey" : 1 } } on : shard0000 { "estimate" : false, "size" : 1881322140, "numObjects" : 3818884 }
                        { "hour" : 12, "date" : { "$minKey" : 1 } } -->> { "hour" : 13, "date" : { "$minKey" : 1 } } on : shard0001 { "estimate" : false, "size" : 2264673264, "numObjects" : 4605082 }
                        { "hour" : 13, "date" : { "$minKey" : 1 } } -->> { "hour" : 14, "date" : { "$minKey" : 1 } } on : shard0002 { "estimate" : false, "size" : 2303920996, "numObjects" : 4686648 }
                        { "hour" : 14, "date" : { "$minKey" : 1 } } -->> { "hour" : 15, "date" : { "$minKey" : 1 } } on : shard0003 { "estimate" : false, "size" : 3298773252, "numObjects" : 6840413 }
                        { "hour" : 15, "date" : { "$minKey" : 1 } } -->> { "hour" : 16, "date" : { "$minKey" : 1 } } on : shard0004 { "estimate" : false, "size" : 3157248456, "numObjects" : 6542235 }
                        { "hour" : 16, "date" : { "$minKey" : 1 } } -->> { "hour" : 17, "date" : { "$minKey" : 1 } } on : shard0005 { "estimate" : false, "size" : 2521432232, "numObjects" : 5130008 }
                        { "hour" : 17, "date" : { "$minKey" : 1 } } -->> { "hour" : 18, "date" : { "$minKey" : 1 } } on : shard0006 { "estimate" : false, "size" : 2551743308, "numObjects" : 5197201 }
                        { "hour" : 18, "date" : { "$minKey" : 1 } } -->> { "hour" : 19, "date" : { "$minKey" : 1 } } on : shard0007 { "estimate" : false, "size" : 1840064336, "numObjects" : 3695385 }
                        { "hour" : 19, "date" : { "$minKey" : 1 } } -->> { "hour" : 20, "date" : { "$minKey" : 1 } } on : shard0008 { "estimate" : false, "size" : 2250083536, "numObjects" : 4526531 }
                        { "hour" : 20, "date" : { "$minKey" : 1 } } -->> { "hour" : 21, "date" : { "$minKey" : 1 } } on : shard0009 { "estimate" : false, "size" : 2556930488, "numObjects" : 5139566 }
                        { "hour" : 21, "date" : { "$minKey" : 1 } } -->> { "hour" : 22, "date" : { "$minKey" : 1 } } on : shard0010 { "estimate" : false, "size" : 2567556160, "numObjects" : 5295961 }
                        { "hour" : 22, "date" : { "$minKey" : 1 } } -->> { "hour" : 23, "date" : { "$minKey" : 1 } } on : shard0011 { "estimate" : false, "size" : 2323384656, "numObjects" : 4784284 }
                        { "hour" : 23, "date" : { "$minKey" : 1 } } -->> { "hour" : { $maxKey : 1 }, "date" : { $maxKey : 1 } } on : shard0023 { "estimate" : false, "size" : 0, "numObjects" : 0 }
                        ...

まとめ

いかがでしたでしょうか?今回は内容がマニアックすぎたので何を言っているのかわからなかった方も多いと思います。実際にSharding環境を構築されて、同じような問題や必要に迫られたときに、もう一度この記事を思い返して頂ければ幸いです。

弊社の新解析基盤も今日お話した設定を反映しながら、26Shard+Replica Sets、データサイズ18TB×2 の規模で運用を開始しています。ここに述べたことだけでなく、自動Shardingや自動Balancingを行わないマニュアルのChunk分割ルールやShardの世代管理、SlaveからもMap Reduceを同時に行ったりHadoopやRedisと連携する集計方法など、色々やっています。またその辺の話も安定して運用できるようになればお話できればと思っています。

今後ともMongoDBを宜しくお願いします。

「ニフティクラウドでMongoDBは使えるか?」を読んで、僕なりの考察を書いてみた

@です。2011年5月19日付けのニフティクラウドさんのブログエントリーが「ニフティクラウドでMongoDBは使えるか?」と称してニフティクラウド上でMongoDBのパフォーマンス比較を行うという、非常に興味深いものでしたので、このエントリーを読んで僕なりの考察をまとめてみました。完全な検証環境がわからないので一部推測を含み、誤解などもあるかもしれませんが、それらのフィードバックも期待して、ブログにすることにしました。ですのでまずはこのエントリーを読んで下さいね。

ニフティクラウドでMongoDBは使えるか? : NIFTY Cloud ユーザーブログ

それではエントリーの中盤、「Benchmark」以降を部分的に引用しながら考察を述べていきたいと思います。

shardKeyを考慮に入れる事の重要性

■ Benchmark 下記のような条件およびプロセスでベンチマークを行い、プロセス完了までの時間を計測しました。
データ twitterのtweetを1.4GB (delete含めて707,259tweet)
条件
・shardingの有無(1台=無 vs. 3台)
・stripingの有無(有 vs. 無)
ニフティクラウドスペック(mini vs large16)

注:stripingについてはニフティクラウドにMySQLは載せられるのか?パフォーマンス大検証!をご覧ください

まず提示された条件については、以下の情報が不足しているように感じました。

shardingを構成する際に、shardKeyとして何を設定したか?

MongoDB のshardingでは、1番始めに行う「shardKeyの設定」が非常に重要です。このshardKeyの設定次第では、shard間の偏りやデータの非効率な取得が生じてしまう原因になります。また、find()やinsert()などの処理に関してもそのshardKeyを条件に含めて実行するかどうかで効率性が大きく異なってきます。shard環境の有無でfind()やinsert()の性能比較を行う場合には、shardKeyを条件に含めた場合と含めない場合の両方でさらに比較する必要があります。このshardKeyが不明ですとこの切り分けができません。

ここでは前提としてstatus_idをshardKeyにすることにします

今回のインプットはTwitterデータということで、最も一般的なstatus_idをshardKeyにした場合で考えることにします。実はstatus_idはMongoDBのshardKeyを考える上で非常に良い例となってくれます(最後のスライドの2枚目をご覧下さい)。
ここでのstatus_idは時間増加する性質をもったものと仮定しています。つまり時間が進みにつれてstatus_idが単調増加する性質です。一時期Cassandraを採用するためにこのstatus_idが時間増加にならないようにするといった話題が出ましたが、Cassandraを採用しなかった今、結局どうなったのでしょうか…(^_^;)

find() の条件にshardKeyを含むか否か、キーにインデックスを作成しているか否かの切り分けは比較としてとても重要なファクターです

プロセス
...
find
 コレクションからデータを取得するコマンドです。MongoDBの場合は、ドキュメントを縦横無尽に条件付けてフィルタリングができるのでこの性能も無視できない重要な性能です。

・1条件 find({source:'web'})
・ネスト条件 find({'user.lang':'ja'})
・否定 find({'geo':{$ne:null}})
・サイズ find({'entities.hashtags:{$size:2}})
・比較 find({'retweet_cnt':{$gt:1}})

MongoDBのパフォーマンス比較においては、これらのfindの条件が

  1. shardKey を含むかどうか
  2. インデックスを作成しているかどうか

を明示的に考慮に入れた方が良いと感じます。

検索条件にshardKeyが含まれている場合とそうでない場合

まず前者について考えていきます。sharding環境のfind()においては、その条件にshardKeyを含ませるか否かでデータ取得の方法が異なってきます。shardKeyが含まれている場合、mongosは該当のshardKeyを持つshardがどこにあるのかを知っていますので、そのshardにだけクエリを実行することができます。

逆に、shardKeyを含まない場合にはmongosは全てのshardに対してクエリを実行して取得したデータをマージしてクライアントに返してくれます。

つまりsharding環境において効率の良いクエリが実行できるのは、find()の条件にshardKeyが含まれていた場合です。ですので性能比較の際にはshardKeyを含ませるかどうかの違いも検証することが重要です。

インデックスを作成しているか、smallインスタンスにインデックスを作り過ぎていないか

後者に関しては、もちろんインデックスを作成した方が速いのは明らかですが、インデックスサイズが大きくなりがちなMongoDBに関しては、元のメモリ量が少ないsmallインスタンスに大量のインデックスを作成した場合に、逆にパフォーマンスが落ちてしまう可能性もあるのではないかと考えたからです。

(余談)mapreduce はcount()やdistinct()との比較もあれば

mapreduce
 MongoDBを十二分に活用するのであれば、避けられないコマンドです。簡単な解析に利用することが出来ます(簡単、と書きましたがかなり使えます)。


・単純なグループ集計(source集計)
・単純なグループ集計(5分ごとのtweet数集計)
・2条件のグループ集計(1時間当たり・1人当たりのtweet数集計)
・少し複雑な集計(1時間当たりのhashtag集計)
・少し複雑な集計(共起するhashtag集計)

count() や distinct() は高速

余談ですが、sharding環境でのMongoDBにおけるmapreduceの比較については、単純なmapreduceを記述する場合にはcount()やdistinct()といった組み込み関数との比較を行うのも面白いと思います。例えばtweet数集計や、sourceの集計、あるいはUUの計算などはsharding環境でもcount()やdinstinct()コマンドが全shardを対象に行ってくれます(正しい値を返さない場合もありますが…)。そしてこれらは実はmapreduceよりも高速に結果を返してくれます。例えば24shard環境で130GB、1億2千万レコードのuseridが特定できているアクセスログに対してUU計算を行った場合、mapreduceで記述した場合が350secに対してdistinct()を用いた場合には200secでした。これは後者がインデックスを利用して効率良く集計くれているからだと思います。

結果に対する考察

それではエントリーに記述されたベンチマーク条件での比較結果について、今まで述べた性質を考慮しながら考察してみたいと思います。



























スペックminilarge16
sharding
striping
mongoimport549.201 520.438 630.101 690.042 259.671 104.757 263.050 193.803
find38.378 42.293 13.618 14.998 1.044 1.030 1.124 1.132
mapreduce226.64 229.46 107.39 106.85 52.53 52.10 26.08 26.08

全体

全体的にざっくりと見ると、当然といえば当然ですが、VMのスペックがimport/find/mapreduceに大きなパフォーマンスの差に影響を及ぼしています。
 また、IO性能に関わるstripingに関しては、import、特にlarge16以外ではほとんど意味のある効果を発揮していませんでした。これはminiの場合はIOよりもmemory/cpuがボトルネックになっているためだと考えられます。

 データサイズが1.4GBであるため、mini(memory=512MB)においてはfindやmapreduceにおいてIO性能の及ぼす影響が強いと予想していましたが、ほとんど見られませんでした。これは今後追求していければと思っています。

findに関しては検索に使用したキーにインデックスを使用していたかがポイントになってくるような気がしました。mapreduceに関しては基本的にドキュメント全件走査しますが、findの場合はインデックスが使用できますので作成しているキーが条件に指定された場合には全件走査をする必要がありません。インデックスを作成した場合とそうで無い場合でもボトルネックはmemory/cpuであったのかも試されると良かったのかもしれません。(恐らく同じボトルネックであったと思いますが。)
また、インデックスを作成した場合、MongoDBはインデックスサイズが大きくなりやすい傾向があります。今回の場合、miniインスタンスでさらにインデックス作成の有無でさらに比較をしたとすると、findにインデックスを作成することの高速化する一方で、大量のメモリ消費の影響がどのくらいありそうなのかが気になりました。

mongos

 上記と重複しますが、スペックにおける影響が非常に強く、また、次に、large16のときにstripingの影響が出ていました。このimport処理では書き込み性能に影響が出るため、shardingの影響を予測していましたが、shardingによる性能の改善は見られないばかりか、1台で行った場合よりも劣化するという結果でした。

sharding環境の有無でmongoimportに差が出なかったのは恐らくshardKeyの問題ではないでしょうか。前述しましたが、shardingは事前に設定したshardKeyに従ってデータを各shardに分割しています。例えばこのshardKeyの値が "_id" を始めとした時間軸に対して増加するキーであった場合には、連続して挿入されるデータは分散されずに同じchunk、つまり同じshardに入ってしまいます。この場合にはshardingしていながら、実際にはシングルサーバーへ書き込みを行っているのと同じ、さらにはmongosサーバーを介することによってシングルよりも無駄なステップを消費しています。今回の結果の原因は、status_idをshardKeyとして使用しているような場合にはこれによるものと考えられそうです。

 この結果は、mongosまたはconfigサーバがボトルネックとなりうるという要因が予測されます。今後、例えばmongosを分割し、それぞれに対して同時にimportを行い、全体としてのパフォーマンスの改善が見られるかを検討する必要があるかと思います。

mongosもconfigサーバーもそれほど負荷のかかる役割を担う処理を行いませんし、複数用意するのは主にレプリケーション目的であって、スケール目的ではありません。ですのでこれらはボトルネックにはなりにくいはずです。
ただ、一度に大量なインサートを行う場合には、mongosを複数用意してデータを分割して、各mongosに並列insertすることによって多少の負荷を軽減することができそうですし、書き込みにゆとりがある場合には並列insertの方が高速に処理してくれそうです。
また、クライアントとのデータ入出力の窓口を増やすことで1つのmongosがダウンしてしまった際にも安定稼働させる上で有用だと思います。

find

IO性能に関わるstripingはほぼ、findにおいては影響を見せませんでした。またshardingによるfindの性能は、miniでは、およそ2倍?3倍程度の改善を示していましたが、large16ではほとんど効果が見られませんでした。同時に、stripingも影響があまり見られませんでした。

 今回の程度のデータサイズであれば、cpu/memory性能が十分に高ければ分散させてIO性能を稼ぐことにあまり意義が無いと考えられます。

find処理においては、検索条件にshardKeyが含まれていたかどうかが重要になります。もしshardKeyが検索条件に含まれていた場合には、mongosはどのshardに該当するデータが存在するかを把握していますので必要なshardに対してのみ検索を行うので効率良く高速に実行できます。しかし、shardKeyが検索条件に含まれていない場合は、全てのshardにfind()を実行し、結果をマージするということを行いますので効率があまりよくありません。
今回差が出なかったのも検索条件にshardKeyが含まれていなかったので全部のshardを確認していったからでは無いでしょうか?
(もちろん、全てのクエリがいつもshardKeyを含んでいるとは限りません、MongoDBはメモリをうまく利用することによって、この場合でもそれなりに速く実行してくれます。)

あるいはsharding環境においては、バックグラウンドでchunkのマイグレーションが行われているかどうかによってパフォーマンスが変わってきます。chunkのマイグレーション中はデータの移動がバックグラウンドで行われており、その際にchunkは丸々メモリにコピーされています。マイグレーションが行われている場合にはこのメモリの消費とサーバーへの負担のせいでパフォーマンスが一時的に下がってしまう可能性があります。
ただ、今回はその状況は起きていないと考えられます。デフォルトのchunkサイズは200MBであり、balancing機能によってマイグレーションが発動するタイミングは、現在アクセスされているshardのchunk数と、最もchunk数の少ないshardとの数の差違で10以上になったときです。すなわち2GB以上の差違が生じた時に発動します。今回のデータは1.4GB、MongoDBに格納した場合に2.5GB程度になると考えられますのでそもそもマイグレーションが発生していたかどうか、またそれが起きたとしても少ないマイグレーション回数でbalancingできるので、検証実行時には既にそのマイグレーションが完了しており、その状況に出くわさなかったことが考えられます。

それでは、なぜminiではshard環境の有無で結果に差が出たかというと、はやりシングルサーバーの場合はmemory/cpuがボトルネックになっていて、それが3つのmemory/cpuにデータを分散できたことで解消されたので、shardKeyが条件指定されておらず、全部のshardを見に行くという処理になっても高速に実行できたのではないでしょうか?

mapreduce

 今回、一番ベンチマークを取った甲斐があるプロセスでした。stripingについては性能にほとんど影響が見られませんでしたが、shardingの効果がよく出ており、簡単とはいえ集計や計算がcpu/memory依存で行われ、mapreduceを複数台で実現することの意味が見出せたものと考えられます。

map/reduceに関してはsharding環境を活用できますので高速化が最も期待できるところです。ただし、ここでもshardKeyの設定によって、集計対象とするデータが1つのshardに偏っている場合などは、その効果があまり期待できません。
また、map/reduceの実行は基本的に1shardあたり1スレッドしか使えません。このことは例えば8coreと1coreのCPUをそれぞれ積んだサーバーで比較しても、1サーバーごとに1shardしか配置されていない場合には大きな差違が出ないということです。
MongoDBのmapreduceはshuffle機能が無いなど、Hadoop等のmapreduceに比べてやや簡素化された仕組みでありますが、この1core/shard制約は最も大きな違いであり、MongoDBのmapreduceがスケールしない、遅いといわれているゆえんであります。
これらを考慮して、データの均等分散に気を遣い、かつコア数だけshardを同居させるといった(これは非推奨です)環境で実行した場合にはより大きな差を生むことが出来るはずです。

mapreduceの特性を理解して環境を構築する重要性

今回、すべてのデータについては、分散はほとんど見られなかったこともあり、ざっくりとした考察だけを行っていますが、いかがでしたでしょうか。実際に使う上でshardingを適切に配置し「Largeシリーズ」を使っていただければ。「Largeシリーズ」を使っていただければ、性能がスケールすることも含め、比較的性能的にもよいパフォーマンスが得られたのではないかと思っております。

1つのサーバーに1shardしか組まないという安全性を重視した設計を行う場合は、例えばメモリが4MBと同じでCPUのコア数のみが異なる「large」と「small4」ならばsmall4を選択してもそれほど問題が無いと思われます。しかしその金額の差違は、月額プランにして1サーバーあたり¥48,300-¥25,410=¥22,980。仮に10台のshardとそのreplicationとしてさらに10台を使用する場合には、月額で ¥22,980 × 20台 = ¥457,800 になります。このようにMongoDBのmapreduceの特性を理解せずに高スペックマシンの方が高速に動作すると思い込んで毎月数十万を無駄する可能性があります。
ニフティクラウドのインスタンス毎の金額設定は以下を参照して下さい:
クラウド 料金 | ニフティクラウド

逆に、あくまで試験的ですが、フルコアを使いきるためにlargeの4コアサーバーに擬似的に4shard構成にすることも可能です。この場合には必要なサーバー台数が1/4にできる一方で、月額は2倍高いだけなので、結果的にsmall4に比べて1/2の金額で済む事になります。ただ、これはmapreduceのみを考慮した話であり、同サーバーでその他の処理も行われていたりする場合などで状況は変わってくるので注意して下さい。

感想

まず、ニフティさん、エントリーに対して少し批判的な内容を含む記事になってしまったことをお詫びいたします。また、誤解や間違いもあるかもしれませんので、その際は指摘して頂けると幸いです。
今回の感想ですが、サービス提供側が、今回のような比較記事を積極的に示してくれることによって、そこから僕たちは非常にたくさんの情報を読み取ることができますので、非常に良いエントリーだと思いました。是非とも今後の継続的なレポートも期待しています。
また、ニフティクラウドを代表するクラウドサービスでMongoDBをメインに使用する環境を構築する場合には、MongoDBの特性を十分に考慮した上で構築しないと、無駄に高い構成になってしまったり、きちんとMongoDBを設定・チューニングできていないことによる低パフォーマンスをスペックのせいにして高スペックプランに乗り換えてしまうといった事態が生じてしまいがちです。十分に注意して下さい。
僕はクラウド上でMongoDBを活用してサービスや解析を展開される方を心より応援しています。気軽に声を掛けて下さい。そしてニフティさん、僕にクラウド環境を貸して頂ければMongoDBの性能検証などを詳細に行うことが可能です。いかがでしょうか?

※最後の最後に、shardKeyは重要ですという話をしましたので、悪いshardKey3例と良いshardKey1例を挙げておきます。

〜うまく動かすMongoDB〜仕組みや挙動を理解する

@です。この業界で非常に強い影響力を持つ@氏が某勉強会でMongoDBについてdisられており、このままではMongoDB自身の存続が危ういと思い、急遽ブログ書きました。(冗談ですよ)

MongoDBを使っているときに出会うトラブルをうまくまとめてくださった「MongoDBあるある」的な良い資料だと思います。今日はここで書かれているトラブルの解決方法を提示したいと思います。恐らく@氏は全ての解決方法を知っていながら、同じトラブルへ悩む人のためにあえてdisったのだと思います。

MongoDB はデータベースもコレクションも存在しなければ自動作成してくれる

f:id:doryokujin:20110518222510p:image

mongoシェルを起動する場合、たいていは

$ mongo :
# mongo localhost:27017

のようにして現在稼働しているmongodのホスト名とポート名を指定してmongoシェル起動するかと思います。この時僕たちは既に「test」データベースの下にいることになります。そしてMongoDBは存在しないコレクション名に対してinsertを実行した場合はコレクションを作成してその中にドキュメントをinsertしてくれるのです。以下のコマンドを試して見てください。

$ mongo localhost:27017
 MongoDB shell version: 1.6.4
 connecting to: name1:27017/test

> show collections # 現在のデータベースに存在するコレクションを確認
 system.indexes

> db.test.insert( {test: 1} ) # 存在しないはずのtestコレクションにドキュメント {test: 1} を挿入!
> show collections  # testコレクションが作成されている
 system.indexes
 test
> db.test.find()    # testコレクション内のデータ(ドキュメント)を確認
 { "_id" : ObjectId("4dd3ca8e0901112f237b9495"), "test" : 1 }

さらにMongoDBは存在しないデータベースに対しても処理を行うことができます。

> show dbs # 現在存在するデータベースを確認
 admin
 test

> use newdatabase # 存在しないnewdatabaseデータベースに移ってみます
 switched to db newdatabase

> db.test.insert( {test: 1} ) # さらに存在しないはずのtestコレクションにドキュメント {test: 1} を挿入!
> show dbs #newdatabaseデータベースができている!
 admin
 newdatabase
 test

> show collections #testコレクションもできている!
 test
 system.indexes

> db.test.find() # そしてデータも入っている
 { "_id" : ObjectId("4dd3cc1c0901112f237b9496"), "test" : 1 }

MongoDBは非常におせっかいなDBなのでこちらの怠惰な操作に関しても自動で色々と面倒を見てくれるのです。ここでのまとめは、

・mongo : で起動した場合、testデータベースにいる。始めから移動しておきたいときは mongo :/ とする
・存在しないデータベース、コレクションに対しても自動作成してinsertなどの処理が行える。エラーや警告は基本的に返さない

注意してください。

Replication時の親切な自動リカバリ機能

MongoDBのReplicationは非常にステキな機能を備えています。この機能の完成度は(他の機能に比べて)高い方だと思います。よく自動フェイルオーバーしないという話を聞きますが、きちんと設定とフェイルオーバーの仕組みを理解しておけば問題なく動作する(はず)です。今回@氏に起こった問題を考えてみましょう。


f:id:doryokujin:20110518225829p:image
f:id:doryokujin:20110518225830p:image

MongoDBのReplica SetはSetメンバーを他のReplica Setと区別をつけるためのと各メンバーのなどで管理しています。ここでは全てのメンバーがPrimaryになる可能性がある設定にしたとします。上記の例では今Primaryであるメンバーがダウンしてしまった状況です。この場合はMongoDBの自動フェイルオーバー機能によって他のSecondaryメンバーから次期Primaryが選択され、(その間に30秒〜のダウンタイムがありますが)引き続き処理を受け付ける状態を保ち続けてくれます。
その後、ダウンしていた元Primaryサーバーを修復して、データを全部消して再起動したのが2枚目の図です。ここでのポイントは前回と同じホスト名・ポート名で再起動したことです。そうするとmongodを再起動した時点ですぐに現Primaryとの同期が始まり、できるだけ速く完全に同期がとれるように"Catch Up"してくれます。mongoを立ち上げてデータベースを確認したときには既に同期が完了していたということです。これをMongoDBの「自動リカバリ機能」と呼びます。自動フェイルオーバー時の挙動や自動リカバリについては以下の図を参考にしてください。


f:id:doryokujin:20110518231814p:image
f:id:doryokujin:20110518231815p:image
f:id:doryokujin:20110518231816p:image
f:id:doryokujin:20110518231817p:image
f:id:doryokujin:20110518231818p:image

ちなみに異なるホスト名やポート名でmongodを起動した場合はこの自動リカバリ機能は発動しません。まずは手動でReplica Setsのメンバーに新しく加える作業を行わなければなりません。ここでの注意は、あまりにもデータが大きい場合や、oplogと呼ばれるコレクションのサイズを小さく設定した場合には、完全な同期を行うことができないので注意してください。MongoDBの同期はPrimrayのoplogに保存されている過去のオペレーションリストをSecondaryメンバーが自身のoplogにコピーして実行することによって行われます。また、このoplogは容量以上のオペレーションを保存しなければならなくなった場合には古いオペレーションをどんどん削除していきます。まっさらなメンバーをReplica Setsのメンバーに加えようとした場合、もしPrimary(または他のSecondaryメンバー)に全てのオペレーションが残っていないと、完全な同期が不可能になりますのでエラーになります。この場合はmongodumpなどの他の手段をとって同期を行うしかありません。

チャンクが移動しまくる・デフォルトのチャンクが大きすぎる


f:id:doryokujin:20110518233215p:image

MongoDBにはShardingとよばれるデータを複数のサーバーに分割する機能を備えています。その機能と特徴は下の図を見てください。


f:id:doryokujin:20110518233216p:image
f:id:doryokujin:20110518233217p:image

MongoDBのShardingは2つのおせっかいな便利な自動機能をもっています。「自動Sharding」と「自動Balancing」機能です。前者はデータの振り分けルールを自動で行ってくれる機能で、始めに「Shard Key」と呼ばれる”どのキーの値によって分割ルールを定めるのか”を指定してさえおけば、後はMongoDBの方で自動的にそのキーで振り分けルールを決めてくれます。しかもデータが入ってくるごとにどんどんとそのルールを変更(細かくしていく)していってくれます。後者はShard間でのデータ偏りが大きくなった場合に、データの移動(マイグレーション)をバックグラウンドで自動で行ってくれる機能です。移動の単位はChunk単位です。この機能によってShard間でデータが均質になるようにMongoDBが頑張ってくれているのです。

自動Sharding

例えばShard Keyに"name"を指定したとします。すると始めはざっくりと「ア行はShard0」に「カ行はShard1」といった具合に振り分けルールを決定します。この「ア行に属するデータ集合」のことをChunk呼びます。各ChunkはShard Keyの値に対して他とかぶらない範囲をもっておりその範囲に属するデータはそのChunkの中に入っていきます。そしてChunkの中にデータが詰まりすぎた場合はそのChunkを等分割してChunkサイズを均等に保とうとします。先ほどの例でいうと、[あ,い,う,え,お]の範囲を持っていたChunkが[あ,い,う]と[え,お]というChunkに分割されます。@氏が触れているデフォルトのChunkサイズは200MBです。つまり200MB以上のデータがそのChunk内に入ってきた場合に分割が行われることになります。データが大量に入ってきている状態の裏で、Chunkの細胞分裂が絶えず行われているのです。

実はデフォルトのChunkサイズは200MBなのですが、Sharding開始時では64MBに下げられています。そしてある程度のデータサイズとなった場合に200MBに変更されます(変更されないという話も聞きますが…)。もちろんこのデフォルトのChunkサイズは変更を行うことができます。mongosを起動するときにオプションとして --chunkSize [MB] を設定してやれば良いのです。

mongos --port 10000 --configdb host1:10001 --chunkSize 500

ここで --chunkSizeを1[MB]に設定してやるとchunkはデフォルトよりも遙かに速いペースで分割されていきます。ただ分割されるといっても、物理的な分割が行われているわけではないことに注意してください。しかしchunkSizeを1に設定すると後述するChunkの移動が絶えず行われるような状態に陥り、様々な問題を引き起こすので注意してください。




f:id:doryokujin:20110518233218p:image

自動Balancing

ShardKeyを適切に設定しなかったり、大量のデータ挿入で振り分けルールの設定が追いつかなかった場合にはShard間でデータの偏りが生じてしまいます。これはどう頑張っても避けられない問題でもあります。しかしMongoDBはデータの偏りがある程度大きくなった時点で偏りの大きいShardから少ないShardへChunkの移動を行うことによってそれに立ち向かってくれます。しかも自動で。


f:id:doryokujin:20110518233219p:image
f:id:doryokujin:20110518233220p:image

Sharding の種々の問題

しかしこれらの機能が完璧に働いてくれることを期待してはいけません。MongoDBは多機能ですが、高性能ではありません。おせっかいですがたくさんドジをします。以下に起こりうる問題の一部をあげました。





@氏の嘆かれている「チャンクが移動しまくる」という問題は、「チャンクのデフォルトサイズがでかい」と矛盾するのですが、チャンクサイズを大きくすれば移動しにくくなります。ただし、chunkの移動時にはそのchunkをまるまるメモリに乗っけるので、そもそもメモリに移動させるだけの空き領域がなければ移動が行われないので注意してください。また、移動させること自身をやめたい(自動Balancing機能をオフにする)場合はmongosのコンソールから以下のコマンドを実行します。

> use config
> db.settings.update( { _id: "balancer" }, { $set : { stopped: true } } , true )

chunkの移動はマニュアルで行うことが出来ますので printShardingSizes() で各shardのサイズ情報を参照にして行うようにしてください。またマニュアルでchunkの分割も行えます。

http://www.mongodb.org/display/DOCS/Moving+Chunks
http://www.mongodb.org/display/DOCS/Splitting+Chunks

余談ですが、このconfigデータベースにはShardingに関する情報が入っています。そしてこのsettingコレクションにはChunkSizeに関する情報も格納されています。ここの値をアップデートすることでchunkSizeの変更も可能であるように見えますが、ここで変更してしまうと様々な問題が生じるらしいのでやめましょう。そして自動ShardingがイヤになったらマニュアルShardingを行えば良いのです。しかしこの設定と以降の管理を行うのはかなり大変です。興味のある人は僕に声をかけてください。

メモリの状況を確認する

完璧なメモリの状況を確認するのは難しいですが、どれくらいのメモリ容量があれば高速にMongoDBが動いてくれるかは以下のコマンドで確認できます。

> db.stats()
{
"collections" : 3,
"objects" : 379970142,
"avgObjSize" : 146.4554114991488,
"dataSize" : 55648683504,
"storageSize" : 61795435008,
"numExtents" : 64,
"indexes" : 1,
"indexSize" : 21354514128,
"fileSize" : 100816388096,
"ok" : 1
}

ここの"indexSize"はインデックスが占める容量になっています。上記の例だと19GBメモリ容量が必要であることがわかります。ただ、メモリがこの容量になくてもMongoDBはもちろん動作します。あくまでこれだけのサイズがあればインデックスは全てメモリ上に載せることができるという指標です。その他にも rs.status() や db.serverStatus() などの情報を参照して種々の状態を監視するようにしてください。(2011年05月19日12時追記)例えば db.sertverStatus() を利用してメモリリークが起きていないかを確認することができます。

> db.serverStatus().mem
{
"bits" : 64,
"resident" : 4074,
"virtual" : 619051,
"supported" : true,
"mapped" : 618515
}
> db.serverStatus().extra_info
{
"note" : "fields vary by platform",
"heap_usage_bytes" : 379296,
"page_faults" : 6322555
}

例えば上の方のコマンドの出力結果で "virtual" と "mapped" の差が実装メモリサイズに比べて大きくなっていればメモリリークの可能性がありますので、より詳細な情報を確認しにいかなければなりません。上記の例では問題なさそうですね。

まとめ

上記のように、MongoDBの挙動を理解して、それに対する対策を適切に行うことによって生じる多くの問題には対応することができます。まだ日本語の情報が整備できていなくて申し訳ないですが、海外ドキュメントや本やブログを細かく読めばいろいろな気づきがあります。僕に聞いてくれればいつでもお答えしますので気軽に質問してください。

といっても、仕様的に解決不可能な問題もたくさんあります。しかしそれらの問題は開発元の10genの人たちが将来的に解決してくれると信じていますし、それを補うツールを自分で作れば良いことです。全ての役割をMongoDBが担ってくれるという期待はせずに、MongoDBが得意な機能だけを使うようにして、MongoDBが苦手な部分はあえて他のツールを使うようにしてください。繰り返しますが、MongoDBは多機能ですが高性能では無いです、様々なトレードオフがあります。

最後に問題定義をして頂いた@氏、どうもありがとうございました。いつしか毎月開催しているMongoDB勉強会で発表されるのを心待ちにしています。

参考資料

文章中で記載した図は次の3つのスライドからの抜粋になります。



↑Shardingを仕組みから理解したい方、Sharding環境で起こりうる問題について知っておきたい方へ↑



↑MongoDBの持つ機能について、仕組みから詳しく理解したい方へ↑



↑Manual ShardingやSlave MapReduceを駆使して解析したい方へ↑

MongoTokyo:10gen エンジニア講演時に行われたQ & A メモ

03月01日(火)に開催されたMongoDB Conference (通称 #mongotokyo) は盛会の内に終了することができました。詳細なレポートは後日アップしていきます。今回はカンファレンスにおいて10genの方々の発表時の質問タイムに議論された内容についてのメモを公開します。
このメモは#mongotokyoに通訳スタッフとして参加していただいた @ さんからいただいたものを少し修正したものです。本当にありがとうございます。あくまでメモですので、文章としてきちんと書いていませんので、そこはご了承下さい。

Q) Complex transactionsは実現しますか?

普通は1つのドキュメントに対するtransactionだけサポートしています。
リレーショナルデーターベースのような複数のドキュメントに対するtransactionは
現在のところサポートしていません。ただし将来的には複数ドキュメントtransactionを
サポートする可能性はあります。

Q) 4MB/16MB以下のバイナリーデータはDBに入れても良いでしょうか?

DBにJPGアバターを入れたりする人はいます。
v1.8前のドキュメント当たりの最大サイズは4MBですが、v1.8後は16MBに拡張されます。
しかしまだv1.8に対応してないドライバーは依然として4MBの制限が残ります。
また、インデックスを作成していれば重いバイナリーデータであっても検索時間に影響がありません。

Q) 現在MongoDBを利用しているサービスの中で最大の運用規模はどれくらいでしょうか?

数TBと数Nodeのデータ規模での運用があります。ただし、数千台のサーバーによる
運用事例はあまりありません。MongoDBはCPUをあまり使わないので。
普通の4コアで20,000-40,000 transactions/sec、70% read, 30% write
なので1日に数億オペレーションは可能です。

Q) ロックについて。どのようなコマンドに対してMongoDBはブロックされますか?

現在グローバルロックはあります。DBは編集時にロックされます。
しかし編集における時間は早いので、10−15%の時間しかロックされていません。
MongoStatのツールはこれを見るために役に立ちます。
今はデータを読むコマンドをロックしないように、グローバルロックをなくすように
MongoDBをアップデートしてます。いつかグローバルロックをないようにしたいです。

Q) リレーショナルデーターベースの世界から来るとジョインなしの世界に慣れるのは難しい。ドキュメントIDでジョインみたいなリンクは可能ですが、リンクはプログラミング言語で手動で書かないといけません。将来にこれを自動でできるようになりますか?またlazy loadingとかも実現しないでしょうか?

そうするつもりはありません。今のドライバはそれをサポートしていません。けれどもドライバ上のマッパーがあります。例えばRubyではMongoidとMongoMapperがあり、オブジェクト指向インターフェースを備えています。

Q) Indices。いつインデックスを作れますか?リアルタイムで?production serverで?

2つのオプションがあります。DBをオフラインにして、ブロックするリアルタイムインデックス構築と、オンラインのままで、低いpriorityでのインデックス構築。後者はそれなりに時間がかかります。比較として、インデックスを作る時間はリレーショナルデーターベースと同じぐらいです。しかしNoSQLのDBは大体非常に大きいので時間が結構かかります。

解析者として僕が大事にしていること

あけましておめでとうございます。@です。今回は技術的な内容ではなく、フロントの解析者・アナリストとして僕が大事にしていること・日々感じていることを書きたいと思います。
このエントリーのきっかけは、最近多くの方から以前の10月に書いたエントリー「解析者の立ち位置」について僕が思うこと。に対して多くの共感のコメントを頂いた事です。この事で僕は今年も解析者として変わらぬ信念を持って、今いっそうの努力を続けていけばよいのだ、やるしかないという決意をもつことができました。コメントを寄せて頂いた皆さん、どうもありがとうございました。

解析者として僕が大事にしていること

ここ数年においては、データが大量に蓄積されてきており、それを解析・マイニングするデータ解析者の重要性が理解されるようになってきているように感じています。それは解析者にとって非常に喜ばしいことでもあると同時に、大きなプレッシャーにもなっているような気がします。

そのプレッシャーの1つに、企画者や経営者・あるいは顧客といった結果を活用する人々(=意志決定者)の「これだけ材料(データ)が揃っているのだから多くの課題が解決できるはずだ」という期待に応えないといけないというプレッシャーがあると思います。しかし正直なところ、いくら大量のデータが揃っていたとしても経営が一気に改善できるようなエクセレントな結果をすぐにもたらすのは非常に難しいと思っています。逆にデータ量が増えて様々な要因が複雑に絡みあって何も見えてこない状況に陥ることもあります。

もちろん僕たち解析者の使命は「データから経営クリティカルな結果をもたらし続けること」であるのでこの困難さから目を背けるわけにはいきません。今日はそんな状況の中で解析者は日々どういう立ち位置で、どういう結果を出していけば良いのかについて僕が大切にしている事を4つお話したいと思います。

  1. 「当たり前の結果」をたくさん出す事の大切さ
  2. 誰のために・何のために解析をやるのかを意識する大切さ
  3. 1人で全部背負わない事の大切さ
  4. 自分の存在価値を高めるための活動の大切さ

1. 「当たり前の結果」をたくさん出す事の大切さ

僕は解析において「当たり前の事をきっちりやる・当たり前の結果をたくさん出す」事は非常に重要な事だと感じています。ただし解析者という専門的な立場である以上、「何も有意な結果が出てこなかった」「当たり前の結果しか出てこなかった」というわけにはいきません。中には常に「何か新しい発見をしないといけない」という焦燥感にとらわれながら仕事をしている人もいるかもしれません。

しかし僕は「意味のある・新しい発見」をもたらすためには「当たり前の結果」を積み重ねることが大前提で、その事をもっと意識して解析をすることがとても重要だと考えています。

その当たり前は本当に当たり前なのか

そもそも「当たり前の結果」というのは誰にとって当たり前なのでしょうか?僕たち解析者の考える当たり前と、意志決定者の考える当たり前は本当に同じなのでしょうか?僕の経験から思うことは「解析者の考える当たり前の半分は意志決定者にとって当たり前ではなく、またその逆もしかり」という事です。

当たり前と思っていた結果を渋々見せると「へぇ、意外に××なんだ、面白い」という反応が返ってきたり、逆に新しい結果が出たと思って見せると「やっぱり」「そりゃそうだよ」といった反応が返ってくることは実は良くあります。つまり解析者がたいした事の無いと思っている結果が実は意志決定者にはとても貴重な情報となる事が多数あるということです。しかし実際は解析者はそういった結果を出す事に注目を置いていないために、多くの意志決定者にとって重要な結果があまりもたされないといった現状に陥ってしまいがちです。

だから僕は「当たり前の結果をたくさん出していこう」という意識を強く持つことはとても重要なことだと思っています。具体的にどうするかというと、できるだけたくさんのデータを出してあげること、しかもわかりやすい表やグラフの形でたくさん出してあげることだと思います。そして何度もその共有を意志決定者と繰り返す。その共有を繰り返すことで相手が何を求めているのか、どういう結果に意味のあるのかがだんだん見えてくると思います。それこそが意味のある有意な結果をもたらす第一歩になると思います。

本当に当たり前の結果を与えることも実は大切

しかし、本当に当たり前の結果、意志決定者が「やっぱりそうか」という結果には意味が無いといえば、決してそんな事はありません。意志決定者が頭の中で感じている事を数値やグラフで裏付けてやるという事はとてもとても意味のあることです。自分の考えている事の正しさがきっちりと検証される事によって得られる安心感と自信は想像以上に大きなものです、そしてそれが次の意志決定の確信度をどんどん高めていきます。相手に安心感を与えてあげるのも僕たちの大きな仕事だと感じています。

「当たり前の繰り返し」が新しい発見を生み出す

「意味のある結果を出さないといけない」という事に執着しすぎに、もっと当たり前の事: " たくさんデータを提示してたくさん表作ってグラフ描く、そしてそれを何度も相手と共有する " 、そういったことをきっちりとこなすことは解析者にとって非常に重要な仕事だと思います。難しいことをやろうとして頭を悩まし、手を止めるよりは、どんどんデータをわかりやすい形で提示して行く方が有意義だと思いませんか?そして結局のところ、新しい発見や経営クリティカルな結果というのはそういった事の繰り返しの上に生まれてくるものでは無いでしょうか?

2. 誰のために・何のために解析をやるのかを意識する大切さ

これは以前のエントリーの中心部分になるのでかぶるのですが、そもそも誰の・何のために解析をするのかという意識を常に明確にしておくことはとても重要だと思っています。

人間の意志決定を支援するために解析をすること・解析結果自身が意志決定を行うのではないこと

解析者は「人間の意志決定を支援するための結果をもたらすことに注力する」という意識を持つ、そのために「難しい事はしない、いかにわかりやすく・簡単に結果を人に提示できるかを意識する」事はとても重要だと思っています。

逆に解析結果自身が意志決定を行う場合というのは、例えば「リコメンドエンジン」や「需要予測システム」を作る場合でしょうか。機械学習や統計モデルといった高度な手法を駆使して、データから機械的に最適な解をもたらし、それを元に自動的にサービスに適用していく。その人の過去の行動から自動的に趣向を理解して適切な商品を推薦する、過去の大量の販売履歴からどれくらいの需要があるのかを数値で提示するといったものです。こういった瞬時に結果を出さないといけない状況や人間の手に負えない状況においては非常に重要です。

しかしそういった目的では無い場合、単にデータを解析する場合においては、解析を「人間の意志決定を支援する」ために注力して行う事が重要だと思います。

そのためには難しい解析手法を用いる必要はありません。例え複雑なモデル式を作ったり、 " 課金額687.3円〜3456.7円・プレイ期間3.4日〜8.9日のユーザークラスタの分布 " といった微妙な境界をもったクラスタリングをしても、それを知った所で人間がどういう意志決定ができるでしょうか?逆に多少誤差があっても、クラスタに偏りがあっても、できるだけ線型モデルや " 課金額1000円〜5000円・プレイ期間1日〜7日のユーザーの分布・傾向 " といったわかりやすい結果で提示してあげることの方が相手にとって意味のあることのように思います。そしてそれをちゃんと可視化してあげて、どれくらい誤差があるのか分布に偏りがあるのかも含めて伝えることも重要です。

解析手法に対するこだわりを捨てること

今まで大学の研究などで非常に高度で複雑な解析手法を用いてすごい事をやってきたという人も多いと思います。機械学習によって自動的に意志決定が行われるということに非常に喜びと価値を感じている人も多いと思います。あるいはそういう高度な知識をアピールする事で周囲に自分の業務の理解や専門性を訴えたくなるとも思います。

そういった人達はどうしてもそういった高度な手法を使いたくなると思いますし、その気持ちはとてもとても理解できます。しかし新しい事をしてジャーナルに掲載されるための研究や、リコメンドや予測といったデータ解析とは違った目的で使用される機械学習、自分の評価のために無駄に複雑なことに時間とお金を費やす行動は、データ解析における目的とは全く異なるものです。

こだわりを捨て明確な目的意識をもって、人間の意志決定を支援するための結果を出す・そのためにどうしたら良いか・どう分かりやすく伝えたらよいか、を必死で考えていくことの方が非常に重要な事のように感じます。

※(追記)もちろん、会社の規模やフェーズなどによっては高度な解析手法や基盤技術を生み出す事もとても重要な解析者の仕事だと思います。

3. 1人で全部背負わない事の大切さ

解析者がデータを集めてきて解析して結果を出す、その事を全て1人で背負う事は非常にしんどいことであり、あまり良くないことだと感じています。初めは僕も「全てを自分でやってどんどん良い結果を提示していかないといけない」と意気込んでいましたが、その困難さに気付くようになりました。

日々増え続け・変わり続けるサービスの全てを把握するのは困難

解析者は自社・あるいは顧客先のたくさんのサービスの解析を一手に引き受けなくてはなりません。そしてWebサービスにおいては非常に早いペースでサービスがリリースされ・絶えず変更されていきます。サービスの全ての内容・特徴・構造、またはログ周りに関するプログラム・データベースのテーブル構造を全てのサービスで把握して解析を行うことは実質不可能です。それにも関わらず全部1人で背負うのは心身共にしんどい事です。

最も優秀な解析者になり得るのは実はサービスを作った人である

これも経験から感じている事ですが、いくら解析の専門技術を持っていたとしても、実際にサービスを作った人の持つ仮説検証能力やデータの読み取り能力には敵わないことが多々あります。そこで「サービスを作った人にも解析に協力してもらう」、「彼らにはない、より科学的な見地で解析を行う」ことが負担を減らす意味でも、良い解析をする意味でも非常に重要なことだと思います。

エンジニアにももっと協力してもらう

一方で解析の元となるログを出してくれるのはインフラエンジニア・アプリケーションエンジニアの人達です。ログのレコードの意味がわからない事や、取得して欲しい項目がある事は日常良くあることです。また、データベースのどのテーブルにどういう形でデータが記録されているのかも全て把握するのは困難です。そしてこれらを自分で頑張って追っていくのは時間の無駄です。そいういう部分は割り切ってどんどんエンジニアに聞く方がよいと思っています。

相手の手をわずらわせないために、どういうデータが見たいのか、あるいはどういう目的で新たな項目を記録して欲しいのかをわかりやすく伝える努力はとても重要です。

4. 自分の存在価値を高めるための活動の大切さ

解析者という業種は環境に依存するところが大きいです。大きなサービスがあるからそこに大きなデータがあり・解析需要が生まれてきます。また、IT産業が拡大し続けているからこそ「データ解析してみよう」という余裕が生まれ、開発者や企画者(=サービスを生む人)以外に解析する人を雇用しようとします。それは現実として会社が傾けばまず解析者の存在が危うくなり、IT産業自身が傾けば市場価値は下がっていく傾向をもたしています。

しかし本当にそれで良いのでしょうか?逆に経営がピンチな時ほど、産業が衰退していく時こそきっちりとデータに基づいた戦略・下支えが必要では無いのでしょうか?悪い原因を一つ一つ地道に突き止めて行き改善していく、そういった事こそが何より重要だと解析者の皆さん自身が一番感じているのでは無いでしょうか?

勉強会など、社外に向けて発信していくことの重要性

だからこそ自分の存在価値を認めてもらうための活動はとても重要だと思います。それを通じて良き理解者や同業種の仲間を増やして行きます。

社内においては結果を出すことに併せて、エンジニアや企画者・経営者と積極的にコミュニケーションをとって解析の良き理解者になってもらうことでしょうか。そして社外では勉強会などで積極的に発表し、解析ノウハウを共有し技術向上に努め、社外の多くの同業種の人と交流を深め、また自身のスキルを広く理解してもらうことではないでしょうか。

特に社外に向けての情報発信はとても重要だと思います。それは業界やコミュニティ発展に大きく寄与することになりますし、今後より良い環境で仕事をしていくための重要な足場作りになります。

2011年は解析者にとって良くも悪くもチャンスの年

2011年はそういった意味で良くも悪くも解析者という存在を認めてもらうためのチャンスの年であると感じています。良いことはHadoop等の大規模分散処理技術やNoSQLの普及によってそれまでお手挙げだった、大規模のデータでも解析することが可能になってきて、それと共にデータ解析の重要性が一般に認知されるようになったことです。そこでデータに基づく戦略を数々打ち出す事ができれば、よりいっそう僕たちの存在価値は高まってくるはずです。

一方で悪いことは今のようなIT好景気がそれ程長く続かない事です。特にソーシャルゲームプロバイダのベンチャーにおいては去年以上の収益をもたらす事は難しく、かつ携帯電話上でのゲームからスマートフォン上でのアプリやその他のサービスへの転換という、大きな舵取りを迫られるはずです。そしてその向かう先は意志決定者にとっては不安だらけです。そういった時に僕たち解析者が彼らの意志決定に自身を持たせ、次にヒットするサービスの大きな下支えになれるかどうかで存在価値は変わってくると思います。

最後に

データ解析というとBtoB向けのアクセスログ解析という印象が強いと思いますが、WebサービスのようなBtoCでもソーシャルデータ特有の解析によって自社サービスを改善していく事での解析者の役割が非常に重要になってきています。GREEには当初からあらゆるデータをきちんとって、それを活かして行こうという気概を持った人とそれを認める文化があったからこそ今のような大規模なサービスを展開できており(えらそうな事をいってごめんなさい)、DeNAも同じく最近では @ さんが解析専門部隊を立ち上げて精力的に活動しています。BtoBで解析をやって来られた方々、解析に興味のある学生の方々、機会があれば是非ともこちらの業界に飛び込んできてみてはどうでしょうか?きっととても楽しいはずです。

参考リンク:
会社の成長を支えるデータ解析/GREE
2100万会員モバゲータウンはデータマイニングの宝の山
楽天技術研究所におけるウェブマイニング

今年も解析者として、普段の仕事もコミュニティ活動も、そして解析者の存在価値を高めていけるような活動も積極的に行っていきたいと思います。そしてたくさんの人に出会あい、刺激をもらうことをとても楽しみにしています。改めて本年も宜しくお願いします。

「第1回 MongoDB JP & CouchDB JP 合同勉強会 in Tokyo」を開催してきました!

こんにちは、@です。本日は12月12日(日)行われました「第1回 MongoDB JP & CouchDB JP 合同勉強会 in Tokyo」の報告をします。当日は忙しい時期にも関わらずたくさんの人に参加していただきました。会場準備で色々を手間取ってしまいましたが、会場提供者のニフティさんと参加者の皆様の暖かいご協力もあって無事に勉強会を終えることができました。どうもありがとうございました!また、当日はgihyo.jpさんの協力のもと、Ustream配信も行いました。このUstream動画と共に勉強会報告の方をgihyo.jpさんの方にも書かせていただく予定ですのでぜひともそちらの方も宜しくお願いします。今回の勉強会のツイートがまとめられたTogetter:第1回 MongoDB JP & CouchDB JP 合同勉強会 in Tokyoもなかなかが臨場感があって面白いですね。
当日の発表内容はこちらになります:

  • 「MongoDB_JP 今後の活動計画」:@
  • 「MongoDBを用いたソーシャルアプリのログ解析 〜解析基盤構築からフロントUIまで、MongoDBを最大限に活用する〜」: @
  • 「Ameba PicoとMongoDB」:@
  • 「Big Couch & CouchDBからのお知らせ」:@
  • 「PostgresSQLからMongoDBへの以降 -アシストオン通販サイトでの実際-」: @
  • 「センサーデータストアとしてのCouchDB 〜そうだ、Couchに入れよう。〜」:@
  • 「MongoDBと位置情報 〜地理空間インデックスの紹介〜」:@
  • CouchDB on Androidでスタンドアローンアプリ。」:@

MongoDB_JP 今後の活動計画

僕の方からは前説としてMongoDB JPの今後の活動計画についてお話しさせて頂きました。ここでの本題は、直近の大きな目標として2011年3月に「MongoDB Conference in Japan」を開催したいという宣言です。(数百名規模を収容する)会場提供者・発表者・スポンサー・協力者を広く募っております。どうぞ宜しくお願いします。

MongoDBを用いたソーシャルアプリのログ解析 〜解析基盤構築からフロントUIまで、MongoDBを最大限に活用する〜

今回も相変わらずログ解析におけるMongoDBの活用事例をお話しさせて頂きました。3ヶ月以上MongoDBを解析データサーバーとして使ってきて改めて感じるのは、検索クエリの豊富さ、コンソールの扱いやすさ、node.jsやRESTといった機能によるデータの取り出しやすさに非常に助けられているということです。解析者に優しいデータベース、それがMongoDBです。また最近データ量が増えてHadoopとの連携がより重要になってきています。MongoDBの開発元である10genもHadoopとの連携を積極的に進めているようですので今後の動向に注目しつつ、それらの機能は積極的に検証していきたいと思います。

Ameba PicoとMongoDB

@さんからは、累計登録ユーザー数 3,000,000、MAU 560,00を誇る大人気サービスAmeba Pico(アメーバピグの海外版)のMongoDBでの運用について、その具体的な方法と注意点などについてお話していただきました。日本でのMongoDBでの運用事例は非常に稀少ですし、ましてやShardingやReplicationをいったスケーラビリティを考慮しないといけない大規模についての事例はとても貴重です。AmazonEC2での具体的なサーバー構成をお話ししていただいた後、遭遇した様々な問題点とその対象方法を具体的に述べていただきました。例えばShardingが偏ってしまう問題はfoursquareでも発生していて、それにどう対処するのかなどは非常に参考になりました。さらにサーバー負荷のモニタリング方法やJava Asynchronou Driverの開発など、ハイレベルなMongoDBの活用事例に見習うところはたくさんありました。本当にありがとうございました!

Big Couch & CouchDBからのお知らせ

@さんからは CouchDBCouchDB JPの2010年の活動を振り返えったあと、Big Couchについてお話ししていただきました。CouchDB JPの皆様は非常に精力的な活動を続けられていますので、MongoDB JPの方も負けじと頑張っていきたいですね。
CouchDBとは何か、Big Couchとは何かの紹介を面白くわかりやすく紹介していただいたあと、Big Couchの仕組みと動かし方を詳細に発表していただきました。特に実際の動かし方の手順をわかりやすく述べて頂いたあと、様々な実験検証を行い、q: シャード数・n: 複製するシャード数・w: 書き込み保証数・r: 読み込み保証数のトレードオフと、それによる運用の難しさをお話ししていただきました。素晴らしい内容でした。ありがとうございました。また、@さんはnode.jsに関して色々と活動をされているみたいなので、そちらの方にも注目・期待しております。

PostgresSQLからMongoDBへの移行 -アシストオン通販サイトでの実際-

@さんからは、構造がしっかりしていないといけないECサイトながら、様々なオーナーの要望に応えないといけないという状況の中でその双方の制約を叶えるのがMongoDB、というステキなイントダクションの後に、原宿の人気セレクトショップのECサイトアシストオン通販サイトを事例にしたMongoDBを使った環境への移行についてお話ししていただきました。元々の構成(PostgresSQL + PHP)から新しい構成(MongoDB + Lithium)への移行について、LithiumでどうMongoDBからデータを取得するのか、その時にどういった所に注意する必要があるのか、その方法と注意点が非常に具体的で非常に参考になりました。さすが@さん、非常に話し上手でわかりやすい、最後まで魅力的な発表でした。ありがとうございました。

センサーデータストアとしてのCouchDB 〜そうだ、Couchに入れよう。〜

@さんからは、CouchAppを使ってとにかくあらゆるデータをCouchDBに入れていこうという話。なんとそのデータの対象が家の中のセンサーデータという斬新な題材であったので、会場は大いに盛り上がりました。「Arduino + ethernet shield + センサー」をCouchDBでモニタリングすれば数千円でオール電化が実現しますとのこと(笑)。SDカードにCouchDBを入れて、ACアダプタサイズのサーバに挿入すれば簡単にホームサーバーのできあがり。構築なんて必要無い、Relax。CouchDBの良さを活かした面白いプレゼンでした。ありがとうございました。sensmonは商標登録した方がいいですね。

MongoDBと位置情報 〜地理空間インデックスの紹介〜

@さんからはMongoDBの特徴的な機能でありながら実はあまり知られていない(かもしれない)地理空間インデックスについて、丁寧にお話ししていただけました。2次元の地理空間情報インデックスして検索が行える機能がver.1.3.3以上で使えるようになりました。Indexの作成の仕方から$near、$maxDistanceなどのクエリ使った検索の仕方について、具体例とともに紹介していただきました。しかし「地球は丸かった」(笑)ということで平面上での近傍検索しか行えない先ほどのクエリに対して、$Sphere という球体情報を考慮した画期的なクエリ(ver.1.7)についてお話し頂きました。非常にエキサイティングで面白かったです。ありがとうございました。

CouchDB on Androidでスタンドアローンアプリ。

@さんからは最後の締めを飾るにふさわしく、まさに漢の家計簿管理方法:CouchDBJQueryとCouchAppを用いた家計簿管理アプリ」についてデモともにわかりやすくご紹介いただきました。ポイントはJavaScriptのみで書かれたスタンドアローンアプリであることで、それだけで簡単に実務に役立つアプリが作れてしまうということです。CouchDBの面白いところですね。最後にCouchDBjquery-mobileで使用する際の注意点などをお話ししていただきました。CouchDBとモバイル端末との連携はアツいですね、今後に期待です。ありがとうございました。

最後に

ユーザーグループの主催、勉強会の主催というのを今回初めて経験させてもらっていますが、非常に楽しく刺激的でもあり、一方で色々と大変だなぁというのが正直な印象です。しかしそこに非常にたくさんの方々の協力支援があって、着々と良い方向に向かって前進していることを確信しています。これからも皆様のお力を借りながら、これらの活動がもっとたくさんの人達を巻き込んで、ハッピーにしていけたらなぁと思っています。いつも助けていただいている皆さん、本当にありがとうございます。今後ともどうぞ宜しくお願いします。