朝晩冷えてきましたね。風邪など引いていませんでしょうか。さて、年末が近づいてくるこの時期に弊社のエンジニアが最も気になるのは、お正月。それも来年1月1日を迎えた瞬間です。

1日1日0時に何があるのでしょう?そう、mixiのサービスで最も日記が書き込まれるタイミングになるのです。個人的に「あけおめことよろアタック」と呼んでいます。今年は日記だけではなく、エコーでもメッセージが飛び交うことでしょう。この時期は携帯電話のキャリアでもさまざまな対策を行っていますが、ミクシィでも年末年始でもユーザの方に快適にサービス提供ができるように努めています

以下は昨年の年末年始の日記投稿数の推移です。青色が12/31から1/1、赤色が1/1から1/2になります

diary_posting.png

1/1の方が全体的に多いですが、特に年が変わる前後の投稿数は倍近くなっていることがわかります。この時に負荷により日記の投稿がしづらい状態になっていたので、もっと多くの日記が書かれていたと思われます。

日記の投稿でエラーが起きた原因は、日記のIDを生成するシステムの過負荷にあります。mixi日記では全体で1つの通し番号が付いていますが、このIDを生成するシステムに負荷が集中し、IDが取得できなくなり日記が書けないという状態が発生してしまっていました。

■ID Generatorの改善

そこで来年に向けて、まずはIDを生成するシステムの改善に手を付けました。
日記のID生成システム、「ID Generator」はMySQLのLAST_INSERT_ID()を利用して実装されており、以下のようなテーブルとスクリプトを用いています。

テーブル

create table idpot (
    id bigint unsigned
);

スクリプト

my $next_id;
my $rv  = $dbh->do('UPDATE id_pot SET id=LAST_INSERT_ID(id+1)');
if ($rv == 1) {
    $next_id = $dbh->{'mysql_insertid'};
}

この実装で通常のサービスで問題がでることはありませんが、正月明けのようなアクセスが特に集中する時には、この部分でMySQLがdead lockを引き起こしてしまいます。実は、この問題は既に去年の11月に当ブログで紹介させて頂いてます。dead lockはMySQLに接続するクライアント数が多くなると発生します。

mybenchを利用したテストでは、接続数が250を超えるとエラーが発生しテストが正常に終了しません。

clients count/client total_count fastest slowest average q/sec
100 10 1000 0.0001 0.085 0.019 19.33
150 10 1500 0.0001 0.13 0.047 71.54
200 10 2000 0.0005 0.17 0.10 206.96
250 10 2020 0.158 0.07 182.51

(MySQL 5.0でテスト。クライアント数250では正確な数値は取得できず)

2008年でもこの問題について把握はしていましたが、クライアント数についての見積もりがあまく、dead lockが予測よりも速く発生してしまったため、日記が書けない事態が発生しました。そこで今年はID Generator自体に手を加えました。

ID GeneratorはMySQLへの接続本数を減らすことで改善を目指しました。具体的にはアプリケーションサーバから直接MySQLに接続するのではなく、専用のAPIサーバを置いて間接的に接続するようにしました。APIのサーバはmod_perlのhandlerとして構築をし、ApacheのMaxClientも1台あたり10程度としました。アプリケーションサーバからの接続はtcpのbacklogに積まれますので、MaxClientが少なくても処理が詰まったりしない限りは問題ありません。もしAPIからIDが取得できない場合は、アプリケーションサーバから直接MySQLを参照します。

id_generate_server_blog.png

APIの性能は「ab」を利用してテストしたところ、1台あたり2000req/s以上の性能がでています。簡単なDBのシミュレーションでも秒間数百の処理ができることも確認しています。

■最新情報DBへの書き込みを非同期に

ID Generatorを改善することで現在顕在化しているボトルネックの解消はできましたが、今年はもう一つ、次に問題になりそうなDBの改善にも取り組みました。mixiの日記DBは弊社でLevel2分散と読んでいるユーザパーティショニングを行っています。日記のDBはノードと呼ばれる日記を保存するDBと、マネージャーと呼ぶ、ユーザの日記がどのノードにあるかを管理するDBの2種類があります。ユーザの日記を表示する時にはマネージャーのDBに接続をし、ユーザIDから、どのノードに日記が保存されているのを問い合わせ、その結果得られたノードDBに再度接続をするようになっています。

