默认情况下nfs-ganesha中的recovery backend用的是fs_backend
c
#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: <IP>-(clid-len:long-form-clid-in-string-form)
clid_entry_t中的cl_name是连接ip-(clid-len:clientid),如果连接经过了四层lb,则连接ip实际是lb的ip
- 网关启动流程:在main函数中,开始宽限期之前,进行recover初始化:
scss
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
scss
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的两个场景:
scss
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 < ht_reap->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目录下的clid_entry_t加入到clid_list链表中),然后将子目录删除
- 遍历v4_recov_dir目录,先将子目录目录项往v4_old_dir目录下对应创建一个,然后将子目录项(每个目录名对应一个clid_entry_t)都加入到clid_list链表中),然后将子目录删除
- 之所以要分v4_old_dir和v4_recov_dir两个目录,是
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记录- 网关运行过程中,每个与网关建连成功(nfs4_op_setclientid_confirm成功)后的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
做备份,防止下次重启) - 在网关启动后的宽限期内(90s),对于恢复clid_entry对应的客户端,允许执行claim操作(open操作携带CLAIM_PREVIOUS标记);否则都不允许,返回NFS4ERR_GRACE错误;
- 在宽限期结束,允许新的open操作(open操作携带CLAIM_NULL标记);不再允许claim操作,返回NFS4ERR_NO_GRACE错误
为什么要用v4_old_dir
和v4_recov_dir
两个目录来持久化clid_entry? 为了应对在宽限期网关再次发生重启的情况:
- 如果只有一个持久化目录
v4_recov_dir
,当网关加载clid_entry记录后删除记录,在宽限期重启会导致再次加载丢失了已经被删掉的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中
- 若在recover阶段重启,最坏情况下就是v4_old_dir已经加载完了都删掉了,此时能保证
- 因此如果宽限期再重启一次,也能保证重启前已经协商的clid_entry记录是完整的
如果只使用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,考虑最坏情况:
- 第一次重启发生在recover刚结束,此时v4_recov_dir是空目录,其原来记录的clid_entry都已经拷贝到v4_old_dir中,客户端还没有跟网关新协商或者恢复
- 第二次重启发生在recover阶段,此时v4_old_dir中的clid_entry加载到内存后就会删除,因此重启后就会丢clid_entry