apache
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" などに設定したらいいと思います。
過負荷をかわす 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 の設定を触るきっかけになったのは チューニンガソンに出場して、 自分でもサーバーのチューニングができるんだという自信がついたからです。 チューニンガソンを開催してくださった運営・スポンサーの方、ありがとうございました。
高負荷でも安定したサービスを提供するためのリバースプロキシ
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 にリクエストを転送しないでエラーを返す必要があります。
時間が無くなってしまったので、今日はこのへんにして、 実際の設定方法は次回に回します。
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 の設定を 紹介します。
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周りの設定から入りましょう。前に挙げたページ遷移図のおさらいです。
それぞれ、1)から5)までのページ(URL)を下記の通りとします。前回その1の例でのファイル名と同じですね。マイページについては、認証による保護がなされ、未ログインの状態でのアクセスは許可しないものとします。
- 1) /login.html
- 2) /doauth
- 3) /mypage/(index.html)
- 4) /dologout
- 5) /logout.html
続いて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>
<html> <body> <a href="/dologout">logout</a> <br> <a href="news.html">news</a> </body> </html>
<html> <body> <a href="login.html">login</a> </body> </html>
3)のマイページに"news"というリンクがありますが、これはマイページからログアウト以外の遷移を行う為のものです。つまり、認証による保護を受けた状態のままでのページ遷移を再現したいという意図です。内容は何でも構わないので、下記のようにしておきます。
<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:パスワード」の形式
さて、ここで一度設定を生かして動作を見てみましょう。
ユーザ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ないしパスワードを入れると、マイページへは遷移せずに再度ログインページに戻っています。
次に試しにログアウトした状態で、マイページに直接アクセスしてみましょう。認証で保護されなければいけませんから、ログインページに飛ばされて欲しいところですが...。
見えちゃいますね。
これは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でそれが消えていることが確認できます。
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)
#!/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サービスで使われているログインフォームを利用した形態にも対応可能となったことは、サイト構築の選択肢をさらに広げることになるのではないでしょうか。
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単独でも構築できるようになるかもしれません。
一般的な認証処理の流れを図示すると下の図のようになります。
太線(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>
このようにして、プログラム・スクリプト部分無しで認証機構を持たせることが可能になるということです。残るはユーザ登録の処理についてはですがこちらは確認できておりません。今後の調査次第もしくは実装が追加されるかもしれませんが、そうでなければ各自で作り込む必要がありそうです。
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}i | Referer:ヘッダの内容 |
"\" \"" | 「"」+空白+「"」 |
%{User-agent}i | User-agent:ヘッダの内容 |
"\"" | 「"」 |
このように、%?の箇所とそれ以外(空白等、置き換えない文字列)とでそれぞれ区切り、小片に分けられます。%で始まる項目は対応するコールバック関数を通し、指定のリクエスト情報を文字列に変換します。つまり、リクエスト情報は一旦LogFormatの項目数分(今回18個)の部分文字列に変換され、最後にこれらをすべて連結してログ1行分として出力しています。
mod_log_msgpackではこの部分を利用、アクセスログ1行をMessagePackのarray1個として置き換えて下記の項目のみ保持することとしました。
(mod_log_msgpack)%h | リモートホスト(を出力する関数をコールバック) |
%l | (identdからもし提供されていれば)リモートログ名 |
%u | リモートユーザ |
%t | リクエストを受け付けた時刻 |
%r | リクエストの最初の行 |
%>s | 最後のステータス |
%b | レスポンスのバイト数。HTTPヘッダは除く。 |
%{Referer}i | Referer:ヘッダの内容 |
%{User-agent}i | User-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との併存を可能にするためにいくつか細かい変更を加えていますが、次回以降に続くということで^^;。
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モ ジュールを一つ作ってみました。
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の仕組みにはやは り目を見張るものがありますね。
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 の使い方について紹介したいと思い ます。
続きを読むなぜ apache module を更新すると Segmentation fault するのか
先日 バージョンアップするために apache module を更新した直後 apache が Segmentation fault してしまうという問題に気が付きました。
Segmentation fault した後に apache を再起動すれば、新しいモジュールが反映されて正常に動作するものの何故この様なことが起こるのか不思議に思い原因を調査してみました。続きを読む
負荷分散環境でブラウザキャッシュが効かないときは - ETagの解説 -
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 の値も同じなるからです。
ディレクティブの処理と設定値の利用 (apache module 開発事初め その3)
今回は前回の記事で予告した通り,Apache の(いくつかのタイプの)モジュールが動作するべきか否かをどうやって判断するか,というお話です.タイトルは「ディレクティブの処理」となっていますが,モジュールがディレクティブを処理することと今回のテーマは密接に結びついています.
続きを読むアクセス制御モジュールを作ってみる (apache module 開発事初め その2)
前回の記事では,apxs が生成したテンプレートをそのまま動かしてみましたが,今度は少しコードを書いてみましょう.同じ handler を作っても面白くないので,アクセス制御をするモジュールにしてみます.Apache のアクセス制御は2種類あって,一つはユーザ認証を目的としたもので,mod_auth の眷属がそれです.もう一つはリクエストの別の側面,例えばクライアントのアドレスによってアクセスを許可したり拒否したりするもので,標準モジュールでは mod_access がそれに当たります.あまり複雑なことをしても話が見えにくくなるので,今回作るモジュールではランダムにアクセスを許可したり拒否したりすることにします.
キャリアのゲートウェイアドレス管理 -- apache module hacking!
KLab は,元々が携帯向けのサービスを主とした会社で,現在も多くの携帯サイトを運用しています.それらのサイトでは,諸般の事情により携帯以外からのアクセスを制限しています.これは,アクセス元アドレスをキャリアのゲートウェイアドレスに限定することで実現しています.この制限は,Firewall でも行っているのですが,同時に Apache の mod_access を使って,Web サーバのレベルでも制限しています.
KLab が運用しているサイトは複数あって,それぞれ対応するキャリアが異なります.ですので,それぞれのサイト毎にどのキャリアのアドレスを許可するか設定する必要があります.ところが,このキャリアのゲートウェイアドレスは,時々増減します.もちろん,その都度そのキャリア向けにサービスを行っている全サイトの設定を変更する必要があります.
当初は,サイト毎に Allow from の設定を記述していましたので,キャリアアドレスの追加がある度に,そのアドレスの Allow 設定を追加していました.しかしながら,当然サイトの数が増えるに従ってこの作業はひどく大変になる上,ミスの可能性がぐっと増えてきました.そこで,各キャリアのゲートウェアドレスは一つのファイルで管理して,必要に応じてそれを Include ディレクティブを使って各々のサイトの設定に取り込む形にできないかと考えました.その当時は Apache の 1.3 系列を使っていたのですが,しかしながら 1.3 系列の Apache ではどうにも上手くいきませんでいた.ということで,新たに Apache のモジュールを作ることにしました.