03| OCFS2集群管理和心跳机制(2)

785 阅读9分钟

接着上一篇02| OCFS2集群管理和心跳机制(1),继续分析OCFS2磁盘心跳机制。

四、磁盘心跳分析

OCFS2每隔2S更新磁盘上的时间戳,并且读取其他节点的时间戳,校验本节点自身写磁盘数据是否正确,同时检测集群中其他节点写磁盘是否成功。一旦发现有节点写磁盘异常,将会触发整个集群的锁转移,从而保证整个集群能对外提供服务。关于集群锁将在后面文章中详细分析。

为何会有磁盘心跳检测?

磁盘心跳的主要目的:当集群中某个节点发生写存储异常时,如果此时这个节点正在写操作(已加锁),那么其journal数据将无法刷新到磁盘上,这个节点持有的集群锁无法释放,这就会造成集群内其他节点拿不到集群锁,从而导致整个集群不可用。有了磁盘心跳的检测,集群发现写磁盘异常的节点后,将进行容错处理,受此异常节点的集群锁将在集群中重新建立,从而避免了集群不可用的问题,做到了高可用,因此,磁盘心跳的检测非常重要。

产生的时机:

在执行mount操作时,OCFS2首先通过configfs文件系统完成心跳区域的配置操作,此时将启动内核磁盘心跳线程o2hb。连续2个稳定的磁盘心跳后,节点UP。

内核态:

磁盘心跳region对象,记录了该节点在某个卷Volume上的磁盘心跳信息,其中struct config_item hr_item就是配置心跳文件的configfs item对象,用户态通过configfs把心跳文件写入新建的集群心跳目录/sys/kernel/%s/config/region_path,block_bytes、start_block、blocks、dev中,这些值最终传递到内核态ocfs2 struct o2hb_region.hr_item。此时,在内核里面就会启动o2hb线程,开始该节点的心跳了。

/* this is acting as commit; we set up all of hr_bdev and hr_task or nothing */
static ssize_t o2hb_region_dev_store(struct config_item *item, const char *page, size_t count)
|-f = fdget(fd);
|-reg->hr_bdev = blkdev_get_by_dev(f.file->f_mapping->host->i_rdev, FMODE_WRITE | FMODE_READ, NULL);
                              |-struct block_device *bdev = blkdev_get_no_open(dev);
                              |-struct gendisk *disk = bdev->bd_disk;
|-ret = o2hb_map_slot_data(reg);//为每一个节点的槽位slot = &reg->hr_slots[i],初始化心跳数据信息
         |-ret = o2hb_populate_slot_data(reg);
                      |-ret = o2hb_read_slots(reg, 0, reg->hr_blocks);//读所有节点槽位的心跳数据上来
                      |-for(i = 0; i < reg->hr_blocks; i++) {//记录下每个槽位上节点的心跳数据:ds_last_time和ds_last_generation
                         slot = &reg->hr_slots[i];
                         hb_block = (struct o2hb_disk_heartbeat_block *) slot->ds_raw_block;
                         slot->ds_last_time = le64_to_cpu(hb_block->hb_seq);
                         slot->ds_last_generation = le64_to_cpu(hb_block->hb_generation);
                   |-}
         |-INIT_DELAYED_WORK(&reg->hr_write_timeout_work, o2hb_write_timeout);//延时启动一个写超时任务,当发现节点写磁盘超时,执行任务。
         |-live_threshold = O2HB_LIVE_THRESHOLD;//连续2次心跳正常,则该节点心跳UP
         |-atomic_set(&reg->hr_steady_iterations, live_threshold);
         |-hb_task = kthread_run(o2hb_thread, reg, "o2hb-%s", reg->hr_item.ci_name);//启动心跳线程o2hb-xxx
         |-reg->hr_task = hb_task;

每个节点槽位slot,包含心跳的数据信息:心跳磁盘数据、心跳变化时间、心跳变化次数等,如下图所示:

image (18).png

磁盘心跳数据如下所示:共28 B,主要检查心跳时间hb_seq的变化、其他信息是否正确。

image (19).png

磁盘心跳线程o2hb:

校验每个节点的磁盘心跳数据是否正确:心跳时间hb_seq、hb_generation、节点号hb_node等信息。如果有节点并不在已存活链表中且连续两次心跳变化正常,则报该节点UP事件;如果有节点连续两次心跳没有变化,或者generation值变化了,则报该节点DOWN事件。

static int o2hb_thread(void *data)
    |-while (!kthread_should_stop() && !reg->hr_unclean_stop && !reg->hr_aborted_start) {
        |-ktime_t before_hb = ktime_get_real();//检查心跳时间之前的时间记录
        |-ret = o2hb_do_disk_heartbeat(reg);//写心跳
             |-ret = o2nm_configured_node_map(configured_nodes, sizeof(configured_nodes));//拷贝集群中配置的节点
             |-o2hb_fill_node_map(live_node_bitmap, sizeof(live_node_bitmap));//从全局心跳UP的变量o2hb_live_node_bitmap中拷贝存活节点
             |-ret = o2hb_read_slots(reg, lowest_node, highest_node + 1);//读集群中所有节点的心跳数据
             |-own_slot_ok = o2hb_check_own_slot(reg);//校验本节点的磁盘心跳数据是否正确:心跳时间hb_seq、hb_generation、节点号hb_node等
             |-o2hb_prepare_block(reg, reg->hr_generation);//准备本节点下次心跳的数据:当前时间cputime、node_num、generation等
             |-ret = o2hb_issue_node_write(reg, &write_wc);//将本节点下次心跳数据写到磁盘上
             |-while((i = find_next_bit(configured_nodes, O2NM_MAX_NODES, i + 1)) < O2NM_MAX_NODES) {
           membership_change |= o2hb_check_slot(reg, &reg->hr_slots[i]);//校验每个节点的磁盘心跳数据是否正确:心跳时间hb_seq、hb_generation、节点号hb_node等信息。如果有节点并不在已存活链表中且连续两次心跳变化正常,则报该节点UP事件;如果有节点连续两次心跳没有变化,或者generation值变化了,则报该节点DOWN事件。
         |-}
            |-if (own_slot_ok) {//本节点检查OK
          |-o2hb_set_quorum_device(reg);//将本节点加入仲裁变量o2hb_quorum_region_bitmap,仲裁时使用。
                     |-set_bit(reg->hr_region_num, o2hb_quorum_region_bitmap);
          |-o2hb_arm_timeout(reg);
                     |-cancel_delayed_work(&reg->hr_write_timeout_work);//取消本节点的磁盘写超时任务
                     |-schedule_delayed_work(&reg->hr_write_timeout_work, msecs_to_jiffies(O2HB_MAX_WRITE_TIMEOUT_MS));//重新延时调度磁盘写超时任务,超时时间120s
               |-reg->hr_last_timeout_start = jiffies;
        |-}
        |-reg->hr_last_hb_status = ret;
        |-after_hb = ktime_get_real();//检查心跳时间之后的时间记录
        |-elapsed_msec = (unsigned int)ktime_ms_delta(after_hb, before_hb);//计算前后时间差
        |-if (!kthread_should_stop() && elapsed_msec < reg->hr_timeout_ms) {//心跳线程没有停止且时间差没有超过超时时间
            msleep_interruptible(reg->hr_timeout_ms - elapsed_msec);//休眠剩余时间后,再次进入while循环,写心跳,循环往复这个动作。
        |-}
    |-}

检查节点心跳逻辑:

static int o2hb_check_slot(struct o2hb_region *reg, struct o2hb_disk_slot *slot)
{
    … …
    cputime = le64_to_cpu(hb_block->hb_seq);
    if (slot->ds_last_time != cputime)
        slot->ds_changed_samples++;//记录磁盘心跳时间变化的次数
    else
        slot->ds_equal_samples++;//记录磁盘心跳时间不变的次数
    slot->ds_last_time = cputime;
… …
fire_callbacks:
    if (list_empty(&slot->ds_live_item) && slot->ds_changed_samples >= O2HB_LIVE_THRESHOLD) {//如果有节点并不在已存活链表中且连续两次心跳变化正常,则报该节点UP事件;
        mlog(ML_HEARTBEAT, "Node %d (id 0x%llx) joined my region\n", slot->ds_node_num, (long long)slot->ds_last_generation);
        set_bit(slot->ds_node_num, reg->hr_live_node_bitmap);//在region的存活节点中将该节点设置为1。
        /* first on the list generates a callback */
        if (list_empty(&o2hb_live_slots[slot->ds_node_num])) {
            mlog(ML_HEARTBEAT, "o2hb: Add node %d to live nodes bitmap\n", slot->ds_node_num);
            set_bit(slot->ds_node_num, o2hb_live_node_bitmap);//在全局存活节点变量o2hb_live_node_bitmap中将该节点设置为1。
            o2hb_queue_node_event(&event, O2HB_NODE_UP_CB, node, slot->ds_node_num);//报该节点UP事件;
            changed = 1;
            queued = 1;
        }
        list_add_tail(&slot->ds_live_item, &o2hb_live_slots[slot->ds_node_num]);//链表中增加该节点的item记录
        slot->ds_equal_samples = 0;//清除磁盘心跳相同记录
        … …
    }
 
    if (slot->ds_equal_samples >= o2hb_dead_threshold || gen_changed) {//如果有节点连续两次心跳没有变化,或者generation值变化了,则报该节点DOWN事件
        mlog(ML_HEARTBEAT, "Node %d left my region\n", slot->ds_node_num);
        clear_bit(slot->ds_node_num, reg->hr_live_node_bitmap);//在region的存活节点中将该节点清除。
        /* last off the live_slot generates a callback */
        list_del_init(&slot->ds_live_item);//链表中删除该节点的item记录
        if (list_empty(&o2hb_live_slots[slot->ds_node_num])) {
            mlog(ML_HEARTBEAT, "o2hb: Remove node %d from live nodes bitmap\n", slot->ds_node_num);
            clear_bit(slot->ds_node_num, o2hb_live_node_bitmap);//在全局存活节点变量o2hb_live_node_bitmap中将该节点清除。
            /* node can be null */
            o2hb_queue_node_event(&event, O2HB_NODE_DOWN_CB, node, slot->ds_node_num);//报该节点DOWN事件
            changed = 1;
            queued = 1;
        }
       … …
}

五、Fence机制:

为何会有fence机制?

Fence机制是OCFS2集群保持高可用的重要解决方案,它的意义在于当网络出现分区和节点写磁盘异常时均会引发Fence机制。当发生fence时,此时集群已经不可用,无法再提供服务,根本原因就是DLM分布式锁管理器在这两种异常情况下自身异常导致。OCFS2原生的代码中fence处理方案有:panic和reset,默认是reset,此时主机将被重启。此时OCFS2寄希望于重启异常分区内的节点和写磁盘异常的节点,来达到集群内DLM锁的重建,帮助集群恢复正常的功能。

static void o2quo_fence_self(void)
|-o2hb_stop_all_regions();
|-switch (o2nm_single_cluster->cl_fence_method) {
    |-case O2NM_FENCE_PANIC:
        panic("*** ocfs2 is very sorry to be fencing this system by panicing ***\n");
        break;
    |-default:
        WARN_ON(o2nm_single_cluster->cl_fence_method >= O2NM_FENCE_METHODS);
        fallthrough;
    |-case O2NM_FENCE_RESET:
        printk(KERN_ERR "*** ocfs2 is very sorry to be fencing this system by restarting ***\n");
        emergency_restart();//重启主机
        break;
}

1)网络分区:

static void o2quo_make_decision(struct work_struct *work)
|-lowest_hb = find_first_bit(qs->qs_hb_bm, O2NM_MAX_NODES);//找到最小的节点
|-lowest_reachable = test_bit(lowest_hb, qs->qs_conn_bm);//在网络分区内是否可达,即:是否跟这个最小的节点在同一个网络分区内。
|-if (qs->qs_heartbeating & 1) { //集群中有奇数个节点
    |-quorum = (qs->qs_heartbeating + 1)/2;
    |-if (qs->qs_connected < quorum) {//少于一半节点的分区节点fence
            fence = 1;
    |-}
|-} else {//集群中有偶数个节点
          |-quorum = qs->qs_heartbeating / 2;
    |-if (qs->qs_connected < quorum) {//少于一半节点的分区节点fence
       fence = 1;
    |-}
    |-else if ((qs->qs_connected == quorum) && !lowest_reachable) {//等于一半节点的分区节点同时与最小节点不在同一个分区的节点fence
       fence = 1;
    |-}
|-}
|-if (fence) {
    |-o2quo_fence_self();
    

2)写磁盘超时:

INIT_DELAYED_WORK(&reg->hr_write_timeout_work, o2hb_write_timeout);
if (own_slot_ok) { //本节点写磁盘OK
    |-o2hb_arm_timeout
        |-cancel_delayed_work(&reg->hr_write_timeout_work); //取消写磁盘超时处理
        |-schedule_delayed_work(&reg->hr_write_timeout_work, msecs_to_jiffies(O2HB_MAX_WRITE_TIMEOUT_MS));//重新调度写磁盘超时处理
 
static void o2hb_write_timeout(struct work_struct *work)
void o2quo_disk_timeout(void)
{
    o2quo_fence_self();
}

现有Fence机制的不足以及优化思路

因为一旦出现fence将导致整个节点的重启不可用,同一个节点上会挂载多个LUN,也就是多个OCFS2共享存储池,当一个LUN不可访问发生fence问题时,就要重启整个主机,这势必会影响这个节点上其他正常LUN所在的OCFS2共享存储池上的业务,引起虚拟机的迁移,用户体验不友好。因此,对fence机制的优化很有必要。导致fence是由于节点写磁盘异常(或存储网异常)和管理网闪断超时导致,因此优化的方向也分为写磁盘优化和管理网优化。

1)写磁盘异常优化思路

当发生fence时,通过管理网通知集群中其他的节点,其他节点受到通知后,收集被异常节点影响的集群锁,然后重新在剩余节点内选出新的锁主,重建锁主信息。异常节点自身释放锁资源,通知用户态监听进程,只对异常的LUN对应的OCFS2共享存储池执行定点umount操作。

2)管理网异常优化思路

因为DLM依赖管理网,因此管理网出现问题,很难进行优化,既然管理网是不可靠的,是无法优化的,那能不能不依赖管理网,能不能找到更可靠的锁依赖基础。因为磁盘阵列和存储网的存在,使得磁盘链路的冗余是存在的,也就是多路径,这就大大降低了磁盘链路异常的概率,因此考虑的解决方案是利用磁盘,即:把锁信息记录在磁盘上,开发新的基于磁盘的分布式锁机制Disk Lock,这样就避开了管理网带来的fence问题。磁盘锁的开发思路可以借鉴OCFS2已存在的子分配器SubAlloctor,比如: global_inode_alloc,系统文件需要从global_inode_alloc分配inode,以及所占的bit位。同理,磁盘锁也可以生成一个子分配器global_dlock_alloc,每个文件从中分配一个磁盘锁Disk Lock。磁盘锁实现的前提是存储支持CAW(Compare And Write)指令,CAW 指令可以原子地完成比较写操作,CAW 指令执行成功的节点即为加锁成功的节点。

=====================================================================

以上便是OCFS2集群管理之磁盘心跳,下篇讲分析mout流程。