宽限期的理解
- 背景:在nfsv4协议之后,客户端与服务端的交互都是有状态的,当nfs服务端发生重启,客户端上已经持有的一些状态(打开的文件,委托,文件锁,客户端租约等等),希望在服务端重启后能够得到恢复,避免状态被其他客户端抢占而导致冲突;如果客户端状态丢失,可能会导致一些问题,例如文件锁丢失可能会导致数据一致性问题;委托丢失可能会导致性能下降;租约丢失可能会导致客户端无法维持锁或委托;打开的文件状态丢失可能会导致性能下降等等
- 基本原理:ganesha服务端在启动后的一段时间内,只允许原客户端去恢复之前持有的一些状态,而不允许客户端去执行新的状态变更操作(如新打开文件,新加文件锁),这段时间称为宽限期(grace period);在宽限期结束后,才允许新的状态变更操作
实现
- 在ganesha中,采用了一个全局静态变量grace_status来表明服务端是否处于宽限期
- 第一位为1(GRACE_STATUS_ACTIVE)表明在宽限期
- 第二位为1(GRACE_STATUS_CHANGE_REQ)表明处于“需要状态变化期”
- 第三位开始到高位记录的是grace状态的引用计数,每次加减引用的步长为4(GRACE_STATUS_REF_INCREMENT),即第3位到高位不为0则表明引用计数不为0(GRACE_STATUS_COUNT_MASK)
- 网关在两种情况下会尝试启动宽限期状态(nfs_start_grace):
- 网关刚开始启动时会将grace_status置为1
- 通过dbus管理命令(网关集群模式)
- nfs_start_grace的实现逻辑
nfs_start_grace { //记录开始宽限期起始时间 ret = clock_gettime(CLOCK_MONOTONIC, ¤t_grace); /* 1. 判断当前状态已经是GRACE_STATUS_ACTIVE,跳过后续步骤 2. 判断当前状态引用数不为0,将状态位“需要状态变化期”置为1;否则尝试将状态位“宽限期”位置1 3. 判断grace_status跟之前相比是否有变化,若无变化则退出,否则重复1,2,3逻辑,1,2,3步操作均为原子操作 4. 以上操作做完后,有三种可能: - 当前状态已经是宽限期状态,跳出后续操作 - 若之前状态不是宽限期状态,且状态引用不为0(此种情况一般是网关运行过程中执行了启动grace操作,比如手工执行,或者网关集群联动),则宽限期启动失败,退出; - 若是系统刚启动,则实际上状态引用必为0(只有与客户端有交互才有可能加引用),因此之前状态不是宽限期状态,此时“宽限期”状态设置成功,可以继续后续操作 */ // 加载重启前保存的clientids,用于后续的客户端状态恢复 nfs4_recovery_load_clids(NULL); } - 在宽限期是如何做状态回收的
- 网关通过持久化目录加载重启前建连并完成协商的clientid,并放入全局clid_list列表(nfs4_recovery_load_clids)
- 对于与服务端建连协商成功的客户端clientid(nfs4_op_setclientid_confirm),且判断在全局clid_list列表中,说明是恢复的clientid,标记clientid为允许回收(cid_allow_reclaim = true)
- 在ganesha中,对于状态操作(打开文件,委托,加锁,设置属性等等)都需要进行网关是否处于宽限期的判断,来决定是否允许操作继续:
- 当前网关处于宽限期,只允许状态回收(relaim,或者说恢复)操作,不允许新的状态操作
- 当前网关不处于宽限期,不再允许状态回收操作,只允许新的状态操作
- 以open操作流程为例:
nfs4_op_open
{
...
res_OPEN4->status = open4_validate_claim(data, claim, clientid, &grace_ref);
...
if (grace_ref)
nfs_put_grace_status();
}
// 在nfs4_op_open中调用open4_validate_claim判断是否允许claim
open4_validate_claim
{
bool want_grace = false;
/* 这里是判断是否底层自身就支持grace,如果支持则不需要ganesha网关这一层去支持,目前实现中默认都不支持,因此take_ref为true */
bool take_ref = !op_ctx->fsal_export->exp_ops.fs_supports(
op_ctx->fsal_export, fso_grace_method);
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
if (take_ref)
{
if (nfs_get_grace_status(want_grace))
{
*grace_ref = true;
}
else
{
status = want_grace ? NFS4ERR_NO_GRACE : NFS4ERR_GRACE;
}
}
else
{
*grace_ref = false;
}
}
nfs_get_grace_status和nfs_put_grace_status:
1. 相当于是给当前的grace状态加引用,引用加成功则返回true,引用加失败则返回false
2. 加引用成功需要两个条件:
- 期望的grace状态(want_grace)与当前网关实际的grace状态(grace_status&1)相同
- 当前网关不处于grace状态变更时期
3. 当加引用成功后,grace状态的引用数不为零,则不允许网关变更grace状态
4. 因此在open操作中,当加grace状态引用成功后,执行完open后续操作,会调用nfs_put_grace_status对grace状态减引用
综合以上分析:
- 对于新open,若当前在宽限期,会返回NFS4ERR_GRACE,否则允许继续open操作
- 对于reclaim open,若在宽限期,允许继续open操作,否则返回NFS4ERR_NO_GRACE
- 宽限期的结束,reaper后台线程中循环调用reaper_run,最后进入nfs_try_lift_grace
nfs_try_lift_grace
{
...
timeout = current_grace; // 宽限期起始时间
timeout.tv_sec += nfs_param.nfsv4_param.grace_period; // 宽限期阈值(90s)
// 判断超过阈值则应结束宽限期(in_grace = false)
in_grace = gsh_time_cmp(&timeout, &now) > 0;
if (!in_grace) {
/*
1. 判断当前状态已经不是宽限期状态(GRACE_STATUS_ACTIVE),直接退出
2. 将状态位“需要状态变化期”置为1(GRACE_STATUS_CHANGE_REQ,这样在客户端与服务端状态交互操作中均不会再加引用计数);
3. 判断grace_status跟之前相比是否有变化,若无变化则退出,否则重复1,2,3逻辑,1,2,3步操作均为原子操作
4. 以上操作做完后,有两种可能:
- 当前状态已经不是宽限期状态,跳出后续操作
- 已经将状态位“需要状态变化期”置为1(GRACE_STATUS_CHANGE_REQ)
*/
// 判断grace_status引用计数为0则可以关闭宽限期
nfs_lift_grace_locked
// fs_clean_old_recov_dir:将fs_clean_old_recov_dir中的备份的记录清空
- nfs_end_grace
//关闭宽限期状态
- cur = __sync_and_and_fetch(&grace_status,
~(GRACE_STATUS_ACTIVE|GRACE_STATUS_CHANGE_REQ));
}
}
后记
- nfs-ganesha作为通用nfs网关,可以对接各种存储后端,在一致性和故障恢复等等需要考虑与底层实现的联动,例如nfs-ganesha+cephfs的方案,cephfs其实有caps机制,当一个cephfs的客户端(nfs-ganesha+libcephfs)挂掉,客户端与mds的session断开,有一个session_timeout(默认300s),在这段时间内授予给原客户端的caps能保证不会授予给其他客户端,因此我理解fsal_ceph是支持grace的