OpenWrt で uWSGI 環境を整える

OpenWrt(23.05)に Web サーバーとして NGINX(SSL 版)を利用し、uWSGIミューウィズジー をアプリケーションサーバーとして連携する方法について記す。

前提状況:USB フラッシュドライブ

WWW 用のデータを置く場所として USB フラッシュの外部ドライブを用意(👉 OpenWrt での USB フラッシュドライブ)し、さらに Extroot 化(👉 OpenWrt のストレージを Extroot 化する)していることを前提としている。

LuCI もろとも Web サーバー(HTTPd)を SSL 対応 NGINX 化する

以前の OpenWrt 18.x とは飛躍的に進歩して、19.07 以降では SSL 対応版の NGINX が opkg として用意されている(以前は SSL 対応にするためには自前で Linux ソースコードからモジュールをビルドする必要があった)のみならず、NGINX 版 LuCI がセットアップされている opkg すら用意されており、OpenWrt コミュニティの旺盛な活動を感じる(👉 LuCI on other web servers > LuCI on nginx)。


opkg update
opkg install luci-ssl-nginx
opkg remove luci-ssl luci
reboot

デフォルトでインストールされている LuCI/uHTTPd の方は不要になるのでアンインストールした。


OpenWrt ルーターで websocket サーバーを運用したいと思ったので、その下準備として、アプリケーションサーバーを整えておく必要がある。Python 系のアプリケーションサーバーとしては uWSGI が定番であり、最近のバージョンでは websocket にもデフォルトで対応しているようなので、まずここでは uWSGI 環境の構築について一通り行いたいと思う。

luci-ssl-nginx

luci-ssl-nginx を導入するとデフォルトの uHTTPd 環境の LuCI に代えて、NGINX(かつ SSL)環境の LuCI が動くようになる。この環境において、Lua プログラムである LuCI に NGINX が CGI としてリレーするために、uWSGI が使われている。この場合は、uWSGI はあくまでも CGI を扱うためのアプリケーションサーバーの一種として使われているだけで、Python 固有の WSGI サーバーとして使われているわけではないが、それでも NGINX ⇔ uWSGI 間のプロトコルは uwsgi プロトコルが使われている点は特筆すべき点だろう(NGINX は uwsgi プロトコルにネイティブ対応している)。

NGINX が uwsgi プロトコルを使うということは、設定ファイル中で uwsgi_* プレフィックスを使った設定が行え、uwsgi_pass でソケットを通じて uWSGI にリレーできることを意味している。

例えば、OpenWrt の luci-ssl-nginx を入れた状態では uWSGI 関連の設定は次のようになっている。

/etc/nginx/conf.d/luci.locations(/etc/nginx/uci.conf.template から起動時の初期化処理においてインクルードされる)

location /cgi-bin/luci {
		index  index.html;
		include uwsgi_params;
		uwsgi_param SERVER_ADDR $server_addr;
		uwsgi_modifier1 9;
		uwsgi_pass unix:////var/run/luci-webui.socket;
}
location ~ /cgi-bin/cgi-(backup|download|upload|exec) {
		include uwsgi_params;
		uwsgi_param SERVER_ADDR $server_addr;
		uwsgi_modifier1 9;
		uwsgi_pass unix:////var/run/luci-cgi_io.socket;
}

location /luci-static {
		error_log stderr crit;
}

location /ubus {
        ubus_interpreter;
        ubus_socket_path /var/run/ubus/ubus.sock;
        ubus_parallel_req 2;
}

uWSGI と関係しているのは、/cgi-bin/luci と /cgi-bin/cgi-(backup|download|upload|exec) のブロック。いずれも uwsgi_pass で UNIX ソケットに繋いでいる。uwsgi_modifier1 を 9 にしているということは、これはプロトコル的に CGI モードである(👉 The uwsgi Protocol)。NGINX と uWSGI は本来、uwsgi プロトコルで直結できるのだが、現状の OpenWrt ではこのように汎用的な CGI としての設定にしてあるようである(👉 OpenWrt 公式ガイド)。

LuCI on nginx is currently supported by using uwsgi as plain-cgi interpreter.

以下、uWSGI 側の設定について見てみる。

/etc/uwsgi/emperor.ini

[uwsgi]
strict = true
pidfile = /var/run/uwsgi.pid

