手头有一个时间序列预测框架,串行运行,效率比较慢 ,考虑用多进程、分布式等方法来优化。本文比较随意地记录下工作和一些笔记。
一、性能瓶颈分析
将整个框架按模块划分,揭露各部分耗时,确定:
- 耗时占比最高的是特征计算的51.1% ;
- 其次是获取原始数据的27.5%;
- 然后是预测部分,耗时占比为15.9%。
分析这三个部分的特点可知:
特征计算和获取数据两个部分的特点在于它们从数据库读写数据,需要耗费较长时间;
特征计算和预测部分的特点在于其内部调用算法进行预测。
所以初步判断“从数据库读取数据”和“预测”是对时序框架效率影响最大的步骤。为了验证以上判断,取若干配置项进行实验,分别记录以上三个部分中的“读取数据”和“预测”的耗时,分析结果知,获取数据的耗时基本全部在于取数据步骤,特征计算的耗时大部分在预测步骤。
根据以上分析可以确定,提升时序预测框架计算效率的关键点是:
- 提升从数据库读取数据的效率;
- 加快预测算法计算效率。
二、多进程
Python的多进程和多线程常用相应的库来实现,多进程使用Multiprocessing库、多线程用Threading库。那么选择多进程还是多线程的方法来优化框架呢?
对于python,存在特殊机制:为了保证数据安全,Python最常用的解释器Cpython中设定有全局解释器锁(Global Interpreter Lock, GIL)。GIL的作用是保证在一个进程中,同一时间只能有一个线程运行,其示意图如下(图片来自知乎)。所以Python的多线程实际上是一个进程内的多个线程之间频繁切换交替运行。根据官方文档的介绍,由于系统调度的原因,在多核处理器下执行多线程的计算,会比不使用多线程还要慢。
下图是stackoverflow上老哥的实验图,对比了python多进程和多线程的表现。
时间序列预测过程中有大量计算,属于CPU密集型任务,故应选择多进程并行方案。此外多进程还有以下优势:
- 鲁棒性强,一个进程挂掉通常不会影响其它进程;
- 编程和维护比较方便;
- 便于以分布式进程的方法进一步优化。
故选择使用多进程的思路优化框架。
测试发现,使用多进程并行方案后,用时减少了61.6%,效率提升很明显。
三、分布式进程
项目还有另一台服务器,干脆也拿过来练练手,学习下分布式啊集群啊啥的。
这台机器很干净,先装yum源:
rpm -qa | grep yum | xargs rpm -e --nodeps
wget http://mirrors.aliyun.com/centos/7.2.1511/os/x86_64/Packages/yum-metadata-parser-1.1.4-10.el7.x86_64.rpm
wget https://mirrors.aliyun.com/centos/7/os/x86_64/Packages/yum-3.4.3-168.el7.centos.noarch.rpm
wget https://mirrors.aliyun.com/centos/7/os/x86_64/Packages/yum-plugin-fastestmirror-1.1.31-54.el7_8.noarch.rpm
wget https://mirrors.aliyun.com/centos/7/os/x86_64/Packages/yum-utils-1.1.31-54.el7_8.noarch.rpm
wget https://mirrors.aliyun.com/centos/7/os/x86_64/Packages/python-urlgrabber-3.10-10.el7.noarch.rpm
rpm -ivh python-urlgrabber-3.10-10.el7.noarch.rpm --force --nodeps
rpm -ivh yum-* --force --nodeps
cd /etc/yum.repos.d/
wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
vi /etc/yum.repos.d/CentOS-Base.repo
:%s/$releasever/7/g
:wq
yum clean all
yum makecache
yum update
搭建项目的环境,部署项目代码等完成之后。按照以下思路实现分布式多进程:
- 在A 机器上建立队列Queue,用来进行进程间通信。服务进程创建任务队列task_queue 用来作为传递任务给任务进程的通道;服务进程创建结果队列result_queue ,作为任务进程完成任务后回复服务进程的通道。在分布式多进程环境下,必须由Queuemanager获得Queue 接口来添加任务;
- 把上一步建立的队列在网络上注册,暴露给其他进程(主机),注册后获得网络队列,相当于本地队列的映像;
- 建立一个对象(Queuemanager(BaseManager))实例manager,绑定端口和验证口令;
- 启动第三步中建立的实例,即启动管理manager,监管信息通道;
- 通过管理实例的方法获得通过网络访问的Queue对象,即再把网络队列实体化成可以使用的本地队列;
- 由ixjob调度A 机器上的定时任务,根据元数据表创建预测任务,启动服务进程将预测任务放入A 机器上的task队列中,自动上传任务到网络队列中,分配给A 和B 机器上的任务进程;
- 启动A 和B 机器上的任务进程接收任务进行处理,各自处理完成后将各自的预测结果插入至数据库,随后通过result_queue 传回一个标志成功/失败的结果。
完成后测试发现:框架的运行时间缩短77.5%,效果不错。
附录
进程:操作系统分配资源的最小单元,每个进程之间独立内存。内含一个或多个线程。
线程:系统调度的最小单元,一个进程内的各线程共享内存。
多进程:创建多个子进程,绕过Python的GIL,利用多个CPU并行计算。能够提升cpu密集型任务的运行效率。
多线程:对python的多进程来说,由于GIL(全局解释锁)的限制,同一时刻CPU执行的任务只有一个,CPU在不同的线程间切换执行。多线程能够提高IO密集型任务的效率。
IO密集型时,大部分线程都阻塞,多线程数:
参考公式:CPU核数 /(1 - 阻系数)
比如8核CPU:8/(1 - 0.9)=80个线程数
阻塞系数在0.8~0.9之间
CPU密集型:也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。
CPU密集型:核心线程数 = CPU核数 + 1
IO密集型:系统的CPU性能相对硬盘、内存要好很多,此时大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,CPU Loading并不高。I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。
IO密集型:核心线程数 = CPU核数 / (1-阻塞系数)
IO密集型:核心线程数 = CPU核数 * 2