42
The case file of Mobage Open Platform Mobage オープンプラットフォームの 事件簿 Toru Yamaguchi <[email protected] > http://d.hatena.ne.jp/ZIGOROu/ DeNA co.,ltd. 2011/10/14 YAPC Asia 2011

Yapc asia 2011_zigorou

Embed Size (px)

DESCRIPTION

Trouble history of Mobage Platform

Citation preview

Page 1: Yapc asia 2011_zigorou

The case file of Mobage Open Platform

Mobage オープンプラットフォームの 事件簿

Toru Yamaguchi <[email protected]> http://d.hatena.ne.jp/ZIGOROu/

DeNA co.,ltd. 2011/10/14

YAPC Asia 2011

Page 2: Yapc asia 2011_zigorou

今日のテーマ

!   未だかつて無い生々しい失敗を限界ギリギリまで語ります !   そのとき何が起きて、どう考えてどう対応したか

!   失敗の積み重ねによってどう進化してきたか

!   ちなみに我々のチームでは毎週金曜の最後のミーティングは、チーム内での障害報告会で、起きた障害と一次対応、恒久対応策の検討、恒久対応の実施状況の確認をやってます。 !   これらのやりとりで思い出深い事件を幾つかピックアップして紹介します。

Page 3: Yapc asia 2011_zigorou

失敗学

!   Wikipedia より !   失敗学(しっぱいがく)とは、起こってしまった失敗に対し、責任追及のみに終始せず、(物理的・個人的な)直接原因と(背景的・組織的な)根幹原因を究明する学問のこと。

!   概要 !   失敗に学び、同じ愚を繰り返さないようにするにはどうしたら良いかを学ぶ学問のこと

!   原因究明 (Cause Analysis)

!   失敗防止 (Failure Prevention)

!   知識配布 (Knowledge Distribution)

Page 4: Yapc asia 2011_zigorou

間違いと失敗は 我々が前進する為の

訓練である

ウィリアム・チャニング (アメリカの神学者・牧師)

Page 5: Yapc asia 2011_zigorou

事件簿1 DeadLock 多発事件

!   ある API (T から始まる奴) にてある時期から突然 Dead Lock が多発するようになった !   その頃から、この API を多用するアプリケーションが増えており、書き込み頻度が高く、まれに Internal Server Error を返すほど性能が劣化していた。

!   書き込み処理におけるトランザクションが比較的長いという事は認識していた。

!   完全に無視出来ない状況へ !   Internal Server Error が出る頻度が露骨になってきた

Page 6: Yapc asia 2011_zigorou

原因究明 (1)

!   そもそもこの API は GET 時の挙動として、その検索条件で全体で何件のレコードがあるかを返していた ! totalResults というフィールドで OpenSearch 由来の概念

!   かつてはこの totalResults の計算に SQL_CALC_FOUND_ROWS を使っていた !   が、このキーワードをつけた SELECT 文は重たい><

!   従って、絞り込みの最も効かない検索条件の際は事前に totalResults のサマリデータを更新時に作っておくようにした。 !   INSERT, DELETE 時の TRIGGER として実装

Page 7: Yapc asia 2011_zigorou

原因究明 (2)

!   当たり前だが SHOW INNODB STATUS をする !   Dead Lock に関するレポートを読んで当たりをつける

!   待ってる方と待たせてる方の両方のレポートがあるので、それらを良く読めば大体分かります。

!   原因究明 !   どうやら特定の UPDATE 文が獲得しようとする特定の行ロックが複数待たされているという事が分かった。 !   どうやらトリガーの中でやっているサマリ生成の為の UPDATE 文が犯人だった

!   改めてトランザクション中のクエリを再点検する事に

Page 8: Yapc asia 2011_zigorou

トランザクション

BEGIN;

SELECT id FROM somedata_group WHERE name = ?;

INSERT INTO somedata(id, group_id, data, published_on) VALUES(?, ?, ?, ?);

UPDATE somedata_summary SET total_results = total_results + 1, updated_on = ? WHERE group_id = ?;

COMMIT;

Page 9: Yapc asia 2011_zigorou

原因究明 (3)

!   Group という概念について !   特定のデータ群をカテゴライズする為の概念

