05| DLM分布式锁管理器(1)

936 阅读14分钟

上一篇文章我们已经分析过mount流程,分布式锁管理器DLM就是在mount的过程中完成一系列初始化和节点加入dlm domain的,本篇文章重点分析DLM的实现。

我们知道在同一个机器上,不同进程访问临界资源是需要加锁,但是集群来说,物理节点在空间上是独立的,如何实现加锁功能?我们来分析一下,在同一个机器上,加锁之所以能起到保护临界资源的作用,是因为锁只有一份,不同进程来抢占锁资源,抢到锁的就加锁成功。那么在空间隔离的集群环境中,想要加锁还是这个思路:必须有一个集中管理锁的地方,且独一份,任何节点想要加、解锁必须获得这一独一份的锁资源得授权才能进行操作。DLM的设计也是如此,虽然DLM是分布式锁管理器,是协调多个节点对磁盘操作的,分布式体现在多个节点对同一个文件进行读写操作,因此每个节点上必须有锁资源,为了防止把文件系统写坏,又需要一个集中管理锁资源的地方,因此需要DLM设计一个锁主Master的概念。由锁主统一协调、授权是否允许加解锁。

分布式锁管理器DLM(Distribute Lock Manager)是OCFS2重要的锁功能实现,OCFS2之所以能做到磁盘共享,同时挂载到多个不同节点上,在同一个LUN上承载不同节点上VM的业务,而不至于把同一个文件系统写坏,其根本保证就是实现了DLM,这也为集群共享文件系统提供报保障,使OCFS2可以应用在集群系统,如果没有DLM,OCFS2与Ext4无异。

一、OCFS2中的锁关系

OCFS2使用的DLM是去中心化的设计,所有的节点是对等的,每个节点都只存储了一部分的锁信息。一个锁会涉及到3个角色:

1、请求锁的节点:发起请求上锁的节点。

2、持有锁的节点:持有当前锁资源上高级别锁的节点。

3、锁主节点:管理锁内容的授权节点。

OCFS2文件系统使用DLM时,加入了ocfs层作为dlm的客户端(调用者),其数据结构对应关系如下,这里假设node1为锁资源的owner,lock1~lock3为该资源上的锁。同一个node上,对于某个资源有且仅有一个锁资源ocfs2_lock_res副本,唯一对应本节点dlm层的一个dlm_lock_res。当节点有在该资源上加锁时,每个节点在dlm对应一个dlm_lock,且lock的级别记录在锁资源ocfs2_lock_res中,同一个节点多次对同一资源加锁时,不产生新的dlm_lock,而仅使用covert接口和lock_res中的ex_holder、ro_holder调整级别。

image (21).png

客户端OCFS2层面的锁资源ocfs2_lock_res->l_lksb中有一个锁资源状态块对象ocfs2_dlm_lksb l_lksb,该对象l_lksb包含DLM锁状态dlm_lockstatus,记录了DLM模式下的锁struct dlm_lock* lockid。所以,锁状态dlm_lockstatus->lockid就是锁dlm_lock,而锁dlm_lock中包含DLM层的锁资源struct dlm_lock_resource *lockres;

(客户端OCFS2层面的锁资源):ocfs2_lock_res->l_lksb <--> dlm_lock* lockid <--> dlm_lock_resource *lockres(DLM层的锁资源)

struct ocfs2_lock_res {
    void                    *l_priv;
    struct ocfs2_lock_res_ops *l_ops;
    struct list_head         l_blocked_list;
    struct list_head         l_mask_waiters;
    struct list_head     l_holders;
    unsigned long        l_flags;
    char                     l_name[OCFS2_LOCK_ID_MAX_LEN];
    unsigned int             l_ro_holders;
    unsigned int             l_ex_holders;
    signed char      l_level;
    signed char      l_requested;
    signed char      l_blocking;
    /* Data packed - type enum ocfs2_lock_type */
    unsigned char            l_type;
    /* used from AST/BAST funcs. */
    /* Data packed - enum type ocfs2_ast_action */
    unsigned char            l_action;
    /* Data packed - enum type ocfs2_unlock_action */
    unsigned char            l_unlock_action;
    unsigned int             l_pending_gen;
    spinlock_t               l_lock;
    struct ocfs2_dlm_lksb    l_lksb;
    wait_queue_head_t        l_event;
    struct list_head         l_debug_list;
#ifdef CONFIG_OCFS2_FS_STATS
    struct ocfs2_lock_stats  l_lock_prmode;     /* PR mode stats */
    u32                      l_lock_refresh;    /* Disk refreshes */
    u64                      l_lock_wait;   /* First lock wait time */
    struct ocfs2_lock_stats  l_lock_exmode;     /* EX mode stats */
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map   l_lockdep_map;
#endif
};

