Manjusaka

Manjusaka

Sanic 的若干吐槽

Sanic 的若干吐槽#

剛剛和红姐,在 哪些 Python 庫讓你相見恨晚? 這個答案下面討論了一下 Sanic 的優劣。

突然想起,我司算是國內應該比較少見的把 Sanic 用在正式生產線上的公司了,作為一個主力推(da)動(shui)者(bi),我這個辣雞文檔工程師覺得有必要來說一下我們在使用 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 較為人知的一個最典型的 Feature 就是,它有個 Global Variable 的概念,比如全局的 g 變量,以及 request 變量,這個是借助 werkzurg 裡面獨立實現的一套類似於 Thread.Local 的機制。在一個請求週期內,在我們業務邏輯,我們可以通過 from flask import request 來獲取當前的 request 變量。我們也可以通過這樣的機制,在上面掛一些數據來實現數據的全局使用。

但是 Sanic 則沒有這個 Global Variable 這個概念,也就是說,我們需要在業務邏輯中使用 request 變量的話,就需要不斷的傳遞一個 request 變量,直到一個請求週期的終結。

這樣方式處理,有好,也有壞,不過我們的吐槽剛剛開始

坑點一:擴展極為不方便#

比如,我們現在有個需求,我們需要寫一個插件,提供給其餘部門的同事使用,在插件中,我們需要給原本的 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 類中的兩個屬性 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):

        # Get name from previous stack frame
        if name is None:
            frame_records = stack()[1]
            name = getmodulename(frame_records[1])

        # logging
        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()

        # Register alternative method names
        self.go_fast = self.run

這是 Sanic 中 Sanic 類的初始化代碼,首先在 Sanic 中,我們沒辦法很輕鬆的替換 Response , 其次,我們通過查看其 __init__ 方法,我們就可以知道,如果要替換默認的 Request 我們需要給其初始化的時候傳遞一個參數 request_class。這就是讓人感覺很迷的地方,這個東西,怎麼可以讓用傳入呢?

誠然我們可以通過重載 Sanic 類的 __init__ 方法,修改其默認的參數來解決這個問題。

但是新的問題也來了,我一直覺得寫組件要默認一個假設,就是所有用你東西的人,智商 emmmm 都不太高。

好了,因為我們是提供的是插件,如果用戶在使用的時候重新繼承了我們定制的 Sanic 類,同時沒有使用 super 調用我們魔改後的 __init__ 方法。那么這個時候,就會出一些很有趣的乱子。

同時,Sanic 內部耦合嚴重,也會造成我們構建插件的時候的困難。

坑點二:內部耦合嚴重#

現在,我們寫插件,想在生成 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):
        """Take a request from the HTTP Server and return a response object
        to be sent back The HTTP Server only expects a response object, so
        exception handling must be done here

        :param request: HTTP Request object
        :param write_callback: Synchronous response function to be
            called with the response as the only argument
        :param stream_callback: Coroutine that handles streaming a
            StreamingHTTPResponse if produced by the handler.

        :return: Nothing
        """
        try:
            # -------------------------------------------- #
            # Request Middleware
            # -------------------------------------------- #

            request.app = self
            response = await self._run_request_middleware(request)
            # No middleware results
            if not response:
                # -------------------------------------------- #
                # Execute Handler
                # -------------------------------------------- #

                # Fetch handler from router
                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"))

                # Run response handler
                response = handler(request, *args, **kwargs)
                if isawaitable(response):
                    response = await response
        except Exception as e:
            # -------------------------------------------- #
            # Response Generation Failed
            # -------------------------------------------- #

            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:
            # -------------------------------------------- #
            # Response Middleware
            # -------------------------------------------- #
            try:
                response = await self._run_response_middleware(request,
                                                               response)
            except BaseException:
                error_logger.exception(
                    'Exception occurred in one of response middleware handlers'
                )

        # pass the response to the correct callback
        if isinstance(response, StreamingHTTPResponse):
            await stream_callback(response)
        else:
            write_callback(response)

這就造成了一個現象,我們只需要對於某一個階段數據進行額外的操作的時候,我們勢必要重載 handle_request 這個大方法。就比如前面說的,我們只需要在 Response 生成的時候,進行一些額外操作,在 Flask 中我們只需要重載對應的 make_response 方法即可,而在 Sanic 中我們需要重載整個 handle_request 。可謂牽一發動全身。

同時,Sanic 不像 Flask 一樣,做到了 WSGI 層的請求處理和 Framework 層的邏輯相互分離。這樣一種分離,有時會給我們帶來很多方便。

比如我之前寫過這樣一篇辣雞文章你所不知道的 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_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 一樣,實現 RouteParser 以及 Forammter(就是 Converter) 彼此相互分離的特性,我們只需要按需重構添加即可,如同文中所舉的例子。

整個這一部分,其實就在吐槽,Sanic 內部耦合嚴重,如果想實現一些額外的操作,可以說牽一發動全身。

坑點三:細節以及其餘的坑#

這一部分大概有幾方面要說。

第一,Sanic 依賴的庫,其實,emmmmmm,不太穩定,比如 10 月份的時候,觸發了一個 bug ,其所依賴的 ujson 在序列化一些特定數據的時候,會拋出異常,這個問題,14 年就已經爆出來了,不過到目前沒修,2333333,同時當時的版本,如果要使用內置的函數的話,是不可以讓用戶選擇具體的 parser 的,具體可以參考當時我提的 PR

第二,Sanic 一些東西實現的並不嚴謹,比如這篇文章有吐槽過日常辣雞水文:一個關於 Sanic 的小問題的思考

第三,Sanic 現在不支持 UWSGI ,同時和 Gunicorn 配合部署的話,是自己實現了一套 Gunicorn Worker ,在我們生產環境下,會有一些諸如未知原因 504 這樣的玄學 BUG,不過我們還在追查(另外有消息聲稱,Sanic 的 Server 部分並不嚴格遵守 PEP333即 WSGI 協議,= = 我改天核查一下)

總結#

Sanic 的性能的確很棒,當時技術驗證時,測試的時候,不同業務邏輯下,基本都能保證其性能在 Flask 的 1.5 倍以上。但是就目前的使用經驗來說 Sanic 距離真正生產可用,還有相當長一段路要走。無論是內部的架構,還是周邊的生態,亦或者是其他。大家可以沒事拿來玩玩,但是如果要上生產線,請做好被坑的準備。

最後祝大家新年快樂,Live Long And Prosper!

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。