DECOLOGでのMySQL BlackHoleエンジンの使い方

2011/03/17 11:38:10

 こんにちわ、ミツバチワークス stoneです。

 DECOLOGでは、データベースにMySQLを使用しています。 ストレージエンジンのメインはInnoDBなのですが、他にもMyISAM、BlackHole、Archiveエンジンを使っています。 今回は、その中でBlackHoleエンジンについて、DECOLOG内での利用方法をご紹介したいと思います。

BlackHoleエンジンについて

 BlackHoleエンジンは、何もしません。 insert、update、deleteを行っても、データは全く変更されませんし、selectをしても、データは何も返ってきません。 実際のデータファイルを見てみても、テーブル定義ファイルの.frm以外のファイルは作成されません。 /dev/nullと似ているイメージです。

 が、BlackHoleのテーブルに対して発行されたinsert、update、deleteは、binlogには残ります。 そのため、レプリケーションを行っている場合、slave側で何か実体のあるストレージエンジンでレプリケーションしていれば、その変更は、slave側に保存されることになります。

 また、内部的には、何もしていないので、サーバーからの応答は、すごく速いです。

記事データの配置

 以前は、DECOLOGでも、データベースもそれほど系統分けされていなくて、記事に関連するデータは、すべて、1台のサーバーがマスターとして稼働していました。

 記事内の画像は、もちろん、本文データも大きくなってしまう傾向にあるので、 別テーブルとして運用しています。

ユーザーの投稿の滞留が発生

 DECOLOGはデコメを専用アドレスに送ることによって記事投稿するブログです。受信メールサーバでは、受信したメールをPHPに渡すことによってDBへの登録を行っています。

 ユーザーがどんどん増えてきて、1日あたりの記事投稿の件数が増えてくると、受信したメールをデータベースへ登録するシーケンスでメールの滞留が発生するようになりました。 よくよく状況を調べてみると、新規のテキストデータ・画像データをinsertする際、MyISAMでのテーブルロックが発生していました。 ご存知のように、MyISAMエンジンの場合、データに何かしら変更が入る場合、テーブル全体がロックされます。 このテーブルロックが、多発していて、それにより投稿されたメールの処理が遅れ、滞留するようになっていました。

対策1 マスターをBlackHole化

 記事のテキストデータ・画像データはともに、マスターのサーバーへselectすることはなく、 また、insertされてから、投稿したユーザーがページへアクセスして、記事を表示(select)されるまで、少なくとも数秒程度のタイムラグがあります。 この特性を利用して、masterをBlackHole化することにしました。

 手順としては、masterのサーバーで

mysql> alter table (テキストテーブル) rename to (テキストテーブル)_saved;mysql> create table (テキストテーブル) like (テキストテーブル)_saved;mysql> alter table (テキストテーブル) engine=BlackHole;

として、テーブルをBlackHole化した後、 slaveのサーバー上で、

mysql> drop table (テキストテーブル);mysql> alter table (テキストテーブル)_saved rename to (テキストテーブル);

として、テーブルを元に戻します。

対策2 memcachedを併用

 「対策1」を導入した後に気づいたのですが、投稿したユーザー本人はその記事を見るまでに、数秒のタイムラグがあるのですが、人気があって頻繁にアクセスされているブログの場合、投稿されたそのタイミングで、他のユーザーからのアクセスがある場合があるんですね。 そうすると、テキストデータ・画像データのレプリケーションがまだ追いついていないケースがあって、その記事が正常に表示できないという問題が出てしまいます。

 そこで、記事テキストと記事画像については、投稿のタイミングで、データベースよりも先に、memcachedにもデータをストアしておいて、レプリケーションのタイムラグを表面化しないようにしました。

 では、また次回に♪

利用特性に合わせたシステムを構築しよう

2011/03/03 12:00:00

 こんにちは、hiroshiです。 自分のことをhiroshiとかアルファベットで表現すると、軽く有名人っぽくて気分がいいです。みなさんもぜひやってみてください。

 さて、以前ITmediaさんで、DECOLOGの画像配信の仕組みについて紹介させていただきました。 あの仕組みは「デコメで投稿される記事に使われている画像」の特性から現状のあの構成になりました。

 一方、DECOLOGで扱われる画像はもちろん記事画像だけではありません。 代表的なものに、ブログTOP画像というものがあります。

 ブログTOP画像は、その名の通りブロガーのみなさんの“顔”ともいえるブログのTOPページを飾る画像です。

