如何在两个远程主机之间同步文件?

我想使用本地 shell 在两个远程主机之间传输文件,但似乎 rsync 不支持在指定两个远程主机时进行同步,如下所示:

$ rsync -vuar host1:/var/www host2:/var/www
The source and destination cannot both be remote.

有没有其他的方法/命令可以用来达到类似的结果呢?

1http://serverfault.com/questions/411552/rsync-remote-to-remote - Ciro Santilli OurBigBook.com
2实际上,你可以通过在第三台主机上利用sshfs来在两个远程主机之间进行rsync。只需使用sshfs将主机1和主机2挂载到主机3上,然后在1和2之间进行rsync操作即可。 - William Legg
1@WilliamLegg 使用sshfs的缺点是,rsync将源文件系统和目标文件系统都视为本地文件系统,因此禁用了其增量算法。此时,您几乎可以使用cp -p。请参阅提出此建议的答案及其后续评论。 - roaima
你可以使用最新版本的scp和那个“咒语”进行传输:scp -3 A:file C:file。详情请参考:https://superuser.com/a/401245/264813。 - Ciprian Tomoiagă
15个回答

正如你所发现的,你不能使用rsync来处理远程源和远程目标。假设这两台服务器无法直接通信,你可以通过本地机器使用ssh进行隧道传输。
不需要使用
rsync -vuar host1:/var/www host2:/var/www

你可以使用这个
ssh -R localhost:50000:host2:22 host1 'rsync -e "ssh -p 50000" -vuar /var/www localhost:/var/www'

第一个实例的/var/www适用于host1上的源,localhost:/var/www对应于host2上的目标。

如果你好奇的话,-R选项在host1上设置了一个反向通道,将端口50000(通过你的本地机器)映射到host2上的端口22。没有直接连接从host1host2


2反向连接不会读取本地端的~/.ssh/config文件 - 需要使用某种可解析的方法,就好像没有SSH配置文件一样。 - Florenz Kley
@Florenz 反向连接确实会读取 ~/.ssh/config,但由于远程主机被映射为端口50000上的 localhost,它可能与您的配置不匹配。 - roaima
@roaima 这更像是一种“自我提醒”。它读取配置文件,在远程服务器上,因此别名配置应在执行主机的范围内解释,毕竟这里运行的是远程shell。 - Florenz Kley
@FlorenzKley 哦,我明白你的意思了。我已经在脑海中将"remote"和"local"互换了,以便这些词在反向连接的上下文中使用。是的,你说得对:在_原始_连接的上下文中,反向隧道只能使用"remote"主机的~/.ssh/config文件。 - roaima
2假设两个服务器无法直接通信。这个解决方案可以绕过防火墙或NAT问题,从而避免直接SSH连接的限制。然而,它并没有解决源用户(在host1上)由于安全原因而没有密钥、凭证或写权限不足的情况。对于这种情况,请参考Kevin Cox的解决方案,或者使用脚本或scp -3进行间接连接。 - Cedric Knight
@CedricKnight同意。我的回答是根据我理解的问题来回答的,在许多情况下应该足够了。在更复杂的情况下,需要一个更复杂的解决方案。 - roaima
@FlorenzKley 你可以将Kevin Cox的技巧与这个结合起来。 - Jean-Bernard Jansen
我在这里尝试了一下(链接)。我收到了“Connection closed by host1 port 22”的消息。根据你的评论“从host1的50000端口建立一个反向通道...连接到host2的22端口”,我无法理解这个消息的意思。 - sancho.s ReinstateMonicaCellio

你没有说明为什么不想登录一台主机然后再复制到另一台主机,所以我来分享一个原因和解决方案。
我不能先登录一台机器,然后再通过rsync复制到另一台机器,因为两个主机都没有可以登录到另一台主机的SSH密钥。为了解决这个问题,我使用了SSH Agent Forwarding来允许第一个主机在我登录时使用我的SSH密钥。 警告:SSH转发允许主机在您登录期间使用您的SSH密钥。虽然他们无法复制您的密钥,但他们可以用它登录其他机器。确保您理解风险,并且不要在您不信任的机器上使用代理转发。
下面的命令将使用SSH Agent Forwarding打开从host1host2的直接连接。这样做的好处是运行该命令的机器不会成为传输的瓶颈。
ssh -A host1 rsync -vuar /var/www host2:/var/www

