1 Rsync

1.1 介绍

rsync是一个为Linux/Unix系统做局部拷贝, 文件同步, 远程文件拷贝, 远程文件同步的命令, 从而实现数据备份或者镜像.
有人说, rsync实际上就是一个高级版的scp命令, 实际上在一定范围内, 这种说法是对的, 那么让我们提出疑问?

  • rsync能够解决何种问题?
  • rsync相比scp命令, 有什么优势? rsync是否不仅仅是scp命令那么简单?
  • rsync的局限性在哪里?

软件开发过程中任何人都不能跳过需求分析, 一款应用必定是在继承传统功能的基础上解决新的问题, 那么让我们先看看scp命令的缺点:

  • 占用大量带宽, 当然, 你可以先进行压缩再进行文件传输
  • 全量拷贝, 速度慢, 占用带宽多, 无法增量备份

rsync能够解决上述提出的问题, 当然, 按照官网的介绍, rsync的主打功能就是增量备份:
provides fast incremental file transfer., 通过特殊的算法实现差异同步上传, 常常用于实时性要求不高的分布式集群文件一致性问题, 分布式日志集中化问题, 文件镜像等.

相比scp命令, rsync提供了更加丰富的功能, 文件一致性同步在很多实时要求不高, 特别是中小型应用上能够非常好的解决问题,例如我们项目组就使用 rsync + inotify 来实现分布式爬虫中日志, 错误 HTML, 最终 PDF 文件等同步问题, 再基于以上基础在数据
中心服务器上搭建 FTP 服务就能完美的将某一个提交引擎任务产生的所有错误文件, 最终结果文件直接呈现给开发者/运营人员.
简单框架如下:
rsync+inotify

这是scp功能远远达不到的需求, 另外因为很多服务器都放在香港机房, 公司也没有购买专用的云 VPN 通道, 网络环境非常不好, 在准备使用logstash + ElasticSearch + Kibana之前没有很好的日志集中化工具, 就算搭建了这个环境, 但是因为 GFW 或者阿里云自身的原因, 可能这个框架并不能非常稳妥的运行, 除非将ELK完全搭建在香港环境, 这明显是不合逻辑的.

这里提到 GFW, 就不得不提醒一下, 不知道 GFW 是怎样的一种防御逻辑, 导致网络非常的不稳健, 经常无法从香港环境直接pull代码, 导致CI(Continue Integrate)根本无法进行, 最终只能通过fabric + scp来完成自动化部署任务. 另外, 偶尔会发生香港环境调用国内的HTTPS/HTTP 请求, 然后再 TCP/IP 层面被丢包弃置, 没有任何响应, 导致调用方一直保持 TCP 连接在Estashlib状态, 从而导致整个python进程卡主..

好了, 回到正题, 上面已经介绍了rsync的优点以及应用场景, 那么局限性呢?

  • 无法在大型应用上使用, 例如日志集中化和分析, 最好还是使用成熟的框架, 例如 ELK
  • 无法在实时性要求非常高的系统上使用
  • 无法在存在大量文件(百万级)的基础上做增量, 其每次都要全盘扫描整个目录, 非常耗费资源
  • 每次生成传输文件列表都需要做大量计算和校验, 这对 CPU 是一个很高的消耗

让我们举一个例子, 现在存在一个目录:Dir, 其下面有两个子目录subdir1, subdir2, 一个文件Readme.md, 其中每一个子目录都包含百万级数量文件, 如果此时更改了Readme.md文件, 那么就会发生: 全盘扫描整个Dir目录, 判断所有发生更改的文件, 这是非常耗费 CPU. 虽然通过一定的脚本, 配置inotify, 能够在底层目录文件发生变化的时候仅仅扫描发生变化的文件所在的目录, 这样减少了扫描的数量, 但是你无法避免某一个时刻Readme.md文件发生变动, 具体实现见 2.3 节的最小同步.

1.2 安装和配置

1
2
3
4
5
# 1 centos
yum install rsync -y
# 2 在服务器上启动rsyncd服务
systemctl restart rsyncd
systemctl status rsyncd

在启动rsyncd服务之前, 让我们先了解一下/etc/rsyncd.conf中各个选项的含义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
uid = root
gid = root
use chroot = no
max connections = 2
timeout = 600
pid file = /var/run/rsyncd.pid
lock file = /var/run/rsyncd.lock
log format = %t %a %m %f %b
transfer logging = yes
log file = /data/XXX/rsyncd.log

