Linux 结合 inotify 和 rsync 实现文件动态同步

本文最后更新于:2022年7月4日 上午

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 事件
1
2
3
4
5
6
7
8
9
10
11
# cat a.sh
#!/bin/bash
#
inotifywait -mrq -e delete,close_write,moved_to,moved_from,isdir /longshuai |\
while read line;do
if echo $line | grep -i delete &>/dev/null; then
echo "At `date +"%F %T"`: $line" >>/etc/delete.log
else
rsync -az $line --password-file=/etc/rsync_back.passwd rsync://rsync_backup@172.16.10.6::longshuai
fi
done

触发事件后逐行进行同步。该脚本中已经尽量少地设置监控事件,使得它尽量少重复触发rsync。但需要明确的是,尽管设计的目标是尽量少触发事件,但应该以满足需求为前提来定义监控事件。如果不清楚如何选择监控事件,回看前文inotify命令以及事件分析。另外,可以考虑对文件、目录、子目录单独定义不同的脚本分别监控不同事件。

该脚本的不足之处主要在于重复触发rsync。该脚本中rsync同步的是目录而非单个文件,所以如果一次性操作了该目录中多个文件,将会产生多个事件,也因此会触发多次rsync命令,在前文中给出了一个拷贝/usr/share/man的示例,它调用了15000多次rsync,其实只需同步一次即可,剩余的上万次同步完全是多余的。

  • 网络同步脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@xuexi www]# cat ~/inotify.sh
#!/bin/bash

watch_dir=/www
push_to=172.16.10.5
inotifywait -mrq -e delete,close_write,moved_to,moved_from,isdir --timefmt '%Y-%m-%d %H:%M:%S' --format '%w%f:%e:%T' $watch_dir \
--exclude=".*.swp" |\
while read line;do
# logging some files which has been deleted and moved out
if echo $line | grep -i -E "delete|moved_from" &>/dev/null;then
echo "$line" >> /etc/inotify_away.log
fi
# from here, start rsync's function
rsync -az --delete --exclude="*.swp" --exclude="*.swx" $watch_dir $push_to:/tmp
if [ $? -eq 0 ];then
echo "sent $watch_dir success"
else
echo "sent $watch_dir failed"
fi
done

注意,该脚本是用来前台测试运行的,如果要后台运行,则将最后一段的if字句删掉。

该脚本记录了哪些被删除或从监控目录中移出的文件,且监控到事件后,触发的rsync操作是对整个监控目录$watch_dir进行同步,并且不对vim产生的临时文件进行同步。同时该脚本会产生多余的资源消耗。

每触发一次事件会同步所有数据,会造成巨大的资源消耗。

inotify 不足之处

虽然inotify已经整合到了内核中,在应用层面上也常拿来辅助rsync实现实时同步功能,但是inotify因其设计太过细致从而使得它配合rsync并不完美,所以需要尽可能地改进inotify+rsync脚本。另外,inotify存在bug。

inotify 的 bug

当向监控目录下拷贝复杂层次目录(多层次目录中包含文件),或者向其中拷贝大量文件时,inotify经常会随机性地遗漏某些文件。这些遗漏掉的文件由于未被监控到,所有监控的后续操作都不会执行,例如不会被rsync同步。

实际上,上面描述的问题不是inotify的缺陷,而是inotify-tools包中inotifywait工具的缺陷。inotifywait的man文档中也给出了这个bug说明。

1
2
BUGS
There are race conditions in the recursive directory watching code which can cause events to be missed if they occur in a directory immediately after that directory is created. This is probably not fixable.

也就是说,那些直接发起inotify相关系统调用的上层工具(如sersync、lsyncd等)可能不会出现这个bug。

inotify+rsync的缺陷

  • inotify 的一个常用的应用为触发文件同步,而由于inotify存在缺陷,导致这种组合使用的策略存在风险

由于inotify的bug,使用inotify+rsync时应该总是让rsync同步目录,而不是同步那些产生事件的单个文件,否则很可能会出现文件遗漏。另一方面,同步单个文件的性能非常差。

使用inotify+rsync时,考虑两方面问题:

  1. 由于inotify监控经常会对一个文件产生多个事件,且一次性操作同一个目录下多个文件也会产生多个事件,这使得inotify几乎总是多次触发rsync同步目录,由于rsync同步的是目录,所以多次触发rsync完全没必要,这会浪费资源和网络带宽;如果是分层次独立监控子目录,则会导致同步无法保证实时性
  2. vim编辑文件的过程中会产生.swp和.swx等临时文件,inotify也会监控这些临时文件,且临时文件会涉及多个事件,因此它们可能也会被rsync拷贝走,除非设置好排除临时文件,但无论如何,这些临时文件是不应该被同步的,极端情况下,同步vim的临时文件到服务器上可能是致命的。

由于这两个缺陷,使得通过脚本实现的inotify+rsync几乎很难达到完美,即使要达到不错的完美度,也不是件容易的事。

  • 因此,为了让inotify+rsync即能保证同步性能,又能保证不同步临时文件,认真设计inotify+rsync的监控事件、循环以及rsync命令是很有必要的。

在设计inotify+rsync脚本过程中,有以下几个目标应该尽量纳入考虑或达到:

  1. 每个文件都尽量少地产生监控事件,但又不能遗漏事件。
  2. 让rsync同步目录,而不是同步产生事件的单个文件。
  3. 一次性操作同步目录下的多个文件会产生多个事件,导致多次触发rsync。如果能让这一批操作只触发一次rsync,则会大幅降低资源的消耗。
  4. rsync同步目录时,考虑好是否要排除某些文件,是否要加上"–delete"选项等。
  5. 为了性能,可以考虑对子目录、对不同事件单独设计inotify+rsync脚本。

inotify+rsync 的最佳实现

在上面已经提过 inotify + rsync 不足之处以及改进的目标。以下是通过修改shell脚本来改进inotify+rsync的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[root@xuexi tmp]# cat ~/inotify.sh
#!/bin/bash

watch_dir=/www
push_to=172.16.10.5

# First to do is initial sync
rsync -az --delete --exclude="*.swp" --exclude="*.swx" $watch_dir $push_to:/tmp

inotifywait -mrq -e delete,close_write,moved_to,moved_from,isdir --timefmt '%Y-%m-%d %H:%M:%S' --format '%w%f:%e:%T' $watch_dir \
--exclude=".*.swp" >>/etc/inotifywait.log &

while true;do
if [ -s "/etc/inotifywait.log" ];then
grep -i -E "delete|moved_from" /etc/inotifywait.log >> /etc/inotify_away.log
rsync -az --delete --exclude="*.swp" --exclude="*.swx" $watch_dir $push_to:/tmp
if [ $? -ne 0 ];then
echo "$watch_dir sync to $push_to failed at `date +"%F %T"`,please check it by manual" |\
mail -s "inotify+Rsync error has occurred" root@localhost
fi
cat /dev/null > /etc/inotifywait.log
rsync -az --delete --exclude="*.swp" --exclude="*.swx" $watch_dir $push_to:/tmp
else
sleep 1
fi
done
  • 为了让一次性对目录下多个文件的操作只触发一次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 的方式来停止脚本,这样脚本中的后台也会中断。

参考资料


Linux 结合 inotify 和 rsync 实现文件动态同步
https://www.zywvvd.com/notes/system/linux/commands/inotify/inotify-rsync/inotify-rsync/
作者
Yiwei Zhang
发布于
2021年12月25日
许可协议