5+1 用例解释了一个有效的使用情况(在host1上的远程用户对目标服务器没有权限的情况下);重要的安全注意事项(使用端口转发-D而不是-A来绕过网络限制而不是密钥限制);解释了优势;命令简短且实际可行。请注意,如果username@host1与本地用户名不同,您可能需要指定它。此外,当连接到host2时,rsync会执行主机密钥验证,因此host1的密钥应该已经存在于host2上的~/.ssh/known_hosts文件中,否则命令将失败。 - Cedric Knight
非常出色的答案,这帮助我在TeamCity中组织了一些以前无法完成的事情(注:对于其他TeamCity用户,在使用ssh -A之前,您必须在构建配置中添加名为“SSH代理”的“构建特性”,请参阅https://confluence.jetbrains.com/display/TCD10/SSH+Agent)。 - John Zwinck
1你忘了提到这个解决方案需要主机1直接访问主机2的网络,而很多时候这并不是实际情况。 - logicor
1这对我来说效果最好。我确实需要先使用ssh-add将我的ssh密钥添加到SSH Agent中。 - Matthias
@Kevin Cox >"为什么你不想登录一个主机然后复制到另一个主机"。这对我起了作用,非常感谢!:) - Tms91
如果我需要在主机1上获得sudo权限,我该怎么办(例如,需要将主机2的文件写入主机1的根目录)?如果不需要sudo,这个答案效果很好。然而,主机1似乎无法在使用sudo时使用我的转发的SSH密钥。例如,ssh -A host1 sudo rsync -vuar /var/www host2:/var/www是无效的。 - Eric Hansen

我喜欢roaima的回答,但两个示例中的路径是一样的,使得它们之间无法区分。我们已经确认以下方法不起作用:
rsync -vuar host1:/host1/path host2:/host2/path

但是这样做就可以了(我省略了localhost的明确绑定地址,因为那是默认值)。
ssh -R 50000:host2:22 host1 'rsync -e "ssh -p 50000" -vuar /host1/path localhost:/host2/path'

请注意,您需要在两个远程主机之间正确设置SSH密钥,其中私钥位于host1上,公钥位于host2上。
为了调试连接,请将其分为两个部分并添加详细状态。
localhost$ ssh -v -R 50000:host2:22 host1

如果这个方法成功,你将在host1上拥有一个shell。现在尝试从host1执行rsync命令。我建议你在另一个窗口中进行操作,这样详细的ssh信息不会与rsync状态信息混合在一起。
host1$ rsync -e "ssh -p 50000" -vuar /host1/path localhost:/host2/path

在我的例子中,路径是源和目标。rsync 在 host1 上启动,并将目标设置为 host2。(你可以在评论中要求澄清。) - roaima
1我本来想评论的,但是如果你没有50+的声望,就不能在别人的帖子上发表评论 - jaybrau