!   このカテゴライズの粒度がアプリケーションごとに増減する概念だった

!   アプリケーションに依存するという事から分かる事 !   仮にモンスターアプリケーションが現れて、このアプリケーションが過激にデータを生成した場合は、原因となった UPDATE 文が特定の行を increment する為に同じ行ロックを獲得しようとする

!   つまり、トランザクションがなかなか完了せず結果的に MySQL では Dead Lock 扱いされてしまう

Page 10: Yapc asia 2011_zigorou

原因究明 (4)

group_id = 1

group_id = 2

group_id = 3

group_id = 4

group_id = 5

Transaction Transaction

Transaction

Transaction

group_id = 3 に対しての、 UPDATE 文が完了しないと後続のトランザクションが処理出来ない

Page 11: Yapc asia 2011_zigorou

失敗防止 (1)

!   どのようにすれば解決するか !   それは簡単でこの UPDATE 文を打たなければいいw

!   しかしサマリデータを生成しないと SELECT の度に totalResults を計算せねばならず、これはこれで受け入れがたい

!   INSERT ならばロックされないので、incr/decr を QUEUE として扱い、ある一定量でまとめて UPDATE 打てば良い

!   非同期処理にしてしまい、totalResults の結果は多少のずれがある状態で妥協

!   という訳で一通り設計して実装は xaicron さん。

Page 12: Yapc asia 2011_zigorou

失敗防止 (2)

/* API のトランザクション */

BEGIN;

INSERT INTO somedata(id, group_id, data, published_on) VALUES(?, ?, ?, ?);

INSERT INTO somedata_summary_queue(id, group_id, affected_number, published_on);

COMMIT;

Page 13: Yapc asia 2011_zigorou

失敗防止 (3)

/* Batch Worker の処理 */

BEGIN;

SELECT id, group_id, affected_number FROM somegroup_summary_queue ORDER BY published_on ASC LIMIT 100;

/* group ごとにプログラム側でまとめて一気にUPDATE */

UPDATE somedata_summary SET total_results = ?, updated_on = ? WHERE group_id = ?;

DELETE somegroup_summary_queue WHERE id IN (?, ?, …, ?);

COMMIT;

Page 14: Yapc asia 2011_zigorou

知識配布 (1)

!   事故に至った理由を改めて考えてみる !   特定の行に対するロックが集中していないうちはまったく問題はなかった

!   しかし、アプリケーションに対する人気に偏りが出始め、常規を逸する偏りとなった為、特定行へのロックが多発した

!   今回は InnoDB を使った QUEUE を用いて、なるべく遅延無く最小の手数(クエリ実行回数)で UPDATE する事で回避は出来たが、そもそもモデリングやデータ分割の設計時点でこの障害は運命づけられていた。 !   一般ユーザーに紐づくデータの偏りに比べてアプリケーションは多くのユーザーが使う為、ぶれ幅が大き過ぎる

Page 15: Yapc asia 2011_zigorou

知識配布 (2)

!   ついでに余談 ! InnoDB を使って QUEUE を実現する場合は原則 INDEX は張らない。 !   INSERT, DELETE のコストが高くついて、enqueue の時に詰まったりする

!   まとめて複数行の queue を取る際に、この処理を並列化するには、WHERE 句に id の剰余などを条件にしてあげれば良い !   検索条件が他のプロセスとかち合わない単純追記型なんであれば、

FOR UPDATE する必要もない。

!   この辺りの話は設計時には分からなかった事で、プロダクションに投入して初めて分かった事。

Page 16: Yapc asia 2011_zigorou

成功するには、 成功するまで

決して諦めない事だ。

アンドリュー・カーネギー 米国の実業家

Page 17: Yapc asia 2011_zigorou

事件簿2 INSERT vs DELETE

!   この事件は未だに完全に解決していない !   しかし、対症療法的な手法はある程度確立されている

!   ある API (例によって T から始まる奴) である !   事件簿1の時期から INSERT がもの凄い量になってきていた !   この API はあるルールに応じてプラットフォーム側でデータを

purge して良いという取り決めになっていた !   ところがある時期から全力で purge 処理(要するに物理削除)を行っても INSERT の量に押し負ける事となった

