02| OCFS2集群管理和心跳机制(1)

1,278 阅读13分钟

云计算讲究集群作战,集群是由多个主机节点Node组成,如何协调集群里这么做的节点协同作战,就涉及到集群的高可用和一致性问题。本文重点分析OCFS2集群的高可用技术实现。

一、高可用集群

高可用集群是指一组通过硬件和 软件连接起来的独立计算机,它们在用户面前表现为一个单一系统,在这样的一组计算机系统内部的一个或者多个节点停止工作,服务会从故障节点切换到正常工作的节点上运行,不会引起服务中断。从这个定义可以看出,集群必须检测节点和服务何时失效,何时恢复为可用。这在分布式系统中,是必须要面对的基础问题:各个模块(节点/服务)如何保证当前状态正常,如何让调用者知道服务还活着?某个节点异常之后,整个集群如何恢复?因此,首要的基础前提是集群中每个成员能够了解其他成员的状态,以便某个节点出现异常时,集群剩余节点能够进行容错处理,使集群对外仍然能正常提供服务。那么,如何能够了解其他成员的状态呢?心跳机制是常见的手段,这个任务通常由一组被称为“心跳”的代码完成。心态检测类似于心跳检测仪,对被检测者起到一个检测的作用。心跳以固定的频率向其他节点汇报当前节点状态的方式,收到心跳后一般认为当前节点和网络拓扑是良好的。

如下是OCFS2集群示意图:

image (16).png

为什么需要心跳机制?

因为网络的不可靠性,在 TCP 保持长连接的过程中,可能由于某些突发情况,例如:网线被拔出,突然掉电等,会造成服务器和客户端的连接中断。在这些突发情况下,如果没有交互,那么它们是不能在短时间内发现对方已经掉线,如果继续访问服务,会造成服务器功能的异常,进而引发无法提供服务或产生错误,心跳机制即可解决此类问题。

什么是心跳机制

百度百科:心跳机制是定时发送一个自定义的结构体(心跳包),让对方知道自己还活着,以确保连接的有效性的机制。

如何实现心跳机制:  OCFS2 集群心跳机制

(1)网络心跳: 确定节点与节点间的连通性,以便节点之间能够了解彼此的状态。

(2)磁盘心跳: 包含两层含义,一是,本地节点自我监控机制,即:本节点每隔2s进行写磁盘探测,以便了解本节点与共享磁盘之间的连通性,当本地节点写磁盘出现问题时,能够主动离开集群,避免集群不一致的产生;二是,本节点读取一块公共的磁盘区域来了解集群中其他节点当前与磁盘的连通性信息,以便集群中出现其他节点写磁盘异常时,能够做出正确的决定并记录集群最新的状态。这就要求本地节点写磁盘信息时,也必须写在这一公共磁盘区域中属于自己的位置上。

二、O2CB集群服务

OCFS2打造了自己的集群服务管理工具o2cb,它将启动OCFS2 集群心跳机制,完成一些列必要的操作。显然,我们在使用OCFS2的时候就要通过o2cb启动OCFS2集群服务。

OCFS2有自己的集群服务结构,叫做O2CB,它包括:

1)NM:节点管理器,它对cluster.conf文件中所有节点进行的监控。

2)HB:心跳服务(Heart beat service),他在节点离开或加入rac时提示up和down的消息。

3)TCP:控制节点间的通讯。

4)DLM:分布式锁管理器,它持续跟踪所有的锁,锁的所有者及状态。

5)CONFIGFS:用户配置文件系统驱动空间,挂节点是/config

6)DLMFS:用户空间和内核空间DLM的接口。

所有这些cluster服务都已经被打包在o2cb系统服务当中,所有的操作,比如:format、mount等,都需要cluster可用。在使用这些命令前,要先启动这些服务。

configfs简介

configfs 是一个基于内存的文件系统,是内核对象管理器(或称为config_items)。作用是在用户空间配置内核对象。

