欢迎光临
我们一直在努力

🚀 Rsync双主/多主服务器文件实时同步(增删改)完整实现教程

——基于 rsync + inotify 的轻量生产级方案。

本文记录了我在搭建双主文件同步系统时踩过的坑、最终的解决方案、完整脚本和部署步骤。
适用于两台或以上服务器互为主节点、任意一台都可能新增/修改/删除文件的场景。

在实际生产环境中,我们经常需要让两台或多台服务器保持文件实时同步,尤其是 Web 静态资源、用户上传文件、配置文件备份等。根据微客的踩坑经历最终总结了一套 真正可用于生产环境的Rsync多主文件同步方案,方案支持:

  • ✔ 新增文件自动同步
  • ✔ 修改文件自动同步
  • ✔ 删除文件安全同步(不会复活)
  • ✔ 移动文件到目录外也能正确识别为删除
  • ✔ 双主模式(任意一台都可以修改)
  • ✔ 不依赖 NFS / GlusterFS / Unison
  • ✔ 仅使用 rsync + inotify-tools

并且包含完整脚本、部署步骤、踩坑记录。

1.背景与需求

我有两台服务器(如果你是多台同理):

  • A:1.xxx.xxx.xxx
  • B:2.xxx.xxx.xxx

两台服务器都运行了 Zdir 程序,上传的文件存放在:

/opt/zdir/data/public

需求:

  • 任意一台新增文件 → 另一台自动同步
  • 任意一台修改文件 → 另一台自动同步
  • 任意一台删除文件 → 另一台也要删除
  • 删除必须安全(不能误删、不能复活)
  • 移动文件到目录外也要视为删除
  • 基于 rsync + inotify 轻量级实现
  • 不使用 NFS、GlusterFS、Unison 等重量级方案

2.遇到的问题(踩坑记录)

❌ 删除无法同步

rsync 不会把“缺失文件”当成删除事件。

❌ 删除期间对端 PUSH 导致文件复活

A 删除 → B 不知道 → B PUSH → A 文件复活。

❌ 修改文件无法判断谁是最新

双向 PUSH 会互相覆盖,谁先跑脚本谁赢。

.delete_log.delete_lock 推送失败

因为使用了 --relative + 绝对路径,rsync 写入失败但被静默忽略。

移动文件到目录外不会触发 delete 事件(最隐蔽的坑)

例如:

mv /opt/zdir/data/public/a.txt /root/

inotify 不会触发 delete,而是触发 moved_from → delete_log 不记录 → B 不会删除 → 下一轮同步 B 会把旧文件推回 A → 文件复活

3.最终设计方案(核心思想)

✔ 新增 / 修改:只 PULL,不 PUSH

rsync PULL 会自动根据 mtime 判断谁是最新版本,避免冲突。

✔ 删除:使用 delete_log + delete_lock + trash

  • inotify 捕获删除事件
  • 写入 .delete_log
  • 写入 .delete_lock
  • 立即推送到对端
  • 同步脚本根据日志执行删除
  • 删除期间禁止 PULL / PUSH

✔ 移动事件处理:监听 moved_from

当文件被移出同步目录时,视为删除。

✔ 删除移动到 .trash,而不是直接 rm

防止误删,可回滚。

4.目录结构

/opt/zdir/data/public/
├── 文件……
├── .delete_log
├── .delete_lock
└── .trash/

5. 安装 rsync

两台服务器都需要安装 rsync。

Debian / Ubuntu

apt update
apt install rsync -y

CentOS / Rocky / AlmaLinux

yum install rsync -y

验证安装

rsync --version

6. 配置 rsync 服务端(rsyncd)

两台服务器都要配置。

编辑配置文件:

vim /etc/rsyncd.conf

写入:

uid = root
gid = root
port = 873
reverse lookup = no
use chroot = no
read only = no
dont compress = *.jpg *.jpeg *.png *.gif *.webp *.avif *.mp4 *.avi *.mov *.mkv *.mp3 *.wav *.aac *.flac *.zip *.rar *.7z *.gz *.tgz *.tar *.pdf *.epub *.iso *.exe *.apk *.dmg *.rpm *.deb *.msi
hosts allow = 127.0.0.1/32 ::1/128
# hosts deny =
max connections = 100
timeout = 1800
lock file = /var/run/rsync.lock
pid file = /var/run/rsyncd.pid
log file = /var/log/rsyncd.log

# zdirpublic-START
[zdirpublic]
path = /opt/zdir/data/public
comment = public sync dir
read only = no
auth users = zdirpublic
hosts allow = *
secrets file = /etc/rsyncd.secrets
# zdirpublic-END

创建密码文件:

echo "zdirpublic:你的密码" > /etc/rsyncd.secrets
chmod 600 /etc/rsyncd.secrets

启动:

systemctl enable rsync
systemctl restart rsync

测试:

rsync zdirpublic@对端IP::zdirpublic

7.监控脚本(watch_delete.sh)

