关于rsync的原始文档 Rsync technical report 以及Andrew Tridgell的论文 Phd thesis (pdf) 都是关于rsync算法原理的极好的文档。但是,这些文档注重的是rsync算法本身,而对算 法的实现方法则描述较少。
本文试图对Linux/Unix下的rsync工具的实现进行分析,并将描述下列问题:
本文主要目的是为读者提供打下一个基础,在此基础上,读者可以更好的理解下列问题:
当谈到Rsync时候,我们将使用一些术语来指代rsync工具在完成其任务的不同阶段 下的各个角色或者进程。下面为一些后文将会用到的术语:
client/客户端 | role/角色 | 客户端对同步过程进行初始化。 |
server/服务器端 | role/角色 | 服务器是指远端的rsync进程或者客户端通过远端shell、socket所连接到的系 统。 服务器(server)是一个通用的术语,注意不要将其与Deamon混为一谈。 |
一旦从Client到Server的链接建立起来,Client(客户 端)/Server(服务 器)的这两个角色的差别,就被Sender(发送者)/Receiver(接收者)所 取代了。 | ||
daemon/守护进程 | 角色,同时也是进程 | Daemon是一个rsync进程,该进程用于等待接收从Client发起的连接。在一 些平台上,Daemon也被叫做服务(Service) |
remote shell/远端shell | 角色,同时也是一系列的进程 | 一个或多个进程,用于向client和远端的server之间提供连通性。 |
sender/发送者 | role and process | 可以存取待同步的文件资源的rsync进程。 |
receiver/接收者 | role and process | 作为角色:指同步过程中的目标系统; 作为进程:指目标系统中,用于接收数据并接数据写入磁盘的进程。 |
generator/生产者 | process/进程 | 生产者进程用于识别文件的变化,并维持文件级别的逻辑。 |
当rsync客户端启动后,它首先通过管道(pipes)或者网络来与server 进程建立 第一个连接。
根据远端连接的建立方式不同,rsync客户端的处理也不同。
当远端为一个通过remote shell建立起来的非Daemon server时,client会fork远 端shell,并借此在远端系统上启动一个服务器(server)。此后,client和 server均通过remote shell上的管道(pipes)来通讯。此过程中,单就rsync进程 而言,不涉及到网络操作。在这种模式下,server进程的rsync选项是通过用于启 动remote shell的命令行来传递的。
当rsync可以通过deamon来通讯时,它实际上是在直接通过网络来通讯。此模式 下,rsync的参数必须通过socket来发送,该过程具体如下:
通讯刚刚开始启动的时候,Client和Server将各自的版本号发送给对方,并选择较 低的版本号作为文件传输的标准。如果Server端的rsync是一个Daemon-Mode,则 rsync的选项由Client发送至Server。之后由Client发送到Server的,是exclude list,即排除的文件列表。
Local Rsync jobs (when the source and destination are both on locally mounted filesystems) are done exactly like a push. The client, which becomes the sender, forks a server process to fulfill the receiver role. The client/sender and server/receiver communicate with each other over pipes.
The first thing that happens once the startup has completed is that the sender will create the file list. While it is being built, each entry is transmitted to the receiving side in a network-optimised way.
When this is done, each side sorts the file list lexicographically by path relative to the base directory of the transfer. (The exact sorting algorithm varies depending on what protocol version is in effect for the transfer.) Once that has happened all references to files will be done by their index in the file list.
If necessary the sender follows the file list with id→name tables for users and groups which the receiver will use to do a id→name→id translation for every file in the file list.
After the file list has been received by the receiver, it will fork to become the generator and receiver pair completing the pipeline.
generator → sender → receiver
The output of the generator is input for the sender and the output of the sender is input for the receiver. Each process runs independently and is delayed only when the pipelines stall or when waiting for disk I/O or CPU resources.
The generator process compares the file list with its local directory tree. Prior to beginning its primary function, if --delete has been specified, it will first identify local files not on the sender and delete them on the receiver.
The generator will then start walking the file list. Each file will be checked to see if it can be skipped. In the most common mode of operation files are not skipped if the modification time or size differs. If --checksum was specified a file-level checksum will be created and compared. Directories, device nodes and symlinks are not skipped. Missing directories will be created.
If a file is not to be skipped, any existing version on the receiving side becomes the "basis file" for the transfer, and is used as a data source that will help to eliminate matching data from having to be sent by the sender. To effect this remote matching of data, block checksums are created for the basis file and sent to the sender immediately following the file's index number. An empty block checksum set is sent for new files and if --whole-file was specified.
The block size and, in later versions, the size of the block checksum are calculated on a per file basis according to the size of that file.
For each file id the generator sends it will store the block checksums and build a hash index of them for rapid lookup.
Then the local file is read and a checksum is generated for the block beginning with the first byte of the local file. This block checksum is looked for in the set that was sent by the generator, and if no match is found, the non-matching byte will be appended to the non-matching data and the block starting at the next byte will be compared. This is what is referred to as the “rolling checksum”
If a block checksum match is found it is considered a matching block and any accumulated non-matching data will be sent to the receiver followed by the offset and length in the receiver's file of the matching block and the block checksum generator will be advanced to the next byte after the matching block.
Matching blocks can be identified in this way even if the blocks are reordered or at different offsets. This process is the very heart of the rsync algorithm.
In this way, the sender will give the receiver instructions for how to reconstruct the source file into a new destination file. These instructions detail all the matching data that can be copied from the basis file (if one exists for the transfe), and includes any raw data that was not available locally. At the end of each file's processing a whole-file checksum is sent and the sender proceeds with the next file.
Generating the rolling checksums and searching for matches in the checksum set sent by the generator require a good deal of CPU power. Of all the rsync processes it is the sender that is the most CPU intensive.
The receiver will read from the sender data for each file identified by the file index number. It will open the local file (called the basis) and will create a temporary file.
The receiver will expect to read non-matched data and/or to match records all in sequence for the final file contents. When non-matched data is read it will be written to the temp-file. When a block match record is received the receiver will seek to the block offset in the basis file and copy the block to the temp-file. In this way the temp-file is built from beginning to end.
The file's checksum is generated as the temp-file is built. At the end of the file, this checksum is compared with the file checksum from the sender. If the file checksums do not match the temp-file is deleted. If the file fails once it will be reprocessed in a second phase, and if it fails twice an error is reported.
After the temp-file has been completed, its ownership and permissions and modification time are set. It is then renamed to replace the basis file.
Copying data from the basis file to the temp-file make the receiver the most disk intensive of all the rsync processes. Small files may still be in disk cache mitigating this but for large files the cache may thrash as the generator has moved on to other files and there is further latency caused by the sender. As data is read possibly at random from one file and written to another, if the working set is larger than the disk cache, then what is called a seek storm can occur, further hurting performance.
When a connection is received for a defined module the daemon forks a new child process to handle the connection. That child process then reads the rsyncd.conf file to set the options for the requested module, which may chroot to the module path and may drop setuid and setgid for the process. After that it will behave just like any other rsync server process adopting either a sender or receiver role.
A well-designed communications protocol has a number of characteristics.
In addition to these characteristics, protocols have varying degrees of statefulness, inter-packet independence, human readability, and the ability to reestablish a disconnected session.
Rsync's protocol has none of these good characteristics. The data is transferred as an unbroken stream of bytes. With the exception of the unmatched file-data, there are no length specifiers nor counts. Instead the meaning of each byte is dependent on its context as defined by the protocol level.
As an example, when the sender is sending the file list it simply sends each file list entry and terminates the list with a null byte. Within the file list entries, a bitfield indicates which fields of the structure to expect and those that are variable length strings are simply null terminated. The generator sending file numbers and block checksum sets works the same way.
This method of communication works quite well on reliable connections and it certainly has less data overhead than the formal protocols. It unfortunately makes the protocol extremely difficult to document, debug or extend. Each version of the protocol will have subtle differences on the wire that can only be anticipated by knowing the exact protocol version.
Specific suggestions for improvement are welcome, as would be a complete rewrite.
Note:
本文前半部分翻译,原文可从rsync官方网站上得到,但是因为 时间原因,没有翻译完成,已翻译的部分也存在词不达意的现象,等以后有时间再修改吧。后半部分是转载的网友的文章,原文地址为 这里