はじめての しごと (from intra blog)

社内ブログでちょっと面白い話題があり、自分もそれについて書いたのですが、せっかくだから社外向けブログにも書いてみます。

ところで、先輩ディレクターの皆様は、
自分が始めて世に送り出した仕事って、今でも覚えてたりするのでしょうか?

続きを読む

アクセスログを集計するプログラムに求められるもの

最近、アクセスログを解析して任意のページの PV や UU を集計する機会に恵まれていて、集計プログラムを何度も書き直しているうちに、アクセスログを集計するプログラムに求められる機能や要素がどういうものなのかなんとなく見えてきた気がするので、整理のために書き出してみます。

まず、自作する必要があるのか?を最初に考えなければいけません。世の中には実績のあるアクセスログ解析プログラムがたくさんあります。 analog とか visitors とか、その他いろいろ。これらを使えないかどうかをまず検討します。今回僕は自作するほうを選びました。理由は「分散処理」と「柔軟さ」が必要だったからで、それについては後に説明するつもりです。

自作することになったのでプログラムを書いていくわけですが、コアとなる機能はシンプルで、単に正規表現でログの一行を要素別に分解し、必要な要素の値だけを抜き出してカウントしていきます。僕は Perl の while() ループを使いましたが、これは C で書かれたプログラムに比べれば遅いとは思いますが、十分実用に耐えるほど高速です。

ウェブサーバが一台だけで、ログファイルの行数も多くて数万行程度ならワンライナーか、短いフィルタスクリプトで十分ですが、ウェブサーバが数十台、サーバ一台あたりの一日のログが 100 万行を超えるような大規模な環境になると、単に集計のループが高速なだけのプログラムでは不十分になってきます。

例えば、50 台のサーバがあり、アクセスログは一日毎にローテートされて gzip 圧縮され、各サーバ上に置いてあるとします。過去 90 日分のアクセスログから集計するとき、一台につき一日分の集計が終わるまでに 60 秒かかるとしましょう。まず、サーバの台数が多いので一台ごとにログインして集計プログラムを実行するのはたまりません。一台のサーバにログファイルをすべてコピーしてきて集積し、集計はその一台だけで行うのはどうでしょうか?50 台 * 90 日分の、しかも圧縮してあっても数十から数百メガも容量があるファイルをすべてコピーしてくる時間はばかになりませんし、転送量もばかになりません。

集計プログラムは各サーバ上で実行し、集計結果をすべて集めてから合算する方法が良さそうです。ログファイルをコピーする手間と時間が省けます。集計プログラムを事前に各サーバへコピーしておいて、 ssh で順番にリモート実行していくのもいいですが、集計プログラムに変更があった場合にいちいちコピーする手間を省くために、僕は集計プログラムを丸ごとワンライナーにしてしまいました。 gunzip -c /path/to/access_log.gz | perl -e 'while (<>) { ... }' という感じのコマンドラインを動的に組み立てて、 ssh で各サーバに対してコマンドを実行します。 gunzip で標準出力へ展開しつつパイプでつないで Perl のワンライナーに流し込みます。 while ループの中でログを解析、集計し、結果を print することで標準入力を通してローカルホストへ結果が送られます。100万行以上のログをすべて読み込まず、 while(<>) {} で逐次処理するのでメモリ消費量も少なく済みます。コマンドラインを組み立てて各サーバへリモート実行し、結果を受け取って合算する、これらの一連の仕事をするスクリプトをつくり、それを実行するようにします。

こうすることで、集計作業は幾分楽になりました。変更すべきファイルは一つだけで、プログラムを実行するサーバも一台だけです。実際にウェブサーバが何十台あろうと、集計作業用サーバから ssh でログインできるように環境を整えておけば、各サーバごとの集計は中央のサーバから発行されたコマンドが行ってくれます。

しかしこのやり方には、集計期間が長くなるのに応じて所要時間が延びるという欠点がありました。各サーバごとに、一日分の集計に 60 秒かかり、50台のサーバに対して90日分のアクセスログを集計する場合、 60 * 50 * 90 秒かかってしまいます。ある一台のウェブサーバ上で集計プログラムが走っているあいだ、他の49台は何もせず、ただ順番を待っています。その時間がもったいないので、並列処理できるようにします。

