TOC

Tornado HTTP 服务的 “ACK”

  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()