Raid1源代码分析--Barrier机制
本想就此结束Raid1的专题博客,但是觉得Raid1中自己构建的一套barrier机制的设计非常巧妙,值得单独拿出来分析。它保证了同步流程和正常读写流程的并发性,也为设备冻结/解冻(freeze/unfreeze)机制提供了保障。
Barrier的意思就是,在某个请求设置上barrier之后,要先挡住barrier请求之后到来的请求,并催促barrier请求之前还未完成的请求执行,等待之前的请求全部返回完成之后,开始处理barrier请求,直到barrier请求完成之后,才允许barrier请求之后到来的请求执行。
关于barrier机制的函数其实总共只有4个,并且每个函数代码都很短,但是设计却很精妙。先列举出4个函数的代码,后面结合同步与读写的并发、设备冻结解冻机制做详细分析。
1 static void raise_barrier(conf_t *conf) 2 { 3 spin_lock_irq(&conf->resync_lock); 4 5 /* Wait until no block IO is waiting */ 6 wait_event_lock_irq(conf->wait_barrier, !conf->nr_waiting, 7 conf->resync_lock, 8 raid1_unplug(conf->mddev->queue)); 9 10 /* block any new IO from starting */ 11 conf->barrier++; 12 13 /* No wait for all pending IO to complete */ 14 wait_event_lock_irq(conf->wait_barrier, 15 !conf->nr_pending && conf->barrier < RESYNC_DEPTH, 16 conf->resync_lock, 17 raid1_unplug(conf->mddev->queue)); 18 19 spin_unlock_irq(&conf->resync_lock); 20 }
1 static void lower_barrier(conf_t *conf) 2 { 3 unsigned long flags; 4 spin_lock_irqsave(&conf->resync_lock, flags); 5 conf->barrier--; 6 spin_unlock_irqrestore(&conf->resync_lock, flags); 7 wake_up(&conf->wait_barrier); 8 }
1 static void wait_barrier(conf_t *conf) 2 { 3 spin_lock_irq(&conf->resync_lock); 4 if (conf->barrier) { 5 conf->nr_waiting++; 6 wait_event_lock_irq(conf->wait_barrier, !conf->barrier, 7 conf->resync_lock, 8 raid1_unplug(conf->mddev->queue)); 9 conf->nr_waiting--; 10 } 11 conf->nr_pending++; 12 spin_unlock_irq(&conf->resync_lock); 13 }
1 static void allow_barrier(conf_t *conf) 2 { 3 unsigned long flags; 4 spin_lock_irqsave(&conf->resync_lock, flags); 5 conf->nr_pending--; 6 spin_unlock_irqrestore(&conf->resync_lock, flags); 7 wake_up(&conf->wait_barrier); 8 }
字段的语义:nr_pending表示正在进行处理但还未返回的正常IO的计数;
barrier表示处与将要处理到返回完成之间这个阶段的同步IO的计数;
nr_waiting表示正在等待处理的正常IO的计数;
为了便于理解,可以结合后面举出的实例来读下面这段文字描述。
1、同步IO与正常IO的并发性
每个正常读写请求到来,在真正处理它之前,先调用wait_barrier函数;在该请求处理完成返回之后,调用allow_barrier函数。
每个同步请求到来,在真正处理它之前,先调用raise_barrier函数;在该请求处理完成返回之后,调用lower_barrier函数。
这四个函数共同控制3个重要的变量:nr_waiting, barrier, nr_pending,来控制同步与正常读写的并发(这3个变量公式conf结构的成员,整个raid1系统都可见)。既保证了同步IO和正常IO的互斥,又能均衡同步IO和正常IO的执行进度。并且在基本平衡同步和正常IO的执行进度的前提下,相对而言偏向于优先相应正常IO请求。
Raid1在做同步的时候,需要互斥掉正常IO,让正常IO等待,不然就会影响同步的正确性;然而Raid1做同步的区域往往很大,耗时通常很长,如果要等待同步完全完成之后才相应正常IO的话,对于应用来说是不可忍受,甚至是灾难性的。那么该怎么办呢?原来,系统在做同步的时候,同步流程会走很多次,每一次同步一个chunk的区域(很多时候我们设置为64K)。所以在某次同步中,需要同步的区域可能很大,但是会被分成以chunk为粒度的众多小IO来分别走同步流程,进而完成全部的同步工作。所以可以使同步IO和正常IO穿插互斥执行,而不会使得在做很大区域的同步的时候,raid1长时间不响应正常IO。并且通过barrier机制的设计,使得raid1相对而言略微偏向于优先响应正常IO请求,这符合一个正常存储系统的运转方式。
1) 当有正常IO在处理的时候,来了一个或多个同步IO,那么raid1将让后续到来的正常IO等待,并尽快处理已经在做的IO。如果后续有正常IO等待,之后还有同步IO来,那么之后来的这些同步IO需要等待正在等待的正常IO处理完之后才处理,即到了下下一轮处理同步IO的那一批。
1.1 在同步线程中,因为此时nr_waiting == 0,nr_pending != 0,一个同步IO来了之后,在raise_barrier函数中,第6行不等待,然后11行将barrier++,在第14行的wait_event中等待,并通过第17行通知raid1赶紧处理正在做的IO请求;当还有同步IO来,同样执行上面的流程,需要递增barrier计数;
1.2 在make request线程中,后续如果有正常IO到来,则进到make_request函数之后马上执行wait_barrier函数,此时barrier != 0,所以第5行nr_waiting++,在第6行的wait_event中等待,同样通过第8行通知raid1赶紧处理正在做的IO请求;当还有正常IO来,同样执行上面的流程,需要递增nr_pending计数;
1.3 在这之后如果还有同步IO到来,则此时nr_waiting != 0,那么在raise_barrier函数中,在第6行的wait_event中等待,同样通过第8行通知raid1赶紧处理正在做的IO请求,barrier不加;barrier机制使得,这时到来的同步IO请求一定在前面正在等待的正常IO之后处理;当还有同步IO来,同样执行上面的流程,在6行等待。这时的这些同步IO要在1.2的正常IO处理完之后的下一批才会处理;
1.4 在这之后,如果还有正常IO到来,则加入到1.2中的那批正常IO中;如果还有同步IO到来,则加入到1.3中的那批同步IO中;
2)当已经在做的正常IO处理完成之后,处理1.1中正在等待的那些同步IO。如果这个过程中没有正常IO来,那么当还有同步IO到来的时候,会继续处理这些同步IO,并且同时处理的同步IO数量不能超过RESYNC_DEPTH(值为32)个;如果这个过程中有正常IO来,那么正常IO仍然需要等待,并且正常IO之后来的同步IO放到需要在下一轮处理同步IO时才处理。
2.1 每一个正常IO返回时都会调用allow_barrier函数,将nr_pending--;当正常IO全部返回时,nr_pending减为0,唤醒并触发raise_barrier函数第15行的继续执行的条件,这时在1.1中等待的同步IO进入处理流程;
2.2 如果接着还有同步IO到来,那么在raise_barrier函数中barrier++,两个wait_event都不等待,继续处理刚进来的同步IO;直到正在处理的同步IO的个数达到RESYNC_DEPTH个时,触发第14行的wait_event,等待正在处理的同步IO个数减至RESYNC_DEPTH && nr_pending == 0时才会触发处理;也就是说要如果在等待的过程中没有正常IO来,那么等到正在处理的同步IO个数小于RESYNC_DEPTH就可以执行因为RESYNC_DEPTH而等待的同步IO,但是如果在等待的过程中有正常IO来,这些因为等待RESYNC_DEPTH的同步IO则会延迟到下一轮才会被处理。
2.3 在这之后的情况,与1.2, 1.3 和1.4的处理方式一致;
3)当本轮要处理的同步IO全部都做完了,接着处理正在等待的正常IO;当后续还有正常IO来的时候,会继续处理这些正常IO;当再有同步IO到来时,则回到了1)中描述的场景,接着做1)中的处理;被延迟一轮处理的同步IO请求,会在接下来的一轮中处理。
3.1 本轮要处理的每一个同步IO返回时都会调用lower_barrier函数,将barrier--;当本轮同步IO全部返回时,barrier减为0,唤醒并触发wait_barrier函数第6行的继续执行的条件,这时在1)和2)中等待的正常IO进入处理流程,nr_pending++;并且nr_waiting--;
3.2 当nr_waiting减为0时,如果存在1)和2)中讲到的延迟到"下一轮"的同步IO,则这些IO终于满足raise_barrier函数中第6行的条件继续继续执行,barrier++,然后在第14行又等待,直到这一轮正常IO被处理完;如果有同步IO到来,则回到了1)中描述的场景。
优先处理正常IO表现在两个方面:
1) 在处理同步IO时,依次来了几个正常IO、同步IO、正常IO,那么两次正常IO会合成一批来处理(拓展到多次也一样);合完之后的顺序一定是正常IO、同步IO;
而在处理正常IO是,依次来了几个同步IO、正常IO、同步IO,那么两次同步IO不会合成一批来处理(拓展到多次也一样);合完之后的顺序一定是同步IO、正常IO、同步IO;
2)同步IO在处理时,有一个同时处理IO个数的上限值,默认为32个。
举例说明整个流程:
normal io nA arrive & work
normal io nB arrive & work
sync io sA arrive & wait at line 14
sync io sB arrive & wait at line 14
normal io nC arrive & wait
sync io sC arrive & wait at line 6
normal io nA back
sync io sD arrive & wait at line 6
normal io nB back
sync io sA work
sync io sB work
sync io sB back
sync io sE arrive & work
normal io nD arrive & wait
sync io sF arrive & wait at line 6
sync io sA back
normal io nE arrive & wait
sync io sE back
normal io nD work
normal io nE work
sync io sC wait at line 14
sync io sD wait at line 14
sync io sE wait at line 14
normal io nF arrive & work
normal io nD back
normal io nF back
normal io nE back
sync io sC work
sync io sD work
sync io sE work
normal io nG arrive & wait
sync io sC back
sync io sE back
sync io sD back
normal io nG work
normal io nG back
2、设备冻结/解冻机制的保障
设备的冻结和解冻都出现在读出错修复的流程中。在修复读出错之前,先冻结盘阵,然后整个盘阵只做读出错修复这一件事,修复完成之后(无论成功还是失败),解冻盘阵,盘阵恢复正常。
1、freeze_array函数在读出错流程开始时执行。它首先barrier++,nr_waiting++,挡住接着可能会新来的正常IO和同步IO,让它们均进入等待中;通知raid1尽快处理已经在做的IO,然后等待已经进入处理流程的IO完成;当整个raid1中只剩下读出错IO时,进入读出错的处理流程;
2、unfreeze_array函数在读出错处理流程结束时执行。它将barrier--,nr_waiting--,让等待的请求进入处理调度,并可以接收新请求,盘阵恢复正常。
转载请注明出处:http://www.cnblogs.com/fangpei/