python web 任务异步处理的探索

1,546 阅读3分钟

发现问题

请求日志写入 kafka 上线后,发现线上一直报警:请求总耗时过长。 然后就去测试了一下操作请求日志的那个函数的耗时,结果是让我挺惊讶的,最长耗时达到了 300ms,最重要的是这个函数影响了全局的接口请求(所有的请求都要经过该函数) 当时增加新操作就直接往函数里加,没有考虑到会如此耗时,大概加了三四个动作吧。

思考问题

既然这个函数如此耗时,那么就直接将这个函数异步咯。想法听上去好像是完美的,然而现实是残酷的。

探索过程

  1. 既然语言用的是 python,那肯定优先考虑使用 coroutine,首先尝试整个函数使用 coroutine 做异步处理,但是由于函数中使用到了 request 这个线程级别的全局变量(只能在请求上下文中使用)。如果这里开了一个新的协程去做处理,因为是异步的,所以原请求会马上结束,该请求所在的请求上下文也随之销毁了。新的协程将无法使用该 reqeust,因为该协程已经不在原请求上下文中,所以第一次尝试失败。
  2. 既然无法直接整个函数做异步,那么就将这个函数拆开,先开一个函数把需要从 request 中获取的信息拿到(这里只是简单的赋值操作,耗时很小)。将对信息的处理的过程又开一个新的函数(主要是处理过程比较耗时),只对处理过程做异步,再次尝试使用协程,然而还是得不到预期的效果。因为协程是属于线程的(相当于线程是属于进程的),当请求结束时,其使用的线程也被销毁了,所以我们刚开的协程也没了,处理还没开始就结束了(当然可以通过 join 等到函数执行结束,但这样已经失去了异步的意义了)。第二次尝试失败。
  3. 最后妥协了,还是使用线程吧,直接开启一个线程去异步执行,看起来应该没啥问题了吧,但线程是比较重量级的,启动一个线程带来的消耗是比较大的,相对于这种毫秒级别的优化显得毫无优势,最后结果就是使用了异步和之前的同步效果相差不大。。。第三次尝试再次失败。
  4. 既然不能每次开启一个线程,那就使用线程池吧,每次使用直接从线程池中取线程就好了,每次只需要提交一个任务到线程池中,主线程就可以结束了,后面的动作无感知,当任务比较多来不及处理时,还可以放到阻塞队列中。这次尝试的结果比较满意,除了线程池第一次启动线程也需要一定的消耗,后面几乎是零消耗。

Diff 效果

单位是微秒

同步执行

print access log cost time:  175002
print access log cost time:  18812
print access log cost time:  32655
print access log cost time:  60613
print access log cost time:  57820
print access log cost time:  6242
print access log cost time:  5541
print access log cost time:  17026
print access log cost time:  19471

异步执行

print access log cost time:  62
print access log cost time:  61
print access log cost time:  67
print access log cost time:  87
print access log cost time:  60
print access log cost time:  149
print access log cost time:  79
print access log cost time:  58
print access log cost time:  65