[appserver-02]
path = /data/XXX/appserver-02/
read only = no
list = no
hosts allow = 172.39.2.0/255.255.255.0
auth users = authusername
secrets file = /etc/rsyncd_server.password

其中各个字段说明(参考官网):

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
27
28
29
uid = 守护进程运行时用户全局配置
gid = 用户所属组
use chroot = 提高安全性, 运行时会chroot到path指定目录下, 无法备份path以外文件, 提高安全性, 比如root
max connections = 客户端最大连接数
timeout = 客户端连接超时时间

read only = 是否只读
write only = 是否只写
hosts allow = 客户端白名单
hosts deny = 客户端黑名单

port = 守护进程监听端口, xinetd端口转发时忽略
address = 守护进程监听IP, xinetd端口转发时忽略
pid file = 守护进程文件

lock file = 多进程日志写入所文件
log format = 日志输出的格式
transfer logging = 是否记录传输文件日志
log file = 日志文件

[模块名称, 方便客户端访问, 无需过多的额外配置]
path = 模块根目录, 即客户端待同步文件所存放的当前服务器目录
read only = no
list = 是否允许列出模块中的内容
hosts allow = 172.39.2.0/255.255.255.0
auth users = 模块验证用户名
secrets file = 模块验证用户名对应的密码存放文件
comment = 注释
exclude = 排除指定的目录, 这个可以在客户端或者服务器配置

注意, 关于权限问题有几天需要注意:

  • 确保服务器这边的 UID/GID 有权限对模块中的path进行写入/读取操作
  • 确保客户端这边执行rsync的用户有权限读取信息
  • 确保客户端能够通过 UID 指定的用户连接服务器, 不然会出现错误
  • 确保所有的密码文件权限都是:600, 不管是客户端还是服务器, 这个很重要

密码文件的格式有两种: 服务器和客户端, 其中服务器的密码格式如下:

1
2
username:password
username2:password2

其中username必须同auth users保持一致, 这个值与客户端的认证用户名保持一致, password会客户端的认证密码. 客户端密码格式如下:

1
password

这个文件在客户端中指定.

1.3 客户端

在已经启动服务器进行监听的前提下, 任何一个在/etc/rsync.conf中指定的模块认证通过的机器都能通过rsync命令来和服务器进行文件
的同步工作: 文件的推送, 文件的拉取. 当然, 正如前文提到的, 客户端所指定的密码文件权限页必须为600, 同时认证用户名和密码必
须同服务器配置文件中的某一个模块配置保持一致.
在完成基本的客户端配置之后, 此时rsync就立刻变成一个升级版的(scp, cp, tar), 具有高安全性, 备份迅速, 增量备份, 但是注意此时同步
文件仍然需要人为手动触发, 这不是一个事件触发机制, 而是一个主动发起的同步机制, 你可以使用crontab来完成一定时间间隔的文件
增量备份操作. 好了, 很多人肯定想到问题了:

  • 手动触发或者通过crontab触发如何控制时间间隔?如何做到实时性?如果调用太快, 但是大部分时候没有增量数据时是否浪费?
  • 对于一个庞大的文件系统, 根据rsync的原理, 在目录越顶层发生变动时, 牵涉到整个庞大社会的文件同步操作, 浪费性能

这时候就有了inotify + rsync配合机制, 当然这种机制实际上也解决不了顶层目录的文件变动导致的庞大数据备份操作, 当时相比而言性能
总归有一定的提升, 这个见下一章的说明. 现在, 让我们先了解一下rsync的同步命令以及常见的参数说明.
rsync总共有 6 中工作方式, 以实现和服务器的增量同步操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1 拷贝本地文件, 就相当于cp命令的优化版
rsync [OPTION]... SRC DEST

# 2 使用一个shell远程程序, 例如shell实现简单的文件拷贝, 将本地文件拷贝到远程服务器上
rsync [OPTION]... SRC [USER@]host:DEST

# 3 使用一个shell远程程序, 将远程服务器文件拷贝到本地
rsync [OPTION]... [USER@]HOST:SRC DEST

# 4 从远程rsync服务器上同步文件到本地, 即常见的PULL方式
rsync [OPTION]... [USER@]HOST::SRC DEST

# 5 将本地文件同步到远程rsync服务器, 即常见的PUSH方式, 日志集中化常常使用这种方式, 一般由客户端发起同步请求
rsync [OPTION]... SRC [USER@]HOST::DEST

# 6 通过URL 来指定服务器信息
rsync [OPTION]... rsync://[USER@]HOST[:PORT]/SRC [DEST]

