#9 Tornado 正在死亡

2021-12-16

2020 年 10 月发布的 6.1,结果到现在,已经一年多过去了,6.2 还没有见到。
Update @ 2022-04-28: 又过去了小半年,6.2 还是没有影子。

img

从 GitHub 提交频率图上明显可以看到 Tornado 的活跃度大幅下滑,2020 年的提交已然不多,2021 年更加惨不忍睹。

官方协程框架 AsyncIO 的诞生,标志着 Tornado 的历史使命已经完成。

是时候带着对 Tornado 的回忆,全面转投原生协程生态了。

#8 Tornado HTTP 服务的 “ACK”

2021-12-13
  1. 一个请求会先到 A 服务,然后通过 HTTP 调用的方式传到 B 服务 (基于 Tornado)
  2. 面对突然涌来的大量请求,B 服务负载迅速升高,响应时间拉长
  3. A 服务的部分请求因为超时断开,然后重发

也就是说,在负载较高的时候,B 会收到一些重复请求。因为负载高的时候,B 其实已经接收过一遍,只是没有响应。
B 服务处理任务结束之后,会将请求加入幂等。但是在 B 服务正在处理的时候,还是会有重复处理的可能性。
如果是一些对数据可靠性有一定要求的场景,这样重复的处理就不能接受。

比较合适的设计应该是 A 和 B 之间通过 MQ 通讯,根本不可能有这样的情况发生,哪怕请求再多也可以按照自己的速度消费,使系统保持最佳状态运行。而且,服务之间解耦之后,可以避免相互影响。

但是,如果不能改变 A B 两个服务的现有设计(调用关系),可以做的事情:

  1. 通过请求的正常返回来当 ACK 机制 (本文原本想重点说的内容)
  2. 将请求的接收和处理拆开
  3. 请求收到,加入队列,然后就可以返回了,这个时间非常短,这个过程出现问题的可能性非常低
  4. 如果在处理请求之前,连接断开,那就结束流程,抛弃任务
  5. A 服务的重试机制延迟一个合适的时间(比如 3 分钟)处理,可以通过 MQ, DB, Redis 实现

两个方案都应该能大幅减小出现重复请求的情况,双管齐下效果会更好。
方案一, 如果引入 MQ 或 DB 的话,就会觉得为什么不在 A 服务做;如果不引入新组件的化,复杂是需要保证服务突然挂掉之后,队列数据的恢复。只能在启动服务时通过扫描日志来恢复数据。
方案二,无论是业务还是代码,影响范围比较小,易于实现。

Tornado 实现 ACK 机制

客户端在正常流程处理完成之前,断开连接,会触发 on_connection_close 调用,可以在这个里面做手脚。

class MainHandler(tornado.web.RequestHandler):
    def post(self):
        if self._finished:
            return
        # do something

    def on_connection_close(self):
        LOG.debug('connection closed')
        self._finished = True
        super().on_connection_close()

#3 tornado: yield

2018-08-05
from tornado import gen, ioloop

@gen.coroutine
def dosth():
    yield gen.sleep(1)
    print('slept for 1 second')

ioloop.IOLoop.current().run_sync(dosth)
from tornado import gen, ioloop

@gen.coroutine
def dosth():
    print('dosth 222222222222222222222')
    yield gen.sleep(1)
    print('slept for 1 second 22222222')
    print('dosth over 2222222222222222')

@gen.coroutine
def test():
    print('test 111111111111111')
    dosth()
    print('test over 1111111111')

print('start')
ioloop.IOLoop.current().run_sync(test)
print('over')

# start
# test 111111111111111
# dosth 222222222222222222222
# test over 1111111111
# over

# start
# test 111111111111111
# dosth 222222222222222222222
# test over 1111111111
# slept for 1 second 22222222
# dosth over 2222222222222222
# over

#2 Tornado & HTTP 599

2015-10-04

Tornado POST 请求直接 599 了,经过 WireShark 抓包发现根本没有发出请求。
最后竟然发现和版本有关,4.1 下才有问题,4.2 以后就好了。

#1 Tornado 1,2,3

2013-10-13

Tornado web server 是使用 Python 编写出來的一个极轻量级、高可伸缩性和非阻塞 IO 的 Web 服务器软件,著名的 Friendfeed 网站就是使用它搭建的。

Tornado 跟其他主流的 Web 服务器框架(主要是 Python 框架)不同是采用 epoll 非阻塞 IO,响应快速,可处理数千并发连接,特别适用用于实时的 Web 服务。

要使用它,必须按照以下套件:

1)Python(建议使用 Python 2.5 / Python 2.6)
2)Simplejson(建议使用 simplejson 2.0.9)
3)cURL(建议使用 curl 7.19.7 或以上版本)
4)Pycurl(建议使用 pycurl 7.16.2.1)
5)Tornado Web Server(这才是主角,版本就照官網上最新的安裝吧)

一个最简单的服务:

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

application = tornado.web.Application([
    (r"/", MainHandler),
])

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

在 Tornado 中运行 Django

#!/usr/bin/env python
# *-* encoding: utf-8 *-*

import os
import sys
import tornado.web
from tornado import autoreload
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.web import url
from django.conf import settings
from django.core.handlers.wsgi import WSGIHandler

if not os.path.dirname(__file__) in sys.path[:1]:
    sys.path.insert(0, os.path.dirname(__file__))

os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'

class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            url(r"/static/(.+)", tornado.web.StaticFileHandler, dict(path=settings.MEDIA_ROOT), name='static_path'),
            url(r"/media/(.+)", tornado.web.StaticFileHandler, dict(path=settings.MEDIA_ROOT), name='media_path'),
        ]
        handlers.append(('.*', tornado.web.FallbackHandler, dict(fallback=WSGIContainer(WSGIHandler()))))

tornado.web.Application.__init__(self, handlers)
http_server = HTTPServer(Application())
http_server.listen(8080)
loop = IOLoop.instance()
autoreload.start(loop) #自动加载修改过的代码
loop.start()