!   最終的に !   ある shard の系統が1週間ほど利用不能になる所まで追い込まれた><

Page 18: Yapc asia 2011_zigorou

こんな感じ

(注) 画像は実際のデータではございません あくまでイメージです

Page 19: Yapc asia 2011_zigorou

こんな感じ

Page 20: Yapc asia 2011_zigorou

原因究明 (1)

!   まずは、purge の処理の問題点 !   全力で MASTER に DELETE 文を打つと SLAVE が遅延するww

!   DELETE はもの凄い遅い! !   そのためシングルスレッドで redo ログを実行する SLAVE では遅延の原因になる

!   そのため一定周期で sleep による wait を入れる事になるが、DB の負荷状況に関わらず一律で最も安全な秒数で wait を掛ける事になる。 !   これは夜間などだと無駄な wait になる

!   遅延を考慮して wait したい !   SLAVE 側での Seconds_Behind_Master の値を参考に wait したい

!   遅れてたら遅れてる分に応じて wait したい

Page 21: Yapc asia 2011_zigorou

Loop::Sustainable (1)

!   という訳で作ってみました

! https://github.com/zigorou/p5-loop-sustainable

!   何が出来るのか !   ループ処理をコールバックで記述 !   ループ処理ごとの戻り値に応じて処理を終了出来る !   ループ処理に対して指定回数毎に戦略に応じて、負荷状況を勘案して適切な wait を入れてくれる !   各ループの実行時間の累計からだったり

!   各ループによって Seconds_Behind_Master がどれだけ経過したかだったり

