问题排查
在从kafka获取数据消费过程中,使用ThreadPoolExecutor(线程池),在数据量大的情况下,导致内存泄露,机器卡死挂掉;
伪代码为:
def deal_func(msg):
# 处理相关逻辑
pass
pool = ThreadPoolExecutor(10)
# 链接kafka
while True:
msg = client.poll(0.1)
# 调用
pool.submit(deal_func, msg)
在调用该程序之后,内存直线上升;在查询文章后,发现原因,在循环调用线程池时,进程会不断的往线程池中扔任务,而不会判断,等待线程池中是否存在空闲进程;
验证程序:
import time
from concurrent.futures import ThreadPoolExecutor
def func(num):
print(f'the {num} run')
time.sleep(2)
return num * num
def main():
pool = ThreadPoolExecutor(2)
result = []
for i in range(100):
pool.submit(func, i)
print(pool._work_queue.qsize())
pool.shutdown()
if __name__ == '__main__':
main()
执行结果如下:
1
...
95
96
97
98
the 2 runthe 3 run
the 4 run
the 5 run
the 6 run
the 7 run
可以看出当前进程并未判断线程池是否存在空闲进程;
进程向线程池下发任务是哪个位置进行下发呢?
查看源码,可以发现:
class ThreadPoolExecutor(_base.Executor):
#...省略代码
self._max_workers = max_workers
self._work_queue = queue.SimpleQueue()
self._threads = set()
self._broken = False
self._shutdown = False
代码中新建了SimpleQueue
为线程池进行通信的队列对象,但是没有指定队列大小,在不断向线程池加入消息过程中,线程池消费速度跟不上生产速度,队列中就会不断积压消息,导致进程内存不断增加;
解决方式
既然线程池使用的为无界队列,那么就可以将类重写,并使用有界队列,如:
import queue
from concurrent.futures import ThreadPoolExecutor
class BoundThreadPoolExecutor(ThreadPoolExecutor):
def __init__(self, *args, **kwargs):
super(BoundThreadPoolExecutor, self).__init__(*args, **kwargs)
self._work_queue = queue.Queue(2)
在初始化函数__init__
中重写线程池的队列对象,赋值为Queue(2)
,这样就可以限制队列的大小,并且在执行过程中,队列如果满了,那么程序就会等待线程池,直到线程池存在空闲线程;
到此为止,解决了进程为什么会内存溢出,但是ThreadPoolExcutor是如何实现的呢,可以进一步分析源码;见下一章节;