OpenWrt の uWSGI での websocket は諦めた
websocket サーバを運用するために OpenWrt(v23.05.2)の uWSGI をあれこれと強引にカスタムしてみたが、結局、諦めざるをえなかった……。
自分の力量不足だっただけかもしれないし、今後のバージョンアップによる情勢の変化もあるかもしれないので、参考までに何を試して駄目だったのかを記録のために記しておくことにする。
OpenWrt の uWSGI パッケージは SSL 非対応なので websocket が不可
最近の uWSGI 自体は、デフォルトで websocket 対応している(“The uWSGI websockets implementation is compiled in by default.”)ようなのだが、OpenWrt で用意された uWSGI のバイナリーイメージは、websocket 非対応でコンパイルされている。この StackOverFlow の回答にもあるように、OpenWrt 上では uwsgi コマンドの help 表示のオプション一覧に、https 関係のオプションが表示されないので、https 対応状態でコンパイルされていないことが確認できる。
uwsgi --help | grep https
websocket プロトコルの規格では、非セキュア通信(ws://)の場合でも、ハンドシェイク確立時にはセキュア通信(https://)を必須としているので、https 対応状態でコンパイルされていることが必要となる。実際に、上述の uWSGI の公式ドキュメントのテスト用のシンプルなエコーサーバーの WSGI プログラムを実行してみると、uWSGI のログに次のようなエラーが記録されていた:
you need to build uWSGI with SSL support to use the websocket handshake api function !!! Traceback (most recent call last): File "echo.wsgi", line 5, in application uwsgi.websocket_handshake(env['HTTP_SEC_WEBSOCKET_KEY'], env.get('HTTP_ORIGIN', '')) OSError: unable to complete websocket handshake
ハンドシェイクに失敗しており、“you need to build uWSGI with SSL support to use the websocket handshake api function” と。
OpenWrt 標準の uWSGI パッケージは諦めて、pip で別個に uWSGI モジュールをインストールする
OpenWrt とは直接関係無しに、pip から独立した Python モジュールとしての uwsgi をインストールする方法を試すことにした。pip をインストールするだけでも、かなり FLASH 容量を消費するので、Extroot 化(参考:OpenWrt のストレージを Extroot 化する)が必要であった。さらに pip によるインストール過程で gcc によるコンパイルが必要となってエラー。OpenWrt の gcc 関連パッケージはさすがに巨大であるが、Extroot 化の恩恵により気にせず gcc もインストール。にもかかわらず、結局はエラーでインストール処理が停止した。次に Python.h が無いというエラーが出たので、python3-dev をインストール、それでもやはり駄目。
それでも諦めず、pip には uwsgi とは別に、pyuwsgi というモジュールもあることを知り、こちらなら「一応は」pip からインストールできた。
──が、上述のように確認するために help 表示をすると、SSL 非対応の状態でインストールされていた……。
pyuwsgi を SSL 対応の状態でインストールする
それでも諦めず、pyuwsgi を SSL 対応の状態でインストールする方法を模索した。pip -v でインストールした場合、ログが表示され、その中で ssl 等のOn/Off の状態が示される。そのうち、ssl は websocket のために当然必要として、pcre と routing が使えないと、OpenWrt の標準の uWSGI パッケージの代用とならない。これらの On/Off の状態は、コンパイル前の設定プログラムによってコンパイル環境に応じて自動的に判定されるが、手動で上書きするには、次のようにする:
UWSGI_PROFILE_OVERRIDE=ssl=true;pcre=true;routing=true pip -vI install pyuwsgi=2.0.21 --no-cache-dir --no-binary=pyuwsgi
ただし、pyuwsgi=2.0.21 のバージョン番号については、OpenWrt 標準の uWSGI のバージョンに合せた方がいいだろう。
このコマンドにより、無理矢理 ssl、pcre、routing を有効なものとしてコンパイルさせると、コンパイルエラーとなるので、何が原因かを突き止めることができる。
まず(gcc と、python3-dev はインストール済として)、ssl=true にすることによって、-lpthread、-ldl、-lcrypt に欠くというリンクエラーを引き起す。これには、ar コマンドで libpthread.a、libdl.a、libcrypt.a を用意するか、OpenWrt のビルドシステム(後述)から該当するファイルをコピーして /usr/lib に持ってくるなどする。
ar -rc /usr/lib/libpthread.a
ar -rc /usr/lib/libdl.a
ar -rc /usr/lib/libcrypt.a
上のエラーを解消しても、次に OpenSSL 関連のファイルがないというエラーが起きる。これはもう、OpenWrt のビルドシステム(後述)の toolchain から該当するファイルを掘り出してきて、/usr/include にコピーするしかない。
それでもまたリンクエラーが出て、-lssl、-lcrypto が無いという。これも ar コマンドで回避することはできず、OpenWrt のビルドシステム(後述)の toolchain から該当するファイルを掘り出してきて、/usr/lib にコピーするしかない。
セグメンテーション・フォールト
以上のような血の滲むような努力の末、ssl、pcre、routing が有効な pyuwsgi を、OpenWrt 標準の uwsgi とは別個に、pip でインストールできた。 help 表示も期待通りのものとなった。
試しに、標準の uwsgi に置き換えて使ってみると、システムログに怪しいエラーメッセージが吐き出された。
!!! uWSGI process XXX got Segmentation Fault !!!
色々調べてみたところ、uWSGI で内部的に使っている各ライブラリー(SQLite 等)の OpenSSL のバージョンに不整合が生じているためのようだ。
OpenWrt のビルドシステムからカスタムビルドを試みる
場当たり的に足りないファイルを持ってきて pip で pyuwsgi を無理矢理に動かして済まそうとしたので、内部的な OpenSSL のバージョンの不整合による実行時のエラーには対処できなったと考えた。
そこで意を決して、OpenWrt のビルドシステム(参考:OpenWrt ビルドシステムに関するメモ)からの uwsgi パッケージのカスタムビルドを試みることにした。
ただし、OpenWrt v23.05 では、OpenSSL が 3 になっており、これがバージョンの不整合の原因となっている可能性も考えて、v22.03 で試みることにしてみた。v22.03 では OpenSSL は v1.1.1 である。
予備のノート PC にインストールした Linux Mint 21.2 (Xfce Edition) に git と make に必要なツールを apt で整えてからソースコードを git でクローンし、make 等していく流れとなる。
ここで OpenSSL は、追加パッケージではなく他のパッケージのビルドにも使われるものという扱いなのか、feeds パッケージではなく、git からの clone 時に package/libs にコピーされるだけで更新不可能だった。このデフォルトの OpenSSL パッケージは 1.1.1t であるが、一方で最新のものは 1.1.1w である。この辺りが先述のセグメンテーション・フォールトの原因なのかもしれないと思い、デフォルトの OpenSSL を 1.1.1w に強引に書き換えて uwsgi を make することにした。
OpenWrt の OpenSSL のソースコードを diff して確かめたが、Makefile に記述された、ダウンロードする OpenSSL ソースコードの「バージョン」と「ハッシュ値」が t と w の違いがあるのは当然として、t に存在した patch の 200 番台のファイルが w では削除されていた。それだけの違いだった。
このように、OpenSSL のバージョンを 1.1.1w が使われるようにした上で、uwsgi パッケージ(package/feeds/packages/uwsgi/src/buildconf/openwrt.ini)の記述を ssl = true に修正、Makefile(package/feeds/packages/uwsgi/Makefile)の DEPENDS:= 行に +libopenssl を追加。これでちゃんと SSL が有効化された状態でコンパイルされた .ipk ファイルが bin ディレクトリーの中に出力される。
セグメンテーション・フォールト再び
以上のような血の迸るような努力の末、ssl、pcre、routing が有効化された uwsgi パッケージ得られた。
試しに、OpenWrt の実機にインストールして使ってみると、システムログにまたあの怪しいエラーメッセージが吐き出された。
!!! uWSGI process XXX got Segmentation Fault !!!
もう駄目だこりゃ……。
uWSGI は捨てることにした
OpenWrt の管理システム LuCI を NGINX 化した場合に CGI 用として標準で使われているのが uWSGI だったので、Python との相性も良さそうだし、これをベースに websocket アプリを使えるようにしたいと思って 1 ヶ月近く頑張って試行錯誤したが、websocket 用としては、もう完全に見捨てた。LuCI/NGINX 専用として存在しているだけのものとして、これ以上は無理に触らないことにする。
捨てる神(uWSGI)あれば、拾う神(websockets)あり
uWSGI については完全に愛想が尽きたので、ここでやっと websockets を試してみたら、アッサリ、一日で websocket アプリが動くようになった。
uWSGI と違って、websockets は WSGI アプリケーションではなく、通常の Python スクリプトなので、別途常駐プロセスとして運用する用意をしなければならない。uWSGI の場合は Emperor というものがありそれで常駐プロセスとしての管理ができたが、websockets の場合はそれがないわけである。ところが websockets の場合は、Supervisor という別の独立したプロセスマネージャーを使うことを推奨しており、Supervisor の方が(uWSGI 組み込みの)Emperor よりも安定性・使い勝手が良い感じである。
結局、プロセスマネージャーの部分(Emperor / Supervisor)を除けば、websocket アプリを、WSGI スクリプトとして記述する uWSGI の場合と、Python スクリプトとして記述する websockets の場合との違いが残ることになる。
そして、前者(WSGi / uWSGI)の場合の方が、記述がすっきりしていて簡単そうに見えたのも、先に uWSGI を使おうと思った理由であった。
後者(Pytyhon / websockets)の場合は、イベントループによるノンブロッキング IO を実装するため asyncio を使った記述になるため、一段複雑なコードとなっているのだが、「それだけ」と言えば、「それだけ」である。uWSGI での苦労を思えば、「どうということはない」。
コメント
コメントを投稿