python多进程的cpu亲和性绑定

关于什么是cpu的亲和性的问题,大家自己去搜吧,不想在这里阐述了这讲义了。

简单说,就是让进程不要可哪跑,给定一个cpu core的范围,这样可以减少cs上下文切换后cpu cache miss的情况,节省了主存复制到cpu cache的时间。

该文章写的有些乱,欢迎来喷 ! 另外文章后续不断更新中,请到原文地址查看更新.   http://xiaorui.cc/?p=4542

最开始接触cpu亲和性绑定是nginx上,我想大家一定都有在nginx配置过worker_cpu_affinity。 nginx 的 worker_cpu_affinity 参数就是用来绑定cpu core的,worker是顺序绑定的。
#nginx
worker_processes 8;
worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;

那么python需要做cpu亲和性绑定么? 那要看场景吧,不管你是c、golang、python语言,如果只是个没流量没任务的服务,那么无所谓了。  在高频流量服务下,还是很推荐cpu亲和性绑定的。 说下经验,爬虫系统和即时通信系统是采用python和golang写的, 任务量很大,流量也很大….  在python多进程配置了cpu亲和性之后, 系统的上下文少了一些,毕竟线程单元不会被迁移到别的core上,也就不会因为cache失效问题引发syscall。

另外我们通过ps aux进程的sys内核态时间,也可以看到优化cpu绑定的效果… … 

如果在命令行下获取进程的cpu绑定信息?

#xiaorui.cc

taskset  -cp 118222
pid 118222's current affinity list: 0-15 

意思是说,pid绑定了0-15核,可以说默认的配置。

在终端下配置进程cpu绑定。

taskset  -cp 0,4 118222
pid 118222's current affinity list: 0-15
pid 118222's new affinity list: 0,4

启动脚本并绑定cpu core
taskset -c 0 python abc.py

 

详细的查看进程所在的cpu核心上, PSR 是cpu id编号, 随便写个脚本会发现psr id不定期会变化
ps -o pid,psr,comm -p 187090
PID PSR COMMAND
187090   7 python 

top -p 187090 , 按下P,然后按下f, 然后按 j (开启smp查看), 然后回车. 在每个进程信息里看到一个 P ,这个P就是psr。 

批量查看脚本:
#xiaorui.cc

pname="nginx"  # for example
for pid in $(pgrep "${pname}")
do 
    [ "${pid}" != "" ] || exit
    echo "PID: ${pid}"
    for tid in \
      $(ps --no-headers -ww -p "${pid}" -L -olwp | sed 's/$/ /' | tr  -d '\n')
    do
    taskset -cp "${tid}"   # substitute thread id in place of a process id
    done
done 

到此为止,我们说完了如何在终端下查看配置cpu亲和性。 但其实也可以在程序内部实现绑定,当然难度不在于绑定,不管你是c、golang、python,都是通过系统系统的方式来实现绑定的,下面说说CPU亲和性在用户态的使用

linux的CPU亲和性在用户态表现为一个cpu_set_t掩码的形式,用户可以调用两个函数设置和获取掩码:

#define _GNU_SOURCE /* See feature_test_macros(7) */
#include
#设置cpu绑定
int sched_setaffinity(pid_t pid, size_t cpusetsize,
 cpu_set_t *mask);
#查看进程的cpu绑定情况
int sched_getaffinity(pid_t pid, size_t cpusetsize,
 cpu_set_t *mask);


sched_setaffinity是设置指定pid亲和性掩码的,mask是传入的参数;sched_getaffinity则是获取指定pid亲和性掩码的,mask是获取的参数。

cpusetsize可以通过sizeof cpu_set_t算出来。cpu_set_t 是一个掩码数组,一共有1024位,每一位都可以对应一个cpu核心,以下宏,都是对这个掩码进行操作的。如果需要,一个进程是可以绑定多个cpu的。


而mask的表现是如此的:如果是0X23,转换成二进制则为00100011,则表明进程绑定在0核、1核和5核上。

绑核需要注意是,子进程会继承父进程的绑核关系。