重新格式化roaima在bash脚本语法中的答案(并添加换行符'\'以提高清晰度)。我随机选择了端口22000...
SOURCE_USER=user1
SOURCE_HOST=hostname1
SOURCE_PATH=path1

TARGET_USER=user2
TARGET_HOST=host2
TARGET_PATH=path2

ssh -l $TARGET_USER -A -R localhost:22000:$TARGET_HOST:22 \
$SOURCE_USER@$SOURCE_HOST "rsync -e 'ssh -p 22000' -vuar $SOURCE_PATH \
$TARGET_USER@localhost:$TARGET_PATH"

2我觉得你所做的只是用变量替换了他任意的主机名,是吗? - Jeff Schaller
6是的,我做了。这对我来说增加了清晰度,让我知道哪个是源机器,哪个是目标机器,以及源路径和目标路径分别指向哪里。我花了一些时间才弄清楚这一切,因为简单的占位符主机名并不明显。 - David I.
下一次请随意直接通过编辑来改进别人的回答。 - roaima
1这个答案对我来说是解决方案,因为它将ssh-agent转发(-A)与反向隧道(-R)结合在一起。 - camelthemammel
这也说明了应该把user1和user2放在哪里! - toto_tico

一个易于使用的脚本
多年来,我已经用过很多次与其他答案中几乎相同的技巧来完成这个任务。然而,由于很容易出现一些细节错误并花费大量时间来解决问题,我想出了下面的脚本:
1. 方便指定所有细节(源、目标、选项); 2. 逐步测试每个步骤,并在出现问题时提供反馈,以便您知道需要修复什么; 3. 解决了“ssh -A”无法传播身份验证数据的情况(不知道为什么有时会发生这种情况,因为解决方法比找到根本原因更容易); 4. 最后完成任务。
如何使用该脚本
1. 确保您可以从本地主机上通过ssh连接到两个主机而无需输入密码。 2. 在脚本的前几行设置变量。 3. 执行脚本。
工作原理
正如我之前所说,它使用了与其他答案中相同的技巧:
  • 使用ssh的-R选项从本地主机连接到主机1,同时设置端口转发,允许主机1通过本地主机连接到主机2(-R localhost:$FREE_PORT:$TARGET_ADDR_PORT
  • 使用ssh的-A选项,方便进行第二个ssh通道的身份验证

这真是复杂!有没有更简单的方法?

当从源地址复制所有或大部分字节到目标地址时,使用tar简单得多

ssh $SOURCE_HOST "tar czf - $SOURCE_PATH" \
    | ssh $TARGET_HOST "tar xzf - -C $TARGET_PATH/"

剧本

#!/bin/bash
#-------------------SET EVERYTHING BELOW-------------------
# whatever you type after ssh to connect to SOURCE/TARGE host 
# (e.g. 1.2.3.4:22, user@host:22000, ssh_config_alias, etc)
# So if you use "ssh foo" to connect to SOURCE then 
# you must set SOURCE_HOST=foo
SOURCE_HOST=host1 
TARGET_HOST=host2 
# The IP address or hostname and ssh port of TARGET AS SEEN FROM LOCALHOST
# So if ssh -p 5678 someuser@1.2.3.4 will connect you to TARGET then
# you must set TARGET_ADDR_PORT=1.2.3.4:5678 and
# you must set TARGET_USER=someuser
TARGET_ADDR_PORT=1.2.3.4:5678
TARGET_USER=someuser

SOURCE_PATH=/mnt/foo  # Path to rsync FROM
TARGET_PATH=/mnt/bar  # Path to rsync TO

RSYNC_OPTS="-av --bwlimit=14M --progress" # rsync options
FREE_PORT=54321 # just a free TCP port on localhost
#---------------------------------------------------------

echo -n "Test: ssh to $TARGET_HOST: "
ssh $TARGET_HOST echo PASSED| grep PASSED || exit 2

echo -n "Test: ssh to $SOURCE_HOST: "
ssh $SOURCE_HOST echo PASSED| grep PASSED || exit 3

echo -n "Verifying path in $SOURCE_HOST "
ssh $SOURCE_HOST stat $SOURCE_PATH | grep "File:" || exit 5

echo -n "Verifying path in $TARGET_HOST "
ssh $TARGET_HOST stat $TARGET_PATH | grep "File:" || exit 5

echo "configuring ssh from $SOURCE_HOST to $TARGET_HOST via locahost"
ssh $SOURCE_HOST "echo \"Host tmpsshrs; ControlMaster auto; ControlPath /tmp/%u_%r@%h:%p; hostname localhost; port $FREE_PORT; user $TARGET_USER\" | tr ';' '\n'  > /tmp/tmpsshrs"

# The ssh options that will setup the tunnel
TUNNEL="-R localhost:$FREE_PORT:$TARGET_ADDR_PORT"

echo 
echo -n "Test: ssh to $SOURCE_HOST then to $TARGET_HOST: "
if ! ssh -A $TUNNEL $SOURCE_HOST "ssh -A -F /tmp/tmpsshrs tmpsshrs echo PASSED" | grep PASSED ; then
        echo
        echo "Direct authentication failed, will use plan #B:"
        echo "Please open another terminal, execute the following command"
        echo "and leave the session running until rsync finishes"
        echo "(if you're asked for password use the one for $TARGET_USER@$TARGET_HOST)"
        echo "   ssh -t -A $TUNNEL $SOURCE_HOST ssh -F /tmp/tmpsshrs tmpsshrs"
        read -p "Press [Enter] when done..."
fi

echo "Starting rsync"
ssh -A $TUNNEL $SOURCE_HOST "rsync -e 'ssh -F /tmp/tmpsshrs' $RSYNC_OPTS $SOURCE_PATH tmpsshrs:$TARGET_PATH"

echo
echo "Cleaning up"
ssh $SOURCE_HOST "rm /tmp/tmpsshrs"

当您需要进行单个(非增量)传输并且传输可以在一次操作中完成时,tar 是一个很好的选择。另一方面,rsync 可以处理重新启动和增量传输。 - roaima
1当然,@roaima——我不认为tar是等同的。我留下了这个简短的提及,以备将来阅读时解决rsync不是100%必需的问题。 - ndemou
非常感谢这个,我本来打算手动编写脚本,所以有现成的bash脚本非常方便。 - Craig Pearson
这绝对值得成为常见发行版的完整套装。 - Martin Braun

最理想的方式是在这些服务器之一上运行rsync。但如果您不想在远程服务器上运行脚本,可以在本地系统上运行一个脚本,通过ssh执行rsync命令。
ssh user@$host1 <<ENDSSH >> /tmp/rsync.out 2>&1
rsync -vuar /var/www host2:/var/www
ENDSSH

此外,正如你可能已经了解的那样,rsync只进行单向同步。如果你想要双向同步,可以看看osync(https://github.com/deajan/osync)。我使用过它并发现它非常有帮助。


你可以使用本地临时目录将数据从远程源服务器复制到本地,然后再将这个临时目录复制到远程目标服务器。
一个例子如下:
SRC=user@source-server:/some/path/in/source/* && \
DST=user@destin-server:/some/path/in/destin/* && \
TMP=/tmp/rsync && \
rm -fr $TMP && \
mkdir -p $TMP && \
rsync -av $SRC $TMP && \
rsync -av $TMP/* $DST/ && \
rm -fr $TMP

不确定这对任何人是否有用……这是我想出来的一个快速而粗糙的解决方案,用于在主机授权访问但远程系统之间无法相互访问的情况下同步文件。
(){ local RSYNC_TMP=$(mktemp); rsync -aP src-host:~/filename $RSYNC_TMP; rsync -aP $RSYNC_TMP dest-host:~/filename; rm -rf $RSYNC_TMP }

这可以复制/粘贴到zsh中,因此如果您的主机是macOS,则非常适用。然而,这不会保护所涉及的文件的安全性。将文件名替换为目录名,并将local RSYNC_TMP=$(mktemp)更改为local RSYNC_TMP=$(mktemp -d)应允许进行目录传输。

对于更安全的临时传输机制,假设您的主机是Linux,我倾向于使用[bwrap(1)][1]或类似工具来创建仅在进程持续时间内存在的tmpfs挂载。


你可以在其中一台电脑上运行一个rsyncd(服务器)。
这是我采取的方法,因为我不想使用ssh来允许“源”(在rsync术语中)以root身份无需密码访问“目标”(这是在脚本中使用rsync进行SSH隧道时所要求的)。
在我的情况下,我只是在目标计算机上设置了一个允许来自源计算机的单个用户的rsyncd服务器,并从源端使用rsync。
效果非常好。

如果您无法在至少一个服务器(host1或host2)上运行rsync,并且您不关心带宽和性能,我总是按照以下方式执行(在第三台服务器上,而非本地),因为这样非常容易记住:
$ mkdir host1 host2
$ sshfs user1@host1:/path1 ./host1
$ sshfs user2@host2:/path2 ./host2
$ rsync -a ./host1/ ./host2/

如果你用sshfs这种方式做的话,在运行这些命令的服务器上就不需要额外的磁盘空间。如果你也不在意磁盘空间,可以简单地这样做(不使用sshfs):
$ mkdir host1
$ rsync -a user1@host1:/path1/ ./host1/
$ rsync -a ./host1/ user2@host2:/path2/

当然,您可以根据自己的喜好更改rsync参数。我基本上只使用 -a。

sshfs是我所来自的路径。不幸的是,sshfs无法处理源文件中的硬链接,因此请注意要复制哪些数据到它上面。 - Paul w. Muad'dib