apache

2011年12月14日

Apache の並列数を CPU コア数に応じて決定する

はてなブックマークに登録

KLab Advent Calendar 2011 「DSAS for Social を支える技術」の10日目です。

昨日の記事 では並列数を設定する基本的な方法を紹介しました。 今日は実際に DSAS for Social で利用している設定方法を紹介します。

背景

実際の並列数の設定はマシンのCPUスペックやアプリの特性(レスポンスタイムの 何割をWebサーバーのCPUを使う処理が占めているか)に応じて設定するのですが、 DSAS for Social ではアプリの負荷に応じて柔軟にWebサーバーを 追加・削除するので、CPUスペックが一定ではありません。 具体的に言えば、 Core2 世代の4コアサーバーと、Core i7世代の4コア8スレッド サーバーが Web サーバーとして利用されています。

でも、Webサーバーごとに違う設定ファイルを用意したくはありません。 なんとかできないかと思っていたところ、 @hamano が httpd.conf 内で環境変数が 使えるよ、と教えてくれました。

CPUコア数から並列数を計算し設定する

DSAS for Social では daemon tools を使って Apache を起動しています。 なので、 run スクリプトでコア数に応じて並列数を計算します。

あるMVCフレームワークを使ったCPU使用率の高いphpアプリでは、 コア数+2 に設定しています。(Core2世代4コアなら6並列、Core i7世代4コア8スレッド なら10並列になります)

CPUCOUNT=`getconf _NPROCESSORS_ONLN`
export MAX_CLIENTS=$((CPUCOUNT+2))

次に、httpd.conf でこの環境変数を利用して並列数を設定します。

  MinSpareServers      ${MAX_CLIENTS}
  MaxSpareServers      ${MAX_CLIENTS}
  StartServers         ${MAX_CLIENTS}
  ServerLimit          ${MAX_CLIENTS}
  MaxClients           ${MAX_CLIENTS}

これで Web サーバーを40台まで増やしても MySQL の接続数は400以下に抑えられますし、 実際にCPU使用率が80%以上になってレスポンスタイムが伸びてきても安定して サービスが継続できています。

補足: ProxyPass の connectiontimeout を1秒未満に設定する

昨日の記事で使っていたテスト環境は apache 2.2.10 を利用していたのですが、 connectiontimeout パラメータは 2.2.10 で導入された後、 2.2.11 から ms という サフィックスを付けて1秒未満の値を設定できるようになりました。

ループバックアダプタへの接続は backlog が足りていたら 1ms もかからないので、 "connectiontimeout=10ms" などに設定したらいいと思います。


@methane

klab_gijutsu2 at 18:39|この記事のURLComments(0)TrackBack(0)
2011年12月13日

過負荷をかわす Apache の設定

はてなブックマークに登録

KLab Advent Calendar 2011 「DSAS for Social を支える技術」の9日目です。

前回は php を動かしている Apache の手前にリバースプロキシを 置く必要性を解説しました。 今日は、 その前の php のプロセス数を絞る設定と合わせて、実際に Apache で 設定する方法を紹介します。

以降、 php を動かしている Apache の事をアプリサーバー、リバースプロキシ+ 静的ファイル配信を行っている Apache の事をプロキシサーバーと呼びます。

基本設定

まずは基本的な設定のおさらいです。

アプリサーバー

並列数を絞るには MaxClients を設定します。アプリがどれくらいの時間を CPUの処理で使って、どのくらいの時間を外部リソース待ちに使っているかにも よりますが、だいたいCPU数の1.5倍〜2倍くらいが適当だと思います。 HypertThreading や TurboBoost などがある場合は、論理CPUを使いきらなくても 他のCPUの性能がその分上がるので、CPU数とイコールでも良いかもしれません。

以下の設定例では並列数を2にしています。 (追記: この設定は後述の試験をした2コアサーバー用のものです。DSAS for Social の本番環境用Webサーバーはもっとコア数多いし、DBやMemcachedの待ち時間もあるので、もっと大きい値を利用します)

  MinSpareServers      2
  MaxSpareServers      2
  StartServers         2
  ServerLimit          2
  MaxClients           2

プロキシサーバー

ローカルホストのアプリサーバーのポート(1234としておきます)にリバースプロキシします。 静的ファイルは /static/ に置くことにして、それ以下のパスはプロキシせずに プロキシサーバーが直接返します。

ProxyTimeout を 4 秒に設定することで、5秒ルールを避けます。

# VirtualHost 内
  ProxyRequests Off
  ProxyPass /static/ !
  ProxyPass / http://127.0.0.1:1234/
  ProxyPreserveHost On
  ProxyTimeout 4

プロキシサーバーはほとんどメモリを食わないので、並列数は128とか 256とか大き目に設定しても大丈夫です。

スロットル制御

本題の、過負荷になったら、アプリサーバーにプロキシせずにレスポンスを返す 方法に入りましょう。 アプリサーバーの並列数を使いきっている状況では、リクエストをキューイング しておき、処理中だったリクエストのどれかが完了したら次のリクエストを キューから取り出すようにします。 キューの長さが一定値を超えたら、それ以上はキューイングしないでエラーに すれば良さそうです。

もう判りましたよね?キューとは backlog のことです。

アプリサーバーの設定

キューの長さは、安定してレスポンスを返していたらこれ以上は溜まらない、 という数に設定します。 たとえば秒間50リクエストを安定して捌ける場合、 バックログを 50 にしたら、バックログの最後にキューイングされた リクエストは1秒前後で返されることになります。これで5秒ルールに十分 間に合います。

この記事を書きながら触っている環境は古い2コアサーバーなので、 20 程度にしておきます。

Listen 1234
ListenBackLog 20

プロキシサーバーの設定

少なくとも Linux では、 Backlog に空きがある状況で接続の syn パケットが来ると、 すぐに syn/ack を返します。 プロキシサーバーとアプリサーバーの間はループバックアダプタ経由で接続されているので、 パケットロスの心配はありませんし、並列数も絞っていてロードアベレージが 極端に増えることもないので、 1ms もかからずに接続が成功するはずです。

一方、 backlog がいっぱいになった場合、 syn に対して syn/ack が返らないので、 通常は接続元であるプロキシサーバーが 3秒ほど待ってから syn を再送するはずです。 connectiontimeout を短い時間に設定することで、接続できなかったときにすぐに エラーを返すことができます。

connectiontimeout を設定するには、上で行った ProxyPass ディレクティブに パラメータを追加します。とりあえず 1 秒に設定しておきましょう。また、接続がエラーになっても リクエストの転送が止まらないように、 retry=0 もセットで設定しておきます。

  ProxyPass / http://127.0.0.1:1234/ connectiontimeout=1 retry=0

効果確認

スロットル制御の動作確認をしましょう。こんな php ファイルを用意しておきます。
<?php
$prime = array();
$i = 2;
while (count($prime) < 1000) {
    $ok = true;
    foreach ($prime as $p) {
        if ($i % $p == 0) {
            $ok = false;
            break;
        }
    }
    if ($ok) {
        $prime[] = $i;
    }
    $i++;
}
まずは、1並列で実行してみます。
$ ab -n100 -c1 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php
...
Concurrency Level:      1
Time taken for tests:   10.129 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
...
Percentage of the requests served within a certain time (ms)
  50%    101
  66%    101
  75%    101
  80%    102
  90%    102
  95%    102
  98%    103
  99%    117
 100%    117 (longest request)

だいたい100msちょっとでレスポンスが返ってきますね。 テストに使っているのは2コアのサーバーで、アプリサーバーは2並列なので、 2並列で負荷をかけてもレスポンスタイムは少ししか増えないはずです。

