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__ メソッドをオーバーロードして、デフォルトのパラメータを変更することでこの問題を解決できます。

しかし、新たな問題も発生します。私は常にコンポーネントを書く際には、すべてのユーザーがそれほど賢くないという前提を持つべきだと思っています。

さて、私たちはプラグインを提供しているので、ユーザーが私たちのカスタマイズされた Sanic クラスを使用する際に、super を使って私たちの改造された __init__ メソッドを呼び出さなかった場合、非常に面白い問題が発生します。

同時に、Sanic の内部は強く結合されており、プラグインを構築する際の困難を引き起こします。

落とし穴 2: 内部の結合が強い#

現在、私たちはプラグインを書いており、Response を生成する際に追加の処理を行いたいと考えています。Flask では次のようにできます。

from flask import Flask

class NewFlask(Flask):
    def make_response(self):
        pass

私たちは直接 Flask クラスの make_response メソッドをオーバーロードして、Response 生成時に追加の操作を行うことができます。

この一見簡単な操作が、Sanic では非常に厄介になります。

Sanic には、Flask のように、リクエストサイクル内の異なる段階のデータフロー処理がそれぞれ独立したメソッドを持っていません。例えば、dispatch_requestafter_requestteardown_request などです。リクエストの処理とレスポンスの処理には明確な境界があり、必要に応じてオーバーロードすればよいのです。

Sanic はリクエストサイクル全体の Request データと Response データの処理を、1 つの大きな 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' was returned while requesting a "
                         "handler from the router"))

                # レスポンスハンドラーを実行
                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(
                        "Error while handling error: {}\nStack: {}".format(
                            e, format_exc()))
                else:
                    response = HTTPResponse(
                        "An error occurred while handling an error")
        finally:
            # -------------------------------------------- #
            # レスポンスミドルウェア
            # -------------------------------------------- #
            try:
                response = await self._run_response_middleware(request,
                                                               response)
            except BaseException:
                error_logger.exception(
                    'Exception occurred in one of response middleware handlers'
                )

        # 正しいコールバックにレスポンスを渡す
        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 初探 では、次のようなシナリオに遭遇しました。

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

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

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