mod_wsgi で WSGIScriptAlias を複数設定する
はじめに
私たちのWeb アプリケーションの中には、 mod_wsgi を使っているものがあります。 これまで、アプリをデプロイする際は、 VirtualHost あたり一つだけ WSGIScriptAlias を指定するような使い方をしてきたのですが、最近、新たなアプリを組み込むために、一つの VirtualHost に複数の WSGIScriptAliasをもたせる方式を検討することになりました。
結局この方式は見送られ、別のやりかたに変えたのですが、その経緯をお話したいと思います。
やろうとしたこと
WSGIScriptAlias は、特定のパスへのリクエストを mod_python が処理するように指示するためのディレクティブです。
単純な Web アプリをデプロイするときは、WSGI アプリを application という名前で定義した Python コードの入ったファイルを指定します:
これで、 mod_wsgi は /hoge 以下のリクエストを hoge.wsgi で処理しようと試みるようになります。 複数のアプリケーションをデプロイしたければ、
のように、エンドポイントごとに別のスクリプトを指定していけばよいはずです。
実際、開発を行ったチームも、この構成で一通り動作確認をして、単体テストレベルでは /hoge と /moge の両方のアプリケーションがそれぞれ正常に稼働することを確認していました。
困ったことがおきた
しかし、リリースバージョンをテストする環境で自動テストを流したところ、突然テストが失敗しはじめました。既存のアプリケーションで 404 エラーが多発し始めたのです。
開発チームは、最初はリリースバージョンの他の変更が原因ではないかと考えたので、他の変更をロールバックしました。いったん症状は改善したかにみえます。・・・しかし何度かテストを走らせると、やっぱりエラーが発生します。しかも、新規のアプリケーションにも同じような症状が現れ始めました。
この時点で、 mod_wsgi の挙動を疑い始めます。テスト用サーバの config を確認したところ、 WSGIApplicationGroup が SERVER に設定されており、これが原因でした。
WSGIApplicationGroup とは
WSGIApplicationGroup とは、ひとことでいえば、「リクエストを mod_wsgi のどのインタプリタで処理するか」の設定です。
Apache では、リクエストを処理する際、ハンドラと呼ばれる処理を順に呼び出します。 mod_wsgi のようなモジュールでは、このハンドラを登録しておき、 config に設定したパスに一致するリクエストが来たときに、WSGIスクリプトを実行してリクエストを処理するような仕組みになっています。
スクリプトの実行は mod_wsgi に組み込まれた Python インタプリタ上で行われ、毎回インタプリタを初期化したり、モジュールを読み込み直さなくてもよいように、インタプリタを再利用する仕組みがあります。
しかし、Apacheが受け取るすべてのリクエストを「一つのインタプリタ」の再利用で処理しているとマズいことになります。
例えば、内容の違う同じ名前のモジュールが使われていると、先にロードした方がキャッシュされ、後続のリクエスト処理で参照されてしまいます。
この挙動を実演するために、以下のようなライブラリを考えましょう。(下記のコードは少々残念な作りですが、そこはお察しください):
このライブラリを使って、以下のようなアプリを組みます:
複数の VirtualHost A と B で運用してみます。 このとき、A と B の config.py にそれぞれ SITE_NAME='SITE A', SITE_NAME='SITE B' を設定しておきます。:
この状態で、それぞれの VirtualHost に繰り返しアクセスしてみます::
各々、'...app1/app.wsgi SITE A', '...app2/app.wsgi SITE B' が表示されます。期待した通りの動作ですよね。
しかし、 WSGIScriptAlias を %{GLOBAL} に設定すると・・・両方のサイトで表示が混じり始めます。 WSGI スクリプトは app1 なのに SITE B と出たり、その逆になったり。リクエストを繰り返していると切り替わったりします。
実は、デフォルトの mod_wsgi の動作ではこういう挙動は滅多に起きません。 mod_wsgi は、一つのインタプリタですべてのリクエストを処理するのではなく、内部的に複数のインタプリタ(サブインタプリタ)を生成して、リクエストの内容に応じて使うサブインタプリタを区別しているからです。
この区別の方法を指定するための設定値が WSGIApplicationGroup で、デフォルトの設定値は "%{RESOURCE}" になっています。
設定できる値とその意味は http://modwsgi.readthedocs.io/en/develop/configuration-directives/WSGIApplicationGroup.html で詳しく解説していますが、おおざっぱにいうと以下の通りです。:
- {GLOBAL} どのリクエストも一つのインタプリタを使う。つまり区別しない。
- {HOST} リクエストを受けた VirtualHost のホスト名部分で区別する。
- {SERVER} ホスト名に加えて、ポート番号も区別する。
- {RESOURCE} ホスト名、ポート番号に加えて、WSGIScriptAlias のパス名でも区別する
RESOUCEが指定されているデフォルトの状態では、各々のサブインタプリタは "<host名>:<ポート番号>|<WSGIScriptAliasのパス名>" という名前で区別され、VirtualHostごと、かつアプリごとにインタプリタを使い分けているので、アプリ間でインタプリタを再利用することがありません。
ところがGLOBALを指定すると、すべてのリクエストを(他のVirtualHostへのアクセスも含めて!)一つのインタプリタで処理しようとします。
我々の環境では、対象ホストに {SERVER} の設定が入っていたためこの事象を踏んでしまったのでした。 SERVERの設定では、VirtualHost間は区別するので、VirtualHostごとに単一のアプリをデプロイしているうちは問題になりませんが、一つのVirtualHostで複数アプリを動かすようになると・・・ひどい目にあいます。
mod_wsgi のドキュメントによると、 サブインタプリタ間は「ほぼ完全に区別」されていて、各々のサブインタプリタに影響を与えることはまずないとされています。とはいえ、拡張モジュールの中にはサブインタプリタ間で問題を起こすものがある (http://modwsgi.readthedocs.io/en/develop/user-guides/application-issues.html#multiple-python-sub-interpreters) ことや、対象のシステムはできるだけ安全に(保守的に?)動かしたかったところから、今回は導入を見送ることになりました。
おまけ: サブインタプリタにどんな名前がついているか調べる
mod_wsgi は、共有ライブラリの形式をとっています。そのため、Python から DLLへのアクセスを可能にする ctypes モジュールを使って、動いている Apache プロセス中の mod_wsgi の中をのぞきこむことができます。
例えば、以下の内容のwsgiアプリケーションをmod_wsgi上で動かすと、 mod_wsgi の静的領域にある wsgi_interpreters というオブジェクトにアクセスできます。このオブジェクトは、サブインタプリタの名前とインタプリタオブジェクトを対応付けていて、実際に mod_wsgi がリクエストを処理するインタプリタを検索するときに使われます。 Pythonの辞書オブジェクトなので、簡単に内容を表示できます:
mod_wsgi のドキュメントによれば、WSGIApplicationGroup が %{GLOBAL} のときには、全てのリクエストは最初に生成されるただ一つのインタプリタで処理され、そのインタプリタの名前は空文字列になります。実際、実行してみると、レスポンスは以下のようになります。いくつアプリケーションを追加しても、この辞書は大きくなりませんし、Apacheのプロセス自体が入れ替わるまで、インタプリタオブジェクトも変化しません::
%{SERVER} に変えて、それぞれのサーバにアクセスすると、別のサブインタプリタが登場します:
%{RESOURCE} に変えると、インタプリタ名にパス名も入ります。
キーが空文字列のインタプリタは、どの設定の時も存在しますが、GLOBAL の設定以外で直接リクエストを処理することはありません。こうして実際に mod_wsgi の中身をのぞくと、ドキュメント通りにインタプリタに名前がついていることがわかって、ちょっと安心できますね。意外なところで使える ctypes 。楽しいです。
まとめ
mod_wsgi の WSGIApplicationGroup をうっかり設定してしまってハマったというお話でした。 それだけではもったいないので WSGIApplicationGroup を変えたときにサブインタプリタの管理がどのように変わるか調べてみました。 でも、そろそろ mod_wsgi は卒業したいですね。
0 件のコメント:
コメントを投稿