Sanic の若干不満#
先ほど、紅姐と どの Python ライブラリがあなたを後悔させるか? という回答の下で Sanic の長所と短所について話し合いました。
突然思い出しましたが、私たちの会社は国内で比較的珍しく、Sanic を正式なプロダクションラインで使用している会社です。主力推進者として、私はこのクソドキュメントエンジニアとして、Sanic を使用する過程で私たちが採用した一連の深い落とし穴について話す必要があると感じました。
本文#
まず Sanic 公式 のスローガンは Flask Like の web framework です。これにより、多くの人が Sanic の内部実装が Flask とほぼ一致しているという錯覚を抱くことになりますが、実際は本当にそうなのでしょうか?
まず、Hello World の一例を見てみましょう。
# Flask
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
if __name__ == '__main__':
app.run()
# Sanic
from sanic import Sanic
app = Sanic()
@app.route("/")
async def hello_world(request):
return "Hello World!"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
皆さんは何か違いに気づきましたか?うん?Sanic の View 関数にパラメータが一つ多いのはなぜでしょうか?
Flask の最も典型的な特徴の一つは、グローバル変数の概念があることです。例えば、グローバルな g 変数や request 変数などです。これは werkzurg 内で独立して実装されたスレッドローカルに似たメカニズムを利用しています。リクエストサイクル内で、ビジネスロジックの中で from flask import request
を使って現在の request
変数を取得できます。このメカニズムを利用して、データをグローバルに使用することもできます。
しかし、Sanic にはこのグローバル変数の概念がありません。つまり、ビジネスロジック内で request
変数を使用する場合、リクエストサイクルの終わりまで request
変数を渡し続ける必要があります。
この方法には良い面も悪い面もありますが、私たちの不満はまだ始まったばかりです。
落とし穴 1:拡張が非常に不便#
例えば、私たちにはプラグインを書く必要があるという要件があります。他の部門の同僚が使用できるように、プラグイン内で元の Request
クラスと Response
クラスにいくつかの機能を追加する必要があります。Flask では次のようにできます。
from flask import Request,Response
from flask import Flask
class APIRequest(Request):
pass
class APIResponse(Response):
pass
class NewFlask(Flask):
request_class = APIRequest
response_class = APIResponse
Flask では、Flask
クラス内の 2 つの属性 request_class
と response_class
を設定することで、元の Request
クラスと Response
クラスを置き換えることができます。
上記のコードのように、私たちは Request
と Response
に追加の機能を簡単に追加できます。
しかし、Sanic ではどうでしょうか?非常に面倒です。
class Sanic:
def __init__(self, name=None, router=None, error_handler=None,
load_env=True, request_class=None,
strict_slashes=False, log_config=None,
configure_logging=True):
# 前のスタックフレームから名前を取得
if name is None:
frame_records = stack()[1]
name = getmodulename(frame_records[1])
# ロギング
if configure_logging:
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
self.name = name
self.router = router or Router()
self.request_class = request_class
self.error_handler = error_handler or ErrorHandler()
self.config = Config(load_env=load_env)
self.request_middleware = deque()
self.response_middleware = deque()
self.blueprints = {}
self._blueprint_order = []
self.configure_logging = configure_logging
self.debug = None
self.sock = None
self.strict_slashes = strict_slashes
self.listeners = defaultdict(list)
self.is_running = False
self.is_request_stream = False
self.websocket_enabled = False
self.websocket_tasks = set()
# 代替メソッド名を登録
self.go_fast = self.run
これは Sanic の Sanic
クラスの初期化コードです。まず、Sanic では Response
を簡単に置き換えることができず、次に __init__
メソッドを見てみると、デフォルトの Request
を置き換えるには、初期化時に request_class
というパラメータを渡す必要があることがわかります。これが非常に混乱を招く点です。このようなものをどうして渡さなければならないのでしょうか?
確かに、Sanic
クラスの __init__
メソッドをオーバーロードして、デフォルトのパラメータを変更することでこの問題を解決できます。
しかし、新たな問題も発生します。私は常にコンポーネントを書く際に、すべてのユーザーがあなたのものを使うときに、知能が emmmm あまり高くないという前提を持つべきだと思っています。
さて、私たちはプラグインを提供しているので、ユーザーが私たちのカスタマイズされた Sanic
クラスを再度継承し、私たちの魔改造された __init__
メソッドを super
で呼び出さなかった場合、非常に面白い混乱が生じることになります。
同時に、Sanic の内部は強く結合されており、プラグインを構築する際の困難を引き起こします。
落とし穴 2: 内部の結合がひどい#
現在、私たちはプラグインを書いて、Response
を生成する際に追加の処理を行いたいと考えています。Flask では次のようにできます。
from flask import Flask
class NewFlask(Flask):
def make_response(self):
pass
Flask クラス内の make_response
メソッドをオーバーロードすることで、Response
を生成する際に追加の操作を行うことができます。
この一見簡単な操作が、Sanic では非常に厄介になります。
Sanic には Flask のように、リクエストサイクル内の異なる段階のデータフロー処理にそれぞれ独立したメソッドがありません。例えば dispatch_request
, after_request
, teardown_request
などです。Request の処理と Response の処理には明確な境界があり、必要に応じてオーバーロードすればよいのです。
Sanic はリクエストサイクルの Request
データと Response
データの処理を、すべて大きな handle_request
メソッド内に統一しています。
class Sanic:
#.....
async def handle_request(self, request, write_callback, stream_callback):
"""HTTPサーバーからリクエストを受け取り、返すレスポンスオブジェクトを返します。
HTTPサーバーはレスポンスオブジェクトのみを期待しているため、例外処理はここで行う必要があります。
:param request: HTTPリクエストオブジェクト
:param write_callback: レスポンスを唯一の引数として呼び出す同期レスポンス関数
:param stream_callback: ハンドラーによって生成された StreamingHTTPResponse を処理するコルーチン。
:return: 何も返さない
"""
try:
# -------------------------------------------- #
# リクエストミドルウェア
# -------------------------------------------- #
request.app = self
response = await self._run_request_middleware(request)
# ミドルウェアの結果がない場合
if not response:
# -------------------------------------------- #
# ハンドラーを実行
# -------------------------------------------- #
# ルーターからハンドラーを取得
handler, args, kwargs, uri = self.router.get(request)
request.uri_template = uri
if handler is None:
raise ServerError(
("'None' がルーターからハンドラーを要求した際に返されました"))
# レスポンスハンドラーを実行
response = handler(request, *args, **kwargs)
if isawaitable(response):
response = await response
except Exception as e:
# -------------------------------------------- #
# レスポンス生成に失敗
# -------------------------------------------- #
try:
response = self.error_handler.response(request, e)
if isawaitable(response):
response = await response
except Exception as e:
if self.debug:
response = HTTPResponse(
"エラー処理中にエラーが発生しました: {}\nスタック: {}".format(
e, format_exc()))
else:
response = HTTPResponse(
"エラー処理中にエラーが発生しました")
finally:
# -------------------------------------------- #
# レスポンスミドルウェア
# -------------------------------------------- #
try:
response = await self._run_response_middleware(request,
response)
except BaseException:
error_logger.exception(
'レスポンスミドルウェアハンドラーの一つで例外が発生しました'
)
# 正しいコールバックにレスポンスを渡す
if isinstance(response, StreamingHTTPResponse):
await stream_callback(response)
else:
write_callback(response)
これにより、特定の段階のデータに対して追加の操作を行う必要がある場合、必然的に handle_request
という大きなメソッドをオーバーロードしなければなりません。前述のように、Response
を生成する際に追加の操作を行いたい場合、Flask では対応する make_response
メソッドをオーバーロードするだけで済みますが、Sanic では全体の handle_request
をオーバーロードする必要があります。まさに一つの動きが全てに影響を与えるということです。
また、Sanic は Flask のように、WSGI 層のリクエスト処理とフレームワーク層のロジックを相互に分離していません。このような分離は、時には多くの便利さをもたらします。
例えば、以前にこのようなクソ記事を書いたことがあります。あなたが知らない Flask Part1: Route の初探 では、次のようなシナリオに遭遇しました。
以前、Flask で正規表現をサポートする必要があるという非常に奇妙な要件がありました。例えば、@app.route('/api/(.*?)')
こうすることで、ビュー関数が呼び出されるときに、URL 内の正規表現でマッチした値を渡すことができます。しかし、Flask のルーティングではデフォルトでこのような方法はサポートされていません。では、どうすればよいのでしょうか?
解決策は非常に簡単です。
from flask import Flask
from werkzeug.routing import BaseConverter
class RegexConverter(BaseConverter):
def __init__(self, map, *args):
self.map = map
self.regex = args[0]
app = Flask(__name__)
app.url_map.converters['regex'] = RegexConverter
このように設定した後、私たちは先ほどの要件に従ってコードを書くことができます。
@app.route('/docs/model_utils/<regex(".*"):url>')
def hello(url=None):
print(url)
皆さんはご覧の通り、Flask の WSGI 層の処理は Werkzurg に基づいて行われています。つまり、URL やその他の WSGI 層に関わるものについては、Werkzurg が提供する関連するクラスや関数をオーバーロード / 使用するだけで済みます。同時に app.url_map.converters['regex'] = RegexConverter
という操作は、ソースコードを見たことがある人ならわかるように、url_map
は werkzurg.routing
クラスの Map
クラスのサブクラスであり、私たちの操作は本質的には Werkzurg に対する操作であり、Flask のフレームワークロジックとは無関係です。
しかし、Sanic にはそのような分離メカニズムがありません。例えば、上記のシナリオについて考えてみましょう。
class Sanic:
def __init__(self, name=None, router=None, error_handler=None,
load_env=True, request_class=None,
strict_slashes=False, log_config=None,
configure_logging=True):
#....
self.router = router or Router()
#....
Sanic では URL の解析は Router()
インスタンスによってトリガーされます。独自の URL 解析をカスタマイズする必要がある場合、self.router
を置き換える必要があります。これは実際には Sanic 自体を変更することを意味し、少し不適切に感じます。
また、ここでの Router
クラスでは、独自の解析をカスタマイズする必要がある場合、Router 内の
class Router:
routes_static = None
routes_dynamic = None
routes_always_check = None
parameter_pattern = re.compile(r'<(.+?)>')
parameter_pattern
属性やその他の解析メソッドをオーバーロードする必要があります。Router には Werkzurg の Router のように、Route
と Parser
および Formatter
(つまり Converter)が相互に分離される特性が実装されていません。必要に応じて再構築して追加するだけで済みます。文中で挙げた例のように。
この部分全体は、Sanic の内部結合がひどく、追加の操作を実現しようとすると、全てに影響を与えることを嘆いているのです。
落とし穴 3:細部やその他の落とし穴#
この部分では、いくつかの点について言及したいと思います。
第一に、Sanic が依存しているライブラリは、実際には、emmmmmm、あまり安定していません。例えば、10 月に特定のデータをシリアライズする際に ujson
が例外をスローするバグが発生しました。この問題は 14 年にすでに発生していましたが、現在まで修正されていません、2333333。また、その時のバージョンでは、組み込みの関数を使用する場合、ユーザーが具体的なパーサーを選択することはできませんでした。具体的には、私が提起した PR を参照してください。
第二に、Sanic のいくつかの実装は厳密ではありません。例えば、この文章では 日常的なクソ記事: Sanic に関する小さな問題についての考察 で不満を述べています。
第三に、Sanic は現在 UWSGI をサポートしておらず、Gunicorn と組み合わせてデプロイする場合、独自の Gunicorn Worker を実装しています。私たちのプロダクション環境では、未知の理由による 504 のような神秘的なバグが発生していますが、まだ追跡中です(さらに、Sanic のサーバー部分が PEP333 つまり WSGI プロトコルを厳密に遵守していないという情報もあります。= = 後で確認します)。
まとめ#
Sanic の性能は確かに素晴らしいです。技術検証時にテストした際、異なるビジネスロジックの下で、基本的にその性能は Flask の 1.5 倍以上を保証できました。しかし、現在の使用経験から言うと、Sanic は本当にプロダクションで使用できるまでには、まだかなりの道のりがあります。内部のアーキテクチャ、周辺のエコシステム、その他すべてにおいてです。皆さんは暇なときに遊んでみてくださいが、プロダクションラインに乗せる場合は、落とし穴に備えておいてください。
最後に、皆さんの新年が幸せでありますように、長生きして繁栄しますように!