如何使用 `Tornado` 的 `gen.Task` 和 `gen.coroutine` 装饰器实现并行

62 阅读3分钟

在后台服务器中引入并行性时,需要查询多个负载均衡器,每个负载均衡器有 5 个不同的查询,并将结果发送回 Web 客户端。后台使用 Tornado 框架,根据文档,使用 @gen.Taskgen.coroutine 应该可以并行处理多个任务。但实际所有请求都是一个接一个地处理的。

2. 解决方案

问题在于 ELBQuery 函数是一个阻塞函数。如果不将它让给另一个协程,协程调度程序就无法交错调用。

如果 calc_range 调用是问题所在,可以将其分解为更小的部分,每个部分都让位于下一个部分,这样调度程序有机会在每个部分之间进入。

但最有可能的是,boto 调用是阻塞的,大部分时间都花在等待 get_metric_statistics 返回,而其他任何事情都无法运行。

有以下几种方法来解决这个问题:

  1. 为每个 boto 任务启动一个线程。 Tornado 使得将协程透明地包装在一个线程或线程池任务中变得非常容易,这神奇地取消了所有阻塞。

  2. boto 任务调度到线程池,而不是一个线程。 与解决方案 1 类似的权衡,尤其是如果只有少数任务。

  3. 重写或修补 boto 以使用协程。 这是理想的解决方案,但工作量最大,并且必须维护更新后的 boto 代码。

  4. 使用 greenlets 并修补足够的库依赖项,以便使其成为异步的。 这听起来很麻烦,但实际上可能是最好的解决方案。

  5. 使用 greenlets 并修补整个 stdlib,以便欺骗 bototornado 而无需意识到。 这听起来像是一个可怕的想法。

  6. 使用单独的进程(甚至是一个进程池),该进程使用类似 gevent 的东西。

如果不了解更多信息,建议先考虑解决方案 2 和 4,但无法保证它们会成为最佳答案。

import tornado.gen
import boto.ec2

@tornado.gen.coroutine
def query_elb(fn, region, elb_name, period):
    callback(fn(region, elb_name, period))

class DashboardELBHandler(RequestHandler):

    @tornado.gen.coroutine
    def get_elb_info(self, region, elb_name, period):
        elbReq = yield gen.Task(query_elb, ELBSumRequest, region, elb_name, period)
        elb2XX = yield gen.Task(query_elb, ELBBackend2XX, region, elb_name, period)
        elb3XX = yield gen.Task(query_elb, ELBBackend3XX, region, elb_name, period)
        elb4XX = yield gen.Task(query_elb, ELBBackend4XX, region, elb_name, period)
        elb5XX = yield gen.Task(query_elb, ELBBackend5XX, region, elb_name, period)

        raise tornado.gen.Return(
            [
                elbReq,
                elb2XX,
                elb3XX,
                elb4XX,
                elb5XX,
            ]
        )

    @tornado.web.authenticated
    @tornado.web.asynchronous
    @tornado.gen.coroutine
    def post(self):
        ret = []

        period = self.get_argument("period", "5m")

        cloud_deployment = db.foo.bar.baz()
        for region, deployment in cloud_deployment.iteritems():

            elb_name = deployment["elb"][0]
            res = yield self.get_elb_info(region, elb_name, period)
            ret.append(res)

        self.push_json(ret)



def ELBQuery(region, elb_name, range_name, metric, statistic, unit):
    dimensions = { u"LoadBalancerName": [elb_name] }

    (start_stop , period) = calc_range(range_name)

    cw = boto.ec2.cloudwatch.connect_to_region(region)
    data_points = cw.get_metric_statistics( period, start, stop,
        metric, "AWS/ELB", statistic, dimensions, unit)

    return data_points

ELBSumRequest   = lambda region, elb_name, range_name : ELBQuery(region, elb_name, range_name, "RequestCount", "Sum", "Count")
ELBLatency      = lambda region, elb_name, range_name : ELBQuery(region, elb_name, range_name, "Latency", "Average", "Seconds")
ELBBackend2XX   = lambda region, elb_name, range_name : ELBQuery(region, elb_name, range_name, "HTTPCode_Backend_2XX", "Sum", "Count")
ELBBackend3XX   = lambda region, elb_name, range_name : ELBQuery(region, elb_name, range_name, "HTTPCode_Backend_3XX", "Sum", "Count")
ELBBackend4XX   = lambda region, elb_name, range_name : ELBQuery(region, elb_name, range_name, "HTTPCode_Backend_4XX", "Sum", "Count")
ELBBackend5XX   = lambda region, elb_name, range_name : ELBQuery(region, elb_name, range_name, "HTTPCode_Backend_5XX", "Sum", "Count")