2020年4月2日木曜日

mod_wsgi で WSGIScriptAlias を複数設定する - MonotaRO Tech Blog

https://tech-blog.monotaro.com/entry/2018/07/04/084733

mod_wsgi で WSGIScriptAlias を複数設定する

はじめに

私たちのWeb アプリケーションの中には、 mod_wsgi を使っているものがあります。 これまで、アプリをデプロイする際は、 VirtualHost あたり一つだけ WSGIScriptAlias を指定するような使い方をしてきたのですが、最近、新たなアプリを組み込むために、一つの VirtualHost に複数の WSGIScriptAliasをもたせる方式を検討することになりました。

結局この方式は見送られ、別のやりかたに変えたのですが、その経緯をお話したいと思います。

やろうとしたこと

WSGIScriptAlias は、特定のパスへのリクエストを mod_python が処理するように指示するためのディレクティブです。

単純な Web アプリをデプロイするときは、WSGI アプリを application という名前で定義した Python コードの入ったファイルを指定します:

<VirtualHost *:80>        WSGIScriptAlias /hoge /path/to/app/hoge.wsgi    </VirtualHost>  

これで、 mod_wsgi は /hoge 以下のリクエストを hoge.wsgi で処理しようと試みるようになります。 複数のアプリケーションをデプロイしたければ、

<VirtualHost *:80>        WSGIScriptAlias /hoge /path/to/app/hoge.wsgi      WSGIScriptAlias /moge /path/to/app/moge.wsgi    </VirtualHost>  

のように、エンドポイントごとに別のスクリプトを指定していけばよいはずです。

実際、開発を行ったチームも、この構成で一通り動作確認をして、単体テストレベルでは /hoge と /moge の両方のアプリケーションがそれぞれ正常に稼働することを確認していました。

困ったことがおきた

しかし、リリースバージョンをテストする環境で自動テストを流したところ、突然テストが失敗しはじめました。既存のアプリケーションで 404 エラーが多発し始めたのです。

開発チームは、最初はリリースバージョンの他の変更が原因ではないかと考えたので、他の変更をロールバックしました。いったん症状は改善したかにみえます。・・・しかし何度かテストを走らせると、やっぱりエラーが発生します。しかも、新規のアプリケーションにも同じような症状が現れ始めました。

この時点で、 mod_wsgi の挙動を疑い始めます。テスト用サーバの config を確認したところ、 WSGIApplicationGroup が SERVER に設定されており、これが原因でした。

WSGIApplicationGroup とは

WSGIApplicationGroup とは、ひとことでいえば、「リクエストを mod_wsgi のどのインタプリタで処理するか」の設定です。

Apache では、リクエストを処理する際、ハンドラと呼ばれる処理を順に呼び出します。 mod_wsgi のようなモジュールでは、このハンドラを登録しておき、 config に設定したパスに一致するリクエストが来たときに、WSGIスクリプトを実行してリクエストを処理するような仕組みになっています。

f:id:ymasuda_whosaysni:20180703181833p:plain

スクリプトの実行は mod_wsgi に組み込まれた Python インタプリタ上で行われ、毎回インタプリタを初期化したり、モジュールを読み込み直さなくてもよいように、インタプリタを再利用する仕組みがあります。

しかし、Apacheが受け取るすべてのリクエストを「一つのインタプリタ」の再利用で処理しているとマズいことになります。

例えば、内容の違う同じ名前のモジュールが使われていると、先にロードした方がキャッシュされ、後続のリクエスト処理で参照されてしまいます。

この挙動を実演するために、以下のようなライブラリを考えましょう。(下記のコードは少々残念な作りですが、そこはお察しください):

# lib.py  import config    def site_name():      return config.SITE_NAME  

このライブラリを使って、以下のようなアプリを組みます:

# app.wsgi  import os, sys; sys.path.insert(0, os.path.dirname(__file__))  from lib import site_name    def application(environ, start_response):      start_response('200 OK', [])      return ['{}, {}'.format(__file__, site_name())]  

複数の VirtualHost A と B で運用してみます。 このとき、A と B の config.py にそれぞれ SITE_NAME='SITE A', SITE_NAME='SITE B' を設定しておきます。:

<VirtualHost *:8081>          ...          WSGIScriptAlias /app "/home/developer/app/app1/app.wsgi"  </VirtualHost>    <VirtualHost *:8082>          ...          WSGIScriptAlias /app "/home/developer/app/app2/app.wsgi"  </VirtualHost>  

この状態で、それぞれの VirtualHost に繰り返しアクセスしてみます::

watch -n .5  curl http://localhost:8081/app  

各々、'...app1/app.wsgi SITE A', '...app2/app.wsgi SITE B' が表示されます。期待した通りの動作ですよね。

しかし、 WSGIScriptAlias を %{GLOBAL} に設定すると・・・両方のサイトで表示が混じり始めます。 WSGI スクリプトは app1 なのに SITE B と出たり、その逆になったり。リクエストを繰り返していると切り替わったりします。

