默认情况下nfs-ganesha中的recovery backend用的是fs_backend
#define RECOVERY_BACKEND_DEFAULT RECOVERY_BACKEND_FS
#define GRACE_PERIOD_DEFAULT 90
CONF_ITEM_TOKEN("RecoveryBackend", RECOVERY_BACKEND_DEFAULT,
recovery_backend_types, nfs_version4_parameter,
recovery_backend),
CONF_ITEM_BOOL("Clustered", true,
nfs_core_param, clustered),
CONF_ITEM_UI32("Grace_Period", 0, 180, GRACE_PERIOD_DEFAULT,
nfs_version4_parameter, grace_period),
struct nfs4_recovery_backend fs_backend = {
.recovery_init = fs_create_recov_dir,
.end_grace = fs_clean_old_recov_dir,
.recovery_read_clids = fs_read_recov_clids_takeover,
.add_clid = fs_add_clid,
.rm_clid = fs_rm_clid,
.add_revoke_fh = fs_add_revoke_fh,
};
static int clid_count; /* number of active clients */
static struct glist_head clid_list = GLIST_HEAD_INIT(clid_list); /* clients */
typedef struct clid_entry {
struct glist_head cl_list; /*< Link in the list */
struct glist_head cl_rfh_list;
char cl_name[PATH_MAX]; /*< Client name */
} clid_entry_t;
cl_name: -(clid-len:long-form-clid-in-string-form)
clid_entry_t中的cl_name是连接ip-(clid-len:clientid),如果连接经过了四层lb,则连接ip实际是lb的ip
main()
{
...
rc = nfs4_recovery_init();
if (rc) {
LogCrit(COMPONENT_INIT,
"Recovery backend initialization failed!");
goto fatal_die;
}
/* Start grace period */
nfs_start_grace(NULL);
...
}
int nfs4_recovery_init(void)
{
...
s_backend_init(&recovery_backend); // recovery_backend = &fs_backend;
...
recovery_backend->recovery_init(); // fs_create_recov_dir
}
fs_create_recov_dir就是创建目录:
v4_recov_dir=/var/lib/nfs/ganesha/v4recov/node0
v4_old_dir=/var/lib/nfs/ganesha/v4old/node0
然后网关第一次进入宽限期:
nfs_start_grace :STATE :EVENT :NFS Server Now IN GRACE, duration 90
nfs_start_grace
{
...
// 将clid_list中的clid_entry清空,clid_count清零,实际上启动刚启动时list里面是空的
nfs4_cleanup_clid_entries();
// recovery_backend->recovery_read_clids -> fs_read_recov_clids_recover
nfs4_recovery_load_clids(NULL);
}
/*
* 1. 先遍历v4_old_dir目录,先将子目录路径拼接出的clid_name对应的clid_entry_t都加入到clid_list链表然后将子目录删除
* 2. 遍历v4_recov_dir目录,先将子目录目录项往v4_old_dir目录下对应复制拷贝一个,将子目录路径拼接出的clid_name对应的clid_entry_t都加入到clid_list链表然后将子目录删除
*/
static void fs_read_recov_clids_recover(add_clid_entry_hook add_clid_entry,
add_rfh_entry_hook add_rfh_entry)
{
int rc;
// add_clid_entry -> nfs4_add_clid_entry
// add_rfh_entry -> nfs4_add_rfh_entry
rc = fs_read_recov_clids_impl(v4_old_dir, NULL, NULL, 0,
add_clid_entry,
add_rfh_entry);
rc = fs_read_recov_clids_impl(v4_recov_dir, NULL, v4_old_dir, 0,
add_clid_entry,
add_rfh_entry);
}
/*
* When not doing a take over, first open the old state dir and read
* in those entries. The reason for the two directories is in case of
* a reboot/restart during grace period. Next, read in entries from
* the recovery directory and then move them into the old state
* directory. if called due to a take over, nodeid will be nonzero.
* in this case, add that node's clientids to the existing list. Then
* move those entries into the old state directory.
* /
static int fs_read_recov_clids_impl(const char *parent_path,
char *clid_str,
char *tgtdir,
int takeover,
add_clid_entry_hook add_clid_entry,
add_rfh_entry_hook add_rfh_entry)
{
...
dp = opendir(parent_path);
for (dentp = readdir(dp); dentp != NULL; dentp = readdir(dp)) {
...
sub_path = gsh_concat_sep(parent_path, '/', dentp->d_name);
/* if tgtdir is not NULL, we need to build
* nfs4old/currentnode
*/
if (tgtdir) {
new_path = gsh_concat_sep(tgtdir, '/', dentp->d_name);
rc = mkdir(new_path, 0700);
}
build_clid = gsh_malloc(total_clid_len);
if (clid_str)
memcpy(build_clid, clid_str, clid_str_len);
memcpy(build_clid + clid_str_len,
dentp->d_name,
segment_len + 1);
...
// 这里sub_path就是v4_old_dir拼接子目录,若cl_name长度大于255,会以255个字符分割递归创建 // 子目录,但是一般cl_name长度小于255,因此实际v4_old_dir只有一层子目录,当前实现中子目录 // 是空的,因此rc返回0
rc = fs_read_recov_clids_impl(sub_path,
build_clid,
new_path,
takeover,
add_clid_entry,
add_rfh_entry);
...
// nfs4_add_clid_entry,就是将cl_name生成新的clid_entry_t并加入全局clid_list
new_ent = add_clid_entry(build_clid);
// 恢复fh,当前cl_name子目录下没有记录fh子项,因此里面没做什么
fs_cp_pop_revoked_delegs(new_ent,
sub_path,
tgtdir,
!takeover,
add_rfh_entry);
rc = rmdir(sub_path);
}
}
以上在网关启动流程中recovery的操作:
v4_old_dir
和v4_recov_dir
中加载clid_entry
v4_old_di中
中老的clid_entry
删掉,将v4_recov_dir
中的clid_entry
拷贝到v4_old_dir
中,将v4_recov_dir
中的clid_entry
删掉以下是服务端增加和移除clid_entry的两个场景:
nfs_client_id_confirm
{
...
// fs_add_clid
nfs4_add_clid(clientid);
}
fs_add_clid
{
...
// clientid->cid_recov_tag = "str_client_addr-(cidstr_lenx:cidstr)
fs_create_clid_name(clientid);
// 后续逻辑就是如果在clientid长度小于255时,直接在v4_recov_dir下创建clid_name子目录;若超过255则每255个字符做分割递归创建子目录
...
}
static int reap_hash_table(hash_table_t *ht_reap)
{
...
for (i = 0; i parameter.index_size; i++) {
RBT_LOOP(head_rbt, pn) {
addr = RBT_OPAQ(pn);
client_id = addr->val.addr;
// 判断client_id过没过期,没过期则跳过
if (valid_lease(client_id)) {
continue;
}
// 会调用nfs4_rm_clid(clientid),最终调用fs_rm_clid将v4_recov_dir下clid_name对应的子目录树都删掉
nfs_client_id_expire(client_id, false);
}
}
}
在clientid确认操作中,对于已经在clid_list中的clientid,说明是恢复的clientid,允许该client进行reclaim操作:
nfs4_op_setclientid_confirm
{
...
/* check if the client can perform reclaims */
nfs4_chk_clid(unconf); // nfs4_chk_clid_impl -> clientid->cid_allow_reclaim = true;
}
在nfs4_op_open中调用open4_validate_claim做是否允许
open4_validate_claim
{
bool want_grace = false;
switch (claim)
{
case CLAIM_NULL:
// CLAIM_NULL表明是新open,若是v4.1及以上且判断relaim未完成,则置NFS4ERR_GRACE即不允许新打开文件
case CLAIM_PREVIOUS:
// CLAIM_PREVIOUS表明是回收之前open的fh,若不是恢复的clientid,或是v4.1及以上且判断relaim已完成,则置NFS4ERR_NO_GRACE,即对于当前客户端宽限期已结束,否则需继续判断是否是在宽限期:want_grace = true;
...
}
// 看当前grace状态是否与want_grace一致,不一致则返回false
nfs_get_grace_status(want_grace)
// 对于新open,若当前在宽限期,会返回NFS4ERR_GRACE,否则允许继续open操作,对于reclaim open,若在宽限期,允许继续open操作,否则返回NFS4ERR_NO_GRACE
}
结合上面代码总结下在宽限期nfs4_recovery_load_clids的作用:
v4_old_dir
和v4_recov_dir
这两个目录是用于ganesha网关恢复clientid的,v4_old_dir
中的数据可能会丢失,因为新的恢复数据会覆盖它。这可能会导致一些客户端无法正确地恢复其状态。Ganesha的设计者考虑到了这种情况。如果在宽限期内重启,Ganesha将尝试合并v4_old_dir
和v4_recov_dir
中的数据,以尽可能保留更多的客户端状态信息。但这不是一个完美的解决方案,因为在极端情况下,可能仍然会丢失一些数据。
整体恢复机制总结:
v4_old_dir
和v4_recov_dir
两个目录v4_old_dir
记录的是前一次重启前建立的clid_entry记录,v4_recov_dir
记录的是本次重启前建立的clid_entry记录v4_recov_dir
目录下;每个过期或者失效的clid_entry会从v4_recov_dir
中移除v4_old_dir
和v4_recov_dir
下的记录恢复clid_entry列表(恢复完会移除v4_old_dir
中的原记录,在将v4_recov_dir
删除前会将记录拷贝到v4_old_dir
做备份,防止下次重启)为什么要用v4_old_dir
和v4_recov_dir
两个目录来持久化clid_entry?
为了应对在宽限期网关再次发生重启的情况:
v4_recov_dir
,当网关加载clid_entry记录后删除记录,在宽限期重启会导致再次加载丢失了已经被删掉的clid_entry记录- 若在recover阶段重启,最坏情况下就是v4_old_dir已经加载完了都删掉了,此时能保证
v4_recov_dir
中记录还在(要么已拷贝到v4_old_dir中,要么还在v4_recov_dir中); - 若是recover阶段已经结束,v4_recov_dir中原来记录的clid_entry都已经拷贝到v4_old_dir中,此时客户端跟网关新协商或者恢复的clid_entry会加入到v4_recov_dir中
如果只使用v4_recov_dir
一个目录,但是在加载完clid_entry记录并不立刻删除,一直等到网关运行过程中定期扫描发现过期的记录才删除,是否可行?
考虑有一个客户端A的clid_entry记录在宽限期内客户端没有重新建立连接并协商恢复,过了宽限期后另一个客户端B与网关建连并且操作了A之前锁定(open也是一种锁)的资源,此时网关应该会授予B锁定资源,此时假定定期线程还没有清除v4_recov_dir
中A的clid_entry记录网关重启,如果客户端A此时恢复建连就可能在宽限期内与网关重新建立连接并协商恢复锁定资源;而按照设计,此时网关不应该将资源授予A
反之,若使用上面两个目录分别存储两次重启前clid_entry,加载完就删除的方案,则不会出现上面的问题
当前fs_backend机制的缺陷:
在宽限期中连续重启仍然会丢clid_entry,考虑最坏情况: