上一篇文章我们已经分析过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调整级别。
客户端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锁是空锁,与所有锁兼容。
三、锁资源及其初始化:
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的进程。
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)。在升级的时候,由于本节点不是锁主或锁主判断锁级别不兼容被阻塞做转换的锁。
3、struct dlm_lock_resource*res->granted,锁资源授予队列:表示加锁成功的锁dlm_lock。
4、struct dlm_lock_resource*res->recovering,锁资源恢复队列:这个链表是把锁资源res加入到dlm recovery上下文锁资源队列dlm->reco.resources,进行recovery恢复处理的锁资源,这些锁资源的原锁主都是fence节点,现在它们要重新找锁主。
锁的状态:
与锁转换相关的记录锁资源的链表队列:这些队列主要是由锁主节点来操作
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,做锁降级。