$ ab -n100 -c2 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php
...
Concurrency Level:      2
Time taken for tests:   5.771 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
...
Percentage of the requests served within a certain time (ms)
  50%    102
  66%    102
  75%    103
  80%    104
  90%    198
  95%    202
  98%    207
  99%    208
 100%    208 (longest request)

ベンチマーク用に用意した専用環境ではなくて共有のテスト環境を使っているので、 一部のリクエストが遅くなっているのはご了承ください。

次は、4並列で負荷をかけてみましょう。2リクエストを並列処理して2リクエストが backlog に溜まるので、平均レスポンスタイムは2倍くらいに増えるものの、 ちゃんとエラーにならずに全部のリクエストを処理できるはずです。

$ ab -n100 -c4 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php
...
Concurrency Level:      4
Time taken for tests:   6.282 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
...
Percentage of the requests served within a certain time (ms)
  50%    205
  66%    207
  75%    210
  80%    304
  90%    440
  95%    450
  98%    476
  99%    477
 100%    477 (longest request)

うまく行きましたね。次は、 backlog と同じ 20 並列で負荷をかけてみましょう。 最悪のレスポンスタイムが 100ms X 20 で 2秒くらいになりますが、ぎりぎりでエラーには ならないはずです。 (リクエスト数も100から200に増やします)

$ ab -n200 -c20 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php
...
Concurrency Level:      20
Time taken for tests:   12.485 seconds
Complete requests:      200
Failed requests:        0
...
Percentage of the requests served within a certain time (ms)
  50%   1042
  66%   1083
  75%   1324
  80%   1559
  90%   2002
  95%   2011
  98%   2017
  99%   2034
 100%   2052 (longest request)

さて、次は倍の40並列です。2リクエストが処理中になり20リクエストが backlog にたまりますが、以降はアプリサーバーがレスポンスを返して新しいリクエストを 受け取るまでは接続できないので、エラーになるリクエストが出はじめるはずです。

$ ab -n200 -c40 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php
...
Concurrency Level:      40
Time taken for tests:   6.223 seconds
Complete requests:      200
Failed requests:        83
...
Percentage of the requests served within a certain time (ms)
  50%   1026
  66%   1119
  75%   1130
  80%   1133
  90%   1138
  95%   1224
  98%   2117
  99%   3187
 100%   3191 (longest request)

20並列の時と比べると、200リクエスト中83リクエストがエラーになり、 テストにかかった時間も 12.5秒から 6.2秒と半分程度になりました。 backlog を超えたリクエストを「処理せずにエラーを返す」ことで、 過負荷状況下でも「秒間正常レスポンス数」を維持していることが解ります。

終わりに

以前は、 MySQL の set コマンドで設定できるパラメータを変更したことはあっても、 Apache の設定は完全に「インフラの人」にお願いしていました。 自分で Apache の設定を触るきっかけになったのは チューニンガソンに出場して、 自分でもサーバーのチューニングができるんだという自信がついたからです。 チューニンガソンを開催してくださった運営・スポンサーの方、ありがとうございました。


@methane

klab_gijutsu2 at 22:51|この記事のURLComments(0)TrackBack(0)
2011年12月12日

高負荷でも安定したサービスを提供するためのリバースプロキシ

はてなブックマークに登録

KLab Advent Calendar 2011 「DSAS for Social を支える技術」の8日目です。

前回は php のプロセス数を絞ることのメリットを解説しました。

プロセス数を絞るには FPM を使うなどの方法もありますが、 DSAS for Social では php は Apache + mod_php を使っていて、 それにリバースプロキシを組み合わせて利用しています。 今日はこのリバースプロキシの役割を説明して行きます。

以降、リバースプロキシのことを単にプロキシと呼びます。

プロキシを使う理由

そもそも、なぜプロキシを使うのかを説明しておきます。

5秒ルール

ケータイ向けのソーシャルアプリでは、ユーザーからのリクエストは 一旦プラットフォームのサーバーを経由して、アプリを提供している Webサーバーに到達します。

このとき、アプリ側のレスポンスがあまりに遅いとプラットフォーム側の リソースを浪費してしまうので、プラットフォーム側は5秒でアプリへの アクセスをタイムアウトさせ、タイムアウトが頻発すると自動的に そのアプリを停止状態にしてしまいます。

なので、何らかの理由でレスポンスを5秒以内に返せない場合は、 強制的にタイムアウトしてレスポンスを返す必要があります。 そのためにプロキシサーバーのタイムアウトを利用しています。

並列接続への対応

前回 php のプロセス数を絞る設定のメリットを紹介しましたが、 Apache + mod_php で動いているサーバーの並列数を単純に絞って しまうと、たとえば通信速度が遅いクライアントとの通信などが あったときに並列数をすぐに使いきってしまいます。

並列数を多めに設定したプロキシと並列数が少ないphp用Webサーバーの 2段構成にすれば、phpのレスポンスはクライアントに転送される前に プロキシ上のバッファに載るので、クライアントへの転送の完了を待たずに phpが次のリクエストの処理を開始できます。

なぜプロキシに Apache を使うのか

いま人気の nginx などを使わない理由は、単に使う理由がないからです。 DSAS for Social では負荷分散は LVS で行っており、プロキシはアプリ用の Apache と同じ Web サーバー上で動いています。つまり、プロキシが 捌かなければいけないリクエスト数は、負荷分散後の各Webサーバーへの リクエスト数だけになります。

nginx はその速度やC10K対応で人気があると思うのですが、この構成では 速度面のメリットは php が遅いために全く活かせません。 Comet などの大量並列接続を必要とするアプリにはそもそも php は使い物に ならないので、 php のアプリを動かす Apache の前に置く プロキシは Apache でも全く問題がありません。

プロキシ設定で抑えておきたいポイント

静的ファイルはプロキシ側で配信する

静的ファイルをphpと同じApacheで配信した場合、その内容が一旦 プロキシのバッファに乗ってからクライアントに転送されるので、 メモリを大量に消費してしまいますし、大量のメモリコピーや データのプロセス間転送が発生するので静的ファイルの配信速度が 低下してしまいます。

そして、静的ファイルのサイズがプロキシのバッファとソケットバッファの 合計を超えた場合は、phpを動かしているApacheのレスポンスが待たされ、 phpが次のリクエストを処理できなくなってしまいます。

アクセスを捌ききれなくなったらすぐにエラーを返す

4000リクエスト/sec しか捌けないのに 4100リクエスト/sec のペースでリクエストが来た時、単にリクエストをphpの動いている Apache に転送してタイムアウトを設定するだけだと、ほとんどの リクエストがタイムアウトになってしまい、サイトがほぼ落ちている 状態になってしまいます。

これは、プロキシがタイムアウトさせたリクエストでも、phpは処理を 続け、CPUやMySQLなどの処理能力を使用し続けているからです。

秒間4100リクエストのうち捌ける4000リクエストは正常に処理し、 それを超える100リクエストだけをエラーにするには、プロキシ側が スロットルの役割を果たして、過負荷になったときに php が動いている Apache にリクエストを転送しないでエラーを返す必要があります。

時間が無くなってしまったので、今日はこのへんにして、 実際の設定方法は次回に回します。


@methane

klab_gijutsu2 at 22:29|この記事のURLComments(0)TrackBack(0)
2011年12月09日

php のプロセス数を絞ろう

はてなブックマークに登録

KLab Advent Calendar 2011 「DSAS for Social を支える技術」の7日目です。 @methane の新シリーズは Apache+php のチューニングです。

今日のお題は、タイトルのとおり、phpのプロセス数(=並列数)を減らすことです。 これはチューニンガソンでも人気のチューニングだったのですが、 今日はそのメリットをまとめます。

