NAS 中备份可以使用rsync,鲁棒又可靠,结合 inotify 可以动态实时同步,本文记录相关方法。
基础知识 {#基础知识}
文件动态同步 {#文件动态同步}
根据 inotify 的相关知识,可以发现,很多动作都涉及了close事件,且大多数情况都是伴随着close_write事件的。所以,大多数情况下在定义监控事件时,其实并不真的需要监控open、modify、close事件。特别是close,只需监控它的分支事件close_write和close_nowrite即可。由于一般情况下inotify都是为了监控文件的增删改,不会监控它的访问,所以一般只需监控close_write即可。
- 建议对监控对象的close_write、moved_to、moved_from、delete和isdir(主要是create,isdir,但无法定义这两个事件的整体,所以仅监控isdir)事件定义对应的操作,因为它们互不重复。如有需要,可以将它们分开定义,再添加需要监控的其他事件。
示例脚本 {#示例脚本}
- 基础同步脚本,监控文件夹下的
delete,close_write,moved_to,moved_from,isdir
事件
触发事件后逐行进行同步。该脚本中已经尽量少地设置监控事件,使得它尽量少重复触发rsync。但需要明确的是,尽管设计的目标是尽量少触发事件,但应该以满足需求为前提来定义监控事件。如果不清楚如何选择监控事件,回看前文inotify命令以及事件分析。另外,可以考虑对文件、目录、子目录单独定义不同的脚本分别监控不同事件。
该脚本的不足之处主要在于重复触发rsync。该脚本中rsync同步的是目录而非单个文件,所以如果一次性操作了该目录中多个文件,将会产生多个事件,也因此会触发多次rsync命令,在前文中给出了一个拷贝/usr/share/man的示例,它调用了15000多次rsync,其实只需同步一次即可,剩余的上万次同步完全是多余的。
- 网络同步脚本
注意,该脚本是用来前台测试运行的,如果要后台运行,则将最后一段的if字句删掉。
该脚本记录了哪些被删除或从监控目录中移出的文件,且监控到事件后,触发的rsync操作是对整个监控目录$watch_dir进行同步,并且不对vim产生的临时文件进行同步。同时该脚本会产生多余的资源消耗。
每触发一次事件会同步所有数据,会造成巨大的资源消耗。
inotify 不足之处 {#inotify-不足之处}
虽然inotify已经整合到了内核中,在应用层面上也常拿来辅助rsync实现实时同步功能,但是inotify因其设计太过细致从而使得它配合rsync并不完美,所以需要尽可能地改进inotify+rsync脚本。另外,inotify存在bug。
inotify 的 bug {#inotify-的-bug}
当向监控目录下拷贝复杂层次目录(多层次目录中包含文件),或者向其中拷贝大量文件时,inotify经常会随机性地遗漏某些文件。这些遗漏掉的文件由于未被监控到,所有监控的后续操作都不会执行,例如不会被rsync同步。
实际上,上面描述的问题不是inotify的缺陷,而是inotify-tools包中inotifywait工具的缺陷。inotifywait的man文档中也给出了这个bug说明。
也就是说,那些直接发起inotify相关系统调用的上层工具(如sersync、lsyncd等)可能不会出现这个bug。
inotify+rsync的缺陷 {#inotify-rsync的缺陷}
- inotify 的一个常用的应用为触发文件同步,而由于inotify存在缺陷,导致这种组合使用的策略存在风险
由于inotify的bug,使用inotify+rsync时应该总是让rsync同步目录,而不是同步那些产生事件的单个文件,否则很可能会出现文件遗漏。另一方面,同步单个文件的性能非常差。
使用inotify+rsync时,考虑两方面问题:
- 由于inotify监控经常会对一个文件产生多个事件,且一次性操作同一个目录下多个文件也会产生多个事件,这使得inotify几乎总是多次触发rsync同步目录,由于rsync同步的是目录,所以多次触发rsync完全没必要,这会浪费资源和网络带宽;如果是分层次独立监控子目录,则会导致同步无法保证实时性
- vim编辑文件的过程中会产生.swp和.swx等临时文件,inotify也会监控这些临时文件,且临时文件会涉及多个事件,因此它们可能也会被rsync拷贝走,除非设置好排除临时文件,但无论如何,这些临时文件是不应该被同步的,极端情况下,同步vim的临时文件到服务器上可能是致命的。
由于这两个缺陷,使得通过脚本实现的inotify+rsync几乎很难达到完美,即使要达到不错的完美度,也不是件容易的事。
- 因此,为了让inotify+rsync即能保证同步性能,又能保证不同步临时文件,认真设计inotify+rsync的监控事件、循环以及rsync命令是很有必要的。
在设计inotify+rsync脚本过程中,有以下几个目标应该尽量纳入考虑或达到:
- 每个文件都尽量少地产生监控事件,但又不能遗漏事件。
- 让rsync同步目录,而不是同步产生事件的单个文件。
- 一次性操作同步目录下的多个文件会产生多个事件,导致多次触发rsync。如果能让这一批操作只触发一次rsync,则会大幅降低资源的消耗。
- rsync同步目录时,考虑好是否要排除某些文件,是否要加上"--delete"选项等。
- 为了性能,可以考虑对子目录、对不同事件单独设计inotify+rsync脚本。
inotify+rsync 的最佳实现 {#inotify-rsync-的最佳实现}
在上面已经提过 inotify + rsync 不足之处以及改进的目标。以下是通过修改shell脚本来改进inotify+rsync的示例。
-
为了让一次性对目录下多个文件的操作只触发一次rsync,通过while read line这种读取标准输入的循环方式是不可能实现的。
-
该方法是将inotifywait得到的事件记录到文件/etc/inotifywait.log中,然后在死循环中判断该文件,如果该文件不为空则调用一次rsync进行同步,同步完后立即清空inotifywait.log文件,防止重复调用rsync。
-
但需要考虑一种情况,inotifywait可能会不断地向inotifywait.log中写入数据,清空该文件可能会使得在rsync同步过程中被inotifywait监控到的文件被rsync遗漏,所以在清空该文件后应该再调用一次rsync进行同步,这也变相地实现了失败重传的错误处理功能。
-
如果没有监控到事件,inotifywait.log将是空文件,此时循环将睡眠1秒钟,所以该脚本并不是百分百的实时,但1秒钟的误差对于cpu消耗来说是很值得的。
另外,脚本中inotifywait命令中的后台符号"&"绝不能少,否则脚本将一直处于inotifywait命令阶段,不会进入到下一步的循环阶段。但需要注意,脚本中(子shell)的后台进程在脚本结束的时候不会随之停止,而是挂靠在pid=1的init/systemd进程下,这种情况下可以直接使用 killall script_file 的方式来停止脚本,这样脚本中的后台也会中断。
参考资料 {#参考资料}
文章链接:
https://www.zywvvd.com/notes/system/linux/commands/inotify/inotify-rsync/inotify-rsync/