1. 为什么高并发场景下我会把票投给 Tornado很多人第一次接触 Python 高并发脑子里蹦出来的第一反应是 FastAPI 或者 Flask 加个 Gunicorn 多开几个 worker。这套组合确实能打应付日常业务绰绰有余。但如果你真的遇到过那种“单机要扛住上万条长连接、每个连接都要实时推送数据”的场景你就会发现传统同步框架的瓶颈来得比想象中快得多。Tornado 就是在这种需求下重新回到我视野里的。Tornado 是一个用 Python 写的 Web 框架同时也是异步网络库。它最早是某家做实时搜索服务的公司内部孵化出来的核心目标只有一个用单台机器撑住尽可能多的并发连接。它靠的是非阻塞 I/O 加事件循环这套机制而不是给每个请求开一个线程或者进程。这个设计思路决定了它在处理长轮询、WebSocket、服务端推送这类“连接多、单次数据量小、等待时间长”的场景时资源利用率比传统同步框架高出一个量级。我先把话说在前面Tornado 不是银弹。如果你的业务是典型的 CRUD 后台请求响应都很快数据库是瓶颈那用 Tornado 的收益并不明显甚至因为异步生态的坑会让你多花不少时间。但如果你要做的是实时消息推送、在线协作、物联网设备接入、长连接网关这类东西Tornado 的事件循环模型会让你在单机上跑出远超预期的连接数。这篇文章我打算按“从理解它为什么快到写出第一个能跑的异步服务再到踩过哪些坑最后怎么部署上线”这条线来展开。中间会穿插大量我在实际项目里遇到的真实问题和处理方式代码都是可以直接复制去跑的。不管你是刚听说 Tornado 想试试水还是已经用过但被异步回调绕晕了应该都能从里面找到对你有用的东西。2. Tornado 的事件循环到底比线程模型省在哪里2.1 一个连接一个线程的代价要理解 Tornado 的价值得先搞清楚传统同步框架在并发连接面前到底卡在哪。假设你用 Flask 起了一个服务前面挂 Nginx后面用 Gunicorn 开 8 个 worker 进程。每个 worker 同一时刻只能处理一个请求因为 Flask 的视图函数是同步阻塞的。当第 9 个请求进来的时候它只能排队等着直到前面某个 worker 空出来。那有人会说我多开点 worker 不就行了问题在于每开一个进程或者线程操作系统都要分配独立的内存栈空间。一个线程默认栈大小在 Linux 上通常是 8MB就算你调小到 512KB一万个线程就是 5GB 内存还没算上下文切换的开销。CPU 在成千上万个线程之间来回切换真正用来干活的时间被切得稀碎。这就是所谓的 C10K 问题——单机同时处理一万个连接用线程模型几乎是不可能完成的任务。Tornado 的做法完全不同。它只有一个主线程跑事件循环所有 I/O 操作都是非阻塞的。当某个连接没有数据可读时它不会傻等而是立刻把控制权交还给事件循环去处理其他已经就绪的连接。这就好比一个餐厅服务员传统模式是一个服务员盯一桌客人从头到尾客人看菜单看半小时服务员就站旁边等半小时。Tornado 模式是一个服务员管所有桌子哪桌举手就去哪桌没人举手就站在中间等着。显然后者能服务的桌数多得多。2.2 事件循环的工作机制拆解Tornado 的事件循环底层依赖的是操作系统的 I/O 多路复用机制Linux 上是 epollmacOS 上是 kqueueWindows 上是 select。这些系统调用的作用用一句话概括就是帮我盯着这一堆文件描述符哪个有动静了就告诉我没动静我就睡着。具体到 Tornado 的运行流程大概是这样的事件循环启动后会维护一个“待处理事件”的集合。每次循环开始时它调用 epoll 等待事件发生如果有连接可读、可写或者有定时器到期epoll 就返回对应的文件描述符列表。Tornado 拿到这些就绪的描述符后找到它们对应的回调函数并执行。回调函数执行完毕后可能又注册了新的 I/O 等待或者定时器然后进入下一轮循环。这个模型的关键在于任何单个回调函数都不能阻塞太久。如果你在回调里写了一个time.sleep(5)那整个事件循环就被卡住 5 秒所有其他连接都得不到响应。这是 Tornado 编程中最容易犯的错误也是很多新手觉得“Tornado 怎么比 Flask 还慢”的根本原因——他们把同步阻塞的代码直接搬过来了。2.3 协程让异步代码写起来像同步早期 Tornado 用回调函数写异步代码嵌套几层之后可读性极差业内戏称“回调地狱”。后来 Python 3.5 引入了async和await语法Tornado 也很快跟进支持。现在你可以用写同步代码的直觉来写异步逻辑遇到 I/O 操作就await一下事件循环会自动在等待期间去处理其他任务。举个例子你要先查数据库拿到用户信息再根据用户 ID 查订单列表最后返回。同步写法就是两行查询顺序执行。异步写法里两个await之间事件循环可以自由地去服务其他请求。代码看起来还是顺序的但执行效率完全不同。这就是协程的魔力——它让开发者用同步的思维写出异步的性能。不过要注意await后面跟的必须是一个“可等待对象”通常是 Tornado 或者异步库提供的协程函数。如果你await了一个普通的同步函数那不会报错但也不会有任何异步效果事件循环照样被阻塞。这个坑我后面会专门讲怎么排查。3. 从零写一个能扛住压测的 Tornado 服务3.1 环境准备与依赖选择先把环境搭起来。Python 版本我建议至少 3.8 以上最好是 3.10 或更高因为新版本在异步性能和语法支持上更完善。虚拟环境用 venv 或者 conda 都行看个人习惯。python -m venv tornado-env source tornado-env/bin/activate # Windows 用 tornado-env\Scripts\activate pip install tornadoTornado 本身是纯 Python 库没有复杂的 C 扩展依赖安装非常干净。但实际项目里你大概率还需要一些配套库。数据库方面如果用 PostgreSQL推荐asyncpg如果用 MySQL可以用aiomysqlRedis 的话aioredis是标配。HTTP 客户端请求外部接口Tornado 自带的AsyncHTTPClient就够用不需要额外装 requests因为 requests 是同步阻塞的在 Tornado 里用会直接卡死事件循环。这里有个选型经验尽量选原生支持 async/await 的库。有些库虽然能用线程池包装成异步但线程切换的开销和调试的复杂度都会上升。能原生异步就原生异步这是原则。3.2 第一个异步 Handler 的完整写法Tornado 的核心概念是Application和RequestHandler。Application负责路由映射RequestHandler负责处理具体请求。下面是一个最基础的异步服务示例import tornado.ioloop import tornado.web import asyncio class MainHandler(tornado.web.RequestHandler): async def get(self): # 模拟一个耗时的异步操作比如查数据库 await asyncio.sleep(0.1) self.write({message: hello tornado, status: ok}) class HealthHandler(tornado.web.RequestHandler): def get(self): self.write({status: healthy}) def make_app(): return tornado.web.Application([ (r/, MainHandler), (r/health, HealthHandler), ]) if __name__ __main__: app make_app() app.listen(8888) print(server started on port 8888) tornado.ioloop.IOLoop.current().start()这段代码里MainHandler的get方法用了async def里面await asyncio.sleep(0.1)模拟了一个异步 I/O 等待。在这 0.1 秒里事件循环可以去处理其他请求。而HealthHandler是同步的因为它没有 I/O 操作直接返回结果用同步写法反而更简单。这里有个细节值得注意self.write()在异步 Handler 里依然可以正常使用Tornado 会自动处理响应缓冲。但如果你在await之后才调用self.write()要确保请求还没有被结束。如果客户端提前断开了连接继续写会抛异常所以生产环境里通常要加一层判断。3.3 路由设计与请求生命周期Tornado 的路由是用正则表达式匹配的这比很多框架的字符串匹配更灵活。比如你想把用户 ID 作为路径参数class UserHandler(tornado.web.RequestHandler): async def get(self, user_id): # user_id 是从 URL 里提取的字符串 self.write({user_id: user_id, name: fuser_{user_id}}) app tornado.web.Application([ (r/user/([0-9]), UserHandler), ])请求进入 Tornado 后的完整生命周期是这样的首先Application根据 URL 找到对应的RequestHandler类然后实例化这个 Handler把Request对象传进去。接着依次调用initialize()、prepare()然后根据 HTTP 方法调用对应的get()、post()等方法。方法执行完后如果没有显式调用finish()Tornado 会自动调用。最后on_finish()被触发可以在这里做资源清理。理解这个生命周期很重要因为很多坑就藏在prepare()和on_finish()里。比如你可以在prepare()里做统一的身份校验校验不通过就直接self.finish()并返回后面的get()就不会执行了。on_finish()则适合做日志记录、连接释放这类收尾工作。3.4 用 AsyncHTTPClient 调用外部接口实际项目里服务很少是孤立的总要调用其他服务的 API。Tornado 自带的AsyncHTTPClient是异步的不会阻塞事件循环from tornado.httpclient import AsyncHTTPClient, HTTPRequest class ProxyHandler(tornado.web.RequestHandler): async def get(self): http_client AsyncHTTPClient() try: response await http_client.fetch( https://api.example.com/data, methodGET, request_timeout5, connect_timeout3 ) self.write(response.body) except Exception as e: self.set_status(502) self.write({error: str(e)})这里设置了两个超时参数request_timeout是整个请求的最大耗时connect_timeout是建立连接的超时。这两个参数一定要设否则外部服务挂了你的请求会一直挂着慢慢耗尽资源。我见过一个线上事故就是因为没设超时下游服务响应变慢导致上游 Tornado 服务的连接数暴涨最后整个服务不可用。另外AsyncHTTPClient默认会缓存实例但如果你需要不同的配置比如不同的超时时间可以创建多个实例。不过更推荐的做法是用tornado.httpclient.AsyncHTTPClient.configure()在启动时统一配置。4. 异步编程里那些让我熬夜排查的坑4.1 阻塞调用是怎么悄悄毁掉性能的这是 Tornado 新手最容易踩的坑没有之一。你在 Handler 里写了一个看似正常的同步函数调用比如requests.get()、time.sleep()、或者某个数据库驱动的同步查询整个事件循环就被卡住了。表面上看服务还能跑但并发能力直接退化到和单线程同步框架一个水平。怎么判断一个调用是不是阻塞的看它有没有await。如果一个函数调用前面没有await而且它内部做了 I/O 操作或者长时间计算那它就是阻塞的。常见的阻塞源包括requests库的所有方法、time.sleep()、同步的数据库驱动如pymysql、文件读写大文件、CPU 密集型计算如大量数据排序、图像处理。排查方法也很直接在压测环境下观察 QPS 和响应时间。如果并发数上去了但 QPS 不涨响应时间线性增加那基本可以确定有阻塞点。更精确的做法是用py-spy这类工具 dump 运行中的调用栈看看事件循环卡在哪个函数里。解决方案分几种情况。如果是 I/O 阻塞换成异步库就行。如果是 CPU 密集计算那没办法只能丢到线程池或者进程池里执行from concurrent.futures import ThreadPoolExecutor import tornado.ioloop executor ThreadPoolExecutor(max_workers4) class ComputeHandler(tornado.web.RequestHandler): async def get(self): loop tornado.ioloop.IOLoop.current() result await loop.run_in_executor(executor, self.heavy_compute) self.write({result: result}) def heavy_compute(self): # 这里是同步的 CPU 密集计算 total sum(i * i for i in range(10_000_000)) return total注意run_in_executor的线程池大小要控制好开太多线程反而会因为 GIL 和上下文切换导致性能下降。一般来说CPU 核心数加一是个合理的起点。4.2 协程里异常处理的正确姿势异步代码的异常传播和同步代码不太一样。在async def函数里如果某个await抛了异常它会像同步代码一样向上传播直到被try/except捕获。但问题在于如果异常没有被捕获Tornado 默认会返回 500 错误但堆栈信息可能不会完整打印出来给排查带来困难。我的做法是在Application层面设置一个统一的异常处理器class BaseHandler(tornado.web.RequestHandler): def write_error(self, status_code, **kwargs): self.set_header(Content-Type, application/json) if status_code 500: self.write({error: internal server error}) elif status_code 404: self.write({error: not found}) else: self.write({error: ferror {status_code}}) async def prepare(self): # 统一的请求前处理 pass然后所有 Handler 继承BaseHandler。这样既能保证返回格式统一又不会把内部错误细节暴露给客户端。同时在write_error里记录日志把完整的异常堆栈写到日志文件里方便事后排查。还有一个容易忽略的点asyncio.CancelledError。当客户端提前断开连接时Tornado 可能会取消正在执行的协程抛出这个异常。它继承自BaseException而不是Exception所以普通的except Exception捕获不到。如果你在协程里做了资源分配比如打开了数据库连接一定要用finally块来释放否则会泄漏。4.3 数据库连接池的配置陷阱异步数据库驱动通常都带连接池但默认配置往往不适合生产环境。以asyncpg为例默认的连接池大小可能只有 10在高并发下很快就不够用了。但也不是越大越好每个数据库连接在服务端都要占资源连接数太多反而会拖慢数据库。我的经验值是连接池大小设置为预期最大并发数的 1.5 到 2 倍但不要超过数据库服务端的max_connections限制。比如你的服务峰值 QPS 是 1000每个请求平均持有连接 10ms那同时需要的连接数大约是 10 个。考虑到波动设置 20 到 30 个比较稳妥。import asyncpg async def init_db_pool(): pool await asyncpg.create_pool( hostlocalhost, port5432, userapp_user, passwordapp_password, databaseapp_db, min_size5, max_size30, max_inactive_connection_lifetime300, command_timeout10 ) return poolmax_inactive_connection_lifetime控制空闲连接多久后被回收设一个合理的值可以避免连接在服务端被防火墙悄悄断开后客户端还拿着死连接用。command_timeout则是单条 SQL 的执行超时防止慢查询拖垮整个服务。还有一个坑是连接池的初始化时机。如果你在 Handler 里每次请求都创建连接池那性能会惨不忍睹。正确做法是在服务启动时创建全局连接池在 Handler 里直接使用。Tornado 的Application可以挂载自定义属性或者用模块级变量来保存连接池引用。5. 压测数据告诉我的性能边界在哪里5.1 用 wrk 和 ab 做基准测试服务写完了不压测就上线等于裸奔。压测工具我用得最多的是wrk它比ab更轻量支持多线程和长连接适合测 Tornado 这种异步服务。wrk -t4 -c1000 -d30s --latency http://127.0.0.1:8888/参数含义-t4是 4 个压测线程-c1000是 1000 个并发连接-d30s是持续 30 秒--latency输出延迟分布。跑完之后你会看到 QPS、平均延迟、P99 延迟这些关键指标。我在一台 4 核 8G 的云服务器上测过一个简单的 Tornado 服务Handler 里只有一个await asyncio.sleep(0.01)1000 并发下 QPS 能稳定在 8000 以上P99 延迟在 50ms 以内。同样的逻辑用 Flask 加 Gunicorn 8 worker 跑QPS 只有 2000 出头而且延迟波动很大。这个差距在连接数越多的时候越明显。但要注意压测结果受很多因素影响网络带宽、CPU 型号、Python 版本、是否开了调试模式。Tornado 的调试模式debugTrue会开启自动重载和详细的错误页面性能会下降不少生产环境一定要关掉。5.2 单机连接数的实际上限Tornado 单机能扛多少连接这个问题没有标准答案取决于你的业务逻辑和硬件配置。如果 Handler 里只是简单返回一个字符串不做任何 I/O那单机几万连接问题不大。但如果每个连接都要查数据库、调外部接口那瓶颈就在下游服务不在 Tornado 本身。我实测过一个长连接推送服务每个连接保持 WebSocket服务端每隔几秒推送一次心跳。在 8 核 16G 的机器上稳定支撑了 5 万个并发连接CPU 使用率在 60% 左右内存占用约 4G。再往上加连接内存和文件描述符就开始吃紧了。文件描述符限制是很多人忽略的点。Linux 默认单进程最多打开 1024 个文件描述符每个网络连接都要占一个。你需要修改/etc/security/limits.conf把nofile调大* soft nofile 65535 * hard nofile 65535然后确认 Tornado 进程实际生效的限制可以用ulimit -n查看。如果没生效可能是 systemd 服务配置里也需要单独设置LimitNOFILE。5.3 从压测结果反推优化方向压测不只是看 QPS 数字更重要的是通过数据定位瓶颈。如果 QPS 上不去但 CPU 没跑满那大概率是 I/O 阻塞或者锁竞争。如果 CPU 跑满了但 QPS 还是低那可能是代码里有 CPU 密集操作或者 Python 的 GIL 在作祟。延迟分布也很有参考价值。如果 P50 延迟很低但 P99 很高说明大部分请求很快但少数请求被卡住了。常见原因是连接池不够用、垃圾回收触发、或者某个下游服务偶尔变慢。这时候要去看日志里有没有超时记录或者用链路追踪工具定位慢请求。还有一个反直觉的发现有时候减少并发数反而能提高吞吐量。因为并发太高会导致频繁的上下文切换和资源竞争实际有效吞吐反而下降。找到那个“甜点”并发数是压测的核心目标之一。6. 部署上线时我踩过的那些运维坑6.1 用 systemd 管理 Tornado 进程开发环境直接python app.py跑没问题但生产环境需要进程守护、开机自启、日志管理。systemd 是 Linux 上最标准的选择。下面是一个可用的 service 配置[Unit] DescriptionTornado Web Service Afternetwork.target [Service] Typesimple Userwww-data Groupwww-data WorkingDirectory/opt/tornado-app EnvironmentPYTHONUNBUFFERED1 ExecStart/opt/tornado-app/venv/bin/python /opt/tornado-app/app.py Restartalways RestartSec5 LimitNOFILE65535 [Install] WantedBymulti-user.target几个关键点Restartalways保证进程崩溃后自动拉起LimitNOFILE覆盖系统的文件描述符限制PYTHONUNBUFFERED1让 Python 的输出不缓冲日志能实时写入。User和Group不要用 root用专门的低权限用户减少安全风险。日志方面Tornado 默认输出到 stdout 和 stderrsystemd 会自动收集到 journal 里。你可以用journalctl -u tornado-app -f实时查看。但如果日志量大建议配置StandardOutput和StandardError重定向到文件配合 logrotate 做切割。6.2 Nginx 反向代理的关键配置Tornado 前面通常要挂一个 Nginx负责 SSL 终止、静态文件服务、负载均衡和缓冲。下面是一段核心配置upstream tornado_backend { server 127.0.0.1:8888; keepalive 64; } server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/ssl/certs/example.crt; ssl_certificate_key /etc/ssl/private/example.key; location / { proxy_pass http://tornado_backend; proxy_http_version 1.1; proxy_set_header Connection ; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 60s; proxy_connect_timeout 5s; } location /static/ { alias /opt/tornado-app/static/; expires 7d; } }keepalive 64让 Nginx 和后端保持长连接减少每次请求建立连接的开销。proxy_http_version 1.1和proxy_set_header Connection 是启用 keepalive 的必要条件缺一不可。proxy_read_timeout要设得比 Tornado 的处理超时略长否则 Nginx 会先断开客户端收到 504。如果是 WebSocket 服务还需要额外加 Upgrade 头location /ws { proxy_pass http://tornado_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; }WebSocket 的连接是长久的proxy_read_timeout要设得很大否则空闲一会儿就被 Nginx 切断了。6.3 平滑重启与灰度发布服务上线后总会有更新需求直接 kill 再启动会导致正在处理的请求失败。Tornado 支持优雅关闭收到终止信号后停止接受新连接等正在处理的请求完成后再退出。import signal import tornado.ioloop def graceful_shutdown(): io_loop tornado.ioloop.IOLoop.current() io_loop.stop() def signal_handler(signum, frame): print(freceived signal {signum}, shutting down gracefully) graceful_shutdown() if __name__ __main__: signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) app make_app() app.listen(8888) tornado.ioloop.IOLoop.current().start()配合 systemd 的Restartalways重启流程就是发送 SIGTERM等进程退出systemd 自动拉起新进程。但这里有个问题如果旧进程退出需要时间新进程启动也需要时间中间会有一段服务不可用的窗口。更好的做法是用 Nginx 做灰度。起两个 Tornado 实例监听不同端口Nginx upstream 里配置权重先切一小部分流量到新实例观察没问题后再全量切换。这样即使新版本有问题影响范围也可控。7. 从单机到多实例的扩展思路7.1 多进程模式下的端口共享Tornado 单进程只能用一个 CPU 核心因为 Python 的 GIL 限制了多线程并行执行。要利用多核最直接的方式是起多个进程。Tornado 支持在listen()时传入reuse_portTrue让多个进程绑定同一个端口由操作系统内核做负载均衡import tornado.httpserver import tornado.ioloop import tornado.web import os def make_app(): return tornado.web.Application([ (r/, MainHandler), ]) if __name__ __main__: app make_app() server tornado.httpserver.HTTPServer(app) server.bind(8888, reuse_portTrue) server.start(0) # 0 表示根据 CPU 核心数自动决定进程数 tornado.ioloop.IOLoop.current().start()server.start(0)会自动 fork 出和 CPU 核心数相同的子进程每个子进程跑独立的事件循环。reuse_portTrue让多个进程可以绑定同一端口内核会把新连接分配给其中一个进程。这种方式比 Nginx 做负载均衡更轻量少了一层网络转发。但要注意多进程模式下进程间不共享内存。如果你的服务里有本地缓存或者会话状态需要放到 Redis 这类外部存储里否则不同进程看到的数据不一致。7.2 会话与状态的外部化Tornado 本身不提供会话管理需要自己实现。最简单的做法是用签名 Cookie 存会话 ID真正的会话数据放 Redisimport tornado.web import redis.asyncio as redis import json import uuid class SessionHandler(tornado.web.RequestHandler): async def prepare(self): self.redis redis.Redis(hostlocalhost, port6379, decode_responsesTrue) session_id self.get_secure_cookie(session_id) if session_id: self.session json.loads(await self.redis.get(session_id) or {}) else: self.session {} async def post(self): session_id str(uuid.uuid4()) self.session[user] self.get_argument(user) await self.redis.setex(session_id, 3600, json.dumps(self.session)) self.set_secure_cookie(session_id, session_id) self.write({status: logged in})set_secure_cookie需要Application配置cookie_secret这个密钥要足够随机且保密。会话数据设了 1 小时的过期时间避免 Redis 里堆积无用数据。每次请求都从 Redis 读会话虽然多了一次网络往返但保证了多实例间状态一致。7.3 监控指标暴露与告警接入服务上线后你需要知道它运行得怎么样。Tornado 可以很方便地暴露一个/metrics接口输出 Prometheus 格式的指标class MetricsHandler(tornado.web.RequestHandler): def get(self): self.set_header(Content-Type, text/plain) lines [ ftornado_active_connections {self.application.settings.get(active_connections, 0)}, ftornado_total_requests {self.application.settings.get(total_requests, 0)}, ] self.write(\n.join(lines))然后在Application里维护这些计数器每次请求进来时递增。更完善的做法是用prometheus_client库它提供了 Counter、Gauge、Histogram 等标准指标类型还能自动计算分位数。告警规则我一般设这几条P99 延迟超过 500ms 持续 1 分钟、错误率超过 1% 持续 5 分钟、活跃连接数超过阈值。这些指标能覆盖大部分线上问题剩下的就是接到告警后去查日志定位根因了。8. 我个人的一些实战体会Tornado 这个框架用好了是利器用不好就是给自己挖坑。我最大的体会是异步编程的思维方式需要刻意练习。刚开始写的时候总是忍不住在 Handler 里塞同步调用结果性能上不去还找不到原因。后来养成了一个习惯写完一个 Handler 就检查一遍所有 I/O 操作是不是都await了有没有漏网的阻塞调用。另一个体会是关于生态。Tornado 的异步生态确实不如 FastAPI 丰富很多库没有原生异步支持。遇到这种情况我的选择是能用异步库就用异步库实在没有就用线程池包装但一定要控制线程池大小并且做好监控。不要为了异步而异步如果一个功能用同步方式实现更简单且性能可接受那就用同步。最后说部署。Tornado 的部署方案很成熟systemd 加 Nginx 这套组合足够稳定。但要注意配置不是一劳永逸的随着流量增长连接池大小、超时时间、进程数这些参数都需要调整。定期压测、定期 review 监控数据比一次性调优更重要。我见过太多服务上线时跑得好好的半年后流量翻了几倍各种超时和连接泄漏就冒出来了。保持对性能指标的敏感度是运维 Tornado 服务的基本功。