监听删除事件 → 写日志 → 写锁 → 推送到对端。

新增:监听 moved_from 事件,解决“移动到目录外不算删除”的问题

#!/bin/bash
set -euo pipefail

export RSYNC_PASSWORD="你的密码"

# 监控目录配置
WATCH_DIR="/opt/zdir/data/public"
DELETE_LOG="$WATCH_DIR/.delete_log"
DELETE_LOCK="$WATCH_DIR/.delete_lock"
TRASH="$WATCH_DIR/.trash"

# 对端 rsync 目标(分号分隔,多端时填写多个)
RSYNC_TARGETS="zdirpublic@1.xxx.xxx.xxx::zdirpublic;zdirpublic@2.xxx.xxx.xxx::zdirpublic"

mkdir -p "$TRASH"

# 将锁文件推送到对端(尽量让对端尽快看到锁)
push_lock_to_peers() {
    IFS=';' read -ra TARGETS <<< "$RSYNC_TARGETS"
    for t in "${TARGETS[@]}"; do
        # 只同步锁文件本身,静默失败,避免阻塞监控
        rsync -az "$DELETE_LOCK" "$DELETE_LOG" "$t/" >/dev/null 2>&1 || true
    done
}

# 递归监听删除事件(delete / delete_self / moved_from)
inotifywait -mr -e delete -e delete_self -e moved_from --format '%w%f' "$WATCH_DIR" | while read -r FILE; do
    BASENAME=$(basename "$FILE")

    # 忽略 delete_log 及其临时文件
    [[ "$BASENAME" == .delete_log* ]] && continue

    # 忽略 delete_lock 自身
    [[ "$BASENAME" == .delete_lock* ]] && continue

    # 忽略 .trash 自己
    [[ "$FILE" == "$TRASH"* ]] && continue

    # 忽略临时文件 .xxx.txt.xxxxxx
    if [[ "$BASENAME" =~ ^\..*\.[A-Za-z0-9]{5,}$ ]]; then
        continue
    fi

    # 忽略目录(只记录文件删除)
    if [ -d "$FILE" ]; then
        continue
    fi

    # 追加一条 pending 记录:时间|路径|pending
    echo "$(date '+%Y-%m-%d %H:%M:%S')|$FILE|pending" >> "$DELETE_LOG"

    # 写入锁(带一点标识信息),并推送到对端
    echo "$(hostname)-$$-$(date +%s)" > "$DELETE_LOCK"
    push_lock_to_peers
done

8.同步脚本(sync_all.sh)

无锁 → PULL(增/改)
有锁 → 不 PULL、不 PUSH,只处理删除

#!/bin/bash
set -euo pipefail

export RSYNC_PASSWORD="你的密码"

DEST="/opt/zdir/data/public"
DELETE_LOG="$DEST/.delete_log"
DELETE_LOCK="$DEST/.delete_lock"
TRASH="$DEST/.trash"

mkdir -p "$TRASH"

# 注意:不要排除 .delete_lock(锁要同步)
EXCLUDES="--exclude 'mnt/' --exclude 'tmp/' --exclude 'cache/' --exclude '.trash/' --exclude '*.log' --exclude '*.tmp' --exclude 'README.md'"

# 另一台服务器的 rsync 源
SRC1="zdirpublic@1.xxx.xxx.xxx::zdirpublic"
SRC2="zdirpublic@2.xxx.xxx.xxx::zdirpublic"

# 0. 判断本地是否有锁
LOCK_PRESENT=false
if [ -f "$DELETE_LOCK" ]; then
    LOCK_PRESENT=true
fi

# 1. 如果本地没有锁,说明当前没有删除在路上,可以先 PULL
if [ "$LOCK_PRESENT" = false ]; then
    rsync -avz --progress $EXCLUDES "$SRC1/" "$DEST"
    rsync -avz --progress $EXCLUDES "$SRC2/" "$DEST"
fi

# 2. 处理 delete_log:pending -> 删除 -> done(幂等)
TMP_LOG="$DELETE_LOG.tmp"
: > "$TMP_LOG"

if [ -f "$DELETE_LOG" ]; then
    while IFS='|' read -r TIME FILE STATUS; do
        # 跳过空行
        [[ -z "$FILE" ]] && continue

        # 忽略 delete_log 自身和锁本身
        [[ "$FILE" == "$DELETE_LOG" ]] && continue
        [[ "$FILE" == "$DELETE_LOCK" ]] && continue

        # 已经是 done 的记录,原样保留
        if [[ "$STATUS" == "done" ]]; then
            echo "$TIME|$FILE|done" >> "$TMP_LOG"
            continue
        fi

        # pending 的记录,尝试删除
        if [ -f "$FILE" ]; then
            # 移动到回收站
            if mv "$FILE" "$TRASH/$(basename "$FILE").$(date +%s)"; then
                # 再确认一下文件确实不存在了
                if [ ! -f "$FILE" ]; then
                    echo "$TIME|$FILE|done" >> "$TMP_LOG"
                else
                    # 理论上不会到这,保险起见保留 pending
                    echo "$TIME|$FILE|pending" >> "$TMP_LOG"
                fi
            else
                # mv 失败,保留 pending,下次再试
                echo "$TIME|$FILE|pending" >> "$TMP_LOG"
            fi
        elif [ -d "$FILE" ]; then
            # 如果是目录,尝试删空目录,不纠结,直接标记 done 避免卡死
            rmdir "$FILE" 2>/dev/null || true
            echo "$TIME|$FILE|done" >> "$TMP_LOG"
        else
            # 文件本来就不存在了,直接标记 done
            echo "$TIME|$FILE|done" >> "$TMP_LOG"
        fi
    done < "$DELETE_LOG"

    mv "$TMP_LOG" "$DELETE_LOG"