1)什么情况下会用到configfs?

当内核需要很多参数需要配置时;当需要动态创建内核对象并且内核对象需要修改配置时;

2)configfs的组织结构:

顶层结构是struct configfs_subsystem,为configfs子系统结构,接着是struct config_group,是configfs目录和属性的容器,struct config_item是configfs目录,代表可配置的内核对象,struct configfs_attribute是目录下面的属性。

3)使用configfs

mount -t configfs none /config

在用户态通过向/sys/kernel/config/创建items(由mkdir(2)),然后以文件的形式创建 config_item 的属性。当用户空间要求对该属性进行 write(2) 时,内核中该方法( ->store )就会被调用。

OCFS2正是使用了configfs,从用户空间对内核的网络心跳和磁盘心跳进行配置的。

OCFS2集群服务启动过程:

o2cb系统配置文件如下:启动o2cb时,请将O2CB_ENABLED=true

#
# This is a configuration file for automatic startup of the O2CB
# driver.  It is generated by running /etc/init.d/o2cb configure.
# Please use that method to modify this file
#
 
# O2CB_ENABLED: 'true' means to load the driver on boot.
O2CB_ENABLED=false
 
# O2CB_BOOTCLUSTER: If not empty, the name of a cluster to start.
O2CB_BOOTCLUSTER=ocfs2

集群节点的配置文件:/etc/ocfs2/cluster.conf,配置集群中所有主机的ip地址、端口号等信息。

初始化集群节点配置文件/etc/ocfs2/cluster.conf,在/etc/init.d/o2cb中使能o2cb,然后执行命令:service o2cb online,该命令将完成如下功能:

1、加载并挂载configfs文件系统
2、启动o2cb集群online
2.1、读取集群节点配置文件/etc/ocfs2/cluster.conf
2.2、依次将节点信息(ipv4_port、ipv4_address、num、local)写入configfs文件系统对应的配置属性节点目录中,随着local的写入,此时,将在OCFS2内核中启动o2net。

service o2cb online命令的实现,截取部分关键代码::

o2cb.init.sh:
online)
    load
    online "$2"
    ;;
load()
|-load_filesystem "configfs"  //加载configfs文件系统
    |-modprobe -s "$FSNAME"
|-mount_filesystem "configfs" "$(configfs_path)"    //挂载configfs路径/sys/kernel/config
    |-mount -t ${FSNAME} ${FSNAME} ${MOUNTPOINT}
|-load_stack_$PLUGIN  //即:load_stack_o2cb,加载o2cb插件
    |-modprobe -s ocfs2_stackglue
    |-modprobe -s "$PLUGIN_MODULE"
    |-mount_filesystem "ocfs2_dlmfs" "/dlm"
online()
|-check_online $CLUSTER
    |-check_online_o2cb()
        |-LOCAL="`cat "$(configfs_path)/cluster/${CLUSTER}/node/${NODE}/local"`" //校验local值是否等于1,如果是1,表示已经online,返回。否则执行下面的online动作。
|-online_$PLUGIN "$CLUSTER" //call online_o2cb()
    |-echo -n "Starting O2CB cluster ${CLUSTER}: "
    |-OUTPUT="`o2cb_ctl -H -n "${CLUSTER}" -t cluster -a online=yes 2>&1`"  //调o2cb_ctl执行online动作。

进入o2cb_ctl的C代码中,截取部分关键代码::

gint main(gint argc, gchar *argv[])
    |-rc = parse_options(argc, argv, &ctxt);
    |-case 'H':
        ctxt->oc_op = O2CB_OP_CHANGE;
 
    |-ret = o2cb_init();//configfs文件系统已经加载,读文件/sys/fs/ocfs2/cluster_stack,一般就是o2cb - classic_stack,然后将该stack设置到configfs配置文件目录:/sys/kernel/%s/config
    |-switch (ctxt.oc_op)
    |-case O2CB_OP_CHANGE:
         rc = run_change(&ctxt);
         break;