例子说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# a 本地主机: 压缩文件
rsync -zvh backup.tar /tmp/backups/

# b 本地主机: 复制src1到/tmp/backups/src1
rsync -avzh /root/src1 /tmp/backups/

# c 拷贝本地文件到远程服务器
rsync -avz /root/src1 root@192.168.10.2:/tmp/backups/

# d 利用ssh将远程服务器上的文件拷贝到本地
rsync -avzhe ssh root@192.168.10.2:/tmp/backups/test.log /tmp/

# e 通过rsync, 将本地文件同步到远程rsync服务器上, 注意MODEL在rsync中已经提前配置好
rsync -avzcR --ignore-errors --exclude-from=忽略文件列表 --password-file=秘钥文件 ${目录1} ${秘钥对应用户}@${HOST}::${MODEL}

其他参数说明, 这里仅仅列出部分参数说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-p --progress           显示传输进度
--include 白名单同步文件, 正则, 例如:'locks/|.*/broker.log'
--include-from 白名单同步文件列表, 基于rsync监听主目录chroot开始的绝对路径, 不能包括正则表达式
--exclude 黑名单
--exclude-from 黑名单文件列表

--max-size 最大文件大小, 超过则忽略
--remove-source-files 传输成功后自动删除源文件

-v 显示rsync过程中详细信息
-R --relative 使用相对路径, 即将命令行上的文件全路径而不是文件名发给服务器
-a --archive 归档模式, 等同于: ``rtopgDl``
-r --recursive 递归到目录中去
-t --times 保持文件mtime属性
-z 传输时进行压缩以提高速率

-n 只测试不传输, 用于调试, 非常有用, 特别是测试规则是否生效
-i 输出要传输文件的路径信息

1.4 规则

对于规则我们肯定有很多疑问: 规则是如何生效的? 它的工作原理是怎样的? 规则的配置是怎么样的? 在了解这些问题之前让我们先了解一下
rsync命令生效的那一刻所做的工作:

  1. rsync开始扫描给定的文件或目录, 排序之后生成一个拷贝树(copy tree)
  2. rsync在扫描完成之后会将生效或待传输的文件或目录记录到文件列表中
  3. rsync开始将文件传输给接收端

其中规则的处理或过滤就在copy tree生成的那个阶段, 规则在整个流程的最前方就开始生效, 通过exclude规则, include规则以及其他
这里未介绍的规则, 生成一个显式的文件列表. 那么如何配置一个exclude规则, include规则呢? 在配置规则的时候我们应该怎么进行
调试呢?

  1. 使用-i, -r, -n来进行调试, 获取生效的文件列表, 从输出中获取绝对路径/相对路径
  2. 绝对路径和相对路径决定了规则文件编写时的路径信息, 这个在后面的例子出说明
  3. 使用正则基本语法来进行匹配操作,例子见下面

通过-nr -i来调试获取绝对/相对路径信息, 下面这段说明和测试是参考他人的例子:

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
27
28
29
30
# 1 显示了传输文件的路径"a/*",也就是说包括了目录a,且是相对路径的.所以要写规则时,需要加上这个a路径
root:~$ rsync -nr -i a b/
cd+++++++++ a/
>f+++++++++ a/1.txt
>f+++++++++ a/2.txt
>f+++++++++ a/3.txt
>f+++++++++ a/4.txt

# 2 例子
root:~$ rsync -nr -i --exclude="a/2.txt" a b/
cd+++++++++ a/
>f+++++++++ a/1.txt
>f+++++++++ a/3.txt
>f+++++++++ a/4.txt

root:~$ rsync -nr -i --exclude="a/*.txt" a b/
cd+++++++++ a/

# 3 如果对a目录, 加上下划线, 输出就会发生变化
root:~$ rsync -nr -i a/ b/
>f+++++++++ 1.txt
>f+++++++++ 2.txt
>f+++++++++ 3.txt
>f+++++++++ 4.txt

# 4 此时排除规则中就不应该存在前缀目录信息
root:~$ rsync -nr -i --exclude="2.txt" ./a/ b/
>f+++++++++ 1.txt
>f+++++++++ 3.txt
>f+++++++++ 4.txt

讲解完如何确定相对路径和绝对路径信息之后了解一些复杂的正则例子:

1
2
3
4
5
6
需求: 同步/tmp/src/文件和目录, 排除/tmp/src/mail/2014/, /tmp/src/mail/2015/cache/目录
正则: '(.*/*\.log|.*/*\.swp)$|^/tmp/src/mail/(2014|201.*/cache.*)'

