ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

rsync协议与进程模型:generator/sender/receiver协同原理与实战排查

rsync协议与进程模型:generator/sender/receiver协同原理与实战排查 rsync 几乎可以说是运维和开发机器之间同步文件的“默认选项”但很多人用了一年两年仍然只把它当成“高级版 cp”。直到某天同步大目录时日志停在某一行或者“卡死”在某个大文件上才会真正意识到rsync 不只是把文件从 A 复制到 B背后是一套完整的协议和三个角色的协作流程。这篇文章就围绕 rsync 的协议与进程模型把 generator、sender、receiver 这三者的分工、交互方式、常见坑和排查思路讲透。先给新同学一个定位rsync 之所以能在增量同步场景里这么稳核心不是“比较文件大小”这种粗粒度判断而是它把“本地文件列表生成”、“远端差异计算”和“实际数据传输”拆分成了三个独立环节分别由 receiver、generator、sender 三个进程或线程承载。理解了这个三角关系以后再看 rsync 的日志、排查卡死问题、甚至二次开发自定义同步工具都会轻松很多。1. 整体进程模型为什么 rsync 要拆成三个角色1.1 从最简单的“复制文件”说起先看一个最简单的场景rsync -av /data /backup。大多数人的直觉是rsync 读取本地目录然后逐文件复制到远端。但这只是表面现象。实际的 rsync 会先在两端各自启动一个进程然后通过 socket 通信把“文件列表”从发送端传送到接收端再由接收端判断哪些文件需要更新最后发起数据传输。这个流程中天然存在两个角色发送数据的一方叫 sender接收数据的一方叫 receiver。但 rsync 在两端各启动了两个线程或者说两个“子任务”其中一端还要额外跑一个 generator专门负责“对比差异并生成需要传输的文件列表”。1.2 Three-Way 进程协作receiver / sender / generator具体展开就是receiver 是数据落地的执行者。它接收 sender 传过来的文件内容和元数据负责写入磁盘、设置权限、处理硬链接等。在外行人眼里receiver 就是“复制”的那个动作。sender 是数据读取端。它根据 receiver实际上走 generator 生成的索引来读取本地文件然后按块block计算校验和把校验信息发送给对端。generator 是“决策中枢”。它不直接参与数据传输而是对比发送端给出的文件列表和接收端已有的文件信息决定哪些文件需要传输、哪些可以跳过、哪些需要删除。它的输出是一张“待传输任务”的索引表sender 严格跟着这张表走。注意这里的角色分配跟“本机还是远程”没有绝对关系。rsync 支持本地到本地、本地到远程、远程到本地三种模式但无论方向如何内部都是这三方在协作只是哪一端扮演哪个角色会根据参数变化。1.3 为什么需要单独的 generator有人会问为什么不让 receiver 顺便做对比省掉一个角色这就要追溯到 rsync 的核心增量算法。rsync 的增量传输基于“整文件分块校验”的思路。发送端把一个文件切成固定大小的块默认约 700 字节左右对每个块生成两个校验值一个弱滚动校验rolling checksum32 位和一个强校验MD5 之类128 位。接收端接收到块校验信息后在自己的旧文件中做同样分块然后对比校验值找出哪些块需要更新、哪些块可以沿用旧数据。问题在于“找出哪些块需要更新”这个操作要在接收端做。但接收端同时还要负责把新数据写入磁盘如果把对比和写入都塞给同一个线程遇到大文件时就会阻塞无法做到“边对比、边下载、边写入”的流水线效果。所以 rsync 把它拆成两条线generator 负责计算差异和生成任务索引receiver 负责根据索引写数据。两条线并行receiver 在写第 N 个块的时候generator 已经在对比第 N1 个块甚至下一个文件了。这也是 rsync 在大量小文件场景下特别快的原因之一——文件列表的生成和文件内容的传输在时间上是重叠的。1.4 进程模型的横向类比如果用生活中的流水线来类比receiver 是流水线终点的“装箱工”sender 是流水线起点的“原料输送带”generator 则是站在中间的质量检测员。原料文件数据从 sender 进来质量检测员决定哪些原料直接走、哪些要回炉装箱工只负责把最终合格品装进箱。三者缺一不可且互相之间有明确的“上下游”关系。理解这个模型后你再看 rsync 的日志输出就不会困惑为什么有时候日志里先出现generator相关的行然后又出现sender相关的行。它们本来就是在不同进程里并行跑的只是输出到同一个终端而已。2. rsync 协议的分层实现从握手到文件索引2.1 协议版本与能力协商rsync 的通信并不是直接用 FTP 那种“命令-响应”模式而是先做一次“能力协商”。两端进程启动后会交换各自的协议版本号protocol version、支持的扩展特性等。这个设计跟 SSH 或 HTTP 的版本协商类似好处是新旧版本之间能够自动降级兼容不至于因为一端升级导致另一端无法使用。实际的握手过程大致是连接建立后客户端发送自己的协议版本号和 flags服务端响应自己的版本号和 flags双方根据较低版本确定后续报文的格式和功能。如果版本差距过大rsync 会直接报错退出而不是尝试用错误的格式继续通信。这一点在实际部署中很重要当你用系统自带的 rsync比如 CentOS 7 上的 3.1.2与一个很旧的版本如 2.6.9互传时最好先确认协议版本兼容否则某些高级参数如--info、--msgs2stderr可能无法正常工作。2.2 目录遍历与文件列表生成在真正的数据传输开始前rsync 会先传输一份“文件列表”。这个列表并不是简单的路径字符串而是一个包含文件属性、权限、时间戳、大小、inode 信息等元数据的结构化数据。rsync 默认采用“递归遍历”的方式把发送端的目录树完整地镜像到接收端然后由接收端的 generator 对列表逐项做“对比”。列表传输的细节往往被大多数人忽略但它在实际中直接影响到同步的性能尤其是文件数量巨大时列表传输本身可能成为瓶颈。关于这里有一个很实用的经验如果你要同步一个包含几十万个文件的目录rsync -av的“扫描阶段”可能会持续几分钟甚至更久这不代表卡死而是列表生成中。判断方法是看进程状态rsync 的 sender 进程 CPU 占用很高但网络流量为 0基本就是在构建文件列表如果网络流量已经上去了则说明列表阶段已完成进入数据传输阶段。2.3 文件索引与传输任务队列generator 对比完文件列表后会给每一个需要传输的文件分配一个索引号index。这个索引号是 sender 和 receiver 之间的“任务标识”后续的块校验、数据传送、确认反馈都靠这个索引号来关联。可能有人会好奇为什么不直接传输文件路径非要搞个索引原因很简单路径字符串长度不固定解析代价高且容易出错而用整数索引sender 和 receiver 在各自的文件列表数组里通过下标直接定位内存访问效率极高。这种设计在大目录同步时优势尤其明显等于把“查字典”变成了“数组下标访问”。此外rsync 协议里还定义了多种报文类型包括文件列表包file list块校验包checksum list数据包data block错误包error message日志包log message。每种报文都有固定的格式头和校验字段确保两端的解析状态始终保持同步。这也是为什么 rsync 协议对“粘包”和“半包”处理得很好——它内部有完善的基于长度的帧解析逻辑不会像某些自定义协议那样一遇到网络抖动就解析错乱。2.4 协议中的错误处理与断点续传rsync 并不是一个“零错误”的协议尤其是在传输大文件时网络抖动、磁盘满了、权限不对等都可能中断连接。好的是 rsync 支持“断点续传”即下一次同步时它会根据已存在的部分文件partial file结合时间戳、大小来判断从哪里继续。它的实现方式是如果接收端已经存在同名文件rsync 会先把这个文件作为“基础文件”在 generator 对比时与发送端的版本进行比较只传输缺失或变更的块。这就是--partial参数存在的意义。默认情况下 rsync 传输中断后会删除半成品文件下次从头开始加--partial后则保留半成品下次续传。我个人的建议是在弱网或跨公网传输大批量文件时务必加上--partial否则因瞬间断网导致全量重传代价会非常高。3. Generator 的决策逻辑增量同步的灵魂3.1 Generator 如何判断“哪些文件要传输”rsync 的增量同步不是简单的“大小不同就传”它有一套决策流程。generator 拿到发送端的文件列表后对本地的对应文件做如下判断文件不存在直接标记为“需要创建”。文件存在但大小不同标记为“需要重新传输”。文件大小相同但修改时间不同标记为“需要比对块校验”。文件属性权限、owner、group不同可能不重新传输内容只需更新元数据。这个逻辑是 rsync 用-a归档模式时的默认行为。但如果你指定了-I忽略时间戳那么即使文件大小和时间戳完全一致generator 也会强制重新比对块校验并传输。还有一点值得注意rsync 对“软链接”的处理是“不跟随”即默认情况下它同步的是软链接本身而非链接指向的目标文件。这意味着 generator 遇到符号链接时不会去检查链接目标的差异而只是检查链接路径是否相同。这个设计避免了很多同步工具因为符号链接导致的死循环问题。3.2 滚动校验和如何做到“只传差异块”有些场景你是无法通过“文件大小时间戳”来判断是否需要传输的比如在服务器上原地修改了一个大文件的中间某部分文件大小不变mtime 也可能被保留某些程序会这么做。如果 stage 直接全量传输那几十 GB 的文件就要白白传完。rsync 用滚动校验和来解决这个问题。它的思路很巧妙把文件分成若干个固定大小的块block每个块计算一个“弱校验和”和一个“强校验和”。发送端传送的是每个块的校验值而不是文件本身接收端拿到校验值后在自己的旧文件上滑动窗口也计算校验和然后做比对。如果某个块的校验值完全匹配就说明这个块的内容在两端是一致的可以直接复用本地的旧数据如果不匹配发送端才真正把这个块的内容传过来。这样即使一个 10GB 的文件只改了一个字节rsync 也只需要传输一个块约 700 字节的数据量。这里有个很微妙的点滚定校验和是在接收端计算的。也就是 generator 在接收端跑负责拿发送端传来的校验值去本地文件里找匹配块。所以你可以看到generator 不仅要读取本地文件还要做大量计算它对磁盘和 CPU 都有一定消耗。如果接收端的磁盘很慢即便网络很好整个同步速度也会被拖慢。3.3 Generator 的“胜负分流”逻辑protect new vs keep old这是 generator 里最容易被忽略却最容易踩坑的地方。当接收端已经存在一个文件但发送端的同名文件更新或旧时rsync 需要决定是以发送端为准还是以接收端为准这个“胜负分流”是由参数决定的一般有两个思路always force默认场景如果发送端觉得自己的文件是“新的”mtime 较新或内容不同就会强制覆盖接收端。这是rsync -av的典型行为。update / ignore times如果你加了--ignore-timesrsync 就不再信任时间戳而是强制走块校验流程保证内容一致性。append / append-verify如果加了--appendrsync 在发现文件大小不同时会选择只补发增量部分而不是覆盖整个文件。在实际的“双向同步”场景里这个胜负逻辑更要命。比如你同时在公司电脑和家里电脑用 rsync 同步同一个文件夹某一天你把公司电脑上的文档改了又把家里电脑上的同名文档也改了rsync 默认不会帮你“合并”两个版本而是看参数谁的时间戳新就赢。这就是很多团队数据丢失的根源。我见过的实际案例某人用 rsync 把本地项目同步到服务器做备份因为误加了--delete且远端服务器上有一些“看起来旧”但实际很重要的文件结果同步一启动远端这些重要文件直接被删除。所以在正式环境使用rsync --delete前务必做好本地演练或先用--dry-run-n过一遍。3.4for_each_slot与 Batching 优化rsync 还有一个面向小文件的优化机制叫做“批处理”batching。简单说就是当大量小文件需要传输时generator 不会为一个文件单独触发一次“建连接-校验-传输-关闭”的全流程而是把相邻的几个文件合并成一个批次共用一次校验和交换。这个机制在协议层是通过for_each_slot遍历来实现的generator 遍历文件列表里的一批文件逐个判断是否要处理然后把这批文件的索引和元数据打包发送给 sender。sender 收到后按批次读取文件、计算校验和再传给 receiver。这样做的结果是每个文件的“固定开销”握手、帧头、确认包等被均摊了整个同步耗时显著降低。不过要注意batching 虽然对小文件友好但在大文件场景下反而可能造成内存占用过高。如果单个文件达到数 GBrsync 会切分成更大的 block size或者退化为“单文件独立传输”模式。这也是为什么 rsync 的默认 block size 会随文件大小动态调整——小文件用 700 字节左右的块大文件可能自动调整到 KB 级甚至 MB 级。3.5 实操心得如何观察 generator 的行为想观察 generator 到底在做什么可以用rsync -avv --progress看详细日志。但-v输出的是文件级信息看不到块校验细节。如果想深入调试可以加--debugGENR或者--debugDELT开启协议级调试输出。在我自己的调试经验里最有用的调试参数组合是rsync -avv --partial --debugGENR --debugDELT /source/ userhost:/dest/这时你会看到类似recv_generator(example.txt,0)、send_files(example.txt, 3)这样的日志分别对应 generator 处理文件和 sender 发送文件的动作。如果某个文件一直卡在send_files而没有任何进度输出说明数据传输环节出了问题而不是 generator 的问题。4. 协议层的常见问题与排查技巧实录4.1 经典问题rsync 复制文件卡死这是网上讨论最多的问题之一也是我实际工作中踩过的坑。“卡死”的表现是日志停在某个文件上进度条不动CPU 和网络流量都很低进程挂在那里不退出。常见原因有几个网络中间设备防火墙、负载均衡对长连接不友好空闲一定时间后主动断链但 rsync 没有及时感知到。接收端磁盘满了receiver 写不进去但 sender 还在等 ACK。文件数量太大generator 在列表比对阶段消耗大量 CPU 和 IO表面上看起来像卡死。我见过的一个真实案例是一个 rsync 进程同步 60 万个文件到 NFS 挂载的目录结果 NFS 服务端出了故障导致 rsync 在创建文件时无限重试表面看就是“卡死”。后来用strace -p pid一看发现进程阻塞在open()系统调用上等了 10 分钟没返回这基本就是存储端的问题。排查思路先看网络流量iftop或nload如果流量长期为 0说明任务已经停滞。再看进程状态ps -ef | grep rsync处于D状态不可中断睡眠通常表示磁盘 I/O 阻塞处于S状态但 CPU 使用率极低则多半在等网络。用strace -p pid -f -t看系统调用卡在哪里这招在绝大多数场景下都能定位问题。如果是接收端磁盘满df -h一眼就能看出如果是网络问题可以加timeout或用tc做限速测试。4.2 增量算法失败为什么每次都全量传输有时 rsync 会“退化”成全量同步即使源文件只改了一个字节。这种现象通常是由于以下原因导致的源文件和目标文件的 block size 不一致导致发送端的校验信息在目标端无法匹配任何块。目标端文件是“稀疏文件”rsync 无法有效复用现有块只能全量重传。文件读取出错如权限不足生成器读取本地文件失败后直接返回“无法比较”的标记sender 只能全量发送。其中最常见的是权限问题。如果目标端 rsync 进程对某个文件没有读权限generator 就没法读取本地块做对比也就无法定位需要更新的差异块。这时候 rsync 会直接把整个文件标记为“需要传输”而日志里往往只有一行file has vanished或read errors mapping file之类的提示。解决思路确保接收端进程有足够的读取权限如需保持权限一致性在两端保持相同的用户和属组。同时注意如果用 root 跑 rsync默认是可以覆盖权限不足的文件的但普通用户跑 rsync 时遇到这种情况就会非常尴尬。4.3 协议版本不兼容导致的部分参数失效rsync 3.x 引入了许多新参数如--info、--msgs2stderr、--open-noatime等但如果你的一端是 rsync 2.x这些参数会被忽略甚至报错。最好的做法是两端都用 3.x并且尽量保证小版本接近。这里有一个比较隐蔽的坑有些操作系统默认还会用rsync --daemon方式提供服务但 daemon 模式默认只走 rsync 协议不走 SSH 加密通道。如果两端之间的网络环境不可信建议用 SSH 模式而不是 daemon 模式。SSH 模式会有额外的握手开销但安全性高很多。4.4 实用速查表现象可能原因排查手段解决方案日志停在某文件网络流量为 0网络设备断链或磁盘阻塞ptrace查系统调用加--timeout或改用 SSH 保活增量同步退化为全量块大小不一致或权限不足对比两端 rsync 版本、检查文件权限统一版本用-a保证权限--delete误删文件目标端文件比源端旧先用-n预演谨慎使用--delete配合--backup大量小文件同步慢无 batch 机制或列表传输慢观察日志确认卡在哪个阶段用--infoprogress2查看整体进度断点续传无效未加--partial查看目标端.xxx临时文件加--partial必要时加--append-verify4.5 关于rsync -e ssh的注意事项最后分享一个很多人忽略的细节当 rsync 走 SSH 模式时SSH 本身会有连接复用机制。如果之前建立的 SSH 连接断了rsync 会重新连接但这中间的延迟在大目录场景下可能被放大。一个很实用的技巧是给 ssh 指定-o ServerAliveInterval60这样可以保持 SSH 连接的健康状态减少 rsync 因 SSH 断链导致的“卡死”。5. 协议设计与监控优化如何真正用好 rsync5.1 理解 rsync 的坑比理解 rsync 的优势更重要在我个人的经验中rsync 并不是一个“开箱即完美”的工具。它的很多参数设计精妙但也容易用错。比如-uupdate和--ignore-times的组合会让“谁赢”的逻辑彻底反转再比如-cchecksum参数虽然可以保证内容一致性但对大目录来说计算开销相当大有时候同步耗时能翻好几倍。所以在使用 rsync 做定期同步或备份任务时我建议先明确这些问题数据流方向是单向还是双向删除操作是否允许、是否要备份是否追求极致的增量开启-c牺牲部分性能目标端的磁盘空间是否足够支持临时文件和最终文件并存5.2 rsync 在监控与自动化中的常见角色rsync 本身不提供“实时同步”能力它更擅长定时同步。很多团队会用 rsync crontab 定期同步数据到备份目录或者结合inotifywait做准实时同步。不过在准实时场景下要注意如果同步间隔小于 inotify 的扫描频率可能会有文件遗漏如果事件触发过频繁则可能同时跑多个 rsync 进程导致锁竞争和资源浪费。我见过一个比较稳妥的做法是用flock加锁确保同一时刻只有一个 rsync 任务在跑。#!/bin/bash exec 9/var/lock/rsync_backup.lock flock -n 9 || exit 1 rsync -av --partial --delete /data/ /backup/5.3 监控 rsync 进程状态的技巧如果想把 rsync 纳入监控系统建议关注这几个指标当前正在传输的文件大小和速度已完成文件数 / 总文件数进程状态是否处于 D 状态网络吞吐量。最简单的方式是开启--infoprogress2它会在日志中输出总体进度百分比。但这种方式对脚本不友好更适合人工观察。如果要脚本化监控可以定期抓取 rsync 进程的--log-file或者用外部工具对目录大小做快照对比。5.4 扩展用 rsync 协议思想自定义同步工具最后想说一点rsync 的“分块校验增量传输”思想其实可以广泛应用于文件同步、镜像、发布等场景。很多现代的发布系统如 CDN 刷新、镜像同步、数据库备份传输都在借鉴 rsync 的做法。理解 generator / sender / receiver 的模型后你在面对类似“如何高效地在两个节点之间同步数据”的问题时会多一层系统思考而不是简单地上scp -r。如果你对协议更感兴趣可以直接阅读 rsync 源码中的generator.c、sender.c、receiver.c这三个文件里面几乎每行注释都在解释“为什么这样做”。比如generator.c里关于for_each_slot的循环逻辑、checksum.c里的滚动校验和实现都是非常值得反复研读的经典范例。6. 安全加固与使用建议6.1 限制 daemon 模式的暴露面如果确实要用rsync --daemon建议配合防火墙限制来源 IP同时启用--read-only参数如果只是做镜像源并禁用模块的write权限。否则攻击者一旦能连上 rsync daemon就可以通过模块遍历读取机器上的文件这是非常危险的。同时daemon 模式的配置文件名是/etc/rsyncd.conf里面可以定义多个模块每个模块可以指定路径、允许用户、是否只读等信息。一个典型的安全配置示例如下[backup] path /data/backup read only yes list no auth users rsyncuser secrets file /etc/rsyncd.secrets注意secrets file的权限必须设置为 600否则 rsyncd 会报错拒绝启动。6.2 使用 SSH 模式时的权限控制在 SSH 模式下rsync 进程以登录用户身份运行所以它的权限取决于 SSH 用户。如果你不希望目标机器的用户能够修改某些目录应该从 SSH 用户权限入手而不是依赖 rsync 本身的配置。如果你的目标是“让远端用户只能同步指定目录”最好的方式是创建一个专用用户并给它对应的目录权限而不是直接给 root 权限跑 rsync daemon。这样即使 rsync 的命令行参数被利用它能够接触到的文件范围也是受限的。6.3 文件完整性与校验值参考在同步重要数据后建议生成一份校验清单方便后续比对。例如rsync -av --partial /data/ userhost:/backup/ ssh userhost cd /backup md5sum -c /backup/checksums.txt当然这只是一种轻量级的校验方式。更严谨的做法是使用--checksum参数让 rsync 在同步时每块都做校验。不过正如前面提到的这会影响性能需要根据数据重要程度来权衡。6.4 适合自己的才是最好的坦白说rsync 的很多“坑”不是在协议层而是在使用层。比如--delete是否该加、--inplace是否会影响服务、--bwlimit限速多少合适这些都要结合具体业务场景来定。我的建议是不要盲目把网上别人抄的参数堆到一起而是先搞清楚每个参数的含义再用-n预演一遍最后才跑真实任务。rsync 作为一个接近四十岁的工具能在今天仍然被广泛使用说明它的核心设计足够优雅。但任何工具都有边界理解它的边界比熟练使用它本身更珍贵。从我做运维和开发这些年的经验来看真正靠谱的做法是第一次搭同步任务时多花十几分钟读一读 rsync 的手册把rsync -av里每个字母的含义都弄清楚生产环境上用任何带删除语义的参数之前一定先跑一遍-n遇到同步“卡死”时不要急着 kill 进程先抓系统调用再决定怎么处理。这些习惯一旦养成rsync 不仅不会给你添乱反而会成为你数据同步方案里最可靠的一环。
返回列表