図1.ブログトップページサンプル

 上図のとおり、ページ上部に配置される画像であるため、携帯画面でのファーストビューはこのブログTOP画像で占めらる割合が大きいです。 つまり、「ブログTOP画像=ブロガーのみなさんの顔」といっても過言ではなく、少しでも早く表示させたい画像なわけです。

 さて、、こちらで、記事画像配信の仕組み変遷の歴史画像を掲載していますが、あれの3代目まではこのブログTOP画像も同じパケットのフローに乗っていました。違っていたのは、一番下のDBだけです。

 しかしながらブログTOP画像と、記事画像では大きな違いが2点あります。

  1. アクセスの頻度が違う。記事画像は時間が経てばが落ちるが、ブログTOP画像は一定
  2. データ量が違う。記事画像は際限なく増加するが、ブログTOP画像は仕様上、ブログ開設数×2が上限

 1から、アクセス頻度がロングテール化する、記事画像のようなアーカイブ方式はそぐわないことが分かります。 このように、大きな違いを持つ2種類の画像が共存していたため、squidのキャッシュ効率も悪く(たぶん)、ブログTOP画像の表示がもっさりする、登録しても反映が遅い(=replication delayの発生)ことがしばしばありました。

 なので、「まずブログTOP画像をなんとかしよう!」ということで対策会議を開きました。といっても技術責任者のstoneと2人でベランダでタバコすいながら「なんかいい方法ないっすかねー」みたいなノリでしたけど。 で、いろいろ議論を重ねた末に、「とりあえずこれでやってみっか!」となったのが下図の構成です。

図2.ブログトップ画像構成

 Before、Afterでの大きな違いは、画像の保持方法を動的(DB管理)から静的(FS管理)に変えたことです。 2の「データ量が違う」ということから、inodeの枯渇の心配もないことが分かり、だったらDBにぶち込まないでそのまま置いといたほうが早くね? ということでそうしてみました。

 画像サーバは読み取りと書き込み用にポートを分けておき、書き込み用には画像を受け取る用のPHPスクリプトを置いておきます。

 書き込み処理の流れを追ってみます。ブログトップ画像の登録はメール送信で行われます。

  1. qmailで受け取ったメールはパイプ処理でPHPプログラムに渡す
  2. phpでメールの中身を解析し、画像ファイルの取り出し・加工処理などを行った後、
  3. DBにぶち込み、
  4. HTTPでブログIDをキーに決められた格納先画像サーバとバックアップ用画像サーバにPUTする

 PUTと言っても、単純にrequestに画像データをぶち込んで画像サーバの81に向けて特定のURLを呼び出すだけです。

 methodとしてのPUTを使ってみる方法(WebDAVとかを使うんだったけかなあ?)も試行錯誤してみたんですが、作業を担当したのが2流の僕なのですぐ音を上げました。「stoneさん、これムリっす!」「じゃあ、あと1時間だけ挑戦してダメだったらやめよっか」やっぱりだめでした……。

 言い訳っぽいのですが、あのまま試し続けていればカッコいい方法が実現できたかもしれません。ただ、慣れてない方法でやるよりも、シンプルかつ自分が完全に理解できている方法でやった方が、トラブル時などの対策案も多々浮かんできます。それになにより、人がいないので、方針を決めたら作業完了までの工数が読めない方式は取りたくない、というのもありました。

 一方で、どこかで新しいことやモノに挑戦していかないと手元のカードは増えません。状況に余裕があるときであれば、新しい手法・試したことのない手法はどんどん突っ込んでいくべきだと思います。

 ということで、まあ当時は余裕がなかったんです。

 さて、話を元に戻します。 ユーザーから送られてきた画像は、3個所に書き込まれます。もちろん理由があります。 内訳はDB1個所に画像サーバ2個所。

 画像サーバ2個所に書き込んである理由はすぐ分かります。画像サーバがぶっ壊れたらすぐ代替と交換できるように、そうしてます。なので、バックアップ用サーバはすべての画像を保持しています。

 DBに書き込んでいる理由も、やっぱり画像サーバがぶっ壊れたときのためです。 どこかの画像サーバがぶっ壊れても、バックアップサーバを突っ込めばサービスとしては問題ない状態になりますが、その間に復旧作業をしなければなりません。

 バックアップ用サーバからrsyncで、とかもちょっと考えましたが、普及中もユーザーによる追加/更新が行われ続けるため、完全に同期がとれるのかがすごく疑問でした。また、その検証方法も思いつきませんでした。

 一方、DBで持っておけば、その辺の取り回しはいくらでも融通が効くので、復旧用のスクリプトさえ用意しておけば数時間で普及できる計算が成り立ちました。ということでDBにも書き込んでいるわけです。

 読み込み処理の流れはシンプルです。URLにその画像がどの画像サーバにあるかが分かるようになってるのでmod_proxyで振り分けるだけです。

 この仕組みは想定どおりの結果が得られ、今でも非常に安定して稼働しています。

 本当は、既存の仕組みから新しい仕組みに切り替えるときに発生する「移行」についてもいろいろ工夫してきた話があるのですが、これはまた後日にさせてください。

 では、また次回に会いましょう。