ロードアベレージが下がる

プロセス数をコア数+α程度に抑えると、ロードアベレージがコア数の数倍〜 数十倍になることがなくなります。

例えばロードアベレージがコア数の100倍になると、1リクエストの処理に かかる時間は100倍以上に増え、せっかく処理したのにクライアント側が タイムアウトしていて完全に無駄骨になったり、最悪では再リクエストが来て さらに負荷が上がる負のスパイラルに陥る可能性があります。

たくさん一気に処理しようとしてアップアップするよりも、 能力の範囲内の処理だけを全力で実行し、次のリクエストは今のリクエストが 終わってから実行するほうが健全です。

並列数を絞ると、処理待ちのリクエストが増えて行きますが、このあたりについては 次回に回します。

コンテキストスイッチが減る

実行可能なプロセス数が多いと、IO待ち以外でもコンテキストスイッチが 発生するようになります。コンテキストスイッチが発生すると、OSの処理も 発生しますし、TLBなどのキャッシュが無効になったりするので、パフォーマンスに 影響します。

通常のマシンではコンテキストスイッチの負荷はphpの処理自体の負荷に比べて 圧倒的に軽いのですが、仮想マシンを使うと実マシンにくらべてオーバーヘッドが 増える傾向があります。 チューニンガソンで並列数を絞る設定が定番なのも、仮想環境でのチューニング だからです。

MySQLの接続数やロック時間、その他のリソースの消費が減る

MySQL への接続を永続化している場合、並列数がそのままMySQLの接続数に なります。

リクエスト毎にMySQLに接続する場合も、phpが最初のほうでMySQLに接続し 最後のほうで切断する場合は、平均処理時間[sec]×秒間リクエスト が MySQL への平均接続数になります。例えば応答速度が 0.1sec で秒間リクエスト数が 4000なら、平均接続数は400になります。

同じことが、トランザクションやロックなどについても言えます。 OSのスケジューラは、phpがMySQLに接続しているかどうかや、ロックを持っているか どうかなどを考慮せずにスケジューリングするので、トランザクション中のphpの 実行を止めて新規リクエストの処理をするプロセスを実行してしまうことが あるからです。

MySQL の接続数や負荷の上限を抑えられる

接続数の上限は並列数になるので、MySQLの max_connections を並列数 (1台あたりの並列数xサーバー台数)以下に設定することで、 接続数をオーバーしなくなります。

MySQLの最大接続数を超えた時、MySQL は Too Many Connections というエラーを 返して接続をすぐに切断するのですが、このエラーが起きたときに php で どう処理していいのかは難しい問題です。バッチなどでコンマ数秒程度重いだけなら リトライすれば良いのですが、MySQLが過負荷になっているときにリトライすると 問題を拡大します。

また、過負荷でMySQLに接続できないときにユーザーにエラーページを返すと、 そのユーザーがリロードして、さらに負荷が増えるという悪循環になることも あります。

並列数を絞った場合は、 MySQL の応答速度が低下した場合はどんどん MySQLに接続するのではなく新規リクエストの処理を待たせることになります。 バッチなどによる短時間の問題の場合は一瞬レスポンスタイムが低下する だけで一切エラーを発生させずにちゃんと捌けます。 MySQLが過負荷に陥った場合も、どんどんMySQLに負荷をかけたあげく タイムアウトで全部のリクエストをエラーにするのではなく、 新規リクエストを php でも MySQL でも処理せずにエラーにすることで、 処理できる範囲のリクエストにはきちんとレスポンスを返すことができます。

他の性能の上限が決まっているバックエンドのリソースについても同じことが言え、 全部のリクエストを処理しようとして過負荷になって全部タイムアウトするのではなく、 性能限界まではきちんと処理を行いレスポンスを返しつつ、上限を超える部分は 一切負荷をかけないで最初からエラーにできます。

まとめ

並列数削減は、安いレンタルサーバーでメモリ消費を抑えるための設定として 紹介されることが多いですが、大量にメモリを積んだ高性能なWebサーバーでも 大きな利点があります。

次回は、phpの並列数を超えたアクセスをどう待たせ、待ち時間が増えたときに どう処理せずにエラーを返すかの解説をしながら、実際の Apache の設定を 紹介します。


@methane

klab_gijutsu2 at 23:01|この記事のURLComments(0)TrackBack(0)
2010年02月26日

Apache 2.3/2.4系に実装中の新機能をちょっと先取りして見てみよう その2

はてなブックマークに登録

さて、先日のエントリでmod_auto_formとmod_sessionを用いたApacheでの新しい認証機構について紹介しましたが、今回はこれを実際に設定して動作を確認してみることにしましょう。

(※以下、バージョンは当記事執筆現在の最新です)

ビルド・インストール

まずはApache 2.3.5のビルドです。http://httpd.apache.org/から、"Apache 2.3.5-alpha Released"の欄を見つけたら、"Download"のリンクをたどってtarballを取得します。なお、従来であれば一緒に入っていたAPR(Apache Portable Runtime)とAPR-utilがありません。バージョン指定もそれぞれ1.3.0以上を要求しますので、インストール済みでなければそれぞれhttp://apr.apache.org/から個別に取ってくる必要があります。今回は、このライブラリ2つともhttpd本体にバンドルした形でビルドを進行することとします。ダウンロードが完了したら、srclibディレクトリ内に解凍して配置してください。

$ tar jxvf httpd-2.3.5-alpha.tar.bz2
$ cd httpd-2.3.5-alpha/srclib
$ tar jxvf ../../apr-1.4.2.tar.bz2
$ mv apr-1.4.2 apr
$ tar jxvf ../../apr-util-1.3.9.tar.bz2
$ mv apr-util-1.3.9 apr-util

ビルドは定番のconfigure & makeですが、configureの引数には--with-included-aprと--with-mysqlを最低限指定します。これは、APRとAPR-utilをsrclib配下に置いてビルドするのと、後述するユーザ認証用DBをmysqlを用いて作成するためです。あとは必要があれば適宜お好みで追加してください。既にApacheがインストールしてあるサーバで行う時は、--prefixオプション等を利用して、インストール先が衝突しないように調整してください。

$ ./configure --enable-mods-shared=all --with-included-apr --prefix=/usr/local/app/apache-2.3.5 --with-mysql
$ make
# make install

make installまで来たら、httpd.confを調整して起動を確認できればOKです。

設定・仮サイト構築

さて、いよいよ本題、mod_auto_form周りの設定から入りましょう。前に挙げたページ遷移図のおさらいです。

chart_auth

それぞれ、1)から5)までのページ(URL)を下記の通りとします。前回その1の例でのファイル名と同じですね。マイページについては、認証による保護がなされ、未ログインの状態でのアクセスは許可しないものとします。

  • 1) /login.html
  • 2) /doauth
  • 3) /mypage/(index.html)
  • 4) /dologout
  • 5) /logout.html

続いてHTMLファイルを作ります。ページ遷移の再現ができればいいので、必要最小限の内容しか置きません。見栄えは気にしないで下さい;-)。

1) /login.html
<html>
<body>
<form method="POST" action="/doauth">
Username: <input type="text" name="httpd_username" value="" />
Password: <input type="password" name="httpd_password" value="" />
<input type="submit" name="login" value="Login" />
</form>
</body>
</html>
3) /mypage/(index.html)
<html>
<body>
<a href="/dologout">logout</a>
<br>
<a href="news.html">news</a>
</body>
</html>
5) /logout.html
<html>
<body>
<a href="login.html">login</a>
</body>
</html>

3)のマイページに"news"というリンクがありますが、これはマイページからログアウト以外の遷移を行う為のものです。つまり、認証による保護を受けた状態のままでのページ遷移を再現したいという意図です。内容は何でも構わないので、下記のようにしておきます。

