无知的 tonyseek

Yet Another Seeker

慎用异步 WSGI Server 运行 Flask 应用

这个标题有点不太准确,其实应该说:慎用异步 WSGI Server 运行基于 Werkzeug 的应用。或者更宽泛一点——慎用异步 WSGI Server 运行使用 Thread Local 的应用。 Flask 是基于 Werkzeug 的故也在此列。貌似使用 Werkzeug 作为底层 WSGI 库的框架还有国内 limodou 老师的 Uliweb 。而其他使用 Thread Local 的 WSGI Application 不计其数,Bottle、web.py 都在此列。但我没有测试过其他的,仅仅掉下过 Flask 的坑。

这个坑问题就出在 Werkzeug 的 Thread Local 上。用过 Flask 的同学应该不会忘记其便捷的 flask.requestflask.gflask.session 等对象。引用的时候就像使用单例对象一样,但实际上对它们的所有赋值操作都只会影响到当前请求(当前线程)。Thread Local 模式的实现一定要有一个 Thread Identity 作为标识,由此产生的 Proxy 对象会根据这个标识创建多个独立的数据空间。我们访问代理对象的时候就像访问全局对象,但代理对象会将消息分发到 Thread Identity 对应的真实对象上。

一般这个实现都会使用系统提供的当前线程 ID 作为 Thread Identity,于是在使用多线程的 Web 服务器上,由于每个请求独享一个线程,每个 Thread Local 的数据空间也就为一个请求所独享。这是 Flask(Werkzeug)可以正常工作的假设前提。但异步 WSGI Server 一般不会给每个请求分配一个线程。为了减少内核调度线程的开销,这类服务器软件一般使用协同式调度——若干个请求都在同一个进程的主线程中运行,遇到 IO 阻塞(例如等待网络)则挂起当前帧栈,切换到另一个帧栈执行,整个过程都是协同式的,并没有时间片轮转这些抢占的做法。如此一来,用线程 ID 作为 Thread Identity 的所有 Thread Local 对象就会退化成单例对象,这也是我遇到的灵异现象的原因:在宿舍登录网站,远在图书馆的电脑会同步登录。

说到底还是 Thread Identity 选取导致的问题。其实 Werkzeug 在设计的时候有考虑到这个问题,其默认的 Thread Identity 工厂选取就优先采用 Greenlet 的“当前协程 ID”,这一点从 Werkzeug Local 可以看到。但并非所有异步 WSGI Server 都使用 Greenlet 作为协程库,我所使用的 uWSGI 就是使用自己实现的 uGreen 作为微线程的。于是上述问题就出现了。

和 Flask 的这个问题相关的是 flask.globals 模块中的两个 werkzeug.local.LocalStack 实例,替换它们的 __ident_func__ 属性应该可以解决问题。但我没有找到 uWSGI 默认使用的 uGreen 对应的 Thread Identity 获取方法。或许对于使用 uWSGI 运行的应用来说,不要去使用异步是个更好的选择吧。当然,不怕麻烦的话也可以给 uWSGI 编译使用 Greenlet 调度的异步支持插件。

Comments