static gint run_change(O2CBContext *ctxt)
    |-rc = load_config(ctxt);  //加载集群配置文件:/etc/ocfs2/cluster.conf
    |-rc = run_change_clusters(ctxt);
        |-rc = run_change_cluster_one(ctxt, cluster);
            |- rc = online_cluster(ctxt, cluster);
                |-name = o2cb_cluster_get_name(cluster);
                |-ret = o2cb_create_cluster(name);
                    |-ret = mkdir(path, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);//在configfs文件系统中创建集群目录/sys/kernel/%s/config/%s/cluster
                |-iter = o2cb_cluster_get_nodes(cluster);//获取集群配置文件中所有的节点nodes信息
                |-while (j_iterator_has_more(iter))
                 {
                     |-node = (O2CBNode *)j_iterator_get_next(iter);
                     |-node_num = g_strdup_printf("%d", o2cb_node_get_number(node));
                     |-node_name = o2cb_node_get_name(node);
                     |-ip_port = g_strdup_printf("%d", o2cb_node_get_port(node));
                     |-ip_address = o2cb_node_get_ip_string(node);
                     |-ret = o2cb_add_node(name, node_name, node_num, ip_address, ip_port, local);
                         |-ret = mkdir(node_path, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);//在configfs文件系统中创建集群目录/sys/kernel/%s/config/%s/cluster/%s/
                         |-err = o2cb_set_node_attribute(cluster_name, node_name, "ipv4_port", ip_port);
                         |-err = o2cb_set_node_attribute(cluster_name, node_name, "ipv4_address", ip_address);
                         |-err = o2cb_set_node_attribute(cluster_name, node_name, "num", node_num);
                         |-err = o2cb_set_node_attribute(cluster_name, node_name, "local", local);//内核中启动o2net
                |-}
    |-rc = write_config(ctxt);
        |-rc = o2cb_config_store(ctxt->oc_config, O2CB_CONFIG_FILE);

三、网络心跳分析

为何会有网络心跳?网络心跳的作用是什么?

管理网主要是为了DLM分布式锁提供服务的。在OCFS2集群中,DLM(Distributed Lock Master)分布式锁管理器,它是依赖管理网进行加锁、解锁操作的。锁主的Master或Leader节点是通过选举产生的,在网络正常的情况下,锁主的选举、加锁和解锁能够顺利进行,一旦出现管理网闪断,将会造成加锁、解锁的异常,因此,网络心跳的作用就是在出现管理网异常时,根据仲裁机制,对于分区内的节点做处理,受影响节点持有的锁将重新在集群内重建,以便维护集群的正常功能。

在同一个集群中,每个节点会根据配置文件中的节点参数,向集群中其它节点发送网络报文,以便维持网络的连通性。需要指出的是:向集群中其它节点发起连接请求是由磁盘心跳的UP事件触发的,从而建立其网络通信的连接,且任意两个节点之间都会建立连接。那么,此时的网络服务启动和内核中的o2net是做什么用的呢?是做好接受网络连接的准备:socket、bind、accept,等待connect连接请求的到来,这是网络连接的服务端。

1、Heartbeat Dead Threshold 主机与存储间心跳。默认配置文件数值为:31。表示超过60s主机与存储间无心跳报文,主机重启。
 O2CB_HEARTBEAT_THRESHOLD = (((timeout in seconds) / 2) + 1)
2、Network Idle Timeout 主机间心跳超时时间。默认数值为:30s。如果主机间出现心跳报文不通,超过默 认值之后,根据主机重启算法进行重启。
3、Network Keepalive Delay 心跳确认时间。默认为2s。
4、Network Reconnect Delay 心跳超时重发时间。默认为2s

内核态: 每个节点的内核加载nodemanager,包含磁盘心跳o2hb初始化和网络o2net心跳初始化。o2hb和o2net共同实现了如下的这样一个状态机,完成了集群中节点间的互联互通。