!   上記の戦略はぷらっがb(ry

Page 22: Yapc asia 2011_zigorou

Loop::Sustainable (2)

process

process

process

process

process

Seconds_Behind_Master : 10 sec

process

process

process

process

process

2sec

2sec

2sec

2sec

2sec

Page 23: Yapc asia 2011_zigorou

原因究明 (2)

!   でも結局使いませんでしたw

!   SET SESSION sql_log_bin = 0 とすればバイナリログに書き込まなくなる !   MASTER にも SLAVE にもこうして直接同じ DELETE 文を発行すればレプリケーションの遅延自体は気にしなくて良い

!   この時点で DELETE 文によるレプリケーションの遅延問題は技術的には大体クリアした! !   結局、一系統に属するデータベースのインスタンス数分、prefork して同じ条件の DELETE を打っては待ち、打っては待ちというプログラムを xaicron さんが書いて、これから試す所 (現在進行形)

Page 24: Yapc asia 2011_zigorou

原因究明 (3)

!   もっと発想を豊かにしよう

!   DELETE は遅い

!   DELETE を使わないで purge 出来ないか? !   ただし partitioning は API の仕様上適用しづらい

!   STOP SLAVE した DB から必要なデータだけ抽出して新しいMaster 候補にしてしまって、レプリケーションを追いつかせて差し替えちゃえばいいんじゃね? !   「おかわり作戦」と命名

!   現在鋭意対応準備中

Page 25: Yapc asia 2011_zigorou

失敗防止 (1)

!   まず出来る限り DELETE を速くする努力をする !   事前にメモリに載っているデータの DELETE は若干早い。

!   DELETE 対象を直前に SELECT しておく

!   処理自体を MySQL にやらせてしまう !   ストアドプロシージャにしてしまう

!   クエリをネットワーク経由で発行しなくて良い分は速いが気休め程度

!   他の RDBMS でのストアドにあるメリットは余り無いです><

!   一方でデメリットも多々あるので迂闊に使うべきではない

!   SET SESSION sql_log_bin=0 にしてバイナリログに書かない !   さらに全系統に永続接続で同じDELETEを打ち続けるようにする

Page 26: Yapc asia 2011_zigorou

失敗防止 (2)

!   頑張って DELETE してデータサイズを保つのは、INSERT が多い系統ではほぼ夢物語 !   設計段階から Partitioning を前提としたシステムに出来るようにすべき

!   あるいは Sharding によりデータサイズが極力均等に分割出来るよう設計すべき

!   それでもダメなら消し込んで良いデータを除外して入れ替え ! mysqldump –w で WHERE 句指定出来る

Page 27: Yapc asia 2011_zigorou

知識配布 (1)

!   おかわり作戦の詳細について !   そもそもデータのフラグメンテーションにより、レコードやインデックスはシーケンシャルに出来ていない

!   おかわりする意義はここにある

!   PRIMARY KEY 以外のインデックスがない状態のテーブルを用意

!   そのテーブルにシーケンシャルにレコードを突っ込む

!   入れ終わったら一個ずつ ALTER TABLE some_table ADD KEY していく

!   この状態が最もデータサイズやインデックスサイズが小さくなる

!   と、M信さんが言ってました!

Page 28: Yapc asia 2011_zigorou

知識配布 (2)

!   おかわり時の注意 !   旧系統にはあって、新系統にはないデータが存在する

!   旧系統のマスターに対する DML の結果に応じて処理を行う場合、新系統のデータで不整合が起こる可能性がある !   先のデータの件数の incr/decr がまさにそれに該当する

!   従って、この辺りはそれぞれのインスタンスにあるデータごとに適切な処理結果になるよう、工夫する必要がある。 !   本来こうした処理こそストアドプロシージャが向いている

Page 29: Yapc asia 2011_zigorou

チャレンジして 失敗することを 恐れるよりも、

何もしないことを恐れろ

本田宗一郎

Page 30: Yapc asia 2011_zigorou

事件簿3 有名人問題

!   有名人や公式ユーザーというのが存在してます

!   これらのユーザーは数十万単位でお友達が存在します!

!   これらのユーザーのお友達のうち、有効なユーザーが全部で何人存在していて、そこから50件ずつユーザーを抽出する API や、Push 型の friend timeline はまさに地獄の様相を呈する問題 !   「友達」や「ユーザー」の情報は当然、別のDBインスタンスにデータが存在します><

!   かねてより、こうしたユーザーが API に対して何かすると、メモリ消費が一瞬跳ねたり、そもそもレスポンスを返しきれない事があった。

Page 31: Yapc asia 2011_zigorou

原因究明 (1)

!   例 !   友達の中で

!   特定のアプリケーションをインストールしていて

!   ユーザーの状態が正常なユーザー

!   そうしたユーザーが全体で何件居るかを返し、さらに count, startIndex (LIMIT/OFFSET みたいな概念) も実装する

!   これはどういう風になるか

Page 32: Yapc asia 2011_zigorou

クエリー

SELECT friend_user_id FROM friends WHERE user_id = ?;

/* 得られたユーザーに対して */

SELECT user_id FROM user_app WHERE app_id = ? AND user_id IN (?, ?, …, ?);

/* 最後にユーザー情報を得る */

SELECT SQL_CALC_FOUND_ROWS user_id, nickname FROM users WHERE user_id IN(?, ?, …, ?) LIMIT 50 OFFSET 0;

SELECT FOUND_ROWS();

Page 33: Yapc asia 2011_zigorou

イメージ

friend 1000 users

installed users 750 users

valid users 500 users

LIMIT 50 OFFSET 0

Page 34: Yapc asia 2011_zigorou

原因究明 (2)

!   friends は n:m, user_app は 1:n, users は 1:1 のテーブル

!   friends で得られたユーザー数が如何に多くとも、(例えば1000件ずつ)分割して user_app テーブルに問い合わせれば、現実的なスピードで、「友達の中で特定のアプリケーションをインストールしているユーザー」を抽出出来る

!   users に対しては SQL_CALC_FOUND_ROWS を用いて、全体数を取得しつつ、LIMIT, OFFSET で切り出しを行う

!   ただし、これは元の friends から得られる母集団が万単位だと単純なクエリでは実行出来なくなる>< !   なので、Temporary Table を作って、そこの INSERT して users と

JOIN すると言う荒技を使っていました。

Page 35: Yapc asia 2011_zigorou

失敗防止 (1)

!   ある検索条件のレコードが万単位の場合 ! selectall_arrayref とかいきなりやると、メモリに大量のデータが載ってしまう !   多分これがメモリが瞬間的に跳ねる原因

!   SQL_CALC_FOUND_ROWS は重たい !   LIMIT, OFFSET つけても、つけなかった場合の検索条件で COUNT(*) するはめになるので、対象となる全レコードをなめる事になる

!   止める方向で。

!   Temporary Table を作るコストが高い !   なんとか最小の手数に分割して実行出来ないか !   人数の確定は分割して実行出来る

!   また user_id の抽出はソート条件が user_id の昇降であるならば、プログラム側で LIMIT, OFFSET 相当の処理を行う事は可能

Page 36: Yapc asia 2011_zigorou

失敗防止 (2)

!   prepare, fetchall_arrayref($max_rows) を利用する !   DBI::st の fetchall_arrayref($max_rows) は高々 $max_rows 件のレコードごとにまとめて取得出来る

!   これを使うと限りなく一気に複数行のレコードをスムースに取得出来ます !   これを使って friends を例えば 1000 件ずつ取得していく

!   得られた 1000 件ずつの users を取得する毎に、user_app テーブルに随時問い合わせインストールしているユーザーのみに絞り込む !   1000件以下のデータが取得出来るはず

!   上記のデータをバッファリングし、1000件以上たまった段階で、やはりusers テーブルに対して妥当なユーザーのみ取得する !   これも高々1000件のレコードになります

!   これらの一連の処理を繰り返す

Page 37: Yapc asia 2011_zigorou

処理のイメージ

friend 1000 users

friend 1000 users

friend 1000 users

installed users 750 users

installed users 750 users

installed users 750 users

installed users 1000 users

installed users 1000 users

installed users 250 users

valid users 750 users

valid users 750 users

100 users

end iteration

Page 38: Yapc asia 2011_zigorou

失敗防止 (3)

!   要するに、前段の処理の積み重ねが指定件数溜まったら、それを一つの単位として列挙出来る仕組みが必要

!   という訳で作りました。

!   Iterator::GroupedRange ! http://search.cpan.org/dist/Iterator-GroupedRange/

!   基本的な仕組みとしては List::MoreUtils の natatime と同じだが、I::GR は処理 (fetchall_arrayref($max_rows) みたいな処理) を渡す事を想定している。

Page 39: Yapc asia 2011_zigorou

失敗防止 (4)

!   まぁ細かい事はさておき

!   friends もわざわざ DB に問い合わせ無いで1000件未満の場合は memcached に get/set 出来るようにしている

! user_app もただの mapping データなので memcached に突っ込んでる ( install していないという情報も突っ込んでる )

!   これらのデータは iteration 中、DB を見なければならない場合は即座にDBから得られたデータを iteration 中に set している

!   と一連の施策でめでたくワーストでも3秒以内にレスポンスが返せるようになりました!

!   ちなみに立案とプロトタイピングは自分で実装は xaicron さんです

Page 40: Yapc asia 2011_zigorou

知識配布 (1)

!   今回の処理は異なるデータベース間でのストリーミング処理みたいなもんです !   従ってストリームが途切れるまで各々のデータベース接続はつかみ続けてしまいます

!   ついでに言うと処理の流れが人間に理解しづらい !   こういう処理がネストするとクエリの実行順序をトレースするとストリーム酔いするw

!   ちなみにこの処理の元ネタは kazuho さんと呑んでる時に聞いた話をうろ覚えから脳内再生したのがきっかけです。

Page 41: Yapc asia 2011_zigorou

何よりもまず、 ものの本質をつかまなければいけない。

出来るだけ早くつかんで、 それを達成出来るよう努力する。

枝葉のことはあまり 目もくれないようにする。

本質に合わない間違った方向に 飛び込んでいくと、

なかなか立ち直れない。

岡崎嘉平太

Page 42: Yapc asia 2011_zigorou

まとめ

!   失敗から得られることは大きい !   今まで出会った事の無い現象や、盲点など様々な気付きに出会えます !   新しい発想とそれを形にしていく

!   スピンアウトして新しいモジュールになる

!   分業は大事 !   今回例に挙げた事件の原因の分析やらは自分がやったりしていて、実装は

xaicron さんがやっています。 !   きちんと問題点や設計を共有出来れば、実装力のある人に任せた方が早いし、安心!

!   失敗や問題の予兆に対しては常に対応策を考えていつでも実施出来るだけの準備をしておく !   即財に対応出来るのが望ましいが、それが出来ない場合でも見てみぬ振りをして先送りにせず、方法を確立しておいたり、頭でシミューレートしておく