说明:
(.*/*\.log|.*/*\.swp)$ 表示排除/tmp/src目录下面所有.log或.swp结尾的文件
^/tmp/src/mail/(2014|201.*/cache.*) 表示排除/tmp/src/mail/2014/目录, 以及所有/tmp/src/mail/下所有 201* 下待cache目录文件

另外, 相比--exclude正则配置, --exclude-from指定的文件更加直观, 繁琐, 该文件详细指定了黑名单, 会将所有待忽略的文件一个个
的列出来.

2 Inotify

2.1 介绍

在上面 1.1 节的介绍中我们提到rsync的缺点以及inotify + rsync的配合使用, 那么:

  • inotify是什么?
  • inotify为何能够解决rsync问题?
  • inotify和rsync是如何配合使用的?
  • 是否有相关的例子说明呢?

inotify是一个细粒度(到单一文件级别), 异步的文件系统事件监控机制, 任何文件的变动都会触发inotify的事件通知. 利用事件通知
机制, 让inotify可以做到实时的文件变动通知, 并以细粒度方式获取变动的文件, 以最小权限影响范围告知rsync进行极小范围的文件
变动, 实现实时性的文件增量同步.

那么事件是什么? 做过linux开发的同学可能比较了解事件监听机制, 里面包括很多事件通知类型, 比如文件的修改, 文件的删除,
文件的新增等等, 目前inotify的事件类型如下:

  • IN_ACCESS: 文件被访问.
  • IN_MODIFY: 文件被write.
  • IN_ATTRIB: 文件属性被修改,如chmod,chown等.
  • IN_CLOSE_WRITE: 可写文件被close.
  • IN_CLOSE_NOWRITE: 不可写文件被close.
  • IN_OPEN: 文件被open.
  • IN_MOVED_FROM: 文件被移出被监控目录,如mv.
  • IN_MOVED_TO: 文件被移入被监控目录,如mv,cp.
  • IN_CREATE: 文件/文件夹被创建.
  • IN_DELETE: 文件/文件夹被删除,如rm.
  • IN_DELETE_SELF: 自删除,即一个可执行文件在执行时删除自己.
  • IN_MOVE_SELF: 自移动,即一个可执行文件在执行时移动自己.
  • IN_UNMOUNT: 宿主文件系统被umount.
  • IN_CLOSE: 文件被关闭,等同于(IN_CLOSE_WRITE|IN_CLOSE_NOWRITE).
  • IN_MOVE: 文件被移动,等同于(IN_MOVED_FROM|IN_MOVED_TO).

利用上述的事件通知机制以及一定的判断逻辑, inotify + rsync能在一定范围内做到较低功耗的实时同步策略, 具体例子见2.3 节介绍.

2.2 安装和参数

1
yum install inotify-tools

inotify的参数相比rsync稍微简单一点, 下面讲解一下日常经常使用的 inotify 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-m              表示始终保持事件监听状态
-r 表示递归查询目录
-q 表示打印出监控事件
-e 指定监控事件, 以逗号分隔的监听事件列表, 其中影响目标包含文件和目录
modify 更改
create 新增
delete 删除
move 移动和重命名
access 访问/读取
attrib 文件属性改动
open 文件被打开
close 文件被关闭

--format 输出格式化
%Xe 事件以X 分隔
%w 发生时间的目录
%f 发生时间的具体文件
%T 时间格式

--timefmt 指定日期的输出格式, 用于设置--format中的%T 输出
--exclude 类似rsync的黑名单正则

2.3 最小同步

让我们考虑如下的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
Dir
├── Readme.md
├── subdir1
│   ├── test1
│   │   └── a.log
│   ├── test2
│   │   └── big.log
│   ├── test2
│   └── testN
└── subdir2

5 directories, 2 files

根据上面的说明, 通过inotify来监听变动文件, 最后使用rsync来进行文件的增量同步, 但是对于庞大的文件系统, 比如假设test2
目录下面有非常多的文件, 并且每个文件都非常大, 那么如果a.log文件发生变动, 那么会发生什么呢? 网络上的很多例子会直接进行整个
Dir目录的同步操作, 这会触发所有文件的扫描以便决定是否进行增量备份, 这是非常耗性能的, 那么我们应该如何避免此类情况呢?

  • 根据最小权限原则, 将影响的文件范围尽可能的控制到最小, 即仅仅同步test1目录
  • 利用rsync指定同步目录

