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 一樣,實現 Route
和 Parser
以及 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!