3.1) /mypage/news.html (ニュースページ)
<html>
<body>
<a href="index.html">index</a>
<br>
</body>
</html>

HTMLファイルは以上で揃いました。続いてmod_auth_formの設定に移りましょう。

<Location /doauth>
  SetHandler form-login-handler
  AuthFormLoginRequiredLocation /login.html
  AuthFormLoginSuccessLocation /mypage/
  AuthFormProvider dbd
  AuthDBDUserPWQuery "SELECT password FROM user WHERE user = %s"
  AuthType form
  AuthName realm
</Location>

<Location /dologout>
  SetHandler form-logout-handler
  AuthFormLogoutLocation /logout.html
  AuthName realm
</Location>

/doauthおよび/dologoutについて、SetHandler、AuthFormLoginRequiredLocationおよびAuthFormLoginSuccessLocationの指定までは前出の通りです。

AuthFormProviderは、この認証でユーザID・パスワードの組をどのストレージで保持するかを指定するものです。dbdはデータベースですが、他にもfile(htpasswdで生成するおなじみのパスワードファイル形式)やldapがあります。データベースにユーザテーブルを作成する場合、mod_authn_dbdやmod_dbdの設定がさらに必要になります。DBDriverでデータベースの種類を選び、DBParamsで接続パラメータ(DB名、DBユーザ名やパスワード、サーバアドレスなど)を指定します。

DBDriver mysql
DBDParams "dbname=apache user=apache pass=xxxxxx host=localhost"

各パラメータは必要に応じて適宜修正してください(DBユーザは、指定のデータベースに対してSELECT,INSERT,UPDATE,DELETE権限をGRANTされているものと仮定します)。そして前出のAuthDBDUserPWQueryと合わせて、ユーザテーブルをCREATE TABLEします。mysqlコマンドからサーバに接続して、以下のSQL文を発行してください。

CREATE TABLE user (user CHAR(32) PRIMARY KEY, password CHAR(32));
INSERT INTO user (user, password) VALUES ('hoge', '****');

****の部分は、実際にはhtpasswdでエンコードされたパスワード文字列が入ります。htpasswdで通常作成するパスワードファイルが、各行を':'で区切ってテーブルに登録することと同じ、という風に考えて頂ければ良いと思います。

# htpasswd -nb hoge fuga
hoge:****  ← 「ユーザID:パスワード」の形式

さて、ここで一度設定を生かして動作を見てみましょう。

login

ユーザIDとパスワードを正しく入力してログインボタンを押すと、マイページに遷移します。アクセスログで確認しても、想定通りになっていますね。

192.168.0.201 - - [25/Feb/2010:14:03:39 +0900] "GET /login.html HTTP/1.1" 200 257
192.168.0.201 - hoge [25/Feb/2010:14:03:42 +0900] "POST /doauth HTTP/1.1" 301 216
192.168.0.201 - - [25/Feb/2010:14:03:42 +0900] "GET /mypage/ HTTP/1.1" 200 95
192.168.0.201 - - [25/Feb/2010:14:05:16 +0900] "GET /dologout HTTP/1.1" 307 222
192.168.0.201 - - [25/Feb/2010:14:05:16 +0900] "GET /logout.html HTTP/1.1" 200 61

また、誤ったユーザIDないしパスワードを入れると、マイページへは遷移せずに再度ログインページに戻っています。

次に試しにログアウトした状態で、マイページに直接アクセスしてみましょう。認証で保護されなければいけませんから、ログインページに飛ばされて欲しいところですが...。

mypage

見えちゃいますね。

これはhttpd.conf中で/mypage/配下の設定をしていませんから、mod_auth_formの働きようがないためです。というわけで、追加しましょう。

<LocationMatch "/mypage/*">
  AuthFormProvider dbd
  AuthDBDUserPWQuery "SELECT password FROM user WHERE user = %s"
  AuthFormLoginRequiredLocation /login.html
  AuthType form
  AuthName realm
</LocationMatch>

内容は/doauthに似ています。mod_authn_dbdでユーザ認証を通すという仕組みは一緒だからです。しかし、/doauthではHTTPレスポンスが/mypage/へのリダイレクトとして固定されていたのに対し、ここではHTTPリクエストでのページをそのまま表示させるという挙動を取りますので、AuthFormLoginSuccessLocationは設定しません。SetHandlerも不要です。さて、これでどうでしょうか。

192.168.0.201 - hoge [25/Feb/2010:14:25:47 +0900] "POST /doauth HTTP/1.1" 301 216
192.168.0.201 - - [25/Feb/2010:14:25:47 +0900] "GET /mypage/ HTTP/1.1" 301 219
192.168.0.201 - - [25/Feb/2010:14:25:47 +0900] "GET /login.html HTTP/1.1" 200 257
192.168.0.201 - hoge [25/Feb/2010:14:25:49 +0900] "POST /doauth HTTP/1.1" 301 216
192.168.0.201 - - [25/Feb/2010:14:25:49 +0900] "GET /mypage/ HTTP/1.1" 301 219
192.168.0.201 - - [25/Feb/2010:14:25:49 +0900] "GET /login.html HTTP/1.1" 200 257
192.168.0.201 - hoge [25/Feb/2010:14:25:51 +0900] "POST /doauth HTTP/1.1" 301 216
192.168.0.201 - - [25/Feb/2010:14:25:51 +0900] "GET /mypage/ HTTP/1.1" 301 219
192.168.0.201 - - [25/Feb/2010:14:25:51 +0900] "GET /login.html HTTP/1.1" 200 257

おかしなことになりました。こんどは、ログインフォームでどんなユーザID・パスワードを入れても、ずっと同じログインフォームに戻って来てしまいます。上はそのときのアクセスログです。一体どうなっているのでしょう。

…しらじらしかったですね;-)。はい、大変お待たせしました。ここでようやくmod_sessionの出番です。下のような設定をそれぞれ追加してください。

DBDPrepareSQL "DELETE FROM session WHERE session_key = %s" deletesession
DBDPrepareSQL "UPDATE session SET session_value = %s, session_expire_time = %lld WHERE session_key = %s" updatesession
DBDPrepareSQL "INSERT INTO session (session_value, session_expire_time, session_key) values (%s, %lld, %s)" insertsession
DBDPrepareSQL "SELECT session_value FROM session WHERE session_key = %s AND (session_expire_time = 0 OR session_expire_time > %lld)" selectsession
DBDPrepareSQL "DELETE FROM session WHERE session_expire_time != 0 AND session_expire_time < %lld" cleansession
<Location /doauth>
  ...
  Session On
  SessionDBDCookieName session path=/
</Location>

<Location /dologout>
  ...
  Session On
  SessionDBDCookieName session path=/
</Location>

<LocationMatch "/mypage/*">
  ...
  Session On
  SessionEnv On
  SessionDBDCookieName session path=/
</LocationMatch>

一緒に、先ほどユーザテーブルを作ったデータベースにやはりSQL文を発行し、セッションデータ保持のためのテーブルを作成します。

CREATE TABLE session (session_key CHAR(250) PRIMARY KEY, session_value TEXT, session_expire_time BIGINT);

これで期待通りの挙動をするようになりました。ユーザID・パスワードの判定およびマイページの認証保護も正規の動作になりました。マイページからニュースページ(/mypage/news.html)への遷移も試してみて下さい、マイページと同じように、認証状態であれば普通に遷移し、未認証状態であればログインページに飛ぶのが確認できると思います。