replicationしてるMySQLのslave増設手順

2011/02/25 12:45:00

こんにちは、hiroshiです。今日は、半定常作業の「MySQL増設作業」について書こうと思います。

 下図のように、master1台←slave2台がLVS+keepalivedで負荷分散構成されているDBがあるとします。

図1.master-slave構成

 この構成の組み方にしようかと思ったのですが、これはググったらいっぱいあったので、ホッテントリは狙えないと思ってやめました。

 なので、今回のテーマは「このテーブルはwriteは余裕だけどreadがきつくなってきたから、slaveを増設しなければ!」となった場合のslaveを増設する手順について書いてみます。

 下図のslaveCを追加するぞ! の場合です。

図2. slave Cを追加するぞ

※今回はengine=innoDBの前提で書きます。

※各種userとかIPとかその辺はそこそこ適当に書いてます。

大まかな手順は以下です。

  1. コピー元となるslaveを1台LVSから切り離す
  2. slave→masterのreplicationを止める
  3. dumpを取る
  4. dumpファイルを移動させる
  5. dumpファイルをインポートする
  6. LVSに参加させる

 こんな感じです。

1. コピー元となるslaveを1台LVSから切り離す

 忙しいDBでいきなり切り離すと、sleepのプロセスが大量発生するので、weightを1にしてから外すことにしています。

 keepalived.conf

# slave A
real_server     192.168.0.101 3306 {
 weight  1 ←1にする。(通常10にしてます)
 inhibit_on_failure
  TCP_CHECK {
   connect_port    3306
   connect_timeout 3
  }
}

# slave B
real_server     192.168.0.100 3306 {
 weight  10
 inhibit_on_failure
 TCP_CHECK {
  connect_port    3306
  connect_timeout 3
 }
}

 weight1にして

svc -h /servie/keepalive
# slave A
#real_server     192.168.0.101 3306 {
#    weight  1 ←1にする。(通常10にしてます)
#    inhibit_on_failure
#    TCP_CHECK {
#        connect_port    3306
#        connect_timeout 3
#    }
#}

# slave B
real_server     192.168.0.100 3306 {
 weight  10
 inhibit_on_failure
 TCP_CHECK {
  connect_port    3306
  connect_timeout 3
 }
}

 コメントアウトして

svc -h /servie/keepalive

 LVSからの切り離しは完了です。

2. slave→masterのreplicationを止める

 slave A上のmysqlでshow processlistするなどして、アクセスされなくなったのをやわらかく確認します。

 小心者のぼくは、最終的にはtcpdumpとかでmysqlのパケットが流れてこないかを確認するのですが、この時点ではまだreplicationしたままなのでバリバリmysqlのパケットが流れています。

mysql> show slave status\G
~中略~
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
1 row in set (0.00 sec)
mysql> stop slave;
~中略~
Master_Host: master
Master_User: hoge
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: mysql-bin.000049
Read_Master_Log_Pos: 760141636
~中略~
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: NULL
1 row in set (0.00 sec)

 ということで、この時点でreplicationが止まりました。

