一门编程语言入门是容易的,至少大家都知道从hello world开始。但这次性能优化的经历告诉我,“换语言”这件事是有门槛的。
这次性能优化是针对数据入库流程中的一个环节(brief)做的。
我们常说解决问题重要,发现问题更重要。没错,这次发现问题就占用了我较长时间。brief部署在X平台上,通过增加日志,我发现brief耗时较长的部分发生在平台内部,接下来主要工作就是找X平台的负责同学沟通了。X平台已经升级到第三代,而我们还停留在第一代,版本低不要紧,关键是问题比较多,对方投入人力少,只找到对应有效的负责同学就费了很大劲(现在知道与时俱进是多么重要了,^-^!)
经过X平台同学的不懈努力,最终定位为brief的吞吐能力不行,导致整体耗时增加。Ok,扫一眼代码,看brief是单线程的,既然吞吐不行,那就改成多线程吧。等等,这就定了方案了?
方法论有问题。需要这么小题大做么?吞吐不行,为啥不先看看单线程慢在哪里呢?C++、java都有对应的profile工具,python估计也有吧。一搜,果然,不仅有,而且使用起来更简单,是python自带的工具。
python -m profile /path/to/your.py
执行结束,程序热点立现。
好吧。确实存在很多CPU计算,不太好优化。转多线程吧。尽管我们最终还是要朝着多线程的方向去思考,但是思路不能断,这有助于培养我们的方法论意识。
多线程能解决问题么? 不能!python解释器内部有一把全局锁GIL(Global interpreter Lock),对于计算密集型程序来说,可认为还是单线程执行,并不能提高程序吞吐。好吧,我一开始并不知道python的多线程是伪多线程,这就涉及到语言设计层面的问题,对于我这种只了解hello world的python程序员,显然发现不了这样的问题。在现在小学生都要普及python教育的时代,我是out了,要加强学习啊。
言归正传,多线程解决不了怎么办?增加并发还是可以使用多进程,从此踏入了不归路。知道python NB的我坚信python肯定有多进程支持,hello world的思想让我在百度很快找到了python多进程demo。把demo搬到brief,执行brief,程序无法退出,CTRL + C后发现,程序在疑似锁等待的位置抛异常了。多进程的生产者消费者模型不好写啊,multiprocessing.Queue竟然没有判断queue关闭的接口,只能自己实现了一套:
queue_closed = multiprocessing.Value('i', 0)
def produce(q, cnt, value, queue_closed):
while cnt:
q.put(value)
cnt = cnt - 1
q.close()
queue_closed.value = 1
q.join()
def consume(q, queue_closed):
while True:
try:
if queue_closed.value == 1 and q.qsize() == 0:
break
value = q.get(timeout=0.01)
process(value, output_queue)
q.task_done()
except Empty:
logging.warning("get queue item timeout")
死锁问题解决了,利用多进程确实提高brief的吞吐,但另外一个问题来了:日志怎么打?logging支持多线程,但不支持多进程。又是一番搜索,找到几个解决方案,multiprocessing_logging,ConcurrentLogHandler,试了一下又出现一直死锁的问题了,被死锁搞得焦头烂额,这次算是比较常见的问题了:多线程环境下fork,即多进程多线程,fork子进程时把父进程持有的锁也复制了,导致父进程释放锁时,子进程并不能感知这个状态,进而死锁,解决方案先fork子进程,后启动线程。最终,通过ConcurrentLogHandler解决日志问题,ConcurrentLogHandler使用到了文件锁,对性能要求很高的程序慎用(当然,如果实在很高,你该选用C++了)。业界有名的软件nginx采用的是单线程多进程的架构,可以学习一下她的日志打印机制。
至此,brief性能优化告一段落。总结一下:
- 学习一门新的语言,从更高层次去了解它的设计思想或许是一种更有效的学习方式,毕竟思想比hello world更通用
- 学习业界著名开源软件设计思想