192.168.0.201 - - [25/Feb/2010:14:44:10 +0900] "GET /login.html HTTP/1.1" 200 257
192.168.0.201 - - [25/Feb/2010:14:44:10 +0900] "GET /favicon.ico HTTP/1.1" 404 209
192.168.0.201 - hoge [25/Feb/2010:14:44:18 +0900] "POST /doauth HTTP/1.1" 301 216
192.168.0.201 - hoge [25/Feb/2010:14:44:18 +0900] "GET /mypage/ HTTP/1.1" 200 95
192.168.0.201 - hoge [25/Feb/2010:14:44:26 +0900] "GET /mypage/news.html HTTP/1.1" 200 66
192.168.0.201 - hoge [25/Feb/2010:14:44:27 +0900] "GET /mypage/index.html HTTP/1.1" 200 95
192.168.0.201 - - [25/Feb/2010:14:44:50 +0900] "GET /dologout HTTP/1.1" 307 222
192.168.0.201 - - [25/Feb/2010:14:44:50 +0900] "GET /logout.html HTTP/1.1" 200 61

アクセスログにもご注目ください。/doauth以降、マイページ内の遷移でユーザID"hoge"が保持されたままページ遷移を続けており、/dologoutでそれが消えていることが確認できます。

cookie

Apacheからは、ブラウザに対してセッションIDをCookieとして発行します。このIDに紐づけられた内容はsessionテーブル内に保持されています。そしてこのセッションIDは、SessionEnvディレクティブをOnにしておくことで環境変数HTTP_SESSIONとしてCGI/Webアプリケーションにも引き渡すことができます。CGIスクリプトを使って試してみましょう。

mysql> SELECT * FROM session;
+--------------------------------------+-------------------------------+---------------------+
| session_key                          | session_value                 | session_expire_time |
+--------------------------------------+-------------------------------+---------------------+
| e5c971ba-c6c4-472e-95ec-f9a541125791 | realm-user=hoge&realm-pw=**** |                   0 | 
+--------------------------------------+-------------------------------+---------------------+
1 row in set (0.00 sec)
printenv.cgi
#!/usr/bin/env perl
print "Content-type: text/plain; charset=utf-8\n\n";
foreach $var (sort(keys(%ENV))) {
    $val = $ENV{$var};
    $val =~ s|\n|\\n|g;
    $val =~ s|"|\\"|g;
    print "${var}=\"${val}\"\n";
}

注意点

注意しなければならないのは、上記のsession_valueカラムにはパスワードが生の状態で入ってしまっている点です。目的としてはセッションIDが発行できればよく、パスワードをここに入れておく必要はないので設定でこれを回避したい所ですが、現時点で解決法は見つかっておりません。

また、さらに重要な点がありまして、Apacheがmod_dbdを使ってデータベースとの間でコネクションを張る場合、そのコネクション数はWebアプリケーションとの間で取り合いになるということです。mod_dbdとWebアプリケーションとの間でDBコネクションを共用したり、コネクションプーリングをしたりといった統合的な仕組みはありませんので、それぞれにコネクション数の上限やタイムアウト時間などのDBチューニングを別個に行う必要があります。そのためどっちかがコネクションを取りすぎて無駄にしたり、逆にDBの処理能力を上回るコネクション要求を出してしまったりなど、DBとWebサーバとの間の調整がさらに難しくなることでしょう。

この問題によって支障が発生する場合は、認証・セッション用のDBはWebアプリケーション用のDBとは別に構築するということも考慮すべきかと思います。

まとめ

このように、まだ基本的な動作検証のみで実用的な構成案まではまだまだといったところですが、Apacheにおける認証機構の新しい形態を見ることができました。従来であればBasic認証やDigest認証といった、HTTPプロトコルそのものの認証への対応のみでしたが、一般的なWebサービスで使われているログインフォームを利用した形態にも対応可能となったことは、サイト構築の選択肢をさらに広げることになるのではないでしょうか。


klab_gijutsu2 at 02:48|この記事のURLComments(0)TrackBack(0)
2010年02月24日

Apache 2.3/2.4系に実装中の新機能をちょっと先取りして見てみよう

はてなブックマークに登録

(追記: 続編をupしました。設定例と動作検証を行っています。→こちらへどうぞ)

Apacheといえば、Webサービスを構築する上でだいたいどこかで必ずお世話になるであろう、HTTPサーバのデファクトスタンダードとして幅広く定着したソフトウェアだと思います。KLabでも各所で運用してお世話になっています。

現在、バージョンは2.2系が安定版(stable branch)となっておりますが、開発版である2.3系が既に並行して公開されているのをご存知でしょうか。新しい機能や技術を取り入れて行く為に2.2系との間の互換性を時に犠牲にしつつ、様々なチャレンジが行われているわけです。将来的にはこのバージョンが2.4系となって新しい安定版として提供されることになるでしょう。

このDSASブログでも、Apache 2.3系で取り組まれている新しい機能について着目し、面白そうなものを一つずつ取り上げながら調べて行きたいと思います。

まず最初に、今回はApacheによる認証機能の強化 - mod_auth_formとmod_sessionを取り上げます。

http://httpd.apache.org/docs/2.3/mod/mod_auth_form.html
mod_auth_form - Apache HTTP Server

http://httpd.apache.org/docs/2.3/mod/mod_session.html
mod_session - Apache HTTP Server

SNSやblog、ショッピングサイトなど、ページ閲覧・遷移をユーザごとに紐付ける必要から、多くのWebアプリケーションではユーザ認証の仕組みを必要としています。この認証の仕組みは従来Webアプリケーション側で実装してきました。しかしこれと同じ仕組みを、Webアプリケーションを使わずにApache単独でも構築できるようになるかもしれません。

一般的な認証処理の流れを図示すると下の図のようになります。

chart_auth

太線(1→2、3→4)はユーザのクリックによる遷移、細点線(2→3、4→5)はサーバからのリダイレクト応答(301 Moved Permanentlyないし307 Temporary Redirect)による遷移を表しています。2と4では、CGIなどでログイン・ログアウト処理が内部で行うとします。

mod_auth_formによる認証は、この一連の遷移上の各ページをhttpd.conf内で指定することで構築できます。 

まず、1)ログインページのフォームは以下のようになります。

<form method="POST" action="/doauth">
Username: <input type="text" name="httpd_username" value="" />
Password: <input type="password" name="httpd_password" value="" />
<input type="submit" name="login" value="Login" />
</form>

/doauthのURLが上図における2)認証処理に相当しますので、SetHandlerディレクティブを用いてform-login-handlerを割り当てます。

<Location /doauth>
  SetHandler form-login-handler
  AuthFormLoginRequiredLocation http://example.com/login.html  # 認証失敗、未認証時のリダイレクト先 → 1)
  AuthFormLoginSuccessLocation http://example.com/mypage/      # 認証成功時のリダイレクト先 → 3)
  ...
</Location>

ちなみにAuthFormLoginRequiredLocationディレクティブは、認証によって保護したいURL配下を指定するためにも使うようです。つまり、<Location>や<LocationMatch>で保護するURLツリーを指定し、そこでAuthFormLoginRequireLocationディレクティブで未認証時は1)のログインフォームのページに飛ばすように指定するわけです。

また、4)のログアウト処理も同じように設定します。form-logout-handlerをSetHandlerで割り当てます。

<Location /dologout>
  SetHandler form-logout-handler
  AuthFormLogoutLocation http://example.com/loggedout.html     # ログアウト後のリダイレクト先 → 5)
  ...
</Location>

あと他に細かい設定がいくつか可能ですが、それを含んでもCGIスクリプト等を全く作成していない点に注意してください。静的なHTMLファイルだけで認証機能付きのサイトを構築することができるのです。