Seconds_Behind_Master: NULL

がreplicationが止まった合図です。で、

Master_Log_File: mysql-bin.000049
Read_Master_Log_Pos: 760141636

 これからdumpをとるのですが、そのデータはmasterのbinログファイルが「mysql-bin.000049」で、そのファイルの位置が「760141636」の状態のものだという意味です。]

 部分的なコピペでもいいんですが、だいたいこんな感じshow slave status自体をファイルに落としておいておくといいかもです。

mysql> \T slave_status.txt
mysql> show slave status \G
mysql> \t

 で、ここでtcpdump -i eth0 port 3306とかやってパケットが流れないのを確認すると、精神衛生上いいので僕はそうしてます。

3. dumpを取る。

 容量のでかいDBの場合、dumpに半日以上かかることもざらなので、screenを使います。screenじゃなくてもnohup使うでもなんでもいいんですが、不慮のネットワーク切断に備えた何かをやっておいた方が賢明です。

$ screen

$ mysqldump -u root -p database_stage > dump_database_stage.sql
$ mysqldump -u root -p database_release > dump_database_release.sql

※テスト環境用のDBもあるので、一緒にダンプします。

 これでひたすら待ちます。ZABBIXのロードアベレージとかがグイーンと上がるので、それが下がったら「終わったかな」と思って見にいきます。dumpが終わったら通知メールを送るようなスクリプトを書いてもいいんですが、必要に思ったことがないので今はそういうのはないです。

 ちなみに、だいたいの場合において、このdumpをとっているスキに増設先のmysqlの構築を済ませておきます。

 dumpが終わったらreplicationを再開させます。

mysql> start slave;

 replicationをとめてた時間などにもよりますが、

mysql> show slave status\G
~中略~
Seconds_Behind_Master: 1348615

 と、こんな感じになっているので、0になるのを待ちます。これも見つめていてもなかなか終わらないので、ZABBIXを利用します。Seconds_Behind_Masterの値は、常時監視項目としてZABBIXで監視+閾値でアラートを設定しているので、これを利用してreplicationが追いつくタイミングを知ることもできます。

 ここでちょっとワンポイントなのですが、replicationが追いついた後、

mysql> select count(*) from table;

をします。

 innodb限定の手順なのですが、これでinnodb_buffer_poolにデータを乗せようとしています。これをやらないでいきなりユーザーからのアクセスにさらすと、処理がおっつかずにreplicationのディレイが発生しちゃったりします。

 ところで、上述のとおり、slave増設はslave1台をサービスから切り離してdumpを取る、という作業手順になります。

 ですが、増設のきっかけは「負荷が厳しいから」だけではなくて「slaveが1台故障したから」というケースもあります。

 故障のケースでmaster1台←slave2台の構成だと、故障後はmaster1台←slave1台という構成と同義になり、そこからslaveを切り離してしまうとサービスに利用できるslaveがなくなってしまいます。最悪負荷の低い深夜帯に、masterにreadも担当させてやるか、サービスを止めるかなどするしかなくなってしまうので、そこそこ途方にくれます。

ですので、slaveは3台が基本です。

masterがblackholeの場合を考慮すると「実体のあるDB3台が基本」という言い方もあります。

 このことはid:naoyaさんの著書「大規模サービス技術入門」にも書いてあったのですが、僕たちは、実際に途方にくれた後にこの著書に出会い、「もっと早くこの本に出会えていれば」と悔やみました。

4. dumpファイルを移動させる。

 sftpでもscpでもいいです。増設先のサーバに移動させるだけです。

 ただ、うちではサーバ間のinとoutのトラフィックをZABBIXで監視してます。で、特に何もしないとscpでも全力でファイル転送されるため、監視の閾値を超えてしまい、アラートメールが飛んできてしまいます。

 そうすると、周りに冷たい目で見られるので, ZABBIXのトラフィック監視をOffにしておくことを忘れてはいけません。

 この移動もファイルの容量次第ではscreenするなりしておきます。

5. dumpファイルをインポートする

$ mysql -u root -p database_stage < dump_database_stage.sql
$ mysql -u root -p database_release < dump_database_release.sql

 これも相当時間がかかるケースがあるので、screenなりをしておきましょう。 インポートが終わったら、replication設定をします。