struct dlm_lock
{
    struct dlm_migratable_lock ml;
    struct list_head list;
    struct list_head ast_list;
    struct list_head bast_list;
    struct dlm_lock_resource *lockres;
    spinlock_t spinlock;
    struct kref lock_refs;
    // ast and bast must be callable while holding a spinlock!
    dlm_astlockfunc_t *ast;
    dlm_bastlockfunc_t *bast;
    void *astdata;
    struct dlm_lockstatus *lksb;
    unsigned ast_pending:1,
         bast_pending:1,
         convert_pending:1,
         lock_pending:1,
         cancel_pending:1,
         unlock_pending:1,
         lksb_kernel_allocated:1;
};

二、锁级别及其兼容性

如下图所示,OCFS2里面使用到的锁级别EX/PR/NL,EX是排他锁,写锁时加EX锁级别;PR是共享锁,读锁时加PR锁;NL是空锁。

由下图兼容性可知,对同一个锁资源lockres来讲:

1)如果一个节点持有EX锁,那么其他节点只能是NL锁,此时如果有节点是EX锁或PR锁,它必然要做降级处理,即:写操作只有一个节点,只允许一个写,也不允许写的时候同时读;

2)如果一个节点持有PR锁,那么其他节点可以获得PR锁和NL锁,即:可以多个节点读操作。

3)NL锁是空锁,与所有锁兼容。

image (22).png

三、锁资源及其初始化:

enum ocfs2_lock_type {
    OCFS2_LOCK_TYPE_META = 0,
    OCFS2_LOCK_TYPE_DATA,
    OCFS2_LOCK_TYPE_SUPER,
    OCFS2_LOCK_TYPE_RENAME,
    OCFS2_LOCK_TYPE_RW,
    OCFS2_LOCK_TYPE_DENTRY,
    OCFS2_LOCK_TYPE_OPEN,
    OCFS2_LOCK_TYPE_FLOCK,
    OCFS2_LOCK_TYPE_QINFO,
    OCFS2_LOCK_TYPE_NFS_SYNC,
    OCFS2_LOCK_TYPE_ORPHAN_SCAN,
    OCFS2_LOCK_TYPE_REFCOUNT,
    OCFS2_LOCK_TYPE_TRIM_FS,
    OCFS2_NUM_LOCK_TYPES
};

在mount过程中,DLM初始化时完成4种全局锁资源的初始化:

ocfs2_super_lock_res_init(&osb->osb_super_lockres, osb);
ocfs2_rename_lock_res_init(&osb->osb_rename_lockres, osb);
ocfs2_nfs_sync_lock_init(osb);
ocfs2_orphan_scan_lock_res_init(&osb->osb_orphan_scan.os_lockres, osb);

superblock锁资源:osb->osb_super_lockres,在mount/umout/recovery文件系统时,都涉及对superblock元数据进行操作,比如:写inode时间戳,都需要获取EX锁。

5种文件锁:open_lock、inode_lock、rw_lock、flock_lock、dentry_lock

OCFS2_I(inode)->ip_open_lockres:多个节点都会有获取文件inode的操作,尤其是系统文件,多个节点访问同一个inode,打开同一个inode,需要先拿到open_lockres。
OCFS2_I(inode)->ip_inode_lockres:(meta lockres)多个节点更新文件inode元数据时,须先拿到inode lockres。
OCFS2_I(inode)->ip_rw_lockres:多个节点对文件进行读写操作。

以创建inode为例,看文件锁的初始化:

__ocfs2_mknod_locked  //创建inode文件
    1)|-ocfs2_populate_inode(inode, fe, 1); //先初始化锁资源默认属性
          |-ocfs2_inode_lock_res_init(&OCFS2_I(inode)->ip_inode_lockres, OCFS2_LOCK_TYPE_META, 0, inode);//访问inode锁资源
          |-ocfs2_inode_lock_res_init(&OCFS2_I(inode)->ip_open_lockres, OCFS2_LOCK_TYPE_OPEN, 0, inode);//打开文件锁资源
          |-ocfs2_inode_lock_res_init(&OCFS2_I(inode)->ip_rw_lockres, OCFS2_LOCK_TYPE_RW, inode->i_generation, inode);//读写操作锁资源
              |-ocfs2_build_lock_name(type, OCFS2_I(inode)->ip_blkno, generation, res->l_name);//初始化资源名称
              |-ocfs2_lock_res_init_common(OCFS2_SB(inode->i_sb), res, type, ops, inode);//初始化资源公共属性
    2)|-status = ocfs2_create_new_inode_locks(inode);//启用锁资源,在锁资源上创建锁信息(加锁)
           |-ret = ocfs2_create_new_lock(osb, &OCFS2_I(inode)->ip_rw_lockres, 1, 1);
           |-ret = ocfs2_create_new_lock(osb, &OCFS2_I(inode)->ip_inode_lockres, 1, 0);
           |-ret = ocfs2_create_new_lock(osb, &OCFS2_I(inode)->ip_open_lockres, 0, 0);
               |-int level =  ex ? DLM_LOCK_EX : DLM_LOCK_PR;
               |-u32 lkm_flags = local ? DLM_LKF_LOCAL : 0; //只有rw_lockres锁资源上有local flag
               |-lockres_or_flags(lockres, OCFS2_LOCK_LOCAL);
                     |-lockres->l_flags |= OCFS2_LOCK_LOCAL;
               |-return ocfs2_lock_create(osb, lockres, level, lkm_flags);
                     |-lockres->l_action = OCFS2_AST_ATTACH; //attach标记在创建锁资源时,第一次加锁即设置。
                     |-lockres->l_requested = level; //加锁的级别
                     |-lockres_or_flags(lockres, OCFS2_LOCK_BUSY);
                     |-gen = lockres_set_pending(lockres);
                     |-ret = ocfs2_dlm_lock(osb->cconn, level, &lockres->l_lksb, dlm_flags, //真正的加锁处理逻辑。
                                      lockres->l_name, OCFS2_LOCK_ID_MAX_LEN - 1);
                     |-lockres_clear_pending(lockres, gen, osb);

四、锁主的概念

文章开头已述,锁主是用来协调、授权集群中节点加解锁的。那么锁主是怎么产生的呢?锁主的产生需要在集群内部达成一致,因此涉及到一致性协议,这属于事物一致性,因此常用两阶段提交2PC一致性算法来实现。

锁主的类型:

enum dlm_mle_type {
    DLM_MLE_BLOCK = 0,
    DLM_MLE_MASTER = 1,
    DLM_MLE_MIGRATION = 2,
    DLM_MLE_NUM_TYPES = 3,
};

锁主mle对象:

struct dlm_master_list_entry {
    struct hlist_node master_hash_node;
    struct list_head hb_events;
    struct dlm_ctxt *dlm;
    spinlock_t spinlock;
    wait_queue_head_t wq;
    atomic_t woken;
    struct kref mle_refs;
    int inuse;
    unsigned long maybe_map[BITS_TO_LONGS(O2NM_MAX_NODES)];
    unsigned long vote_map[BITS_TO_LONGS(O2NM_MAX_NODES)];
    unsigned long response_map[BITS_TO_LONGS(O2NM_MAX_NODES)];
    unsigned long node_map[BITS_TO_LONGS(O2NM_MAX_NODES)];
    u8 master;
    u8 new_master;
    enum dlm_mle_type type;
    struct o2hb_callback_func mle_hb_up;
    struct o2hb_callback_func mle_hb_down;
    struct dlm_lock_resource *mleres;
    unsigned char mname[DLM_LOCKID_NAME_MAX];
    unsigned int mnamelen;
    unsigned int mnamehash;
};

在获取一个锁资源struct dlm_lock_resource *res的时候(没有锁资源,则新建),就会确定下锁资源的锁主是哪个节点。假设集群中有A、B、C三个节点,现节点A执行如下函数。

简述如下的函数的实现:

struct dlm_lock_resource * dlm_get_lock_resource(struct dlm_ctxt *dlm, const char *lockid, int namelen, int flags)

1、 锁资源总会是在某个节点上新建的,新建锁资源的时候,这个锁资源会关联一个struct dlm_master_list_entry *mle对象,该mle对象就是标识一个锁资源锁主的对象,这个mle对象是在dlm中单独的哈希表struct dlm_ctxt *dlm->master_hash[]中维护的。

1.1) 节点A先本地查找mle对象,如果查找到了mle对象,说明锁主已经存在,此时,blocked标记=1(表示本节点上查到的锁主mle是个DLM_MLE_BLOCK类型);既然锁主已经存在,那就不用再选锁主了(跳过2PC第一阶段),进行检查如下:

a) 如果mle->master不存在,但锁资源的拥有者res->owner存在,节点A只要向锁主res->owner发送master request消息,直接从锁主那里同步锁状态就好了,等锁主回复yes,并向节点A发送assert master消息,本地记录下mle->master就好了;

b) 如果mle->master已经有了,就直接退出,锁主确定。

