一,背景
在服务器压力特别大,心跳经常丢失从而造成服务超时。经过分析发现网络没有问题,心跳网络包都发过来了而且也正常进入了dispatch队列,但是由于dispatch在处理别的request的时候耗时过长,而且要命的是它还hold着一把全局的锁,导致队列里面的其他queue也无法正常被dispatch。所以我们想到可以利用设置CPU亲和性来保证核心进程/线程得到足够的时间片,从而不让服务超时。
二,CPU隔离
说到亲和性,如果我们不隔离CPU,那么就只能减少CPU切换,提高cpu cache的命中率,从而减少内存访问损耗,提高程序的速度。但是这样做只能保证自己不被调度到的别的CPU,却不能阻止其他线程不来我这个CPU。这样就成了“我的是公共的,别人的我不能用!”,岂不是我得到的时间片更少了?所以为了防止这样的窘境,我们还得先“隔核”再“绑核”。
隔离CPU核心
我们可以用如下命令来查看自己的服务器有多少个核心:
[root@xt1 ~]# cat /proc/cpuinfo |grep "model name"
model name : Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz
model name : Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz
model name : Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz
model name : Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz
model name : Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz
model name : Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz
隔核方法: 修改grub
环境: CentOS7
具体步骤: (隔离4,5核心)
找到内核启动项,在其后面添加 “isolcpus=4,5”
linux16 /boot/vmlinuz-3.10.0-514.26.2.el7.x86_64 root=UUID=8c3205a6-2433-4799-8413-c74e4fe97f5c ro crashkernel=auto rhgb quiet LANG=en_US.UTF-8 systemd.debug isolcpus=4,5
reboot 重启服务器
检查启动项是否设置成功
[root@xt1 ~]# cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-3.10.0-514.26.2.el7.x86_64 root=UUID=8c3205a6-2433-4799-8413-c74e4fe97f5c ro crashkernel=auto rhgb quiet LANG=en_US.UTF-8 systemd.debug isolcpus=4,5
检查隔离是否生效
// 如果设置成功,则落在该核心的线程会很少
ps -eLo pid,user,lwp,psr | awk '{if($4==4) print $0}' //检查核心4
ps -eLo pid,user,lwp,psr | awk '{if($4==5) print $0}' //检查核心5
// 也可以用top命令来查看
执行top,按数字1,就可以调出每个核心的使用状态,然后按下f按键,向下找到"P= Last Used Cpu (SMP)"这一行,按下回车,再按q按键返回,就可以看到每个线程具体落在哪个核心上面
top - 16:28:40 up 5 days, 22:03, 2 users, load average: 0.02, 0.08, 0.44
Tasks: 260 total, 2 running, 258 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.0 us, 0.3 sy, 0.0 ni, 99.3 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 st
%Cpu1 : 0.0 us, 0.3 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu4 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu5 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 7840224 total, 738160 free, 506176 used, 6595888 buff/cache
KiB Swap: 9727996 total, 9250224 free, 477772 used. 6992616 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND P
2894 root 20 0 51192 18928 2884 S 1.0 0.2 74:29.74 etcd 1
2836 root 20 0 1855044 50728 3296 S 0.7 0.6 25:13.31 ceph-osd 3
4648 root 20 0 146304 2200 1436 R 0.7 0.0 0:00.10 top 1
1813 root 20 0 554324 64492 9676 S 0.3 0.8 0:12.40 ceph-mds 1
3041 root 20 0 0 0 0 S 0.3 0.0 4:10.88 xfsaild/dm-8 1
3120 root 20 0 1818688 97464 2580 S 0.3 1.2 24:13.09 ceph-osd 2
30405 root 20 0 0 0 0 S 0.3 0.0 0:01.33 kworker/0:2 0
1 root 20 0 43476 2472 1340 S 0.0 0.0 0:30.26 systemd 3
三,sched_setaffinity/sched_getaffinity
更改一个进程的CPU affinity mask,可以设置一个进程在某个CPU核心上执行,也可以设置该进程在除了某CPU 核心之外的其他CPU核心上执行。如果第一个参数pid为0,则设置当前进程的mask。
其实这个mask是一个针对线程组内的线程属性,可以被独立调节。所以我们可以用gettid()的参数作为第一个参数,同样如果第一个参数为0,则设置当前线程。如果传的getpid的返回值,则设置该线程组的主线程mask属性。
int sched_setaffinity(pid_t pid, size_t cpusetsize,
cpu_set_t *mask);
int sched_getaffinity(pid_t pid, size_t cpusetsize,
cpu_set_t *mask);
实验1
#include
#define __USE_GNU
#include
#include
#include
#include
void * fn(void *arg)
{
cpu_set_t set;
int i = 0;
CPU_ZERO(&set);
CPU_SET(1, &set);
printf("child thread is arriving .../n");
sched_setaffinity(0, sizeof(cpu_set_t), &set);
while (i<100000000)
{
i++;
if (i%5000 == 0)
{
printf ("i is %d\n", i);
sleep(1);
}
}
}
int main()
{
int isolated = 1;
pthread_t tid;
pthread_create(&tid, NULL, fn, NULL);
pthread_join(tid, NULL);
return 0;
}
四,pthread_setaffinity_np/pthread_getaffinity_np
更改一个线程的CPU affinity mask, 同样也是可以设置一个线程在某个CPU核心上执行,也可以设置该线程在除了某CPU 核心之外的其他CPU核心上执行。这两个接口在底层也是调用sched_setaffinity/sched_getaffinity。当第一个参数为0时,就是设置当前thread的mask。
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include
int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize,
const cpu_set_t *cpuset);
int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize,
cpu_set_t *cpuset);
Compile and link with -pthread.
实验2
#include
#define __USE_GNU
#include
#include
#include
#include
void self_sleep()
{
int i = 0;
while (i<100000000)
{
i++;
if (i%5000 == 0)
{
printf ("i is %d\n", i);
sleep(1);
}
}
}
void * fn(void *arg)
{
cpu_set_t set;
pthread_attr_t tattr;
pthread_attr_init(&tattr);
CPU_ZERO(&set);
CPU_SET(1, &set);
printf("child thread is arriving .../n");
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &set);
self_sleep();
}
int main()
{
int isolated = 1;
pthread_t tid;
pthread_create(&tid, NULL, fn, NULL);
pthread_join(tid, NULL);
return 0;
}
五,属性继承
上面我们介绍了两种设置CPU affinity mask的接口,但是无论是哪一种接口有个共同的特性:创建出来的子线程默认会继承父亲线程的CPU affinity mask。在复杂的系统中有时并不希望这个属性的出现,因为可能会导致某个隔离出来的cpu上还是运行了很多的thread。所以我就想了一个解决方案来解决这个问题。
让所有的线程创建出来之后都跑在除了隔离出来的核心之外的其他核心上面,然后在单独为需要绑定核心的线程来设置CPU affinity mask
实验3
#include
#define __USE_GNU
#include
#include
#include
#include
void self_sleep()
{
int i = 0;
while (i<100000000)
{
i++;
if (i%5000 == 0)
{
printf ("i is %d\n", i);
sleep(1);
}
}
}
void * fn2(void *arg)
{
printf("child2 thread is arriving .../n");
self_sleep();
}
void * fn(void *arg)
{
int i, cpus;
cpu_set_t set;
pthread_t tid;
pthread_attr_t tattr;
pthread_attr_init(&tattr);
cpus = sysconf(_SC_NPROCESSORS_CONF);
printf("have %d cpus\n", cpus);
CPU_ZERO(&set);
for (i=0; i
{
if (i == 1)
continue;
CPU_SET(i, &set);
}
printf("child thread is arriving .../n");
printf("set child thread cpu to 0 .../n");
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &set);
pthread_create(&tid, NULL, fn2, NULL);
self_sleep();
pthread_join(tid, NULL);
}
int main()
{
int isolated = 1;
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(1, &set);
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &set);
pthread_t tid;
pthread_create(&tid, NULL, fn, NULL);
pthread_join(tid, NULL);
return 0;
}
参考文档