mysql> change master to master_host='マスターホスト名', master_user='repl', master_password='repl', master_log_file='ログファイル名', master_log_pos=ポジション;

 この「ログファイル名」と「ポジション」に2でメモったMaster_Log_FileとRead_Master_Log_Posを入れます。 その後は

mysql> start slave;

して、Seconds_Behind_Masterが0になるのを待ってselect count(*)やっての手順は先ほどまでと同じです。

6. LVSに参加させる

これで最後です。 keepalived.conf

# slave A
real_server     192.168.0.101 3306 {
 weight  10
 inhibit_on_failure
 TCP_CHECK {
  connect_port    3306
  connect_timeout 3
 }
}

# slave B
real_server     192.168.0.100 3306 {
 weight  10
 inhibit_on_failure
 TCP_CHECK {
  connect_port    3306
  connect_timeout 3
 }
}

# slave C
real_server     192.168.0.99 3306 {
 weight  1 ←1からはじめる
 inhibit_on_failure
 TCP_CHECK {
  connect_port    3306
  connect_timeout 3
 }
}

 新しく追加したDBのweightは1からはじめます。何かしくじってたときのダメージを低くするためです。アプリのエラーログを監視しながらこの作業は行い、エラーがでれば即戻します。各種グラフを見ながらweightを10にまで持っていけたら作業完了です。 以上が単純なslaveの増設方法です。

これ、役に立つと思うので、ぜひ試してみてください。

では、また会いましょう。

Redisの実用例。Redis速いよRedis

2011/02/17 12:05:00

 こんにちは、hiroshiです!

 今回は、最近DECOLOGかいわいで大ブームのRedisについて、その利用用途や導入方法についてお話ししたいと思います。

 今回のお話と関連する過去エントリ(本館)に以下がありますので、こちらに目を通していただいた上で本エントリを読んでいただくと分かりやすいと思います。

 ……と、これまでのエントリからは実運用できてるのかどうか微妙なタッチになっているかもしれませんが、結論からいうと実運用できてます!

 「redis導入後にトラブル発生、そのレポート」ではTTLを設定した場合にうまくいかないケースがあったのですが、TTLなしのデータでは特に問題なく運用できました。

■現在のRedisの利用用途

 いまのところ、以下の用途ですね。

  • カウンターデータ
  • 広告配信データ
  • ユーザーのアクティビティ統計データ

 最初に導入してみたのは、広告配信データでした。

 「なぜRedisを選んだか」ですが、そもそもMySQL以外のツールを探し始めたきっかけが、readよりもwrite負荷、つまりmaster負荷が原因でスループットが上がらない処理がいくつかあったためであり、そこをなんとかしたかったんです。

 shardingすれば済むし、実際にそうやって対応していましたが、処理内容(カウントアップするだけ)や保持しているデータ(前述のデータはkey-value形式なデータで、valueは数値のみ)を考えると、なんとも「サーバがもったいない。富豪エンジニアリングになってしまってるなあ」という感を強く持っていました。

※「富豪エンジニアリング」は必ずしも悪だとは考えてないです。念のため。

 余談ですが、金をかければだいたいのことが解決するわけです。なので、技術陣はよそからの要求をただ実現するだけでなく、「低コスト」でやってナンボだと思っています。「サーバ増やせばできるよ」という結論も多々あると思いますが、「サーバを増やさずできる」方法を常日ごろから考えておきたいところです。

 話を元に戻します。前述の課題となっていたウチのデータ群では、広告配信データが最も更新負荷が高いデータだったので、「ここがイケれば全部イケル」ということからここを選びました。

 さらに広告配信データは「DECOLOGでのMySQL Archiveエンジンの使い方」と同じスキームでやっていました。つまり

  1. archiveエンジンを使ってとにかくinsert
  2. 定期的にテーブルスワップし、スワップしたテーブルに対して集計プログラムを走らせる
  3. で、その結果を集計DBに格納

 といった按配です。

 広告配信データは、1日に1回のテーブルスワップだと半端ないデータ量になってしまうのと、運用側としてもimpressionなどの集計データの本日分が参照できないのはリアルタイム性にかけすぎるので、テーブルスワップのサイクルは、1時間に1回でやっていました。

 なので、「ここがイケれば全部イケル」という理由に加えて、集計スパンを既存に合わせれば、仮に導入試験中にRedisが原因で未集計のデータがぶっ飛ぶようなことがあっても「MAX1時間分のデータがパーになる」というのが想定される最大の被害規模であり、それだったら基本的に内部以外に迷惑かけずに済む、という算段もありました。

