Linux 网络文件系统简介
网络文件系统(NFS)协议是由 Sun MicroSystem 公司在 20 世纪 80 年代为了提供对共享文件的远程访问而设计和实现的,它采用了经典的客户机/服务器模式提供服务。为了达到如同 NFS 协议通过使用 Sun 公司开发的远在本机上使用本地文件系统一样便捷的效果,NFS 通过使用远程过程调用协议(RPC Protocol)来实现运行在一台计算机上的程序来调用在另一台远程机器上运行的子程序。同时,为了解决不同平台上的数据交互问题,它提供了外部数据表示(XDR)来解决这个问题。为了灵活地提供文件共享服务,该协议可以在 TCP 协议或者是 UDP 协议上运行,典型的情况是在 UDP 协议上运行。在此基础上,NFS 在数据的传送过程中需要 RPC 命令得到确认,而且在需要的时候会要重传,这样既可以通过 UDP 协议获得较高的通信效率,也能通过 RPC 来获得较高的通信可靠性。
由于 NFS 基于 C/S 模式提供服务,所以它的核心组件主要包括客户机和服务器两部分。图 1 详细说明了 NFS 的主要组件以及主要的配置文件。在服务器端,portmap、mountd、nfsd 三个监控程序将在后台运行。portmap 监控程序用来注册基于 RPC 的服务。当一个 RPC 的监控程序启动的时候,它告诉 portmap 监控程序它在哪一个端口进行侦听,并且它在进行什么样的 RPC 服务。当一个客户机向服务器提出一个 RPC 请求,那么它就会和 portmap 监控程序取得联系以确定 RPC 消息应该发往的端口号。而 Mountd 监控程序的功能是来读取服务器端的/etc/exportfs 文件并且创建一个将服务器的本地文件系统导出的主机和网络列表,因而客户机的挂接(mount)请求都被定位到 mountd 监控程序(daemon)。当验证了服务器确实具有挂接所请求的文件系统的权限以后,mountd 为请求的挂接点返回一个文件句柄。而 nfsd 监控程序则被服务器用来处理客户机端发送过来的请求。由于服务器需要同时处理多个客户机的请求,所以在缺省情况下,操作系统将会自动启动八个 nfsd 线程。当然,如果 NFS 服务器特别忙的时候,系统有可能根据实际情况启动更多的线程。
图 1 网络文件系统简图
NFS 的客户机与服务器之间通过 RPC 进行通信,通信过程如下所示:
- 用户将 NFS 服务器的共享目录挂载到本地文件系统中。
- 客户访问 NFS 目录中的文件时,NFS 客户端向 NFS 服务器端发送 RPC 请求。
- NFS 服务端接收客户端发来的 RPC 请求,并将这个请求传递给本地文件访问程序,然后访问服务器主机上的一个本地的磁盘文件。NFS 服务器可以同时接收多个 NFS 客户端的请求,并对其进行并发控制。
- NFS 客户端向服务器主机发出一个 RPC 调用,然后等待服务器的应答。NFS 客户端收到服务器的应答后,把结果信息展现给用户或应用程序。
NFS 下的数据备份、恢复的主要功能
对数据进行备份与恢复是保证数据安全和业务连续性的非常成熟的做法。在 Linux 下的本地文件系统(例如 Ext2、Ext3 等)中,数据备份和恢复一般采用常规的办法来进行操作,例如使用 Tar、Archive 等。而对于 NFS 来说,其数据备份需要采用量身定制的方法来进行。
为了保证数据在灾难环境中的可用性和业务连续性,针对它的数据备份、恢复方案应具备如下重要功能:
- 通过对系统重要数据的快速备份,切实保证系统数据的安全;
- 可以根据指令完成备份系统的实时切入,保证服务不被中断,保持系统持续运行的能力;
- 通过实时记录所有文件的操作日志,系统管理员能够在发生灾难的情况下对日志进行分析和取证,从而发现入侵者的蛛丝马迹。
NFS 多版本备份技术
为了保证服务器出现故障后能迅速恢复,要求系统数据能快速恢复到一个最近的正确状态,所有这些都需要多版本技术的支持,通过同步记录文件的在某些时刻的状态,在整个系统范围内建立起类似于数据库系统的”检查点”,以保证上述目标的实现。
对于多版本系统而言,需要较好地解决两个方面的问题:性能和空间利用率。对于前者,最主要的是保证在生成版本的时候能够快速完成,同时恢复时也具有较好的性能。此外,系统引入多版本造成的整体开销也应该比较理想。对于第二点,主要考虑是节约磁盘空间,虽然随着硬件技术的不断发展,磁盘空间越来越大,性价比也越来越高,但是当版本较多而且文件数量较多、较大时,引入多版本增加的开销也可能相当可观,同时,较大的空间也意味着版本生成时可能需要更多的写操作,这样也必将影响总体性能。
为了保证引入多版本特性后文件系统仍具有较好的性能,以及保证较高的空间利用率,我们开发了一种高效的惰性版本生成算法。主要思想是:生成版本时不进行文件的复制,仅复制目录结构,在新版本生成后到下一版本生成前,如果有文件需要修改,则第一次修改时对该文件进行复制,从而保证该文件状态与对应的版本保持一致。
在一般情况下,目录结构的数据量远远小于文件的数据量,因而这种方法可以大大降低版本生成时需要复制的数据量,因而具有较高的性能。同时,这种把单个文件版本生成的实际操作推后到非做不可的时候,并且任意文件在两次版本之间最多生成一次版本,因此这种惰性策略可以使需要实际生成版本的文件数量达到最少,同时还可以把多个文件版本生成操作分散到具体的文件操作中,从而避免了集中的一次性版本生成方法可能造成的服务暂时停顿的问题。
版本生成后的结构如图 2 所示。
图 2 多版本生成示意图
具体算法包括两个部分,即版本生成算法和文件第一次修改处理算法,版本生成算法主要完成版本生成工作,主要过程如下:
- 找到需要形成版本的最高层目录作为原目录;
- 利用文件系统提供的函数,生成新的目录节点,称为新目录;
- 把原目录中的结构复制到新目录;
- 在原目录中找到所有的子目录,重复 2、3 步;
- 把新的子目录对应的 inode 号替换上一层目录中的老 inode 号;
- 重复上述过程,及到目录树中的所有目录得到复制为止。
在上述策略中,新版本并没有复制所有的文件,只是在复制的目录结构中记录下了该文件的 inode 号(即复制了目录的结构,而不是把文件都进行复制,从而节省了存储和计算资源),因此,当有 NFS 请求需要对文件进行版本生成后的第一次修改时,需要复制该文件,生成新的版本。该实现过程参见如下流程图:
图 3 写时复制算法示意图
这种文件复制策略其实是一种惰性算法,也即我们常说的写时复制的方法,这个方法在 Linux 操作系统的子进程对父进程资源的继承中有所体现。这个策略一方面可以最大限度减少复制文件的数量,另一方面则可以避免瞬间过大的文件复制工作量,影响文件服务的性能。该算法的过程如下:当文件操作为写操作时,判断该文件是否版本生成后的第一次写操作;若是则利用文件系统提供的底层函数生成一个新的文件,复制源文件的数据到新生成的文件,同时把该文件当前版本的 inode 节点中的版本号置为当前版本号,这样新文件就成为该文件的最新版本。
虽然我们采用的算法可以有较好的性能,存储开销也是最优,但是,每次版本生成肯定会造成服务性能的下降和空间的占用,而这些代价在一个比较安全可靠的环境中是可以适当降低的,即当系统比较安全的时候,可以选择让系统以更低的频率进行版本生成,相反,当系统安全状况比较糟糕的时候,可以通过提高版本生成频率适当降低服务性能来获得更高的数据安全性能,当系统处于紧急状态时,甚至可以要求立即进行版本生成。
基于这些考虑,我们采用了自适应的备份策略,灾情评估系统可以动态评估系统的灾情程度,然后可以立即修改版本生成策略,以适应当时的安全要求。
NFS 数据恢复技术
企业应用 NFS 的一个重要目标就是要保证系统的高可用性,即使在出现严重灾难、故障、攻击等情况下能具有较好的生存能力。因此,当一个系统出现故障时,如何快速地恢复系统,迅速投入到服务备份中去是相当重要的,所以,对于文件系统数据的恢复而言,也需要专门的考虑和设计。
本方案被配置成多个站点互为备份的情况,即平时只有一个主站点在服务,其他站点处于同步备份状态,当某个站点出现故障或灾难时,或者是被非法入侵者攻破时,系统可以立即分配新的主站点把被破坏的站点替换下来,进入恢复状态,其他正常的站点仍可提供正常的服务。
当然,也存在所有站点均出现故障的情况,但是由于我们采用了多种措施,如动态随机迁移、灾情评估与响应策略等,再配合传统的防火墙、IDS 等安全系统,可以极大限度地减少这种几率。因此,我们的数据恢复问题主要考虑上述这种情形,即个别服务器出现故障退出服务而其他系统依然正常的情况。
首先,我们来分析一下系统退出后数据的情形,主要涉及到退出的服务器和正常的主服务器与备份服务器,如图 4 所示:
图 4 一个系统退出后数据状态示意图
在上图中,退出服务器最后生成的版本号为 i,系统退出后,一方面主文件服务器会察觉到同步数据无法从退出服务器返回结果,这样的话它就会重发同步请求,经过 3 次重发后,如果依然没有返回信息,则认为该服务器退出服务,因此会把同步数据备份到磁盘文件中,并记录下该服务器在同步数据文件中的起始位置,这当由多个文件服务器退出时可以分别识别出来。由于退出系统无法继续保持同步,因此其状态会与工作的文件服务器不一致,具体表现在以下几个方面:
- 当退出时间很短时,数据不一致仅存在于缓冲区中,这时如果退出服务器能立即重新投入使用,则不需要进行额外的数据恢复,数据同步可以通过主服务器同步请求的重试来达到。
- 当主服务器确认退出服务器退出后,会把未同步的数据写入特定的同步数据文件中,这时的不一致性包括了缓冲区中的数据和同步数据文件中的数据,这时的数据恢复需要做两方面的工作:
- 把同步数据文件中的正确数据一次性发送给退出服务器,退出服务器把它写入本地的同步数据文件;
- 建立本地的缓冲区,建立起同步机制,接收同步数据,同时启动数据同步进程,先同步数据文件中的数据,当缓冲区数据因没有处理而达到一定程度时,会自动把部分数据追加到同步数据文件的后面,这时,退出服务器已经恢复了正常工作,实际上也不需要过多的数据恢复工作。
由于主文件服务器一般需要处理文件的读写请求,写请求仅占一部分,需要同步而执行的操作造成的负载要小于主服务器,因此可能在较短的时间内完成同步。当需要退出服务器(此时已经进入同步阶段)成为主服务器时,则必须等所有同步数据同步完成后才能开始服务。
- 如果退出服务器是因为较严重的故障或灾难而退出的,则可能需要较长时间的处理,如更换硬件、系统重启、甚至重装系统等,这时就可能出现一些困难的情况,一种是如上图所示的,工作正常的系统已经生成了新的版本,如服务器退出时的最新版本是 i,经过一段时间后,正常系统生成了新的版本,这时主系统会清空同步数据文件,重新从版本生成后进行记录。对于这种情况,可以有两种处理办法:
- 基于本地版本的快速恢复:当退出文件服务器本地至少存在一个版本与其他正常机器上的版本相同时,可以采用这种恢复策略。具体而言,先确定一个最新的正确版本,用本地版本恢复,这一过程非常简单快捷,仅涉及到两次 inode 的修改;然后选择一台正常服务器,请求它生成一个正常系统上最新版本与恢复版本的增量升级数据,这样的数据量不会很大,而且不需要象基于操作的同步那样逐步进行,同步效率非常高,因此可以大大提高恢复速度。同步到正常系统的最新版本后,然后就按照上述第 2 条的情况进行同步数据文件的同步。
- 基于分布版本的快速恢复:当停顿时间太长而不存在一个相同的版本,或文件服务器数据出现损坏(如磁盘故障造成数据损毁)时,需要采用此种方法。具体办法如下:直接把正常服务器上的最新版本传送到退出服务器,然后按照上述的第 2 种情况进行同步数据文件的同步。
正如上面所述,全部服务器均出现问题的概率是很小的,但是,不能简单的排除这种情况的出现,特别是本方案采用数据同步机制,即多个站点的数据是保持快速同步的,这虽然能保证动态迁移的顺利完成,但是也带来较大的风险,就是会出现数据”污染”的自动传播,当主文件服务器中的文件数据因为某些原因(主要是对文件的非法访问)造成数据非法修改时,会立即传播到其他备份节点,这样的话,不管服务迁移到哪台机器均会出现错误。
针对这种情况,我们采取了以下措施:当发现非法修改造成数据污染时,系统可以自动命令各站点恢复到指定的版本,如前一版本(可以由管理员配置成前一、二、三个版本);管理员也可以干预这一过程,强制各站点恢复到同一指定的版本,从而保证全局文件系统使用同一正确版本。
NFS 文件细粒度恢复技术
在传统恢复技术中,一方面由于数据备份不是实时进行的,当出现事故需要恢复时,最新的备份数据与最新数据之间存在一个时间差,这样就造成了该时间段内数据的丢失(见图 5);同时,传统的数据备份是一定时间段后数据的增量备份,是一段时间内所有文件操作叠加后的结果,因而无法精确知道在这段时间内实际数据的变化过程,因而也无法从所有这些操作中定位非法操作,并进行选择性的恢复,以保证数据的正确性。
图 5 因非实时备份造成恢复时的数据丢失示意图
基于上述考虑,我们不但采用了增量方式的多版本备份恢复技术,同时还对文件的修改日志进行了实时的备份,这样就可以在事故发生后进行基于文件操作的精确恢复,并支持允许剔除非法操作的选择性恢复,这样既能尽量避免因事故造成的数据丢失问题,又能通过选择性恢复较好保证数据的正确性,同时,还可以通过对日志的分析,结合数据的精确恢复,达到发现犯罪线索、获得有效证据的目的,为打击网络犯罪提供有力的技术手段。在这里,精确性恢复指的是恢复某一时段的所有操作,一般是在某一版本后的所有操作,不由用户进行选择,而选择性恢复则指的是某一时间段内的所有操作构成的集合的子集,需要恢复的操作由用户通过查询、浏览等工具来进行选择。在我们的定义中,实际上可以认为精确恢复为选择性恢复的一个特例。
我们首先需要有相关的解决方法来记录下具体的操作信息,形成操作日志文件,从而作为分析的证据(参见图 6)。我们使用的策略是通过修改服务器操作系统内核调用 nfsd_write()、nfsd_create()……。从中取到调用处理对象的文件、目录的全路径名,写进文件,在内核中截获相应的文件操作请求。下面以 nfsd_rename()系统调用为例,进行扩充、修改而实现记录操作日志的功能。
- int nfsd_rename(struct svc_rqst *rqstp, struct svc_fh *ffhp, char *fname,
- int flen,struct svc_fh *tfhp, char *tname, int tlen)
- {
- struct dentry *fdentry, *tdentry, *odentry, *ndentry;
- struct inode *fdir, *tdir;
- int err;
- char *name;
- mem_segment_t oldfs;
- int fd;
- err = fh_verify(rqstp, ffhp, S_IFDIR, MAY_REMOVE);
- if (err)
- goto out;
- rqstp->rq_path1 = rqstp->rq_path2;
- err = fh_verify(rqstp, tfhp, S_IFDIR, MAY_CREATE);
- if (err)
- goto out;
- fdentry = ffhp->fh_dentry;
- fdir = fdentry->d_inode;
- tdentry = tfhp->fh_dentry;
- tdir = tdentry->d_inode;
- ……
- //加入的代码进行处理工作
- if((!rqstp->rq_recover)&&(!S_ISDIR(odentry->d_inode->i_mode))
- &&(odentry->d_inode->i_nlink>1)){
- rqstp->rq_copy->wait = 1;
- rqstp->rq_copy->done = 0;
- name = get_total_name(dentry,NULL);
- oldfs = get_fs();
- set_fs(KERNEL_DS); //进入内核模式
- fd = sys_open("/backupserv/changfilename.c",0,31681);
- sys_write(fd,name,strlen(name));
- sys_close(fd);
- set_fs(oldfs); //从内核模式返回
- while(!rqstp->rq_copy->done){
- schedule_timeout((HZ+99)/100);
- ……
- }
- }
该文件是在 nfs 服务器端执行 nfs 客户机发送过来的修改文件或者是目录的原函数。在这里,我们可以通过添加自己的代码,来将创建的目录和文件名存入一个磁盘文件当中,以备后面的备份和恢复操作。
图 6 NFS 文件细粒度恢复日志产生示意图
获得了操作日志信息,然后就可以进行精确恢复和选择性恢复时。首先由用户利用数据查询、浏览工具确定需要恢复的文件操作集,然后利用相应的日志数据按记录产生顺序逐条生成恢复请求,发送给文件服务器端的代理程序,由它通过 proc 文件请求 NFS 文件系统恢复模块进行恢复,恢复模块收到请求后,取出相关数据,然后通过调用底层 ext3 文件系统基本操作完成该次文件操作的”重放”,最后返回执行结果,通过 proc 文件通知代理程序,代理程序再通知管理端,管理端再发送下一条恢复请求,及到所有选中的操作全完成为止。具体实现模式请参看图 7:
图 7 恢复流程示意图
数据快速同步技术
在系统中,各文件服务器之间的数据需要及时同步更新,这样才能保证服务迁移后到新的环境后相关数据环境的一致性,从而保证服务迁移在语义上的正确性。在本方案中,每个文件服务器均采用 NFSv3 协议向外提供文件服务,当系统开始工作时,管理员会指定一台主服务器,由该服务器负责向外提供服务,其他文件服务器为备份服务器,接收同步数据,进行数据的同步更新,并不对外提供服务,只有当系统决定迁移后,选定的迁移目标对应的文件服务器才成为主文件服务器。
由于主文件服务器负责对外的文件服务,因此,数据同步的发起者应该是主文件服务器,而所有的备份服务器均为被动的同步数据接收者。因此,数据的快速同步包含两方面的工作:主文件服务器产生同步数据和备份文件服务器接收同步数据完成同步。具体的数据流向如图 8 所示:
图 8 同步数据的产生与流动示意图
为了达到数据快速同步的目的,我们采用了记录文件写操作(包括创建、修改、删除、改名、属性修改等所有的改变文件或目录属性、内容的操作)的具体参数的方法来生成同步数据,这样每次生成的数据量比较少,而且可以满足及时更新的目的。同步数据的格式及相关代码段如下:
- struct Log {
- int length; //整个数据包的长度
- int ops; //操作的类型
- char* data; //与操作相关数据
- };
- //下面代码段从内核将同步数据包发往其他文件服务器
- long send(struct socket* sock, void * buff,size_t len)
- {
- int err;
- mm_segment_t oldfs;
- struct msghdr msg;
- struct iovec iov;
- static int total = 0;
- down(&log_sem);
- iov.iov_base=buff;
- iov.iov_len=len;
- msg.msg_name=NULL;
- msg.msg_iov=&iov;
- msg.msg_iovlen=1;
- msg.msg_control=NULL;
- msg.msg_controllen=0;
- msg.msg_namelen=0;
- total += len;
- msg.msg_flags = MSG_SYN;//DONTWAIT;
- oldfs=get_fs();
- set_fs(KERNEL_DS);
- err = sock_sendmsg(sock, &msg, len);
- set_fs(oldfs);
- if(err<0){
- dprintk("send err(errNo=%d len = %d)\n",err,len);
- netbroken = 1;
- }
- ……
- up(&log_sem);
- return(err);
- }
同步数据产生后,先放入一个缓冲区中,而不是立即发送到备份文件服务器,这样可以较大程度改善系统的总体性能。缓冲区中的数据由同步管理进程管理,当达到一定数据量时,同步管理程序负责把缓冲区中的数据发送到备份文件服务器上,并根据返回的应答结果决定是否需要把重发数据,当确认某个服务器无法响应后,自动把同步数据定期写入一个仅可追加的文件,以便于随后可能需要的恢复阶段同步的需要,当这个写入的文件数据量超出一定限制时,并且系统确认已经至少有一个新的版本生成,可以把该文件清空。
当数据到达备份文件服务器时,由独立的接收进程负责把数据放入接收缓冲区,经核对数据无误后给主服务器发送确认信号,另一个独立进程即更新管理进程把接收缓冲区作为输入,从中解析出一个个的顺序的操作日志,从每个日志中得到操作类型,然后在剩余的数据中按照特定的操作类型提取所需的参数,利用文件系统调用完成相应操作。
总结
Linux 网络文件系统已经为企业在数据备份和共享领域得到了广泛应用。如何保证其多版本备份、实时恢复是一个非常关键的问题,本文将详细介绍针对该网络文件系统的数据备份、恢复及同步机制在内核的具体实现,给广大系统管理员和研发人员提供技术参考。