で、認証が通ってもその状態を保持できなければHTTPリクエストのたびに同じ処理を繰り返す必要が出てきます。なので、一度認証を通したらセッションIDを発行してログアウトまで有効になるようにしておきたいですね。ここでmod_sessionの出番です。先ほどの/doauthのhttpd.conf設定に次のように関連ディレクティブを追加します。

<Location /doauth>
  SetHandler form-login-handler
  AuthFormLoginRequiredLocation http://example.com/login.html  # 認証失敗、未認証時のリダイレクト先 → 1)
  AuthFormLoginSuccessLocation http://example.com/mypage/      # 認証成功時のリダイレクト先 → 3)
  ...
  Session On
  SessionCookieName session path=/
  SessionCryptoPassphrase secret
</Location>

このようにして、プログラム・スクリプト部分無しで認証機構を持たせることが可能になるということです。残るはユーザ登録の処理についてはですがこちらは確認できておりません。今後の調査次第もしくは実装が追加されるかもしれませんが、そうでなければ各自で作り込む必要がありそうです。


klab_gijutsu2 at 07:05|この記事のURLComments(0)TrackBack(0)
2009年12月22日

ApacheのアクセスログをMessagePack形式で出力するためのモジュールを作りました

はてなブックマークに登録

Apacheモジュールのログ出力、こんどはMessagePack版を作成しました。続いてはこちらをご紹介します。

Apacheのアクセスログを使い、ユーザアクセスの集計やパターン解析などというのは一般にどこでもやられていることだと思います。通常のアクセスログはテキストファイルなので、集計を行うためにスクリプト上で扱える変数・オブジェクト化が必要になりますね。1行ごとの各ログ項目を取り出すのに正規表現を使ったり、cutやawkなどを使い空白で分割するなど、色々工夫されていることと思います。

今回、MessagePack版のアクセスログ出力をやってみようと思い立ったのは、アクセスログをあらかじめ構造化済みの状態で保存しておければ、読み込みの際の解析する手間を省くことで解析処理の高速化が期待できるのではないか、そう考えたためです。MessagePackであれば、PythonやRubyはじめ様々なスクリプト言語でのバインディングを備えており、取り扱いも簡単ですから、ファイル読み込みからログデータの取り出しまでを速やかに行い、その分集計処理の実装に集中することができるわけです。また、時折イレギュラー的に長かったり、予期しない文字がリクエストに入る等したためにログ解析の正規表現がエラーになったりすることがありますが、構造化済みの形式で保存するのであれば、この問題も回避できます。

何より、元々構造化されたデータがアクセスログ上では一度文字列になり、これを解析する時に再 度解釈処理されるという二度手間になっているわけで、テキストデータ化せずデータ構造を保った バイナリとしておくのはこの手間を回避する目的も持っています。

ソースはこちらです。
http://log.blog.klab.org/support/mod_log_msgpack/mod_log_msgpack-0.1.0.tar.gz

***

それでは、実際にどのような変更を加えたのでしょうか。モジュールの中身を見てみましょう。

mod_log_msgpackは、従来型のログ出力モジュールであるmod_log_configからその多くを受け継いでいます。mod_log_configの構造がそのまま、mod_log_msgpackにも当てはまるとお考えください。

mod_log_configは、起動直後にLogFormatで定義された書式文字列を読み込み、アクセスログに出力する一行分の内容を定義します。例えば、

LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" combined

このようなログ書式の指定があったとします。mod_log_configは、このディレクティブを解釈して下記のような内部的なデータ構造に分解して落とすのです。http: //httpd.apache.org/docs/2.2/ja/mod/mod_log_config.html#formats を参考にしながら、当てはめてみるとこうなります。

(mod_log_config)
%hリモートホスト(を出力する関数をコールバック)
" "空白
%l(identdからもし提供されていれば)リモートログ名
" "空白
%uリモートユーザ
" "空白
%tリクエストを受け付けた時刻
" \""空白+「"」(ダブルクオテーション)
%rリクエストの最初の行
"\" "「"」+空白
%>s最後のステータス
" "空白
%bレスポンスのバイト数。HTTPヘッダは除く。
" \""空白+「"」(ダブルクオテーション)
%{Referer}iReferer:ヘッダの内容
"\" \""「"」+空白+「"」
%{User-agent}iUser-agent:ヘッダの内容
"\""「"」

このように、%?の箇所とそれ以外(空白等、置き換えない文字列)とでそれぞれ区切り、小片に分けられます。%で始まる項目は対応するコールバック関数を通し、指定のリクエスト情報を文字列に変換します。つまり、リクエスト情報は一旦LogFormatの項目数分(今回18個)の部分文字列に変換され、最後にこれらをすべて連結してログ1行分として出力しています。

mod_log_msgpackではこの部分を利用、アクセスログ1行をMessagePackのarray1個として置き換えて下記の項目のみ保持することとしました。

(mod_log_msgpack)
%hリモートホスト(を出力する関数をコールバック)
%l(identdからもし提供されていれば)リモートログ名
%uリモートユーザ
%tリクエストを受け付けた時刻
%rリクエストの最初の行
%>s最後のステータス
%bレスポンスのバイト数。HTTPヘッダは除く。
%{Referer}iReferer:ヘッダの内容
%{User-agent}iUser-agent:ヘッダの内容

つまり、用があるのは%?の項目のみということにして、それ以外の固定文字列に関しては無視して います。%rや%iについていた「"」での囲みも、項目の内容が空白を含みうることを想定して区切 りとしての空白と区別がつかなくなることを回避するためのものなので、arrayとして分割できて あれば、不要なものとなるわけです。

さらに、これらのコールバック関数はrequest_rec構造体(と、%{…}?となったときの{}の中身)を引数にとり、部分文字列を返値とするわけですが、msgpack_packer構造体を渡す引数を1つ追加、ここにログの内容を順次packしていく処理に切り替えています。

static const char *log_remote_host(request_rec *r, char *a)
{
    return ap_escape_logitem(r->pool, ap_get_remote_host(r->connection,
                                                         r->per_dir_config,
                                                         REMOTE_NAME, NULL));
}

こちらが元々のmod_log_config版。"%h"に対応するコールバックです。mod_log_msgpackになりま すと下のように変更になりました。

static int _strpack(const char* s, msgpack_packer* pk)
{
    if (s) {
        size_t n = strlen(s);
        msgpack_pack_raw(pk, n);
        msgpack_pack_raw_body(pk, s, n);
    } else {
        msgpack_pack_nil(pk);
    }
    return 0;
}

static int log_remote_host(request_rec *r, char *a, msgpack_packer* pk)
{
    const char* str = ap_escape_logitem(r->pool, ap_get_remote_host(r->connection,
                                        r->per_dir_config,
                                        REMOTE_NAME, NULL));
    return _strpack(str, pk);
}

log_remot_host()の第3引数が追加され、msgpack_pack_xxxの関数が呼び出されていることが確 認できると思います。

あとはここまでログ内容を集めたmsgpack_packer構造体を、ファイルに書き出す関数 apr_file_write()に渡して書き出しを行います。渡す際、mod_log_msgpackであれば行っている部分文字列の連結の箇所がありますが、mod_log_msgpackでは不要なので省略しています。

static apr_status_t ap_default_log_writer( request_rec *r,
                           void *handle,
                           const char **strs,
                           int *strl,
                           int nelts,
                           apr_size_t len)

{
    char *str;
    char *s;
    int i;
    apr_status_t rv;

#if 0
    // ここから、部分文字列の連結になるので
    str = apr_palloc(r->pool, len + 1);

    for (i = 0, s = str; i < nelts; ++i) {
        memcpy(s, strs[i], strl[i]);
        s += strl[i];
    }
    // ここまでスキップ。
#else
    str = *strs;
#endif

    rv = apr_file_write((apr_file_t*)handle, str, &len);

    return rv;
}
***

おおよその変更の概要は以上です。あとは、mod_log_configとの併存を可能にするためにいくつか細かい変更を加えていますが、次回以降に続くということで^^;。


klab_gijutsu2 at 17:39|この記事のURLComments(0)TrackBack(0)
2009年10月22日

Apacheのアクセスログをsyslog経由で出力するためのモジュールを作りました

はてなブックマークに登録

皆さんは、負荷分散環境でのApacheのアクセスログをどのように取り扱ってますか?

通常、Apacheのログは動作サーバ上のローカルファイルとして出力されるので、 Webサーバを同時に何台も稼働させて負荷分散を行うような環境では、それらすべ てのWebサーバのログファイルを集めなければなりません。ローカルファイルとし て出力されるということは、Webサーバの台数分だけログファイルがばらけること を意味します。考えるだけでめんどくさいですね。

KLabでは、このApacheログを2パターンを使い分けて集めています。ひとつは syslogによるリモート出力を使い、全Webサーバからのログ出力を一か所に集中さ せる方法です。これは、CustomLogディレクティブにloggerコマンドを使用するこ とで可能です。


    CustomLog "|/usr/bin/logger -p local6.info --" (書式文字列)

これにより、通常ファイル出力されるアクセスログをloggerコマンド経由でsyslog に渡すことができます。あとはsyslogの設定で、リモートのログ集積サーバに集め させるというわけです。

ただし、これには問題もあります。syslogのログ転送はUDPやバッファリングなど の関係で取りこぼしがどうしても発生してしまう点です。いくつか対策は考えられ ますが、私たちはこれに従来型のファイル出力のログを併用することで解決してい ます。これが2パターンの2つめで、すなわち従来のように各Webサーバ上で出力す るログファイルを日次バッチで集積するという方法です。詳細なログ解析などはこ の日次の集積バッチによるログファイルを使用し、その日その日の緊急のアクセス 確認などはsyslog出力の側を見る、という使い分けを行っているのです。

***

さて、このsyslog出力について、もう一度CustomLogを確認します。loggerコマン ドへのパイプを用いていますので、httpdの子プロセスとしてloggerプロセスが多 数立ち上がることになります。

しかしふと考えてみると、syslogに送るって言ってもやることはsyslog()関数を 呼ぶくらい(他にもopenlog()とかありますが)なので、わざわざ外部プロセスにし なくても、Apache本体から出力できたっていいよね、と考えたわけです。