f:id:ymasuda_whosaysni:20180629194204p:plain

実は、デフォルトの mod_wsgi の動作ではこういう挙動は滅多に起きません。 mod_wsgi は、一つのインタプリタですべてのリクエストを処理するのではなく、内部的に複数のインタプリタ(サブインタプリタ)を生成して、リクエストの内容に応じて使うサブインタプリタを区別しているからです。

f:id:ymasuda_whosaysni:20180703185424p:plain

この区別の方法を指定するための設定値が WSGIApplicationGroup で、デフォルトの設定値は "%{RESOURCE}" になっています。

設定できる値とその意味は http://modwsgi.readthedocs.io/en/develop/configuration-directives/WSGIApplicationGroup.html で詳しく解説していますが、おおざっぱにいうと以下の通りです。:

  • {GLOBAL} どのリクエストも一つのインタプリタを使う。つまり区別しない。
  • {HOST} リクエストを受けた VirtualHost のホスト名部分で区別する。
  • {SERVER} ホスト名に加えて、ポート番号も区別する。
  • {RESOURCE} ホスト名、ポート番号に加えて、WSGIScriptAlias のパス名でも区別する

RESOUCEが指定されているデフォルトの状態では、各々のサブインタプリタは "<host名>:<ポート番号>|<WSGIScriptAliasのパス名>" という名前で区別され、VirtualHostごと、かつアプリごとにインタプリタを使い分けているので、アプリ間でインタプリタを再利用することがありません。

f:id:ymasuda_whosaysni:20180703191418p:plain

ところがGLOBALを指定すると、すべてのリクエストを(他のVirtualHostへのアクセスも含めて!)一つのインタプリタで処理しようとします。

f:id:ymasuda_whosaysni:20180703190706p:plain

我々の環境では、対象ホストに {SERVER} の設定が入っていたためこの事象を踏んでしまったのでした。 SERVERの設定では、VirtualHost間は区別するので、VirtualHostごとに単一のアプリをデプロイしているうちは問題になりませんが、一つのVirtualHostで複数アプリを動かすようになると・・・ひどい目にあいます。

f:id:ymasuda_whosaysni:20180703191251p:plain

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の辞書オブジェクトなので、簡単に内容を表示できます:

from json import dumps  from ctypes import cdll, py_object  dll = cdll.LoadLibrary('/usr/lib/apache2/modules/mod_wsgi.so')    def application(environ, start_response):      start_response('200 OK', [('Content-type', 'application/json')])      interpreters = py_object.in_dll(dll, 'wsgi_interpreters').value      response = {          'i': [(k, str(v)) for k, v in interpreters.items()],      }      return [dumps(response).encode('utf-8')]  

mod_wsgi のドキュメントによれば、WSGIApplicationGroup が %{GLOBAL} のときには、全てのリクエストは最初に生成されるただ一つのインタプリタで処理され、そのインタプリタの名前は空文字列になります。実際、実行してみると、レスポンスは以下のようになります。いくつアプリケーションを追加しても、この辞書は大きくなりませんし、Apacheのプロセス自体が入れ替わるまで、インタプリタオブジェクトも変化しません::

{"i": [["", "<mod_wsgi.Interpreter object at 0x7f1728fd3660>"]]}  

%{SERVER} に変えて、それぞれのサーバにアクセスすると、別のサブインタプリタが登場します:

{"i": [["", "<mod_wsgi.Interpreter object at 0x7f1eb95ec660>"],         ["10.0.X.X:8081", "<mod_wsgi.Interpreter object at 0x7f1eb95ec9f0>"],         ["10.0.X.X:8082", "<mod_wsgi.Interpreter object at 0x7f1eb81c16f0>"]]}  

%{RESOURCE} に変えると、インタプリタ名にパス名も入ります。

{"i": [["", "<mod_wsgi.Interpreter object at 0x7f45345ca660>"],         ["10.0.X.X:8081|/app", "<mod_wsgi.Interpreter object at 0x7f45345ca9f0>"],         ["10.0.X.X:8082|/app", "<mod_wsgi.Interpreter object at 0x7f4530166fc0>"],         ...  ]}  

キーが空文字列のインタプリタは、どの設定の時も存在しますが、GLOBAL の設定以外で直接リクエストを処理することはありません。こうして実際に mod_wsgi の中身をのぞくと、ドキュメント通りにインタプリタに名前がついていることがわかって、ちょっと安心できますね。意外なところで使える ctypes 。楽しいです。

まとめ

mod_wsgi の WSGIApplicationGroup をうっかり設定してしまってハマったというお話でした。 それだけではもったいないので WSGIApplicationGroup を変えたときにサブインタプリタの管理がどのように変わるか調べてみました。 でも、そろそろ mod_wsgi は卒業したいですね。

0 件のコメント:

コメントを投稿