Manjusaka

Manjusaka

Sanic の若干のツッコミ

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_classresponse_class を設定することで、元の Request クラスと Response クラスを置き換えることができます。

上記のコードのように、私たちは RequestResponse に追加の機能を簡単に追加できます。

しかし、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_mapwerkzurg.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 のように、RouteParser および 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 は本当にプロダクションで使用できるまでには、まだかなりの道のりがあります。内部のアーキテクチャ、周辺のエコシステム、その他すべてにおいてです。皆さんは暇なときに遊んでみてくださいが、プロダクションラインに乗せる場合は、落とし穴に備えておいてください。

最後に、皆さんの新年が幸せでありますように、長生きして繁栄しますように!

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。