■設計

 設計ってほどのことでもないですが、1台でどこまでいけるのか? というのを試したかったので、Redisサーバは1台のみです。仮にぶっ壊れたりしたとしても、プログラムをを2~3行コメントアウトしてリリースすればよいですし。

 ところで、Redisにはdatabaseという概念があります。これは数字で指定する、ちょうど配列の添字みたいなものです。この単位でデータは管理されています。なので、データのflushもこの単位でできます。で、これは上限があるのかどうか分からないんですが、任意の数を指定できます。この添字的なものをdbidと言うっぽいです。

 データ構造のイメージとしてはこんな感じ。

[0] {
array(key1 => value, key2 => value, key3 => value ……)
},
[1] {
array(key1 => value, key2 => value, key3 => value ……)
}
:
:
[n] {
array(key1 => value, key2 => value, key3 => value ……)
}

 ※設定ファイルに指定した数-1

 広告は1時間ごとに集計したいので、この仕様を利用して、databaseの数を24にしておきます。こうするとDBは0~23の範囲を指定できます。

 プログラムでも、この仕様に合わせる形を取っておきます。

increment($ad_id."/".$carrier);
    }

 これはいわゆるDBクラスですね。

parent::__construct(REDIS_SERVER_AD_VIEW, $index);

の$indexがdbidです。

 現在時間(h)をdbidとして扱っているため、10時のデータはdbid=10のところに入っているわけです。

 KEY_PREFIXは、keyの一部になります。ベースクラスで、このKEY_PREFIXを元に

    {ENV}/{KEY_PREFIX}/{KEY}

というkeyを組み立てるようにしてあります。

$this->increment($ad_id."/".$carrier);

のincrementですが、Redisにはincrというコマンドがあり、これがチョー便利です。

 valueが数値だった場合に$deltaの数だけインクリメントしてくれる、というシンプルなものなのですが、何が便利って該当するkeyがない場合にエラーとしないで、key=$deltaを自動的に作ってくれるんです!

 「キーが存在すればupdate、存在しなければinsert」みたいなコードを書かなくていいのはすごく気持ちがいいです。

 こんな感じでデータを収集していきます。

 24時間経つと、dbidが重複することになってしまいますが、1時間ごとにデータ集計バッチが走るので、そこで現在時間-nのDBをflushする、というコマンドを入れて対応しています。

■導入に当たって

 新しいものを導入するときのウチの基本なのですが、いきなり切り替えず、既存ロジックと平行させて導入します。

 かつ、100分の1で新しい処理を通るようにする、みたいなコードを入れて、グラフを見ながら徐々に100%にあげていきます。

 100%になれば、既存ロジックの集計結果と同じになるはずなので、それを確認します。

 広告集計結果をストアするsummaryというテーブルがあるとすると、summary_testというまったく同じスキーマのテーブルを用意します。

 Redisで取得したデータの集計データは、summary_testに格納します。あとは、確認するだけです。

 で、これがけっこうあっさりうまくいっちゃったので、苦労話とか特にないです。いや、これをやること自体がそれなりに苦労するんですけど。

 しかもこれまで7台でやっていたものを1台で受けきって、かれこれ数カ月問題も起きていません。

■感想

 いやー、7台が1台になっちゃいました!

 正確にいうと、7台で構成されていた既存DBは、actionlog、ランキングデータ、広告配信データが格納されており、いずれもそれぞれ別のRedisサーバに分けられたので「7台が3台になった」が正しいですね。ただ、こう書くと約半分程度なので、ちょっとインパクトに欠けるかもしれません(?)が、この例ではないところで4台が1台にできたのは確認できています。

 実際は、valueにオブジェクトも格納でき、それを利用しているところもあるのですが、がっちり使っているのはvalueに数値を持ち、カウントアップしていく性質のデータを対象としています。

 っていうか、そういうデータだったら、かなり最強の部類に入るのではないか、と思っています。

 また、Redisの開発はかなり活発です。あまりにも活発すぎて燃え尽きないか、と心配になるくらいです。

    DECOLOGが使っているphpクライアントのphpredisの開発も活発です。

 こういうのは多くの人が使えば、今後ももっともっと活発に活動し続けてくれると思うので、皆さん奮ってご利用ください!!

 ではでは!

 see also:「仲間大募集のお知らせ