; https://uwsgi-docs.readthedocs.io/en/latest/Emperor.html
emperor = /etc/uwsgi/vassals/*.ini
; spawn the emperor as soon as possibile
early-emperor = true
; 終了時に全部のファイルやソケットを片付ける
vacuum = true
; 本当は末尾の / は、無い方がいいと思う
emperor-on-demand-directory = /var/run/
; vassal からの生存報告を何秒間受け取らなかったら、死亡と見做すか
; https://uwsgi-docs.readthedocs.io/en/latest/Emperor.html#heartbeat-system
emperor-required-heartbeat = 99

; vassal-set は全ての vassal にそのオプション付で起動させる
; die-on-idle は、--idle オプションによる停止時に、(単なる停止ではなく)完全に終了させる。
; これによって完全な起動待ち(オンデマンド)状態に戻り、メモリーを占有し続けないようにする。
; Combining on demand vassals with --idle and --die-on-idle
; https://uwsgi-docs.readthedocs.io/en/latest/OnDemandVassals.html#combining-on-demand-vassals-with-idle-and-die-on-idle
vassal-set = die-on-idle=true

uWSGI は emperor.ini がオンデマンドで vassal(皇帝の配下の国王)の起動などを行っており、各 vassal の設定はこの emperor.ini にパスが定義されている vassals/*.ini にある。LuCI の WebUI 用の .ini と、LuCI が呼び出す入出力用の CGI のための .ini の 2 種が定義されている。

/etc/uwsgi/vassals/luci-webui.ini

[uwsgi]
; 無意味な記述を無視するのではなくエラーにすることでデバッグしやすくする
; https://www.bloomberg.com/company/stories/configuring-uwsgi-production-deployment/
strict = true

; if-not-env ~ end-if が条件分岐ブロック
; https://uwsgi-docs.readthedocs.io/en/latest/ConfigLogic.html
; 変数 UWSGI_EMPEROR_FD が存在しない場合のみ、ソケット用ファイル(パーミッション rw-rw-rw-)を作成している
if-not-env = UWSGI_EMPEROR_FD
socket = /var/run/luci-webui.socket
chmod-socket = 666
; これ(cheap)を if ブロックに入れる理由は不明だが、
; 実際、最初にリクエストが発生するまではワーカープロセスを開始させないようにして、メモリー消費を節約する
; idle(アイドル状態で一定時間を経過したワーカーを停止して cheap モードに入らせる)とセットで考えるオプション
; https://serverfault.com/a/402171/951307
; https://buildmedia.readthedocs.org/media/pdf/uwsgi-docs-additions/latest/uwsgi-docs-additions.pdf
cheap = true
end-if =

plugin = cgi
; force CGI-mode for plugins supporting it
; When enabled, responses generated by uWSGI will not be HTTP responses, but CGI ones; namely, the ``Status:`` header will be added.
; cgi プラグインなのに、わざわざ CGI モードを強制する必要があるのかと思いきや、これはプラグイン側に対する設定ではなく、uWSGI 側に対するモード設定。
; HTTP ヘッダーではなく、CGI ヘッダーを出力するため。もちろん、アプリが出力するのが、HTTP ヘッダーならば、false でよい。
; CGI  https://datatracker.ietf.org/doc/html/rfc3875#section-6
; HTTP https://datatracker.ietf.org/doc/html/rfc2616#section-6
cgi-mode = true

; 以下の 2 行とも、本当は末尾の / は、無い方がいいと思う
cgi = /www/
chdir = /usr/lib/ucode/luci/

; リクエスト・ヘッダーの上限サイズ。デフォルトは 4KB。
; https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html
buffer-size = 10000
; プロセス、ワーカーを強制停止するまで 8 秒の猶予時間を与える
reload-mercy = 8
; 2000 回繰り返したらワーカーを再起動する
max-requests = 2000
; プロセスの総メモリー使用量を 1000MB に制限する
limit-as = 1000
; アドレス領域が 256MB 以上に達したらワーカーを再起動する
reload-on-as = 256
; RSS メモリーが 192MB 以上に達したらワーカーを再起動する
reload-on-rss = 192

; Python プログラム側がマルチスレッドの場合は、on にしないと正常に動作しない
; 本来はデフォルトで on にされるべきオプションだが、パフォーマンス上の理由で off がデフォルト扱いになっている
; https://www.bloomberg.com/company/stories/configuring-uwsgi-production-deployment/
; https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html
enable-threads = true

; リクエスト・ボディを uWSGI に読み込ませる(バッファサイズ 8KB)
; https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html
post-buffering = 8192

; ソケット接続の時間制限
socket-timeout = 120

; Thundering herd 問題に対する対策
; https://uwsgi-docs.readthedocs.io/en/latest/articles/SerializingAccept.html#uwsgi-developers-are-fu-ing-cowards
; 他の OS の事情などを鑑みてデフォルトでは off 扱いだが、Linux 系 OS の場合は on にすべきオプション
thunder-lock = true

plugin = syslog
; luci のシスログにログを出力する
; https://skydum.hatenablog.com/entry/2022/07/06/215056
logger = luci syslog:uwsgi-luci

; ログの正規表現による分別
; the regular expression leaves for successful de/activation only one line each:
log-route = luci ^(?!... Starting uWSGI |compiled with version: |os: Linux|nodename: |machine: |clock source: |pcre jit |detected number of CPU cores: |current working directory: |detected binary path: |uWSGI running as root, you can use |... WARNING: you are running uWSGI as root |chdir.. to |your processes number limit is |limiting address space of processes...|your process address space limit is |your memory page size is |detected max file descriptor number: |lock engine: |thunder lock: |uwsgi socket |your server socket listen backlog is limited to |your mercy for graceful operations on workers is |mapped .* bytes |... Operational MODE: |initialized CGI path: |... no app loaded. going in full dynamic mode ...|... uWSGI is running in multiple interpreter mode ...|spawned uWSGI worker |announcing my loyalty to the Emperor...|workers have been inactive for more than |SIGINT/SIGQUIT received...killing workers...|worker .* buried |goodbye to uWSGI.|...gracefully killing workers...|Gracefully killing worker|worker .* killed successfully)

; uWSGI 自体のアクセスログは off にして……
disable-logging = true
; アクセスログもシスログに出力する
req-logger = syslog:uwsgi-luci

; ログの出力フォーマット
log-format=%(method) %(uri) => return %(status) (%(rsize) bytes in %(msecs) ms)

; 1 つのワーカープロセスごとに予めスレッド化しておく数
threads = 1
; max (processes) = 3; min (cheaper) = 1 の間でどのようにワーカーを増減させるかという高度な設定が cheaper
; https://uwsgi-docs.readthedocs.io/en/latest/Cheaper.html
processes = 3
; spare2 の作者(日本人)による cheaper-algo の比較
; https://qiita.com/methane/items/226e94ac7470a88a2e24
; spare2 はアイドルワーカーの個数を一定に保とうとするので、デフォルトの spare よりもスムーズに増減する
cheaper-algo = spare
cheaper = 1
cheaper-initial = 1
cheaper-step = 1

; 通常は on で使うべきもので、Emperor を使った複雑なシステムやデバッグ目的以外では滅多なことで off にしない。
; https://www.bloomberg.com/company/stories/configuring-uwsgi-production-deployment/
master = true

; アイドル状態で一定時間を経過したワーカーを停止して cheap モードに入らせる。cheap とセットで考えるオプション
; https://serverfault.com/a/402171/951307
idle = 360
/etc/uwsgi/vassals/luci-cgi_io.ini

(luci-webui.ini と共通のものは分析を省略する)


[uwsgi]
strict = true

if-not-env = UWSGI_EMPEROR_FD
socket = /var/run/luci-cgi_io.socket
chmod-socket = 666
cheap = true
end-if =

plugin = cgi
cgi-mode = true

cgi = /www/
chdir = /usr/lib/ucode/luci/

buffer-size = 10000
reload-mercy = 8
max-requests = 2000
limit-as = 1000
reload-on-as = 256
reload-on-rss = 192

; luci-webui.ini と違って master にしていないのは、ワーカープロセスを増殖させない、単一のプロセスだからか?

; automatically kill workers if master dies (can be dangerous for availability)
no-orphans = true

post-buffering = 8192
socket-timeout = 120

thunder-lock = true

plugin = syslog
disable-logging = true
req-logger = syslog:uwsgi-luci-cgi_io
log-format=%(method) %(uri) => return %(status) (%(rsize) bytes in %(msecs) ms)

; if ブロック内の cheap にしても、chmod-socket が 2 カ所に重複存在する理由も、意図が不明
chmod-socket = 666

; https://github.com/unbit/uwsgi/blob/master/plugins/cgi/cgi_plugin.c
; skip security checks if the cgi file is under the specified path
cgi-safe = /usr/libexec/cgi-io
; call symbolic link directly instead of the real path
cgi-dontresolve = true
; close STDIN on input EOF
cgi-close-stdin-on-eof = true

cheap = true
idle = 360

ログファイルの設定など色々と細かい設定が並んでいるが、特記すべきなのは、socket を定義している点と、plugin として cgi を使い、その cgi のルートパスを設定している点だろう。socket は NGINX 側で設定しているものと対応しているのがわかるし、NGINX 側でも uwsgi_modifier1 によって CGI プロトコル(9)に設定してあるのだから、uWSGI 側でも CGI モードで設定するのは当然である。


Python プログラムを CGI として動かす

重要なのは、plugin を cgi にし、NGINX の設定をソケットを合わせ、ローカル側のパスを指定することであるが、その他の細かい設定は luci-webui.ini を敷衍して、同じ emperor 配下の vassal の一つとして定義してみた:

/etc/uwsgi/vassals/python-cgi.ini

[uwsgi]
; https://uwsgi-docs.readthedocs.io/en/latest/Options.html
; https://www.bloomberg.com/company/stories/configuring-uwsgi-production-deployment/
; https://buildmedia.readthedocs.org/media/pdf/uwsgi-docs-additions/latest/uwsgi-docs-additions.pdf

; 無効なオプションを指定した場合に、無視するのではなくエラーが発生するようにする。
strict = true

if-not-env = UWSGI_EMPEROR_FD
	; emperor.ini で emperor-on-demand-directory = /var/run/ と末尾に / 付きで指定されているため、
	; run// としないと、vacuum が上手く働かない(cgi の場合。wsgi の方は vacuum 時に問題ないようだ)。
	socket = /var/run//python-cgi.socket
	; socket ファイルのパーミッション。実行用のファイルではないので、実行権限を削っておけばよい。
	chmod-socket = 666
end-if =

plugin = cgi
; force CGI-mode for plugins supporting it
; When enabled, responses generated by uWSGI will not be HTTP responses, but CGI ones; namely, the ``Status:`` header will be added.
; cgi プラグインなのに、わざわざ CGI モードを強制する必要があるのかと思いきや、これはプラグイン側に対する設定ではなく、uWSGI 側に対するモード設定。
; HTTP ヘッダーではなく、CGI ヘッダーを出力するため。もちろん、アプリが出力するのが、HTTP ヘッダーならば、false でよい。
; CGI  https://datatracker.ietf.org/doc/html/rfc3875#section-6
; HTTP https://datatracker.ietf.org/doc/html/rfc2616#section-6
cgi-mode = true

; URL パスを使う場合の、ローカル側のパスのルートとなるプレフィックス。
cgi = /www

; buffer-size ~ socket-timeout は LuCI 用の設定をそのまま敷衍

; リクエスト・ヘッダーの上限サイズ。デフォルトは 4KB。
; https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html
buffer-size = 10000
; プロセス、ワーカーを強制停止するまで 8 秒の猶予時間を与える
reload-mercy = 8
; 2000 回繰り返したらワーカーを再起動する
max-requests = 2000
; プロセスの総メモリー使用量を 1000MB に制限する
limit-as = 1000
; アドレス領域が 256MB 以上に達したらワーカーを再起動する
reload-on-as = 256
; RSS メモリーが 192MB 以上に達したらワーカーを再起動する
reload-on-rss = 192
; リクエスト・ボディを uWSGI に読み込ませる(バッファサイズ 8KB)
; https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html
post-buffering = 8192
; ソケット接続の時間制限
socket-timeout = 120

; マルチインタープリターモードは通常の用途では不要
; https://www.bloomberg.com/company/stories/configuring-uwsgi-production-deployment/
single-interpreter = true

; Thundering herd 問題に対する対策
; https://uwsgi-docs.readthedocs.io/en/latest/articles/SerializingAccept.html#uwsgi-developers-are-fu-ing-cowards
; 他の OS の事情などを鑑みてデフォルトでは off 扱いだが、Linux 系 OS の場合は on にすべきオプション
thunder-lock = true

; 通常は on で使うべきもので、Emperor を使った複雑なシステムやデバッグ目的以外では滅多なことで off にしない。
; https://www.bloomberg.com/company/stories/configuring-uwsgi-production-deployment/
master = true

; Python プログラム側がマルチスレッドの場合は、on にしないと正常に動作しない
; 本来はデフォルトで on にされるべきオプションだが、パフォーマンス上の理由で off がデフォルト扱いになっている
; https://www.bloomberg.com/company/stories/configuring-uwsgi-production-deployment/
; https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html
enable-threads = true

; 1 つのワーカープロセスごとに予めスレッド化しておく数
threads = 1
; max (processes) = 3; min (cheaper) = 1 の間でどのようにワーカーを増減させるかという高度な設定が cheaper
; https://uwsgi-docs.readthedocs.io/en/latest/Cheaper.html
processes = 3
; spare2 の作者(日本人)による cheaper-algo の比較
; https://qiita.com/methane/items/226e94ac7470a88a2e24
; spare2 はアイドルワーカーの個数を一定に保とうとするので、デフォルトの spare よりもスムーズに増減する
cheaper-algo = spare2
cheaper = 1
cheaper-initial = 1
cheaper-step = 1

; 実際、最初にリクエストが発生するまではワーカープロセスを開始させないようにして、メモリー消費を節約する
; idle(アイドル状態で一定時間を経過したワーカーを停止して cheap モードに入らせる)とセットで考えるオプション
; https://serverfault.com/a/402171/951307
cheap = true
; アイドル状態で一定時間を経過したワーカーを停止して cheap モードに入らせる。cheap とセットで考えるオプション
; https://serverfault.com/a/402171/951307
idle = 360

そして NGINX 側でも、この socket に対して CGI プロトコルで接続する設定を適当な場所に追加する。

/etc/nginx/conf.d/python-cgi.location

location ~* ^/cgi-bin/.*\.py$ {
	include uwsgi_params;
	uwsgi_param SERVER_ADDR $server_addr;
	uwsgi_modifier1 9;
	uwsgi_pass unix:/var/run/python-cgi.socket;
}
/etc/nginx/conf.d/server1.conf

server {
	(...)
    include conf.d/python-cgi.location;
	(...)
}

(server1.conf は上の python-cgi.location の設定をインクルードして使いたいサーバーの設定ファイルとする)

以上で /cgi-bin 以下に置かれた拡張子 .py の CGI プログラムが動かせるようになるはずである。ただし、設定変更を反映させるため、uwsgi のリスタート、nginx のリロードは必要である。


service uwsgi restart
service nginx reload

Python プログラムを WSGI スクリプトとして動かす

正解に辿り着くまでに結構苦労した点が、Python プログラムを CGI としてではなく、WSGI スクリプトとして動かす設定についてである。

今こうして一旦正解に辿り着いてしまえば、それほどのことでもないのだが、ポイントは、NGINX 側ではちゃんとプロトコルを uwsgi にすること(デフォルトなので uwsgi_modifier1 を記述しなければ 0 になるのだが、試行錯誤していた当初、LuCI 用の .ini の設定をコピーしたものを叩き台にしたため、9 に指定した状態のまま CGI プロトコルになってしまっていたために却ってうまく動かなかった)、wsgi.ini 側で plugin に python を指定することである。そうすれば、wsgi-file オプションで指定した WSGI スクリプトが実行される(下の例では wsgi-file オプションではなくより複雑な mount オプションを使っている)。

とりあえず、最終的に動くようになった例が以下:

まずは uWSGI の Python プラグインをインストールしておく必要がある。


opkg install uwsgi-python3-plugin

その上で nginx と uwsgi を設定する:

/etc/nginx/conf.d/python-wsgi.location

location /wsgi {
	include uwsgi_params;
	uwsgi_param SERVER_ADDR $server_addr;
	uwsgi_modifier1 0;
	uwsgi_pass unix:/var/run/wsgi.socket;
}
/etc/nginx/conf.d/server1.conf

server {
	(...)
    include conf.d/python-wsgi.location;
	(...)
}

(server1.conf は上の python-wsgi.location の設定をインクルードして使いたいサーバーの設定ファイルとする)

/etc/wsgi/vassals/wsgi.ini

[uwsgi]
; https://uwsgi-docs.readthedocs.io/en/latest/Options.html
; https://www.bloomberg.com/company/stories/configuring-uwsgi-production-deployment/
; https://buildmedia.readthedocs.org/media/pdf/uwsgi-docs-additions/latest/uwsgi-docs-additions.pdf

; 無効なオプションを指定した場合に、無視するのではなくエラーが発生するようにする。
strict = true

if-not-env = UWSGI_EMPEROR_FD
	; emperor.ini で emperor-on-demand-directory = /var/run/ と末尾に / 付きで指定されているため、
	; run// としないと、vacuum が上手く働かない(cgi の場合。wsgi の方は vacuum 時に問題ないようだ)。
	socket = /var/run//wsgi.socket
	; socket ファイルのパーミッション。実行用のファイルではないので、実行権限を削っておけばよい。
	chmod-socket = 666
end-if =

; 'opkg install uwsgi-python3-plugin' が必要
plugin = python

chdir = /www/wsgi
mount = /wsgi/app1=app1/app1.py
mount = /wsgi/app2=app2/app2.py
mount = /wsgi/app3=app3/app3.py
; tell uWSGI to rewrite PATH_INFO and SCRIPT_NAME according to mount-points
; 要するに、URL として /wsgi/app1 にアクセスした場合にローカルの /www/wsgi/app1/app1.py を実行する形にする
manage-script-name = true

; buffer-size ~ socket-timeout は LuCI 用の設定をそのまま敷衍

; リクエスト・ヘッダーの上限サイズ。デフォルトは 4KB。
; https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html
buffer-size = 10000
; プロセス、ワーカーを強制停止するまで 8 秒の猶予時間を与える
reload-mercy = 8
; 2000 回繰り返したらワーカーを再起動する
max-requests = 2000
; プロセスの総メモリー使用量を 1000MB に制限する
limit-as = 1000
; アドレス領域が 256MB 以上に達したらワーカーを再起動する
reload-on-as = 256
; RSS メモリーが 192MB 以上に達したらワーカーを再起動する
reload-on-rss = 192
; リクエスト・ボディを uWSGI に読み込ませる(バッファサイズ 8KB)
; https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html
post-buffering = 8192
; ソケット接続の時間制限
socket-timeout = 120

; マルチインタープリターモードは通常の用途では不要
; https://www.bloomberg.com/company/stories/configuring-uwsgi-production-deployment/
single-interpreter = true

; Thundering herd 問題に対する対策
; https://uwsgi-docs.readthedocs.io/en/latest/articles/SerializingAccept.html#uwsgi-developers-are-fu-ing-cowards
; 他の OS の事情などを鑑みてデフォルトでは off 扱いだが、Linux 系 OS の場合は on にすべきオプション
thunder-lock = true

; 通常は on で使うべきもので、Emperor を使った複雑なシステムやデバッグ目的以外では滅多なことで off にしない。
; https://www.bloomberg.com/company/stories/configuring-uwsgi-production-deployment/
master = true

; Python プログラム側がマルチスレッドの場合は、on にしないと正常に動作しない
; 本来はデフォルトで on にされるべきオプションだが、パフォーマンス上の理由で off がデフォルト扱いになっている
; https://www.bloomberg.com/company/stories/configuring-uwsgi-production-deployment/
; https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html
enable-threads = true

; 1 つのワーカープロセスごとに予めスレッド化しておく数
threads = 1
; max (processes) = 3; min (cheaper) = 1 の間でどのようにワーカーを増減させるかという高度な設定が cheaper
; https://uwsgi-docs.readthedocs.io/en/latest/Cheaper.html
processes = 3
; spare2 の作者(日本人)による cheaper-algo の比較
; https://qiita.com/methane/items/226e94ac7470a88a2e24
; spare2 はアイドルワーカーの個数を一定に保とうとするので、デフォルトの spare よりもスムーズに増減する
cheaper-algo = spare2
cheaper = 1
cheaper-initial = 1
cheaper-step = 1

; 実際、最初にリクエストが発生するまではワーカープロセスを開始させないようにして、メモリー消費を節約する
; idle(アイドル状態で一定時間を経過したワーカーを停止して cheap モードに入らせる)とセットで考えるオプション
; https://serverfault.com/a/402171/951307
cheap = true
; アイドル状態で一定時間を経過したワーカーを停止して cheap モードに入らせる。cheap とセットで考えるオプション
; https://serverfault.com/a/402171/951307
idle = 360

;logto = /www/wsgi/wsgi.log
/www/wsgi/app1/app1.py

def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/plain')])
    return [b'wsgi test ok!']
https://(IP)/wsgi/app1 にブラウザでアクセスした結果
wsgi test ok!

wsgi の場合、wsgi スクリプトを更新した場合は、都度、uwsgi の方を restart して読み込ませ直す必要がある点は留意しておくこと。


service uwsgi restart

uWSGI で websocket アプリを運用する

結論から先に言うと、今回の環境(OpenWrt 23.05.2)でこれは不可能だった。

その諦めるに至った顛末は別の記事にまとめた:OpenWrt の uWSGI での websocket は諦めた

まあ、OpenWrt の uWSGI パッケージは、あくまでも、管理画面の LuCI のために、NGINX で Lua プログラムを CGI として動かすために用意されているので、uWSGI 単独で動かすことを想定されていないのだろう。LuCI は現状では、標準出力を通じて(つまり CGI モードで)NGINX と連携しており、WSGI 化すらされていない。そんな状況だから、当分は、OpenWrt の uWSGI パッケージが、websocket 対応でコンパイルされることは期待薄である。

当初の目的であった websocket については諦める結果になったとはいえ、Python 3.10 以降では、「3.13 で cgi が標準ライブラリーから外れる予定である」という警告が出るようになった。つまり、CGI で Python を実行することは時代の流れ的に非推奨となり、wsgi で使うことが標準となることが確定的なので、OpenWrt の uWSGI で wsgi 環境を確立できたことは無駄ではなかったと思う。


最後に

最初のうち、理解しにくくややこしく感じたのが、サーバー(デーモンプログラム)同士である、uWSGI と NGINX の間は、uwsgi プロトコルで接続されていて、それは socket 設定行によって示されている(Web サーバー側が uwsgi プロトコル非対応の場合は socket 設定ではなく http-socket 設定になるだろうし、Web サーバーを別途置かずに uWSGI に Web サーバーも兼用させる場合は http 設定になる)という話と、Python プログラムと uWSGI の間は WSGI で接続する場合と従来の CGI で接続するという場合があるという話が、ごっちゃになっていた点かもしれない。試行錯誤の末に結果を出した、その後になってから見つけた情報なのだが、Django のための uWSGI の公式チュートリアルに、この辺りの関係性を下図のように分類してわかりやすく解説してあるものがあった(先に見付けていればもっと楽ができたと思うが、自分は Django は視野に入れていなかったせいもあって、スルーしていたのだと思う)。

the web client ↔ the web server ↔ the socket ↔ uwsgi ↔ Django
the web client ↔ uWSGI ↔ Python
the web client ↔ uWSGI ↔ Django
the web client ↔ the web server
the web client ↔ the web server ↔ the socket ↔ uWSGI ↔ Python

また、uWSGI に http プロトコルをしゃべらせれば、NGINX は不要なのだが、なぜあまりそういう使い方をしないのかという点も、すぐに納得行かず、一日モヤモヤしていた。だが、mount 設定でイチイチ REQUEST_URI と実際のパスの対応付けを予め列挙しておかなければならないのを見てもわかるように、uWSGI は本来、あまり静的コンテンツ処理向きのものではない。なので基本的には静的コンテンツは NGINX で捌きつつ、動的コンテンツのみピンポイントで uWSGI にリレーするという運用方法が標準的に想定されたものなのだろうと思う。また、OpenWrt デフォルトの uWSGI は、LuCI 用のみを想定していて、そのような NGINX をプロキシとして利用する設定となっているという面もある。


参考

NGINX / uWSGI 等の違いについて、「イベントループによるノンブロッキング IO」という観点から考察を行っており、秀逸な着眼点だと思う記事。

コメント

このブログの人気の投稿

清水俊史『上座部仏教における聖典論の研究』

シークエンスパパともの先見の明

シークエンスパパとも 本物の霊能力