diary_partitioning1.png

ただし、この状態ではホームなどのマイミク最新日記を取得する場合に、全部のノードに1つ1つ接続をしてマイミクの最新の日記を検索しなればならないので、ノードとは別に最新日記のIDとタイトルだけを入れる最新情報DBを用意してあります。ちょうど1週間のデータを保存していますので、Weekly DBと呼んでいます。日記が投稿された際には、各ノードとWeekly DBの2カ所にデータを保存しています。

Weekly DBは負荷的な問題は現状ありませんが、1月1日のような日記が集中して書かれる瞬間では、書き込みが多くのアプリケーションサーバから一斉に行われるため、ボトルネックになることが予測されます。

diary_weekly.png

そこでエコーでも用いたQ4MというMySQLのStorage Engineとして開発されているJob Queueのシステムを日記投稿にも投入しました。

diary_weekly_queue.png

書き込みを一旦Q4Mでキューイングし、表側の非同期にWeekly DBへと書き込んで行きます。日記の投稿が集中した場合、Weekly DBではDisk IOを分散することが難しいですが、Q4Mを用いる事でIOを時間軸で分散することができるのでWeekly DBへの負荷が集中し、サービスに影響が出てしまうのを防ぐ事ができます。また、Q4M自体の負荷が高まった場合は、単純に台数を増やす事で解決ができます。

Q4Mを導入した事でもう一つ耐障害性が高まった部分があります。Weekly DBにハードウェア障害が起きた場合にも最新日記をQ4M上に貯めておき、DBが復旧した時点であらためてDBに挿入していくことができます。これによってマイミクの最新日記をQ4M導入前よりも確実に届けることができます。

■まとめ

2009年1月1日にむけて2つの改善を行いました。1つはIDを生成するDB、もう一つは最新情報DBへの保存の非同期化です。2009年”は”ユーザの皆様の「あけおめことよろ」をすべて正常に受け付けることができると思います。来年1月1日に良い結果がでれば、またエンジニアブログで紹介したいと思います。

こんにちは。mixi開発部のyouheiです。
今回は先日8月4日にリリースした「エコー」について書きたいと思います。

■エコーとは

まずはエコーとはどういう機能かのご紹介ですが、プロモーションページがございますのでそちらをご覧いただければ幸いでございます。
http://mixi.jp/guide_echo.pl

いくつか抜粋しますと、

あなたの“今”を一言にしてみませんか?誰かに伝えたいこと、ひとりごと等、何でもOK! 気軽な新コミュニケーション機能です。
たとえば、「今日はいい天気だな~」という、ひとりごとから、「お腹すいたー!誰かランチにいこうよ!」というメッセージ的な使い方まで、「エコー」の楽しみ方はあなた次第!
マイミクシィ同士で「エコー」を使うとホームにお互いの書きこみが表示されます。気になった書きこみには、返信することもできちゃいます。あなたがふと書きこんだ一言に、思わぬ返信があるかも!?

といった機能です。

当エンジニアブログではエコーの機能的な側面よりも、裏側はどうなっているのか、そのあたりをメインに書きたいと思います。

■基本はLevel2分散

mixiの分散アーキテクチャは今までにも様々な場で紹介させていただいておりますが、mixi社内では分散の方法を

  • 「日記」「コミュニティ」など、機能ごとにDBを分割する垂直分散をLevel1分散
  • さらにそれをユーザーごとに別のDBに分割する水平分散をLevel2分散

と呼んでいたりします。
今回のエコーも例によってLevel2の分散を行っています。
即ち、

  • 各ユーザーのエコー投稿を貯め込むEcho Node DB
  •  どのユーザーがどのノードに書かれているのかマッピングを管理するEcho Manager DB

基本的にはこの2つのDBを立てることでLevel2分散が実現できます。

■Level2分散が仇になる点