HadoopによるApacheのログ解析・実例

2011/02/15 12:00:00

 こんにちは、ミツバチワークス stoneです。

 今日は、DECOLOGで行っているApacheのログ解析について、ご紹介してみようかと思います。

 現在、DECOLOGでは、リバースプロキシが8台あって、その8台の1日のApacheのログは、全部で、200Gバイト以上になっています。

 これを、13台のHadoopのスレーブノードで解析を行っています。

※別館注:本記事は2010年10月時点の内容です。この後CDNを導入し、解析対象ログは80GB、解析時間は1時間半程度に圧縮されました。Hadoopのノードも11台に減らしています

 全体の流れとしては、

  1. リバースプロキシからHDFSにログを転送
  2. 解析用のサーバで、HDFSにログの転送が終わるのを監視
  3. ログの転送が終わったら、Hadoopを起動、解析
  4. Hadoopの解析結果をデータベースに保存

 以下では、各ステップを個別に見ていくことにしますね。

1. リバースプロキシからHDFSにログを転送

 当初、Hadoopのプロセスが立ち上がっていないと、HDFSにはアクセスできないと思い込んでいたので、解析用のサーバが、

  1. 各リバースプロキシからのログをscpで収集
  2. 収集したログをHDFSへ

というステップを踏んでいたのですが、このステップだけで、昼過ぎまでかかるようになったため、現在では、リバースプロキシから直接HDFSへ展開するように変更しました。

 Apacheログは、cron.dailyで起動されるlogroateでローテーションがかかるので、そのcron.dailyの最後にHDFSへの展開するスクリプトが起動するようになっています。

/etc/cron.daily/zz_log_to_hdfs.sh

#! /bin/sh

HOST=`hostname -s`
TODAY=`date +%Y%m%d`

HADOOP='/path/to/hadoop/bin/hadoop dfs'

HDFS_BASE='hdfs://hadoop-master:9000/accesslog'
LOG_FILE="$HDFS_BASE/$TODAY/$HOST"
SIGNUP_FILE="$HDFS_BASE/signup/$HOST"

LOCAL_SRC="/path/to/log/access_log.1"

$HADOOP -copyFromLocal $LOCAL_SRC $LOG_FILE
$HADOOP -chmod 777 $LOG_FILE
$HADOOP -touchz $SIGNUP_FILE
$HADOOP -chmod 777 $SIGNUP_FILE

2. 解析用サーバで、HDFSにログの転送が終わるのを監視

 各リバースプロキシから直接HDFSへ展開することで、時間は大幅に短縮できたのですが、1点、問題がありました。それは、

 リバースプロキシからのログの転送がいつ終わったのか、分からない

という問題です。

 そのため、ちょっと、工夫をして、HDFS上にsignupディレクトリというモノを作りました。

 各リバースプロキシは、ログの転送が終わったタイミングで、このsignupディレクトリにホスト名のファイルをtouchします。前出のスクリプトの下から2行目でそのtouchをしています。

$HADOOP -touchz $SIGNUP_FILE

 解析用のサーバは、適当な時間に起動した後、このsignupディレクトリを監視して、すべてのサーバが出そろった段階で、次のHadoopでの解析のステップに進みます(signupディレクトリは、転送が始まる前に事前に空にしています)。

3. Hadoopでの解析

3.1 Key-Valueのマッピング

 ご存じのように、Hadoopでは、mapのステップで、あるデータを「key => value」の形にマッピングして、reduceのステップで、同一のkeyの値を取りまとめてくれます。

 DECOLOGでは、以下のような切り口でログ解析をしています。

  • 1時間ごとのHit数、PV数
  • ページグループ毎のPV数
  • 携帯キャリアごとのHit数

 具体的なキーは、こんな感じです。