根据上面的思路, 我们通过inotify获取发生变动的文件INO_FILE, 获取该文件的上层目录, 然后仅仅开始同步该目录, 这样就能以
最小的功耗上线我们的目的, 整个例子如下:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 根据不同的事件来决定不同的同步策略
listen()
{
cd ${CLIENT_DIR} # rsync同步的特性, 需先进入源目录

inotifywait -mrq --exclude ${INOTIFY_EXCLUDE} --format '%Xe %w%f' \
-e modify,create,delete,attrib,close_write,move ./ | while read filename
do
INO_EVENT=$(echo $filename | awk '{print $1}') # 把inotify输出切割 把事件类型部分赋值给INO_EVENT
INO_FILE=$(echo $filename | awk '{print $2}') # 把inotify输出切割 把文件路径部分赋值给INO_FILE
echo ${INO_FILE} '-----' $(dirname ${INO_FILE})
echo "-------------------------------$(date)------------------------------------"

# 事件: 增加, 修改, 写入完成, 移动到
if [[ $INO_EVENT =~ 'CREATE' || $INO_EVENT =~ 'MODIFY' || $INO_EVENT =~ 'CLOSE_WRITE' || $INO_EVENT =~ 'MOVED_TO' ]];then
echo 'CREATE or MODIFY or CLOSE_WRITE or MOVED_TO:' ${filename}
# $(dirname ${INO_FILE}): 每次只针对性的同步发生改变的文件所在的目录
# -R: 递归保证目录结构一致性
rsync -avzcR --ignore-errors --exclude-from=${RSYNC_LISTEN_EXCLUDE} --password-file=${SERVER_PASSWD} \
$(dirname ${INO_FILE}) ${SERVER_USER}@${SERVER_HOST}::${SERVER_MODEL}
fi

# 事件: 删除,移动出
if [[ $INO_EVENT =~ 'DELETE' ]] || [[ $INO_EVENT =~ 'MOVED_FROM' ]]; then
echo 'DELETE or MOVED_FROM'
rsync -avzR --ignore-errors --delete --exclude-from=${RSYNC_LISTEN_EXCLUDE} --password-file=${SERVER_PASSWD} \
$(dirname ${INO_FILE}) ${SERVER_USER}@${SERVER_HOST}::${SERVER_MODEL}
fi

# 修改属性事件: touch, chgrp, chmod, chown等操作
if [[ $INO_EVENT =~ 'ATTRIB' ]]; then
echo 'ATTRIB'
if [ ! -d "$INO_FILE" ]; then
# 如果是目录 则不同步, 后续再说, 没必要浪费
rsync -avzcR --ignore-errors --exclude-from=${RSYNC_LISTEN_EXCLUDE} --password-file=${SERVER_PASSWD} \
$(dirname ${INO_FILE}) ${SERVER_USER}@${SERVER_HOST}::${SERVER_MODEL}
fi
fi
done
}

那么上面的解决方式是否有其他隐患呢? 一般来说, 只要上面的应用每次都正确的运行, 那么就能实现整个庞大文件系统的增量备份, 但是万一
某一次小的同步出现问题呢, 然后此次同步相关的文件在一个非常非常小的角落中, 然后后续的所有同步都没有将该文件包含进去, 那么就会
出现如下的奇妙现象: 这个小文件一直没有同步成功, 虽然这个几率非常小. 为了解决这个问题, 我们仍然需要一个crontab来定时
的对整个庞大文件系统进行全局扫描和增量同步, 实现方式如下:

1
2
3
4
5
6
7
rsync_all()
{
# 同步整个visa目录, 一般晚上执行一次
cd ${CLIENT_DIR} # rsync同步的特性, 需先进入源目录
rsync -avzcR --ignore-errors --exclude-from=${RSYNC_EXCLUDE} --password-file=${SERVER_PASSWD} \
./ ${SERVER_USER}@${SERVER_HOST}::${SERVER_MODEL}
}

最后编写crontab来定时的调用rsync_all, 使用nohup来启动一个后台进程, 调用listen以实时监控待同步的文件目录, 最终
完成一个较为稳妥的零散文件, 日志集中化, 实时增量同步框架.

3 配置实例

3.1 规则

a) 权限错误
原因: 确保Client和Server都是同样一个用户权限, 例如Client使用root来启动脚本, Server使用root来启动rsyncd服务,
否则会报权限错误

具体例子:

1
2
3
rsync: chgrp "test1" (in cn-app-server2) failed: Operation not permitted (1)

rsync: recv_generator: mkdir "test1" (in cn-app-server2) failed: Permission denied (13)

参考:
rsync官网
rsync算法
rsync使用
rsync-fast
rsync介绍