ということで、Apacheから直接syslogにアクセスログを出力するためのApacheモ ジュールを一つ作ってみました。

  • mod_syslog-0.1.0.tar.gz
  • syslogに送る設定は、以下のようにCustomLogを変更して行います。

    
        CustomLog syslog:foo (書式文字列)
    
    

    "syslog:"をプレフィックスとして付加することで、mod_syslogがsyslogへの出力 であることを判別して処理を行いますが、パイプやファイルパス形式、つまり "syslog:"が付かない場合は引き続き従来と同じ処理が行われます。

    ***

    mod_syslogのやることは至ってシンプルです。Apacheは、ログ出力処理のための関 数2つを下に表わすように定義しています。

      static ap_log_writer_init* ap_log_set_writer_init(ap_log_writer_init *handle);
      static ap_log_writer* ap_log_set_writer(ap_log_writer *handle);
    

    ap_log_set_writer_init()は、ログ出力のための初期化処理(ファイルのオープン など)を行うための関数、またap_log_set_writer()は、実際のログ出力を行う際に 呼ばれる関数をそれぞれコールバック登録します。あとは、Apacheがそれらのコー ルバック関数を必要なタイミングで呼び出してくれます。

    さらに上記の関数2つは返値を持ち、それぞれのコールバック関数の事前の値を表し ます。mod_syslogは"syslog:"のプレフィックス以外のログ出力先、すなわちパイプ やファイル出力については従来の処理に任せますので、mod_syslog自身のコールバッ ク関数登録と同時に従来のコールバック関数へのポインタを、この返値によって取得、保持します。

    #define PREFIX_SYSLOG   "syslog:"
    #define PREFIX_SYSLOG_LENGTH    7
    
    static void *
    ap_syslog_writer_init(apr_pool_t *p, server_rec *s,
                          const char* name)
    {
        syslog(LOG_DEBUG, "%s: prev_log_writer_init = %p, name = %s", __func__, prev_log_writer_init, name);
    
        if (strncasecmp(PREFIX_SYSLOG, name, PREFIX_SYSLOG_LENGTH) == 0) {
            return &dummy[0];
            // NULL以外を返す。
            // 同じ値(&dummy[0])かどうかをap_syslog_writer()関数で判別できればよい。
        }
    
        if (prev_log_writer_init) {
            return prev_log_writer_init(p, s, name);
        }
    
        return NULL;
    }
    
    // ap_hook_pre_config()でフック登録
    static int
    syslog_pre_config(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp)
    {
        ...
    
        if (!prev_log_writer_init) {
            void* f;
    
            f = ap_log_set_writer_init(ap_syslog_writer_init);
            if (f != ap_syslog_writer_init) {
                prev_log_writer_init = f;
            }
            f = ap_log_set_writer(ap_syslog_writer); // ap_syslog_writerの内容については省略
            if (f != ap_syslog_writer) {
                prev_log_writer = f;
            }
        }
    
        return OK;
    }
    

    つまり、上記の例で言えばprev_log_writer_initとprev_log_writerに従来の処理 へのコールバック関数のポインタが入るわけです。 なお、この2関数が返値を返すようになったのはApache2.2以降なので、Apache2.0 系ではそのままでは使用できません。一応、同名の関数から旧い値を返させるだけ で動作するところまでは確認していますが、もしお試しの際はご注意ください。

    ***

    このように、Apacheからアクセスログを出力するにあたり、モジュールの追加だけ でも結構な機能追加ができることが分かります。しかも、添付の例をご覧のように、 わずかなコード量で実現できています。あったらいいな、こんなことできたらうれ しいな、というちょっとした発想を柔軟に取り入れられるApacheの仕組みにはやは り目を見張るものがありますね。


    klab_gijutsu2 at 16:56|この記事のURL
    2008年01月22日

    mod_mod: Apache module を動的にコンパイルして実行する Apache module

    はてなブックマークに登録

    現在 WEBアプリケーションの開発言語といえばいわゆる Light Weight Langage が主流の様な気がしますが、C言語で WEBアプリケーションを書きた いと思った時、どのような方法があるでしょうか。一つはコンパイルした実行 オブジェクトを CGI として呼び出す方法、もう一つは apache module を書く という方法があると思います。

    CGI の場合プロセス起動のオーバーヘッドがありますが apache module の 場合非常に高速です、にも関わらず apache module による WEB アプリケーショ ンの開発があまり流行っていないのはやはり、コード変更の度にコンパイルし なければならない事と、反映の際に apache を再起動しなければならない事が 原因ではないかと思います。

    そこで、apache module っぽい C言語のコードをサーバー上で動的にコンパ イルし、実行する apacheモジュール mod_mod を作りました。

    コンパイルや実行を行う際に新たなプロセスを起動するのではなく apache プロセス内で行います、また DocumentRoot 下のソースコードを変更すればダ イナミックに変更が反映されます。

    今回は、この apache module mod_mod の使い方について紹介したいと思い ます。

    続きを読む
    klab_gijutsu2 at 10:20|この記事のURLComments(0)TrackBack(0)
    2007年03月29日

    なぜ apache module を更新すると Segmentation fault するのか

    はてなブックマークに登録

    今回は、apache module について気になることがあったので紹介します。
    先日 バージョンアップするために apache module を更新した直後 apache が Segmentation fault してしまうという問題に気が付きました。
    Segmentation fault した後に apache を再起動すれば、新しいモジュールが反映されて正常に動作するものの何故この様なことが起こるのか不思議に思い原因を調査してみました。続きを読む
    klab_gijutsu2 at 15:25|この記事のURLComments(0)TrackBack(1)
    2006年08月04日

    [補足記事]Apache 2.0 の hook 一覧(apache module 開発事初め その3-3)

    はてなブックマークに登録

    先日この記事において hook の呼び出しに関してコメントを頂きました.
    実際のところよく分かってない部分もあったので,hook に関してまとめてみました.続きを読む
    klab_gijutsu2 at 21:11|この記事のURLComments(2)TrackBack(1)
    2006年08月01日

    負荷分散環境でブラウザキャッシュが効かないときは - ETagの解説 -

    はてなブックマークに登録

    ETag とはなんぞやというと、Apache が返すレスポンスヘッダのひとつで、

    HTTP/1.1 200 OK
    Date: Mon, 24 Jul 2006 06:18:07 GMT
    Server: Apache
    Last-Modified: Wed, 13 Apr 2005 21:48:55 GMT
    ETag: "3b-60273fc0" ←これ★
    Accept-Ranges: bytes
    Content-Length: 59
    Connection: close
    Content-Type: text/html


    オブジェクトに付与される属性です。

    で、何に使うかというと、ブラウザのキャッシュ管理に使われます。

    一度、http://example.jp/index.html にアクセスした後でもう一度(リロードとかで) アクセスすると、ブラウザは最初のリクエストのときに得た ETag の値を覚えておいて、

    If-None-Match: "3b-60273fc0"


    というリクエストをサーバに投げます。

    このリクエストを受け取ったサーバは、要求されたオブジェクトの ETag を調べて、"3b-60273fc0" なら 304 Not Modified を返します。んで、304 を受け取ったブラウザは自分のキャッシュの中にあるデータを表示するわけです。

    304 の場合はオブジェクトそのもののデータはやり取りされないので、速く表示できるわけですね。

    で、この ETag なんですが、サーバはどうやってこの属性値を決めるかというと、Apache のデフォルトでは


    • iノード番号
    • 最終更新時刻
    • サイズ


    を基に生成しています。なので、ファイルが変更された場合は、ETag が変わるのでちゃんと変更後のデータが返されるわけです。

    が、実はこれでは分散環境でうまく動かないんです。

    分散環境下の各 Web サーバで、最終更新時刻とサイズを同一にすることはできるのですが、iノード番号まで揃えるのは現実的に不可能です。

    なので、デフォルトの設定では、オブジェクトは同じであるにも関わらず、分散された実サーバが違うと (iノードの値が異なるため) ETag の値も異なるので、304 でなくレスポンスコード 200 と共に、オブジェクトのデータが返されてしまいます。

    これを回避するには、Apache の設定で、

    FileETag MTime Size


    とすればよいです。こうすると、iノード番号を使わずに ETag の値を生成するので、分散環境で実サーバが異なっても、ETag が同じになるわけです。

    ちなみに、この場合、ETag の値は一意な ID としては使えません。iノード番号も生成に含める場合は別ですが、最終更新時刻とサイズだけだと、オブジェクトが異なってもこの2つのファイル属性が同じならば ETag の値も同じなるからです。
    klab_gijutsu2 at 14:50|この記事のURLComments(1)TrackBack(2)
    2006年07月27日

    [補足記事]ディレクティブ処理関数登録マクロ一覧 (apache module 開発事初め その3-2)

    はてなブックマークに登録

    前回の記事で後回しにした AP_INIT_XXX() シリーズの一覧です.続きを読む
    klab_gijutsu2 at 17:09|この記事のURLComments(0)TrackBack(0)
    2006年07月21日

    ディレクティブの処理と設定値の利用 (apache module 開発事初め その3)

    はてなブックマークに登録

    今回は前回の記事で予告した通り,Apache の(いくつかのタイプの)モジュールが動作するべきか否かをどうやって判断するか,というお話です.タイトルは「ディレクティブの処理」となっていますが,モジュールがディレクティブを処理することと今回のテーマは密接に結びついています.

    続きを読む
    klab_gijutsu2 at 21:22|この記事のURLComments(7)TrackBack(0)
    2006年07月14日

    アクセス制御モジュールを作ってみる (apache module 開発事初め その2)

    はてなブックマークに登録


    前回の記事では,apxs が生成したテンプレートをそのまま動かしてみましたが,今度は少しコードを書いてみましょう.同じ handler を作っても面白くないので,アクセス制御をするモジュールにしてみます.Apache のアクセス制御は2種類あって,一つはユーザ認証を目的としたもので,mod_auth の眷属がそれです.もう一つはリクエストの別の側面,例えばクライアントのアドレスによってアクセスを許可したり拒否したりするもので,標準モジュールでは mod_access がそれに当たります.あまり複雑なことをしても話が見えにくくなるので,今回作るモジュールではランダムにアクセスを許可したり拒否したりすることにします.

    続きを読む
    klab_gijutsu2 at 18:31|この記事のURLComments(4)TrackBack(1)
    2006年07月12日

    apache module 開発事始め

    はてなブックマークに登録


    先日は,必要に迫られて Apache 1.3 の mod_access を改造したというを書きました.その時は単にあるものを改造しただけでしたが,ふと思い立って,一から Apache 2.0 用のモジュールを書いてみました.書く上で色々 Web サイトを探してみたのですが,あまり日本語の入門向けの文章が見あたらなかったので,開発する上で分かったこと(と言うほど大したものじゃないですが)をまとめておこうと思います.

    続きを読む
    klab_gijutsu2 at 22:52|この記事のURLComments(6)TrackBack(1)
    2006年06月21日

    キャリアのゲートウェイアドレス管理 -- apache module hacking!

    はてなブックマークに登録


    KLab は,元々が携帯向けのサービスを主とした会社で,現在も多くの携帯サイトを運用しています.それらのサイトでは,諸般の事情により携帯以外からのアクセスを制限しています.これは,アクセス元アドレスをキャリアのゲートウェイアドレスに限定することで実現しています.この制限は,Firewall でも行っているのですが,同時に Apachemod_access を使って,Web サーバのレベルでも制限しています.

    KLab が運用しているサイトは複数あって,それぞれ対応するキャリアが異なります.ですので,それぞれのサイト毎にどのキャリアのアドレスを許可するか設定する必要があります.ところが,このキャリアのゲートウェイアドレスは,時々増減します.もちろん,その都度そのキャリア向けにサービスを行っている全サイトの設定を変更する必要があります.

    当初は,サイト毎に Allow from の設定を記述していましたので,キャリアアドレスの追加がある度に,そのアドレスの Allow 設定を追加していました.しかしながら,当然サイトの数が増えるに従ってこの作業はひどく大変になる上,ミスの可能性がぐっと増えてきました.そこで,各キャリアのゲートウェアドレスは一つのファイルで管理して,必要に応じてそれを Include ディレクティブを使って各々のサイトの設定に取り込む形にできないかと考えました.その当時は Apache の 1.3 系列を使っていたのですが,しかしながら 1.3 系列の Apache ではどうにも上手くいきませんでいた.ということで,新たに Apache のモジュールを作ることにしました.

    続きを読む
    klab_gijutsu2 at 22:35|この記事のURLComments(0)TrackBack(0)
    Blog内検索
    このブログについて
    DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
    このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
    最新コメント
    最新トラックバック
    Archives