並列化するには単に fork(1) を使いました。 Parallel::ForkManager というモジュールを使って、 max_procs = 50 としてサーバの台数分の子プロセスを作り、各子プロセスに一台のサーバでの集計コマンドを実行させます。すると所要時間は 60 * 1 * 90 となり、 1/50 になります。サーバの台数が増えれば増えるほど効率が良くなるので、規模の大きさに応じてメリットが増えます。

並列化するとき気をつける点は、集計結果の受け取り方です。子プロセスが標準入力から受け取った集計結果は、変数に入れておくだけでは親プロセスからアクセスできません。集計結果をみすみす捨ててしまうわけにはいかないので、 IPC::Shareable などのモジュールを使ってプロセス間で変数を共有するか、一時ファイルに書き出しておくなどの工夫が必要です。僕の場合は IPC::Shareable が少し不安定だったので、子プロセスごとに別名の一時ファイルへ書き出すようにしました。単一のファイルを共有すると、書き込み内容が混ざってしまい結果が乱れるため、別ファイルを使っています。 Danga::Socket などを使って非同期処理をさせれば、この点はクリアできそうなので、今後改善していきたいと思っています。

分散処理、並列処理を行うようにしたことで、速度面ではかなりのパフォーマンスを発揮できるようになりました。


次に、柔軟な集計ができるようにしなくてはいけません。一つのウェブサイトの中にも、数多くの URL があります。例えばブログサービスの場合、トップページの URL、パーマリンクの URL、アーカイブの URL、などなど。また、複数のドメインでアクセスできるブログの場合、コンテンツとしては同一であってもパーマリンクが複数存在することがあります。携帯版がある場合も同様で、同じコンテンツでも URL にバリエーションがある、 URL の規則からして違ってくる場合があります。そういう場合、意味上の同一コンテンツは URL の表現にバリエーションがあっても一つに合算して結果を出す必要があります。

あるふたつの URL が表現は違えど同一のコンテンツであるかどうかは、単純に数を数えるプログラムには判断できません。つまり、ウェブサイトに固有の要件を考慮したうえで集計するプログラムを書かなければいけません。僕が analog などの既存のプログラムを利用しなかったのは、一般的でない要件を満たせるほどの柔軟性を持たせるのは難しいと思ったからです。 analog で高速に一次集計だけを終わらせ、その結果を要件に沿うように再集計することもできますが、どうせならまとめてやってしまおうと思ったのです。

どの程度までの柔軟性が求められるかは場合によりけりなので、プログラムの設計方針としてはあとあと変更しやすいように、細かくモジュール化しておくのがよさそうです。僕の場合は、通常のウェブサーバ用、独自ドメインのウェブサーバ用、携帯版のウェブサーバ用にそれぞれ、各サーバのログを分解する正規表現、各サーバで発行するワンライナーの集計プログラムを別モジュールにして、組み合わせて使っています。限定した機能をもつ小さいモジュールに分解することで、ユニットテストがしやすく、ソースコードやプログラム全体の見通しもよくなります。


本当はここで、「そしてこれが僕の自作したアクセスログ集計プログラムです」とソースコードを公開したいところなのですが、いまのお仕事に特有の部分があるのでちょっと公開は難しそうで、残念です。といっても特有なところなんて、アクセスログのフォーマットくらいなんですけどね。


そんなこんなで、小さいフィルタスクリプトからはじめていまではちょっとしたフレームワークっぽいモノまで進化させてきましたが、やはりオレオレで突っ走るのはどうもあまりいい手とはいえない気がしてきたので、最近は App::Hachero に注目しています。僕の自作したものより設計がスマートで見通しがよく、プラガブルになっているので柔軟さも得られそうです。現在の仕組みは単一のサーバ上で実行するようになっていそうで、分散処理をしたければ Hadoop 等と併用することで力を発揮できるようです。このあたり、僕の作ったものとは前提条件となる環境に違いがあるようで、自作のものと置き換えるには少し工夫が必要そうですが、ソースコードを読んだり、実際に動かしてみたりして検証を重ねていきたいと思っています。 Hadoop も導入してみたいですが、さすがに大きいのでちょっと難しいかな。。

画像テスト

記事検索
プロフィール

刺身☆ブーメラン

訪問者数
  • 今日:
  • 昨日:
  • 累計:

リラックマ時計
  • ライブドアブログ