【异常情况处理】 :如果发送master request消息时,锁主宕机了,节点A会休眠一会儿,继续检查锁主还在不在,如果还在就再次尝试发送master request消息;如果不在了,故障隔离机制会把锁主从锁资源中清理出去,节点Arecovery流程会设置fence节点的锁资源res->owner = UNKNOWN,这样就会在检查“节点成员是否变化”和“投票是否完成”时,因此锁主故障隔离了,节点成员发生变化,需要重新进行选举流程,进入2PC第一阶段。

1.2) 如果没有mle对象,节点A本地就初始化一个mle对象,节点A会在本地初始化mle类型是DLM_MLE_MASTER,表示节点A想成为这个锁资源的锁主Master,mle->mleres会关联对应的锁资源res,节点A在mle->maybe_map中设置。

2、 接下来进入2PC第一阶段(投票阶段):这个想成为锁主的节点A会向其他节点(B、C)发送master request消息,如果遇到有节点宕机的,跳过,继续下一个询问节点。

2.1) 其他节点(B、C)做出投票,回复yes/no/maybe作为答复:

a) 其他节点上:锁资源res存在,如果res->owner记录了锁主是请求节点A,或mle->master记录了锁主是请求节点A,就回复yes;如果不是,就回复no;如果res->owner没有锁主,mle->master也没有锁主,这个节点(假如B)也想参与竞选,就回复maybe,同时将请求节点A记录在本地的mle->maybe_map中。

b) 其他节点上:锁资源res不存在,看mle对象是否存在,mle对象也不存在,说明本节点(B或C)不持有该锁资源,那这个节点(B或C)就初始化一个DLM_MLE_BLOCK类型的mle对象,回复no;mle对象存在,如果类型是DLM_MLE_MASTER,那这个节点(B或C)也想参与竞选,就回复maybe,否则就回复no。

需要指出的是:如果其他节点(B或C)中有节点已经是锁主了,它会向其他节点发送一次assert master消息,在集群中同步一次锁主信息。

2.2) 请求节点A收到其他节点(B、C)的答复消息:

a) 如果回复的yes,表示这个回复节点(B或C)已经是这个锁资源的Master了,如果这个Master节点的号比节点A小,那么节点A就退出选举过程,承认Master节点的锁主地位;否则,节点A会继续向其他节点发送master reuest消息询问投票。

b) 如果回复的no,表示这个回复节点(B或C)不是这个锁资源的Master;

c) 如果回复maybe,表示回复节点(B或C)上要么res和mle中锁着未定,要么不存在锁资源,自己初始化了一个类型是DLM_MLE_MASTER的mle对象,也想成为锁主,那节点A就把它也记录在mle->maybe_map中。

3、 节点A检查选主是否结束,统计两个值:集群节点成员是否发生变化members和投票是否完成voted。

a) 如果集群节点成员发生变化members(mle->node_map关联了节点UP/DOWN事件,能实时感知节点变化,更新mle->node_map),则重新开始选主流程,锁资源的res->owner也重新被设置为UNKNOWN,回到2PC第一阶段。

b) 如果集群间节点没有变化,说明没有节点被故障隔离,也没有出现网络分区或主机故障。如果只是投票还未完成,这个时候就休眠一会儿,再检查一遍;如果休眠时被唤醒,可能是有其他maybe节点出现了fence,于是提前唤醒了睡眠,节点间成员发生变化。

c) 投票完成了,就从mle->maybe_map中选取号最小的那个节点跟节点A比较号大小,号小者为王。如果节点A更小,那么节点A就是锁主,然后进入2PC第二阶段(执行阶段);如果节点A大,那本节点A就休眠,等待那个号小的节点确立锁着地位,等着收到它的assert master确认消息,再进入2PC第二阶段(执行阶段)。

4、 进入2PC第二阶段(执行阶段):成为锁主的节点现在本地把mle->master设置为自己,然后向其他节点发送assert master确认消息。其他节点收到assert master确认消息,在本地找到锁资源res对应的mle对象,把mle->master设置为锁主节点,然后把res->owner也设置为锁主节点。等所有节点完成确认后,锁主节点也在本地把锁资源res->owner设置为自己。

至此,锁主选举完成。。。

五、DLM锁资源(dlm_lock_resource)中的链表队列解释:

struct dlm_lock_resource
{
    /* WARNING: Please see the comment in dlm_init_lockres before
     * adding fields here. */
    struct hlist_node hash_node;
    struct qstr lockname;
    struct kref      refs;
    /*
     * Please keep granted, converting, and blocked in this order,
     * as some funcs want to iterate over all lists.
     *
     * All four lists are protected by the hash's reference.
     */
    struct list_head granted;
    struct list_head converting;
    struct list_head blocked;
    struct list_head purge;
    /*
     * These two lists require you to hold an additional reference
     * while they are on the list.
     */
    struct list_head dirty;
    struct list_head recovering; // dlm_recovery_ctxt.resources list
    /* Added during init and removed during release */
    struct list_head tracking;  /* dlm->tracking_list */
    /* unused lock resources have their last_used stamped and are
     * put on a list for the dlm thread to run. */
    unsigned long    last_used;
    struct dlm_ctxt *dlm;
    unsigned migration_pending:1;
    atomic_t asts_reserved;
    spinlock_t spinlock;
    wait_queue_head_t wq;
    u8  owner;              //node which owns the lock resource, or unknown
    u16 state;
    char lvb[DLM_LVB_LEN];
    unsigned int inflight_locks;
    unsigned int inflight_assert_workers;
    unsigned long refmap[BITS_TO_LONGS(O2NM_MAX_NODES)];
};

下图显示的是资源和队列的关系,granted queue中记录的是所有已经获得的lock的进程,convert queue记录的是所有等待转换lock的进程,而wait queue(blocked queue)中记录的是所有阻塞等待loc的进程。

image (23).png

1、struct dlm_lock_resource*res->blocked,锁资源阻塞队列:这个队列中都是加锁请求的锁,由于本节点不是锁主或锁主判断锁级别不兼容而被阻塞的锁。

这个队列中的锁dlm_lock,

a)要么锁主不是自己,发送给远端锁主处理的,本地把锁dlm_lock加入锁资源res->blocked阻塞队列,等待锁主处理;

b)要么自己是锁主,由于锁级别不兼容且还没有处理的锁dlm_lock,锁主把锁dlm_lock先加入锁资源res->blocked阻塞队列,等着dlm thread线程去处理。

2、struct dlm_lock_resource*res->converting,锁资源转换队列:这里的都是转换请求的锁dlm_lock,这些锁已经授予过granted,现在要升级或降级(in-place)。在升级的时候,由于本节点不是锁主或锁主判断锁级别不兼容被阻塞做转换的锁。

image (24).png

3、struct dlm_lock_resource*res->granted,锁资源授予队列:表示加锁成功的锁dlm_lock。

4、struct dlm_lock_resource*res->recovering,锁资源恢复队列:这个链表是把锁资源res加入到dlm recovery上下文锁资源队列dlm->reco.resources,进行recovery恢复处理的锁资源,这些锁资源的原锁主都是fence节点,现在它们要重新找锁主。

锁的状态:

image (25).png

与锁转换相关的记录锁资源的链表队列:这些队列主要是由锁主节点来操作

1、dlm->dirty_list:dlm脏队列,针对的是锁资源dlm_lock_resource,由dlm thread线程负责取该队列中的锁资源处理。

锁主已经完成锁请求授予的锁资源res,或者锁级别不兼容被阻塞的锁资源res(加锁请求或锁转换请求时),都会将res加入该脏队列,等待锁主dlm thread线程来处理:

a)或授予res->granted后,锁加入dlm->pending_ast队列,向该锁请求节点lock->ml.node发送AST消息;

b)或找到不兼容的锁lock,将其加入dlm->pending_basts队列,向该锁的持有节点lock->ml.node发送BAST消息。

2、dlm->pending_asts:dlm等待AST处理队列,针对的是请求锁dlm_lock,dlm thread线程会处理这些锁。

已授权到授予队列res->granted的锁lock,这些锁已由锁主完成授予处理,锁主的dlm thread线程接下来会向这些锁的请求节点发送AST消息,通知请求节点可以加锁或锁转换处理了。

3、dlm->pending_basts:dlm等待BAST处理队列,针对的是发生冲突的已授权锁dlm_lock,dlm thread线程会处理这些锁。

DLM在处理不兼容的锁请求时,这些已授予锁由于跟请求锁发生冲突,导致加锁的节点不能加锁,所以锁主发送BAST消息给持有高级别锁的节点,让它降级。

4、dlm->reco.resources:顾名思义,就是dlm中等待recovery的锁资源res。本节点查找到锁主是fence节点的锁资源,将该锁资源设置recovery标记,并将锁资源挂接到dlm recovery上下文dlm->reco.resoures链表上,等待recovery线程来处理。

5、struct ocfs2_super*osb->blocked_lock_list:osb阻塞锁队列,由downconvert线程处理。

需要做downconvert处理的锁资源res加到该osb阻塞锁队列,downconvert线程会处理这个队列中的锁资源res,做锁降级。