ところがLevel2分散をすると逆に実装しづらくなる機能として、「みんなのエコー」という機能があります。これは、自分や自分のマイミクたちの発言をタイムライン上で一気に見れるページなのですが、この機能を実装するにあたっては、複数のDBにデータを分散するLevel2分散では、一度データを収集した後でそれを並び替える、という工程が発生しコストがかかってしまうため逆に仇となってしまいます。
同様な機能のページとしては「マイミクシィ最新日記」等のページもそうです。

■Recent DB

これらのページを実現するにあたって、Echoでは「Recent DB」と呼ぶものを用意しています。
このDBはその名の通り、最近のみなさんの書き込みを貯め込むDBで、すべてのユーザーの発言が集約されています。
「すべてのユーザーの発言」という表現をするとデータ量が肥大化しそうに感じますが、その点においてはプロモーションページにも明記していますが、「みんなのエコー」ページに表示される書きこみは直近2日分のみという仕様なのでそれほど膨らむことはありません。

また、すべてのユーザーが「みんなのエコー」のページを表示する度にこのDBにアクセスしてくるためreadの負荷が高そうにも感じますが、その点についてはMySQLのレプリケーションを利用しSlaveを多数用意するという通常の方法をとることで単純にスケールアウトできるため、それほど問題になることはありません。
また、断トツに高いページビューのhome.plですが、こちらに表示されている最新エコー5件のRead負荷についてはmemcachedから取得しているため、こちらも負荷対策が出来ています。

問題は、このRecent DBへのWriteの負荷です。
MySQLのレプリケーションは基本的にはWriteはMaster1台に行う必要があります。
そしてこのDBはすべてのユーザーの発言時にWriteされるDBです。
ここがスケールアウトしづらいポイントとなり、高負荷時にはボトルネックとなりやすいポイントになります。
特にお正月の「明けましておめでとうー!」書き込み時などは負荷が非常に高い状態になります。

■Q4Mでのピーク負荷平滑化

前述の問題は日記等でも同様ですが、投稿量で考えると圧倒的にエコーの方が高くなると予想されるため、エコーでは高負荷時でもRecent DBへのWriteがボトルネックとならないように、本エンジニアブログにも何度も登場しています弊社運用チームのkazeburoや、タンポポ開発チーム(という名前のチームが実際に存在するのです)のエンジニアと議論の結果、Q4Mを導入し高負荷に耐えられうる構成にしてみようということになりました。

Q4Mについては、MySQL5.1のプラガブルストレージエンジンの1つで、サイボウズ・ラボ株式会社の奥一穂氏が開発されています。

Q4M (Queue for MySQL) は MySQL 5.1 のプラガブル・ストレージ・エンジンとして動作するメッセージキューであり、堅牢・高速・柔軟であるよう設計されています。昨年12月遅くに開発が開始され、まだ非常に原始的ですが、かなり高速に動作します。

http://labs.cybozu.co.jp/blog/kazuho/archives/2008/01/q4m.php より抜粋

エコーではこのQ4Mをバッファリングする機構として導入することにより、Recent DBの能力を超えてしまうようなWrite負荷が発生した場合でも、一度Q4M上にキューイングされ、その後コンスタントにRecent DBへ伝播していくようにして、ピーク負荷を平滑化することができるような構成にしました。

■エコーシステム構成

結果、エコーのシステム構成図は下記のようになりました。

echo_system

※実際のサーバー数は図のとおりではありません

キュー反映スクリプトからRecent DB (Master)への接続数は、今後、キューのたまり具合やRecent DBの負荷を見つつ、数を調整していきたいと思っています。

■まとめ

今回のエコーでは負荷対策として、日記のように各投稿に対して一意なidを発行する採番部分を排除し、member_idとpost_timeで一意キーとするなどして負荷の集中を避ける等の細かな工夫も随所にしていますが、Q4Mを導入してみたというところが新たな試みとして一番大きな点です。まだQ4Mが本領発揮するほどの負荷は発生していないのですが、引き続き高負荷時のQ4Mの働きに注目していきたいと思います。

Q4Mは非常に有用かつ興味深いストレージエンジンだと思いますので皆さん是非試されてみてはいかがと思います。

最後に、素晴らしいエンジンを 開発下さった奥一穂氏に改めてお礼申し上げます。