知乎是怎么运行 tornado web 服务的

You build it, you run it. - Werner Vogels

现在很多互联网公司采用了微服务架构,将业务拆分,保持代码仓库尽量精简。同时一个小团队负责开发和维护一个服务,提升了开发和部署效率(软件本身的组织结构与软件团队的组织结构式一致的,即康威定律)。 知乎很多业务后端采用了 python web 框架 tornado,都是开源的、成熟稳定的技术(虽然笔者更喜欢 flask 全家桶)


用 gunicorn + gevent 跑 tornado app

先来写个无聊的 tornado handler,统计多个网站页面的 html 长度之和。原始代码如下,为了简单起见省去所有异常处理 (tornado_app.py):

import gevent.pywsgi
import requests
import tornado.wsgi
from tornado.web import Application, RequestHandler


def get_html_length(url):
    return len(requests.get(url).text)


URLS = ['https://www.baidu.com?page={}'.format(i) for i in range(100)]


class CurlHandler(RequestHandler):
    def get(self):
        length = 0
        for url in URLS:
            length += get_html_length(url)
        self.write(str(length))


def get_tornado_application():
    application = Application(
        [
            (r'/', CurlHandler),
        ],
        debug=True
    )
    return application


def get_wsgi_application():
    application = get_tornado_application()
    return tornado.wsgi.WSGIAdapter(application)


app = get_wsgi_application()


def run():
    server = gevent.pywsgi.WSGIServer(('', 8000), app)
    server.serve_forever()


if __name__ == '__main__':
    run()

如果安装了 gevent,可以直接 python tornado_app.py 运行此 app。不过我们一般使用 gunicorn 指定 worker 为 gevent 来运行 tornado app。
在命令行中用如下命令启动(实际上就是用的 gevent wsgi):

gunicorn tornado_app:app -b 0.0.0.0:8000 -w 4 -k gevent

一般一个请求的流程如下:http request -> Nginx -> HAProxy -> gunicorn(gevent wsgi) -> tornado app
由于是容器部署,会根据容器的 cpu 和内存资源适当调整 worker 的值,具体需要根据真实的部署环境实测一把。


在 tornado 中使用 gevent 并发

mysql 连接上使用了 PyMySQL,用 sqlalchemy core 请求数据,纯 python 实现的 mysql driver 支持被 gevent patch

Pure python driver support gevent’s monkey patch, so they support cooperative multitasking using coroutines. That means the main thread won’t be block by MySQL calls when you use PyMySQL

经常在一个 api 接口里需要请求多个数据(多个数据库请求、rpc调用、网络请求等),比如用户、文章等,这个时候如果不是并发请求数据,速度将是不可接受的。我们用 gevent 来实现并发请求,具体大家可以参考 gevent 的文档。一个简单的并发请求的例子如下,实际上很简单,我们使用 gevent.pool (池(pool)是一个为处理数量变化并且需要限制并发的greenlet而设计的结构。):

class PoolCurlHandler(RequestHandler):
    def get(self):
        length = 0
        pool = gevent.pool.Pool(20)
        res = pool.map(get_html_length, URLS)
        length = sum(res)
        self.write(str(length))

完整的代码如下:

import gevent.pywsgi
import requests
import tornado.wsgi
from tornado.web import Application, RequestHandler


def get_html_length(url):
    return len(requests.get(url).text)


URLS = ['https://www.baidu.com?page={}'.format(i) for i in range(100)]


class CurlHandler(RequestHandler):
    def get(self):
        length = 0
        for url in URLS:
            length += get_html_length(url)
        self.write(str(length))


class PoolCurlHandler(RequestHandler):
    def get(self):
        length = 0
        pool = gevent.pool.Pool(20)
        res = pool.map(get_html_length, URLS)
        length = sum(res)
        self.write(str(length))


def get_tornado_application():
    application = Application(
        [
            (r'/', CurlHandler),
            (r'/pool', PoolCurlHandler),
        ],
        debug=True
    )
    return application


def get_wsgi_application():
    application = get_tornado_application()
    return tornado.wsgi.WSGIAdapter(application)


app = get_wsgi_application()


def run():
    server = gevent.pywsgi.WSGIServer(('', 8000), app)
    server.serve_forever()


if __name__ == '__main__':
    run()

启动 app 后用我们请求看下差距:

time curl http://localhost:8000/
time curl http://localhost:8000/pool

至少能看到数倍的时间差距。


参考:

What is gevent? - gevent 1.3.0.dev0 documentation

Tin/sqlalchemy-gevent-mysql-drivers-comparison

Asynchronous Python and Databases - sqlalchemy 作者的文章