那么python多进程内部如何实现cpu绑定?当然你如果不想这么折腾,完全可以在外部做cpu绑定。

python在3.4之后在os模块里加入了cpu_affinity方法,但是参数不太友好,pid绑定的cpu核是掩码,而不是taskset那样易懂的核心数。

推荐使用该模块,https://github.com/algodirect/affinity/blob/master/affinity/src/affinity/__init__.py

他比python3.4内置的os.sched_setaffinity 多了这么一个参数及返回值的处理。

# xiaorui.cc

def sched_setaffinity(pid_, cpus_):  # same name as in python3
    cpu_set_ = cpu_set(cpus_)
    result_ = set_affinity(pid_, ctypes.sizeof(cpu_set), cpu_set_)
    if result_ != 0:
        errno_ = errno_location().contents.value;
        raise OSError(errno_, os.strerror(errno_))

def sched_getaffinity(pid_):  # same name as in python3
    cpu_set_ = cpu_set()
    result_ = get_affinity(pid_, ctypes.sizeof(cpu_set), cpu_set_)
    if result_ != 0:
        errno_ = errno_location().contents.value;
        raise OSError(errno_, os.strerror(errno_))
    return cpu_set_.to_list()

# 参数转换
class cpu_set(ctypes.Structure):
    #cpu掩码
    _fields_ = [("bits", ctypes.c_ulong * (CPU_SET_SIZE / SZ_LONG))]  # to represent cpu_set_t from sched.h
    
    def __init__(self, cpus_ = None):
        for i_ in xrange(CPU_SET_SIZE / SZ_LONG):
            self.bits[i_] = 0
        if cpus_:
            for cpu_id_ in cpus_:
                self.enable(cpu_id_)
    
    def enable(self, cpu_id_):
        if cpu_id_ < 0 or cpu_id_ >= NO_OF_CPU:
            raise ValueError, "Invalid CPU id %d, it doesn't exist", cpu_id_
        self.bits[cpu_id_ / SZ_LONG] |= 1 << (cpu_id_ % SZ_LONG)
    
    def is_enabled(self, cpu_id_):
        if cpu_id_ < 0 or cpu_id_ >= NO_OF_CPU:
            raise ValueError, "Invalid CPU id %d, it doesn't exist", cpu_id_
        return ((self.bits[cpu_id_ / SZ_LONG]) & (1 << (cpu_id_ % SZ_LONG))) != 0
    
    def to_list(self):
        return [ i_ for i_ in range(NO_OF_CPU) if self.is_enabled(i_) ]
    os.sched_setaffinity = sched_setaffinity # 补丁方法
    os.sched_getaffinity = sched_getaffinity # 补丁方法

如果你是python2.7 可以使用Pyobject实现的cpu affinity模块,一老外写的…   简单看了下实现的代码,通过Python.h构建方法,通过include sched.h系统调用。

https://pypi.python.org/pypi?%3Aaction=search&term=affinity&submit=search

那么,如何在项目里合理给进程池配置cpu亲和性绑定?

我这边的进程管理模型是参考nginx master worker原理实现的。 但是跟nginx 工作进程区别在于,nginx的worker是一种工作类型,处理的逻辑是一模一样。既然都一样了,那么nginx可以按照worker pid list顺序的绑定cpu核心。 但我这边的架构是worker分组的,也就是说,有很多种的worker group,每种worker的cpu消耗不一致。

那么首先,我们要避免多个cpu密集的任务集中在一个cpu上。  首先我们预先知道那种任务进程会消耗cpu,那么可以统一的先把cpu密集的pid收集起来,最后在一个个的配置cpu亲和性。
那么剩下的无关紧要的管理进程,可以按照区间的方式配置。 避免某个cpu密集任务把cpu打满,你的其他进程虽然无关紧要,但也不能太可怜了,所以24core的cpu,配置0-3, 5-8 以此类推…. 

总结:

用python构建的高频服务还是少数,在python里推荐大家使用多进程加协程池模型,该模型配置cpu亲和性是有效果的…. 还有cpu亲和度绑定是否有必要,看你的项目了。

END


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc