小问题汇总

最近遇到一些开发部署等方面的几个小问题,留个笔记。

第一个 编程问题

#include 
#include 
#include 

int main(int argc, char *argv[])
{
	int ret;

	pthread_mutex_t mt;
	ret = pthread_mutex_init(&mt, NULL);
	if (ret != 0) {
		printf("failed to init (ret: %d)\n", ret);
		goto fail;
	}
	ret = pthread_mutex_unlock(&mt);
	if (ret != 0) {
		printf("failed to unlock (ret: %d)\n", ret);
		goto fail;
	}
	ret = pthread_mutex_destroy(&mt);
	if (ret != 0) {
		printf("failed to destroy (ret: %d)\n", ret);
		goto fail;
	}

	printf("complete\n");
	return 0;
fail:
	printf("fail\n");
	return -1;
}
#编译
gcc test.c -o test -lpthread
#运行
./test
#结果
failed to destroy (ret: 16)
fail

pthread_mutex_destroy返回了EBUSY(16)错误,看来pthread的限制还挺严格的,只做了unlock的mutex不能正常destroy, 为什么?最后找到了 这个链接 看这意思,maintainer死活不肯改,觉得这个行为是正确的。既然maintainer不肯,那我也只好在程序里用pthread_cond代替了mutex,免得pthread_mutex_destroy老报错。

第二个 编程问题

#include 
#include 

int main(int argc, char **argv[])
{
	char test[ARG_MAX];
	memset(test, 0, ARG_MAX);
}

这段代码编译正常。但是如果我们在第一行再加一个"#include ",在debian下还能正常编译,但在centos6上编译就报错:

/root/a.c: In function ‘main’:
/root/a.c:7: error: ‘ARG_MAX’ undeclared (first use in this function)
/root/a.c:7: error: (Each undeclared identifier is reported only once
/root/a.c:7: error: for each function it appears in.)

原因是/usr/include/bits/local_lim.h里有一堆宏定义,最后把ARG_MAX又给undef了,颇为诡异。难道在centos6上就不应该limits.h和linux/limits.h一起用?实际代码如果多了,难免两个头文件都要include的。这里 给了一个标准的解决办法,就是用sysconf(_SC_ARG_MAX)代替ARG_MAX。

第三个 ssh通道打通

ssh通道打通是常见操作,但是我在机器上发现虽然按照步骤打通了,但是本机ssh还是需要密码,最后coly帮忙找到了原因:selinux。还要在/etc/selinux/config中将selinux设置成disabled,然后 setenforce 0,ssh通道才真“通”了。
感谢coly的发现,我不用继续输密码了。

tpps: 一个可做cgroup资源隔离的高效IO调度器

(本文里说的“资源隔离”主要是指cgroup根据blkio.weight的值来按比例调配io的带宽和IOPS,不包括io-throttle即blkio.throttle.xxx的一系列配置,因为linux的io-throttle机制不依赖于IO调度器)

由于现在的SSD越来越快也越来越便宜,如何高效的利用SSD就变得十分重要。高效利用SSD有两个办法,一个是加大应用程序发出的io深度(比如用aio),对于无法加大io深度的一些重要应用(比如mysql数据库),则可以用第二个办法:在一个高速块设备上跑多个应用实例。
现在问题来了,既然跑多个实例,就要避免它们互相干扰,比如一个mysql由于压力大把io带宽都占了,这对另一个mysql实例就不太公平。现在常用的办法当然是用cgroup来做io资源的隔离(参考这篇),但是,有个尴尬的事情,目前只有cfq这一个io调度器是支持cgroup的,而cfq调度器在高速设备上表现却不尽人意。
deadline调度器准备了两个队列,一个用来处理写请求,一个用来处理读请求,然后根据io快要到期的时间(即deadline)来选择哪个io该发出去了,这个选择同时也要考虑“读比写优先”、“尽可能连续发射io”等约束,但是不管怎么说,既然把io的响应时间做为了该调度算法的第一要素,那deadline调度器的io延时性就是相对最好的(即response time很短),它也是SSD设备的推荐调度器。问题在于,“两个队列”,这结构太简单了,自然也无法支持cgroup这样复杂的特性,所以,虽然deadline调度器很快,RT很短,但是做不了资源隔离。
再来看cfq,cfq最早就是基于磁盘来做优化的,它的算法尽可能保证io的连续性,而不保证io的及时响应(所以,在对磁盘做高io压测的时候,cfq调度器有时会制造出长达几秒的io响应来),比如,对于单个io,它会做一个小小的等待,看有没有和这个io连上的下一个io到来,如果有,可以一起发出去,以形成连续的io流,但是,这个”小小的等待“,就大大延长了io响应时间。cfq调度器为cgroup中的每个group单独创建一个"struct cfq_queue“实例,然后以各group的weight所占的权重来决定改发哪个group的io,所以cfq是支持资源隔离的。我讲的这些还没有碰到cfq的皮毛,它四千多行的代码有很多复杂和技巧性的算法和数据结构,我估计只有fusionio的李少华能把它说清楚,我这里就不班门弄斧了。
介绍IO调度器的文章,可参考这篇

总的来说,deadline适合SSD设备,但是它不支持资源隔离;cfq支持资源隔离,但是在SSD上跑延时又太差,且代码复杂,不易改造。(noop铁定不支持资源隔离,而anticipator跟deadline代码相差不大,这两个都出局了)最终,经过我、高阳伯瑜三个人的讨论,得出决定:如果要方便应用方的使用,最好是做一个新的、代码非常简单的新IO调度器。这样,应用方如果是用硬盘,那就选择cfq;如果是用SSD,就选择deadline;如果既用了高速设备,又想做资源隔离,那就选这个新调度器。既方便应用方灵活选择,又方便我们自己维护。(如果把新调度器做进deadline,后期的维护和backport就会越来越吃力)。新调度器的原理很简单:通过类似cfq的xxx_queue结构来记录各group的信息,针对每个group都创建一个list,存放从该group到来的io,然后在dispatch io时,用该设备的io request容量(即nr_request)减去已经发出但还没有处理完成的io数(即rq_in_driver),得出的就是该设备还可处理的io数(即下面代码中的quota),然后根据这个“可处理io数”和各group的权重,算出各group的list上可以dispatch的io数,最后,就是按照这些数去list上取io,把它们发出去了。

static int tpps_dispatch_requests(struct request_queue *q, int force)
{
        struct tpps_data *tppd = q->elevator->elevator_data;
        struct tpps_group *tppg, *group_n;
        struct tpps_queue *tppq;
        struct list_head *next;
        int count = 0, total = 0, ret;
        int quota, grp_quota;

        if (unlikely(force))
                return tpps_forced_dispatch(tppd);

        if (!tppd->total_weight)
                return 0;

        quota = q->nr_requests - tppd->rq_in_driver;
        if (quota < MIN_DISPATCH_RQ)
                return 0;

        list_for_each_entry_safe(tppg, group_n, &tppd->group_list, tppd_node) {
                if (!tppg->nr_tppq)
                        continue;
                tpps_update_group_weight(tppg);
                grp_quota = (quota * tppg->weight / tppd->total_weight) -
                                tppg->rq_in_driver;
......

这样,就是按cgroup的权重比例来发出io了,而且,不像cfq一个队列一个队列的处理,这个新调度器是拿到quota后从每个group list里都取一定量的io,所以也还保证了一定的“公平性”,同时,quota是按照设备的处理能力算出来的,所以也能保证尽可能把设备打满。简单、按比例并发处理(准确的说,是公平处理),所以我们给这个新io调度器起名字叫“Tiny Parallel Proportion Scheduler",简称tpps。代码已经合并到了ali_kernel,patch链接是1,2,3,4,5,6

另外申明一下,这个调度器并不复杂,也不高端,讲求的是简单好用,所以如果有哪位内核高手看到这个调度器后发表不满:“这么简单也好意思拿出来说?!”,那大可不必。我们就是做出来解决问题的,问题解决就ok,不必做得那么高深。我写这篇文章也只是方便有兴趣的DBA或者SA来试用和提建议,不是要说明这个调度器有多牛逼。

想要使用tpps调度器需要使用ali_kernel:

git clone git@github.com:alibaba/ali_kernel.git
git checkout dev
#编译并重启内核

机器重启后

modprobe tpps-iosched
#再用 cat /sys/block/sdX/queue/scheduler 可以看到多个io调度器,其中有tpps
echo tpps > /sys/block/sdX/queue/scheduler

我用一个快速设备测试了一下cfq和tpps对cgroup的支持,fio测试脚本如下:

[global]
direct=1
ioengine=libaio
runtime=20
bs=4k
rw=randwrite
iodepth=1024
filename=/dev/sdX
numjobs=4

[test1]
cgroup=test1
cgroup_weight=1000

[test2]
cgroup=test2
cgroup_weight=800

[test3]
cgroup=test3
cgroup_weight=600

[test4]
cgroup=test4
cgroup_weight=400

测试结果:

各个cgroup的IOPS

io调度器 test1 test2 test3 test4
cfq 6311 5334 3983 2569
tpps 8592 8141 7152 5767

各个cgroup的平均RT(Response Time)单位:毫秒

io调度器 test1 test2 test3 test4
cfq 161 221 241 398
tpps 114 127 141 177

各个cgroup的最大RT(Response Time)单位:毫秒

io调度器 test1 test2 test3 test4
cfq 312 387 445 741
tpps 173 262 260 322

从测试看来,tpps既能实现资源隔离,同样的设备和io压力下性能也略高于cfq。
重要提示:对tpps来说,只有当设备被压满时才会有不同cgroup的不同iops比例出现,所以,测试时压力一定要大。

linux默认kernel.pid_max值

今早石祤同学发现了一个问题:同样的两台服务器,相同的OS版本、内核版本、CPU型号、CPU核数,只是厂家不同,但是机器启动后sysctl里的kernel.pid_max值,一台是128k,一台是32k。看了一下/etc/sysctl.conf,两台都没在配置文件里做更改,应该是内核自己选定的默认值。那内核到底是怎样选定这个默认值的呢?为何两个厂家的服务器默认值就不同?怎么让它们一致?

看了一下内核代码,决定kernel.pid_max的值是在pidmap_init()函数里:

int pid_max = PID_MAX_DEFAULT;
......
void __init pidmap_init(void)
{       
        /* bump default and minimum pid_max based on number of cpus */
        pid_max = min(pid_max_max, max_t(int, pid_max, 
                                PIDS_PER_CPU_DEFAULT * num_possible_cpus()));
......

其中,PIDS_PER_CPU_DEFAULT值为1024,也就是,内核基本上认为一个CPU核差不多最大跑1024个task,至于num_possible_cpus(),是通过计算cpu_possible_mask

const struct cpumask *const cpu_possible_mask

这个结构里被置为1的bit数来确定possible cpus的,即可用的最高CPU核数。possible cpu这个概念是为热插拔CPU准备的,比如,一台机器一共可以插24个CPU核,但是目前只插了12个,那么possible cpu核数应该是24,pid_max应该为“没插的cpu核“做预备,应该是24k。但是pid_max的默认值PID_MAX_DEFAULT是32k,比24k大,所以按代码,应该是选32k为值。

那是谁设置了cpu_possible_mask里的这些bit呢?再看看内核启动的函数:

asmlinkage void __init start_kernel(void)                                     
{                                     
        char * command_line;
        extern struct kernel_param __start___param[], __stop___param[];
......
        setup_arch(&command_line);
......
        pidmap_init();
......
}

里面是在setup_arch()里做了一些bit设置的事情:

setup_arch() --> prefill_possible_map()

__init void prefill_possible_map(void)                       
{      
        int i, possible;
                                      
        /* no processor from mptable or madt */
        if (!num_processors)
                num_processors = 1;
                                                
        printk(KERN_INFO "num_processors: %u, disabled_cpus: %u",
                          num_processors, disabled_cpus);
        if (setup_possible_cpus == -1)
                possible = num_processors + disabled_cpus;       
        else                                             
                possible = setup_possible_cpus;
                                                          
        total_cpus = max_t(int, possible, num_processors + disabled_cpus);
                                               
        /* nr_cpu_ids could be reduced via nr_cpus= */
        if (possible > nr_cpu_ids) {                                      
                printk(KERN_WARNING
                        "%d Processors exceeds NR_CPUS limit of %d\n",
                        possible, nr_cpu_ids);
                possible = nr_cpu_ids;
        }                                                             
                                              
        printk(KERN_INFO "SMP: Allowing %d CPUs, %d hotplug CPUs\n",
                possible, max_t(int, possible - num_processors, 0));

        for (i = 0; i < possible; i++)                              
                set_cpu_possible(i, true);                          

        nr_cpu_ids = possible;        
}                                         

setup_possible_cpus的值默认是-1,所以是根据ACPI驱动返回的num_processors和disabled_cpus的和来确定possible cpu数的。不同厂商的ACPI返回的disabled_cpus是不同的,所以possible cpu核数不同,自然kernel.pid_max值也不同。在系统启动的日志里可以看disabled cpu的不同:

bash# dmesg|grep Allowing
[    0.000000] SMP: Allowing 24 CPUs, 8 hotplug CPUs

说明当前CPU核数为16(24减去8),disabled的CPU核数为8。
那,怎么解决这个问题呢?怎样统一所有机型的默认kernel.pid_max值呢?还是得注意setup_possible_cpus这个值,这个值是可以通过grub来改的,只要在grub命令里kernel那一行的后面加上 possible_cpus=128 就可以把possible cpu数都统一成128了,参考这篇文档。当然,其实最方便的还是改/etc/sysctl.conf。

感谢石祤同学的细心,我又学到了很多。

2013年读书

一年一度的读书总结又来了。虽然16本书听起来很多,但是很多都是翻翻而已,泛读,比如《大话存储2》(对不住了,冬瓜兄)。

上学的时候偶尔看过王小波的一些文章,当时不太有感觉。现在走上社会多年以后,再回头去看,突然有了很多感触。做一只《特立独行的猪》,的确令人羡慕,但是,真的非常非常不容易。

flashcache支持资源隔离的改造

去年年底的时候对flashcache做了一番研究,并进行了改造,所谓改造,其实就是希望它能支持cgroup,也就是说,当有在不同cgroup里运行的进程对flashcache进行读写时,flashcache能根据cgroup的不同权重分配这些进程以不同的iops(IO per second)。详细的资料在这里:《flashcache原理及改造》

事情还没有结束,现在能给flashcache设备设io-scheduler了,设成cfq就可以按cgroup weight来分配iops,但是,做为稀缺资源的ssd设备,其cache空间并没有像IOPS那样按比例分配给各个cgroup,所以,今年,我继续把“cache空间按cgroup比例分配给进程”的功能做完了。patch系列在这里

原理:flashcache按hash把io缓存到ssd设备的不同的LRU链表上。我加了一个“struct flashcache_group”,将cgroup的信息存入其中,同时由它来管理”子LRU“。比如,有4个cgroup的进程在对flashcache做读写,那每个LRU都要被拆成4个由"struct flashcache_group”管理的”子LRU“,每个“子LRU”所用的cache空间的大小(即flashcache所谓的block的个数),就是由它所在的cgroup的权重决定的。
当flashcache设备刚初始化好时,所有的“子LRU”都是空的,cache都在根group的手中(正好对应cgroup里的root group概念),一旦有进程的IO来了,才根据它所在cgroup的权重,慢慢将block划入它所属的”子LRU“(当然,不能超过最大比例);当该进程退出后,cgroup即被destroy,于是,对应的”子LRU“也会将自己占的cache返还给”主LRU“。

由于这些改动不仅涉及flashcache本身,还要牵扯kernel里很多block层的代码,所以,目前还不能单独提交给flashcache社区,只能先保留在ali_kernel里,想试用的同学,可以用如下方法:

1. 下载ali kernel的代码并编译dev分支,然后安装内核并重启机器

git clone git@github.com:RobinDong/ali_kernel.git
git checkout -b dev origin/dev
make localmodconfig #根据自己机器的硬件情况选择config文件
make -j20
make modules_install
make install
vim /etc/grub.conf #修改grub以启动ali kernel
reboot

2. 进入ali kernel后,下载flashcache工具并编译stable_v2.1分支

git clone git@github.com:RobinDong/flashcache.git
git checkout -b stable_v2.1 origin/stable_v2.1
make

3. 使用编译的flashcache工具创建设备(加"-r"参数以确保flashcache进去request base模式)

src/utils/flashcache_create -p back -r cachedev /dev/rxdrv /dev/sdf

咱们接着用以下fio文件做个测试:

[global]
direct=1
ioengine=libaio
runtime=60
bs=4k
rw=randwrite
iodepth=8
filename=/dev/mapper/cachedev
numjobs=1

[group1]
cgroup=test1
cgroup_weight=1000

[group2]
cgroup=test2
cgroup_weight=800

[group3]
cgroup=test3
cgroup_weight=600

[group4]
cgroup=test4
cgroup_weight=400

测试结果:

cgroup IOPS
test1 684
test2 581
test3 425
test4 264

再看看SSD设备里block的分配情况:

cat /proc/flashcache/rxdrv+sdf/flashcache_blocks

Group		Weight		Block Count
/test2		800		54865
/test1		1000		68582
/test3		600		41149
/test4		400		27433

看来cache空间的分配比例还是很准确的

1 2 3 4 5 14