YYMMDD-hourly_hit-HH (時間ごとのヒット数)
YYMMDD-hourly_page-HH (時間ごとのPV数)
YYMMDD-page_group-PAGE (ページグループごとのHit数)
YYMMDD-page_group_pv-PAGE (ページグループごとのPV数)
YYMMDD-user_agent-CARRIER (携帯キャリアごとのHit数)

 アクセスされたURLから、

  • ページビューなのか画像なのか?
  • ページビューの場合、HTTPステータスが200か?
  • どのページグループに属するURLか?

を判別しています。なので、例えば記事ページへのアクセスがあった場合、

aaa.bbb.ccc.ddd - - [13/Oct/2010:11:05:09 +0900] "GET /en/00000/00000 HTTP/1.1" 200 999 "-" "DoCoMo/2.0 D905i(c100;TB;W24H17)" "-"

という感じのログになるのですが、これをmapperに通すと、

20101013-hourly_hit-11    1
20101013-hourly_page-11    1
20101013-page_group-Entry    1
20101013-page_group_pv-Entry    1
20101013-user_agent-docomo    1

という結果が出力されることになります。

3.2 mapper/reducerの記述

 実際のmapperとreducerは、Javaではなく、Perlで記述しています。Perlで記述したスクリプトをHadoop Streamingを利用して、mapper/reducerとして、稼働させています。

 Hadoop Streamingは、データの入出力に標準入力/標準出力を使用するため、動作の検証は実際のログデータをmapperに標準入力から流し込んだときに、想定どおりの出力が得られるか? で行えます(reducerは、そのmapperの出力を流しこめばOKです)。

4.解析結果をDBに保存

 Hadoopの解析結果は、「part-0001」みたいなファイル名でHDFS上に出力されているのですが、これをうまい具合に取り出す方法がよく分かっていなくて、現状、以下のようにして、ローカルに取り出しています。

$HADOOP dfs -cat "$OUTPUT_DIR/part-*" | grep $YESTERDAY > $TMP_FILE

保存するデータベースは、以下のようなテーブル定義になってます(データベースは、MySQLです)。

カラム名データタイプ
log_datechar(8)
sectionenum("hourly_hit", "houly_page" ...)
section_keyvarchar(255)
log_countint unsigned

解析結果は、上記のmapperの出力に準じた形になっているので、

20101013-hourly_hit-11    999

と出力されていて、これを、

log_date: 20101013 section: houry_hit section_key: 11 log_count: 999

と保存しています。

 ごく初期の段階では、Webalizerを使って、サーバ1台で解析をしていたですが、解析が夜までかかるようになったため、Hadoopによる解析に切り替えました。

 当時(2009年4月ごろ)は、サーバの負荷対策で、ほぼ毎日のようにサーバの増設や役割の変更、それに伴うプログラムの修正を行っていました。

 その際、対策の根拠となるのが、前日のZABBIXのグラフとアクセス解析の結果でした。

 なので、アクセス解析が夜までかかると、負荷対策の立案にとても困った記憶があります。参考までに、切り替えを行ったタイミングでは1日あたりだいたい4700万PV/2億6000万Hitでした(ログのサイズは、データが残っていませんでした)。

 現在は、1日あたり、だいたい2億PV/15億Hitなのですが、お昼までには解析が終了しています。

■蛇足ですが……

 ウチでは、このシステムを「HAL」と読んでます。

 「Hadoop for Apache Log」の頭文字をとって「HAL」としていますが、本当は、HAL9000からパクって、「Hadoop for なんちゃら」は、後からこじつけました。が、他の年下のエンジニアからは、「何スか? HAL9000って?」と言われ、ジェネレーションギャップを感じました
(え? 知らない方が多数派ですかね、これって?)。

 では、また次回に♪

@IT Special 注目企業
@IT Special ラーニング

エンジニアライフ 最新の投稿コラム

@IT自分戦略研究所 新着記事

コラムニスト プロフィール

DECOLOG TECH
大規模ブログサイト「DECOLOG」の開発・運用ノウハウを公開します。

2011年3月

    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31    

バックナンバー

月間バックナンバー

カテゴリー

検索

Powered by TypePad
- PR -
@IT Special 注目企業
インデックス

イベントカレンダー

アクセスランキング

もっと見る

@IT Special ラーニング