fi

# 3. 如果日志里已经没有 pending,说明这轮删除处理完了,可以删锁
if [ -f "$DELETE_LOG" ]; then
    if ! grep -q '|pending' "$DELETE_LOG"; then
        rm -f "$DELETE_LOCK"
    fi
else
    rm -f "$DELETE_LOCK"
fi

# 4. 不再做任何 PUSH!
#    - 新增文件:通过对端的 PULL 自动拉过来
#    - 修改文件:通过 PULL + rsync 按 mtime 选择最新版本
#    - 删除文件:通过 delete_log + inotify 推送日志来驱动

9.为监控脚本创建 systemd 服务

为了让监控脚本 随系统自动启动、崩溃自动重启、日志可控,我们需要为它创建一个 systemd 服务。

① 创建服务文件

vim /etc/systemd/system/zdirpublic_watch.service

写入:

[Unit]
Description=Zdir Public Delete Watcher
After=network.target

[Service]
Type=simple
ExecStart=/opt/zdir/sync/zdirpublic_watch_delete.sh
Restart=always
RestartSec=3
User=root
WorkingDirectory=/opt/zdir/data/public
StandardOutput=append:/opt/zdir/sync/log/zdirpublic_watch.log
StandardError=append:/opt/zdir/sync/log/zdirpublic_watch.err

[Install]
WantedBy=multi-user.target

② 重新加载 systemd

systemctl daemon-reload

③ 启动服务

systemctl start zdirpublic_watch

④ 设置开机自启

systemctl enable zdirpublic_watch

⑤ 查看运行状态

systemctl status zdirpublic_watch

如果看到:

Active: active (running)

说明监控脚本已经成功常驻后台。

10.部署步骤(从零开始)

① 安装 inotify-tools

apt install inotify-tools -y

② 配置 rsyncd(每台都要)

见上文第6步。

③ 放置脚本

/opt/zdir/sync/watch_delete.sh
/opt/zdir/sync/sync_all.sh

赋权:

chmod +x /opt/zdir/sync/*.sh

④ 启动监控脚本(后台常驻)

如果之前做过第9步,请忽略此步骤!

nohup /opt/zdir/sync/watch_delete.sh &

⑤ 定时执行同步脚本(每分钟)

如果你是使用面板程序可以直接创建,此处不再赘述。

*/1 * * * * /opt/zdir/sync/sync_all.sh

11. 验证方法

✔ 新增测试:

在 A 创建文件:a.txt ,几秒后 B 应该出现。

✔ 修改测试:

在 B 修改:a.txt ,A 应该自动同步最新版本。

✔ 删除测试

在 A 删除:a.txt ,B 应该自动删除,并且不会复活。

✔ 移动到目录外测试(最关键)

mv a.txt /root/

另一台应自动删除,不会复活。

12.常见坑(必须注意)

  • 删除期间禁止 PUSH
  • .delete_log.delete_lock 必须同步
  • .trash 不同步
  • moved_from 必须监听
  • 两台服务器时间必须同步(NTP)

13.完整流程图

删除事件 → inotify → 写 delete_log → 写 delete_lock → 推送到对端
        ↓
同步脚本检测锁 → 不 PULL、不 PUSH → 处理 delete_log → 删除文件
        ↓
删除完成 → 清除锁 → 恢复正常 PULL(增/改)

14.总结

这套方案的核心是:

新增/修改靠 rsync PULL,删除靠 delete_log,移动事件靠 moved_from,删除期间禁止 PUSH。

它比 Unison 更轻量,比纯 rsync 更安全,比双向 rsync 更可靠。

现在我们拥有了一个 真正可用于生产环境的双主同步系统

赞(1) 赏口饭吃
版权声明:本文采用知识共享 署名4.0国际许可协议 [BY-NC-SA] 进行授权
文章名称:《🚀 Rsync双主/多主服务器文件实时同步(增删改)完整实现教程》
文章链接:https://www.vckers.com/notes/201.html
本站资源仅供个人学习交流,请于下载后24小时内删除,不允许用于商业用途,否则法律问题自行承担。
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

微客网络

真的勇士永不止步

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫打赏

微信扫一扫打赏