节点node的网络状态机: 服务端在accept等待客户端连接的到来,客户端节点UP后,开始运转网络状态机。

image (17).png

o2net内核线程: 每个节点均启动o2net内核线程

当写local属性时,o2cb_set_node_attribute(cluster_name, node_name,"local", local);内核中将启动o2net线程。

创建socket,初始化o2net_accept_many工作任务,bind端口和地址,listen监听,等待客户端节点发起connect连接。

1)服务端接收到其他节点发来的connect请求,分配一个sock服务该连接,等待TCP完成三次握手之后,客户端节点和服务端节点先后建立ESTABLISHED连接,o2net_node状态机进入“可用态”。

2)服务端节点收到客户端节点发来的o2net握手报文,校验通过之后,o2net_node状态机进入“有效态”,标记节点间连接有效valid,然后回复给对端一个o2net握手报文。

static ssize_t o2nm_node_local_store(struct config_item *item, const char *page, size_t count)
    |-struct o2nm_node *node = to_o2nm_node(item);
    |-cluster = to_o2nm_cluster_from_node(node);
    |-if (tmp && !cluster->cl_has_local) { //如果是一个新的local节点上线,则开始监听:启动o2net线程。
        |-ret = o2net_start_listening(node);
            |-mlog(ML_KTHREAD, "starting o2net thread...\n");
            |-struct workqueue_struct *o2net_wq = alloc_ordered_workqueue("o2net", WQ_MEM_RECLAIM);//启动o2net内核线程
            |-ret = o2net_open_listening_sock(node->nd_ipv4_address, node->nd_ipv4_port);
                |-ret = sock_create(PF_INET, SOCK_STREAM, IPPROTO_TCP, &sock); //创建一个新的socket
                |-sock->sk->sk_user_data = sock->sk->sk_data_ready;
                |-sock->sk->sk_data_ready = o2net_listen_data_ready; //如果数据ready,且socket处于TCP_LISTEN,则发起监听工作。
                      |-if (sk->sk_state == TCP_LISTEN) {
                          |-queue_work(o2net_wq, &o2net_listen_work);//监听开始之后,在o2net_wq工作队列中调度accept处理任务。
                |-struct socket *o2net_listen_sock = sock;
                |-INIT_WORK(&o2net_listen_work, o2net_accept_many); //监听开启后,开始accept处理工作
                |-sock->sk->sk_reuse = SK_CAN_REUSE;
                |-ret = sock->ops->bind(sock, (struct sockaddr *)&sin, sizeof(sin)); //bind操作
                |-ret = sock->ops->listen(sock, 64); //本节点开启监听listen,64个socket,监听哪些节点会向本节点发起connect连接
        |-o2quo_conn_up(node->nd_num);
            |-qs->qs_connected++;
            |-set_bit(node, qs->qs_conn_bm);
            |-o2quo_set_hold(qs, node);

网络服务端监听连接请求: 当socket状态变成TCP_LISTEN后,在工作队列o2net中调度accept处理任务。需要指出的是:如果本节点num号比发起connect的节点num号大,则绝connect请求。即:节点号大的向节点号小的发起connect请求。

static void o2net_accept_many(struct work_struct *work)
|-struct socket *sock = o2net_listen_sock;
|-for (;;) 
     |-o2net_accept_one(sock, &more);
         |-ret = sock_create_lite(sock->sk->sk_family, sock->sk->sk_type,
                                      sock->sk->sk_protocol, &new_sock);//新分配一个socket,处理accept请求。
         |-new_sock->type = sock->type;
         |-new_sock->ops = sock->ops;
         |-ret = sock->ops->accept(sock, new_sock, O_NONBLOCK, false);//accept阻塞等待connect请求连接,用新分配一个socket处理新来的连接请求。
         |-new_sock->sk->sk_allocation = GFP_ATOMIC;
         |-tcp_sock_set_nodelay(new_sock->sk);
         |-tcp_sock_set_user_timeout(new_sock->sk, O2NET_TCP_USER_TIMEOUT);
         |-ret = new_sock->ops->getname(new_sock, (struct sockaddr *) &sin, 1);
         |-node = o2nm_get_node_by_ip(sin.sin_addr.s_addr);//解析出发送connect请求的节点node
         |-if (o2nm_this_node() >= node->nd_num) { //如果本节点num号比发起connect的节点num号大,则绝connect请求。即:节点号大的向节点号小的发起connect请求。
             |-ret = -EINVAL;
             |-goto out;
         |-}
         |-struct o2net_node *nn = o2net_nn_from_num(node->nd_num);
         |-struct o2net_sock_container *sc = sc_alloc(node);//初始化网络套接字容器
             |-
         |-sc->sc_sock = new_sock;
         |-o2net_set_nn_state(nn, sc, 0, 0);//accept一个节点的连接后,已新生成一个socket,将该sock设置到o2net_node状态机。
         |-o2net_register_callbacks(sc->sc_sock->sk, sc);
             |-sk->sk_data_ready = o2net_data_ready;
                 |-o2net_sc_queue_work(sc, &sc->sc_rx_work);//调度数据接收任务:接收报文
             |-sk->sk_state_change = o2net_state_change;
                 |-switch(sk->sk_state) {
                 case TCP_ESTABLISHED:
                     o2net_sc_queue_work(sc, &sc->sc_connect_work);
                     break;
         |-o2net_sc_queue_work(sc, &sc->sc_rx_work);//调度接受报文任务
         |-o2net_initialize_handshake(); //初始化握手报文
         |-o2net_sendpage(sc, o2net_hand, sizeof(*o2net_hand));//发生握手报文 

节点UP事件: 写本节点自己的磁盘心跳信息,同时读所有节点的磁盘心跳数据,如果检测到某个节点UP,则报这个节点UP事件。

o2net_hb_node_up_cb
|-o2quo_hb_up(node_num);
    |-qs->qs_heartbeating++;
    |-set_bit(node, qs->qs_hb_bm);
    |-o2quo_set_hold(qs, node);
|-if (node_num != o2nm_this_node()) { //只有非本节点,其他节点的UP事件,本节点才会响应,开始轮转该节点nn状态机。
    |-if (nn->nn_persistent_error)
        o2net_set_nn_state(nn, NULL, 0, 0);

节点DOWN事件: 如果检测到某个节点DOWN,则报这个节点DOWN事件。

 o2net_hb_node_down_cb
|-o2quo_hb_down(node_num);
    |-qs->qs_heartbeating--;//心跳计数减一
    |-clear_bit(node, qs->qs_hb_bm);//从仲裁bitmap中清除该节点
    |-o2quo_clear_hold(qs, node);
        |-test_and_clear_bit(node, qs->qs_hold_bm)
        |-if (--qs->qs_holds == 0 && qs->qs_pending) {
            |-schedule_work(&qs->qs_work);//调o2quo_make_decision做仲裁处理
|-if (node_num != o2nm_this_node()) //只有非本节点,其他节点的DOWN事件,本节点才会响应,开始轮转该节点nn状态机。
    |-o2net_disconnect_node(node);
        |-o2net_set_nn_state(nn, NULL, 0, -ENOTCONN);
        |-if (o2net_wq) {//取消该节点的连接超时和连接任务
            cancel_delayed_work(&nn->nn_connect_expired);
            cancel_delayed_work(&nn->nn_connect_work);
            cancel_delayed_work(&nn->nn_still_up);
            flush_workqueue(o2net_wq);

网络客户端发起connect请求: 初始化好网络套接字容器,客服端新生成一个sock,向服务端发起connect连接请求。

static void o2net_start_connect(struct work_struct *work)
|-node = o2nm_get_node_by_num(o2net_num_from_nn(nn));
|-mynode = o2nm_get_node_by_num(o2nm_this_node());
|-sc = sc_alloc(node);//初始化网络套接字容器
    |-
|-ret = sock_create(PF_INET, SOCK_STREAM, IPPROTO_TCP, &sock);
|-sc->sc_sock = sock;
|-myaddr.sin_family = AF_INET;
|-myaddr.sin_addr.s_addr = mynode->nd_ipv4_address;
|-myaddr.sin_port = htons(0); /* any port */
|-ret = sock->ops->bind(sock, (struct sockaddr *)&myaddr, sizeof(myaddr));
|-tcp_sock_set_nodelay(sc->sc_sock->sk);//立即发起
|-tcp_sock_set_user_timeout(sock->sk, O2NET_TCP_USER_TIMEOUT);
|-o2net_register_callbacks(sc->sc_sock->sk, sc);
    |-sk->sk_data_ready = o2net_data_ready;
        |-o2net_sc_queue_work(sc, &sc->sc_rx_work);
    |-sk->sk_state_change = o2net_state_change;
        |-switch(sk->sk_state) {
        case TCP_ESTABLISHED:
            o2net_sc_queue_work(sc, &sc->sc_connect_work); //建立起网络长连接,然后发送o2net handshake报文。
            break;
|-o2net_set_nn_state(nn, sc, 0, 0);//本节点发起connect连接之后,本节点设置状态机:新生成socket
|-remoteaddr.sin_family = AF_INET;
|-remoteaddr.sin_addr.s_addr = node->nd_ipv4_address;
|-remoteaddr.sin_port = node->nd_ipv4_port;
|-ret = sc->sc_sock->ops->connect(sc->sc_sock, //发起connect连接请求,TCP3次握手之后将建立起网络长连接,然后发送o2net handshake报文。
                    (struct sockaddr *)&remoteaddr, sizeof(remoteaddr), O_NONBLOCK);

网络套接字容器: 无论客户端还是服务端都需要生成如下的sc,无论客户端还是服务端都会做如下的工作:初始化o2net握手报文并发送、接收报文、节点在服务端accept和客户端发起connect时,都准备好了套接字容器sc:sock_container

static struct o2net_sock_container *sc_alloc(struct o2nm_node *node)
|-sc->sc_node = node;
|-INIT_WORK(&sc->sc_connect_work, o2net_sc_connect_completed);//TCP连接已建立TCP_ESTABLISHED,客户端和服务端都会初始化o2net握手报文并发送。
    |-o2net_initialize_handshake();
    |-o2net_sendpage(sc, o2net_hand, sizeof(*o2net_hand));
|-INIT_WORK(&sc->sc_rx_work, o2net_rx_until_empty);//数据接收任务:接收报文
|-INIT_WORK(&sc->sc_shutdown_work, o2net_shutdown_sc);
    |-o2net_sc_cancel_delayed_work(sc, &sc->sc_keepalive_work);
    |-kernel_sock_shutdown(sc->sc_sock, SHUT_RDWR);
    |-o2net_ensure_shutdown(nn, sc, 0);
        |-o2net_set_nn_state(nn, NULL, 0, err);
|-INIT_DELAYED_WORK(&sc->sc_keepalive_work, o2net_sc_send_keep_req); //发送保活报文,网络心跳包。
    |-o2net_sendpage(sc, o2net_keep_req, sizeof(*o2net_keep_req));
|-timer_setup(&sc->sc_idle_timeout, o2net_idle_timer, 0);//超时发生网络分区,处理逻辑。
    |-o2quo_conn_err(o2net_num_from_nn(nn));
        |-qs->qs_connected--;
        |-clear_bit(node, qs->qs_conn_bm);

至此,集群中节点之间的连通性就建立起来了,接下来节点之间通过管理网发生保活报文keepalive,维持网络心跳,为DLM分布式锁管理器提供基础支撑。

下一篇讲介绍OCFS2集群管理之磁盘心跳。