この記事は ハンズラボ AdventCalendar2022 5日目の記事です。
ハンズラボ AdventCalendar2022 2日目の記事として「Pythonあまり使っていない人がDjangoの動作環境を調べてみた話」を書きました。調査の際にuWSGIのマニュアルに「Emperor mode」という記述をみてしまいました。
なんだかわかりませんが、とにかくエンペラーモードという響きがカッコいい!ということで使ってみました。タイトルも多少違和感がありますが、カッコよさ重視のタイトルにしてみました。
Emperor modeとは
Emperor modeが何かを理解しておく必要があるので公式ドキュメントを確認しておきます。
https://uwsgi-docs.readthedocs.io/en/latest/Emperor.html
ものすごく大雑把に書くと、設定ファイルを検出して、対応するuWSGIインスタンスを起動してくれるものだと思いました。
uWSGI上で動かしたいアプリケーションを作るたびに、OS設定してuWSGIサービスを追加するのは手間だから、設定ファイル作れば対応するuWSGIが起動してくれるのは手間が減るということのように感じました。
ドキュメントを読んで理解したこと
(すべて書くとマニュアル翻訳になってしまうので、必要そうなところのみ書いてます)
単一のサーバ、もしくはサーバ群に多くのアプリケーションをデプロイする必要があるときに、Emperor モードが適切。
特別なuWSGIインスタンスで、特別なイベントを監視し、必要に応じて(Emperorに管理されているときに、vassalという)インスタンスを作り、停止、再ロードしてくれる。
デフォルトで、EmperorはuWSGI設定ファイルの指定されたディレクトリをスキャンする。スキャンは、imeperial monitorプラグインを使って拡張が可能。dir://
とglob://
はコアに含まれているからロードの必要がなく、自動的に検出される。dir://
がデフォルト。
新しい設定ファイルを検出すると、設定ファイルに基づきuWSGIインスタンスが起動する。
設定ファイルが変わると、インスタンスが再ロードされる。
touch --no-dereference
が友達とある。シンボリックリンク自体の変更日付を変えるために使うコマンドだからだと考えられる。
(まずは)uWSGI単体の動作確認
Emperorモードを確認するためには、複数のuWSGI上で動作するアプリケーションが必要だと考えました。
とはいえ、Djangoのアプリを2つ作るのはやりすぎなように感じました。
そこで、今回は最小構成を意識して、uWSGIアプリケーションを起動してみることにしました。
アプリケーションの準備
WSGIに則ったインターフェースをもつアプリケーションを準備しました。(ドキュメントにかかれているのものを完全コピーさせていただきました)
def application(env, start_response):
start_response('200 OK', [('Content-Type','text/html')])
return [b"Hello World"]
参照先:
https://uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html#the-first-wsgi-application
uWSGIのインストール
手元のPython環境をごちゃごちゃさせたくなかったため、venv環境を作り、以下のコマンドでuWSGIをインストールしました。当初、brewをつかってuWSGIをインストールしましたが、python pluginが入っていないようで手間がかかりそうなため、pip経由にしました。
python -m pip install uwsgi
アプリケーションの起動
こちらも公式ドキュメントを真似て起動しました。
uwsgi --http-socket :9090 --wsgi-file test.py
ブラウザでhttp://localhost:9090
を確認するとアプリケーションが応答します。
このアプリケーションをコピーして2つにします。2つのアプリケーションをEmperorモードで起動してみることにしました。
Emperor modeの起動方法でハマる
実は、公式ドキュメントを読んでもEmperor modeの構成に関する理解が進まず、非常に困難でした。
当初は、以下のように構成を作り起動させようとしました。
apps
├── app1
│ └── test.py
├── app2
│ └── test.py
├── app2.ini
config
└── config.skel
└── app1.ini -(symlink)-> config.skel
└── app2.ini -(symlink)-> config.skel
[uwsgi]
chdir = %dapps/%n
threads = 2
socket = /tmp/sockets/%n.sock
wsgi-file = test.py
設定ファイルを1つ作り、それをシンボリックリンクとしてアプリケーションの数だけ作ることで設定ファイルを個別に書かなくても良いようにします。skel拡張子は、uWSGIの設定ファイルではないのでスキップされ、実質、app1.ini, app2.ini の2つのアプリケーションの設定となります。
uwsgi --emperor ./config
これで、さくっと動くのかなと思いました。
[emperor] vassal app2.ini is ready to accept requests
[emperor] vassal app1.ini is ready to accept requests
なんか動いている感はあるが、これにどうやってアクセスするんだろう?
iniファイルにsocketを書いたけどプロトコル、つなぎ方もわからないな。ということで悩みつつ、ググりました。
悩み抜いた末に
公式ドキュメントでもピンときておらず、ググっても明確な答えは見つけられませんでした。
しかし、以下のGithubのリポジトリをみて理解できました。(うん、むしろやりたかったことがすべて書いてある。)
設定をみて、管理用のuWSGIインスタンスが必要で、(vassalだと思われる)それとは別にアプリケーション別のuWSGIインスタンスが必要ということだと理解しました。また、socketについては、uwsgiプロトコルでNginxといったサーバと接続させるものと理解しました。
Webサーバは必須のようなので、nginxはインストールしておきました。
brew install nginx
Emperor modeの設定
これまでの理解をもとに、今回の構成を以下のようにしました。
apps
├── app1
│ └── test.py
├── app1.ini -> config.skel
├── app2
│ └── test.py
├── app2.ini -> config.skel
└── config.skel
emperor.ini
nginx.conf
それぞれのファイルは以下のようにしました。
[uwsgi]
http-socket = :8000
master = true
emperor = %dapps
emperor-on-demand-directory = %dapps
emperor-stats-server = :8001
daemon off;
error_log /dev/stdout info;
events {
worker_connections 1024;
}
http {
access_log /dev/stdout;
server {
listen 9000;
server_name _;
charset utf-8;
# max upload size
client_max_body_size 75M; # adjust to taste
location /app1 {
# リバプロとかでHTTPプロトコルを使う場合
# proxy_pass http://unix:./apps/app1.socket:/;
uwsgi_pass unix:./apps/app1.socket;
}
location /app2 {
# リバプロとかでHTTPプロトコルを使う場合
# proxy_pass http://unix:./apps/app2.socket:/;
uwsgi_pass unix:./apps/app2.socket;
}
}
}
(参考にさせていただいた設定そのままというのも味気ないので)今回は、リバプロをする必要はないと判断したので、uwsgiプロトコル、socketベースでuWSGIとやりとりするように設定してみました。
[uwsgi]
chdir = %d../apps/%n
threads = 2
socket = %d%n.socket
wsgi-file = test.py
master = true
protocol = uwsgi
# HTTP プロトコルにしたい場合
# protocol = http
config.skelについては、最小構成で動作させる例がなく、指定できるオプションの説明が見つけられず、かなり苦戦しました。自分なりに考えた結論としては、ファイルの内容にuwsgiコマンドのオプションを書く感じかなと考えています。protocolに何を指定するのかについてですが、uwsgiのオプションをprotocolでgrepすると、以下のようなものがあるので、おそらくfastcgi, scgiなども指定できそうだと考えています。
--http-socket bind to the specified UNIX/TCP socket using HTTP protocol
--http11-socket bind to the specified UNIX/TCP socket using HTTP 1.1 (Keep-Alive) protocol
--fastcgi-socket bind to the specified UNIX/TCP socket using FastCGI protocol
--fastcgi-nph-socket bind to the specified UNIX/TCP socket using FastCGI protocol (nph mode)
--scgi-socket bind to the specified UNIX/TCP socket using SCGI protocol
--raw-socket bind to the specified UNIX/TCP socket using RAW protocol
--puwsgi-socket bind to the specified UNIX/TCP socket using persistent uwsgi protocol (puwsgi)
test.pyについては、app1, app2のどちらかがわかるように App1
, App2
という文字列を追記しています。
def application(env, start_response):
start_response('200 OK', [('Content-Type','text/html')])
return [b"Hello World App1"]
def application(env, start_response):
start_response('200 OK', [('Content-Type','text/html')])
return [b"Hello World App2"]
Emperor mode 発動!(単なる実行です)
以下のコマンドを実行して、Emperor modeで複数のuWSGIアプリの起動をさせます。
uwsgi --ini emperor.ini --enable-threads
ついで、Nginxを起動します。
nginx -c `pwd`/nginx.conf
ブラウザでhttp://localhost:9000/app1
, http://localhost:9000/app2
に接続するとアプリケーションが動作していることが確認できます。app1であれば、以下のようになっています。
touch --no-dereference は友達なのか?
冒頭に書きましたが、touch touch --no-dereference
は友達とありました。
実際に友だちになれるのか確認してみました。
touch touch --no-dereference app1.ini
を叩いてみると想定通りにapp1だけがリロードされました。
[emperor] reload the uwsgi instance app1.ini
最後に
Emperor mode を使うことで設定・運用の手間が減らしつつ、複数のWSGIアプリケーションを扱えるようにすることができることがわかりました。たとえば、使わずに1つのサーバで2つのDjangoアプリケーションを動かそうとすると、どのくらい手間が減るかイメージしやすそうです。
残念ながら情報量が少なめな気がするので、使う場合には事前検証が重要だと感じました。
今回は触れませんでしたが、vassalに関する設定も共通化することができたり、Tyrantモードなる、セキュアなマルチユーザホスティング機能もあるようですので、Emperorモードには一見の価値はあると思います。