Make local copy using rsync as fast as with cp


Just like as almost everyone else, I use rsync for updating backups. However the speed of sequential write on APU2 is only 40 MB/s tops, which is less than half the speed I can reach when copying using cp to external hard drive connected via USB 3. I can get 50 MB/s if I specify “–protocol=29” so rsync uses MD4 instead of MD5.

To work around this, I first copied only the new files with the help of “-u (update)” option of cp and then used rsync for syncing deletions. This works perfectly but it doesn’t allow me to use advanced excludes for which I need to use rsync alone without the first cp.

Using checksums for network synchronization of files makes sense but not so much if I only need to use rsync as more advanced cp locally. Unfortunately there is no option to turn all checksums off in rsync (as of 2017).

Luckily in 2009, Amir Goldor sent a patch to the rsync mailing list that disables all checksums for local file transfer. The patch is against rsync version 3.0.6 but it works perfectly against 3.0.9 as well (which is the version used in RHEL/CentOS 7).

diff -ur rsync-3.0.9/io.c rsync-3.0.9.fast/io.c
--- rsync-3.0.9/io.c	2011-09-23 08:33:47.000000000 +0200
+++ rsync-3.0.9.fast/io.c	2017-05-27 09:22:40.321958609 +0200
@@ -29,6 +29,7 @@

#include "rsync.h"
#include "ifuncs.h"
+#include <linux/socket.h>

/** If no timeout is specified then use a 60 second select timeout */
#define SELECT_TIMEOUT 60
@@ -1729,6 +1730,82 @@
writefd(f, str, len);
}

+int send_fd(int socket, int fd_to_send)
+{
+       struct msghdr message;
+       struct iovec iov[1];
+       struct cmsghdr *control_message = NULL;
+       char buffer[CMSG_SPACE(sizeof(int))], data[1];
+       int ret;
+
+       memset(&message, 0, sizeof(struct msghdr));
+       memset(buffer, 0, CMSG_SPACE(sizeof(int)));
+
+       data[0] = 'F';
+       iov[0].iov_base = data;
+       iov[0].iov_len = 1;
+
+       message.msg_iov = iov;
+       message.msg_iovlen = 1;
+
+       message.msg_control = buffer;
+       message.msg_controllen = CMSG_SPACE(sizeof(int));
+
+       control_message = CMSG_FIRSTHDR(&message);
+       control_message->cmsg_level = SOL_SOCKET;
+       control_message->cmsg_type = SCM_RIGHTS;
+       control_message->cmsg_len = CMSG_LEN(sizeof(int));
+
+       *((int *) CMSG_DATA(control_message)) = fd_to_send;
+
+       message.msg_controllen = control_message->cmsg_len;
+
+       do ret = sendmsg(socket, &message, 0);
+       while (ret == -1 && (errno == EINTR || errno == EAGAIN));
+       return ret;
+}
+
+int recv_fd(int socket)
+{
+       struct msghdr message;
+       struct iovec iov[1];
+       struct cmsghdr *control_message = NULL;
+       char buffer[CMSG_SPACE(sizeof(int))], data[1];
+       int ret;
+
+       memset(&message, 0, sizeof(struct msghdr));
+       memset(buffer, 0, CMSG_SPACE(sizeof(int)));
+
+       iov[0].iov_base = data;
+       iov[0].iov_len = 1;
+
+       message.msg_iov = iov;
+       message.msg_iovlen = 1;
+
+       message.msg_control = buffer;
+       message.msg_controllen = CMSG_SPACE(sizeof(int));
+
+       do ret = recvmsg(socket, &message, MSG_WAITALL);
+       while (ret == -1 && (errno == EINTR || errno == EAGAIN));
+
+       if (ret <= 0)
+               return -1;
+
+       for(control_message = CMSG_FIRSTHDR(&message);
+                       control_message != NULL;
+                       control_message = CMSG_NXTHDR(&message,
+                               control_message))
+       {
+               if( (control_message->cmsg_level == SOL_SOCKET) &&
+                               (control_message->cmsg_type == SCM_RIGHTS) )
+               {
+                       return *((int *) CMSG_DATA(control_message));
+               }
+       }
+
+       return -1;
+}
+
/* Send a file-list index using a byte-reduction method. */
void write_ndx(int f, int32 ndx)
{
diff -ur rsync-3.0.9/pipe.c rsync-3.0.9.fast/pipe.c
--- rsync-3.0.9/pipe.c	2009-01-17 22:41:35.000000000 +0100
+++ rsync-3.0.9.fast/pipe.c	2017-05-27 09:49:21.054011604 +0200
@@ -29,6 +29,7 @@
extern mode_t orig_umask;
extern char *logfile_name;
extern struct chmod_mode_struct *chmod_modes;
+int local_socket = 0;

/**
* Create a child connected to us via its stdin/stdout.
@@ -111,11 +112,15 @@
pid_t pid;
int to_child_pipe[2];
int from_child_pipe[2];
+	int child_socket[2];

/* The parent process is always the sender for a local rsync. */
assert(am_sender);

if (fd_pair(to_child_pipe) < 0 ||
+#ifdef HAVE_SOCKETPAIR
+               fd_pair(child_socket) < 0 ||
+#endif
fd_pair(from_child_pipe) < 0) {
rsyserr(FERROR, errno, "pipe");
exit_cleanup(RERR_IPC);
@@ -141,6 +146,9 @@

if (dup2(to_child_pipe[0], STDIN_FILENO) < 0 ||
close(to_child_pipe[1]) < 0 ||
+#ifdef HAVE_SOCKETPAIR
+                       close(child_socket[1]) < 0 ||
+#endif
close(from_child_pipe[0]) < 0 ||
dup2(from_child_pipe[1], STDOUT_FILENO) < 0) {
rsyserr(FERROR, errno, "Failed to dup/close");
@@ -150,6 +158,9 @@
close(to_child_pipe[0]);
if (from_child_pipe[1] != STDOUT_FILENO)
close(from_child_pipe[1]);
+#ifdef HAVE_SOCKETPAIR
+               local_socket = child_socket[0];
+#endif
#ifdef ICONV_CONST
setup_iconv();
#endif
@@ -157,6 +168,9 @@
}

if (close(from_child_pipe[1]) < 0 ||
+#ifdef HAVE_SOCKETPAIR
+           close(child_socket[0]) < 0 ||
+#endif
close(to_child_pipe[0]) < 0) {
rsyserr(FERROR, errno, "Failed to close");
exit_cleanup(RERR_IPC);
@@ -164,6 +178,9 @@

*f_in = from_child_pipe[0];
*f_out = to_child_pipe[1];
+#ifdef HAVE_SOCKETPAIR
+	local_socket = child_socket[1];
+#endif

return pid;
}
diff -ur rsync-3.0.9/receiver.c rsync-3.0.9.fast/receiver.c
--- rsync-3.0.9/receiver.c	2011-03-26 18:01:37.000000000 +0100
+++ rsync-3.0.9.fast/receiver.c	2017-05-27 12:41:05.044048604 +0200
@@ -36,6 +36,8 @@
extern int write_batch;
extern int batch_gen_fd;
extern int protocol_version;
+extern int local_server;
+extern int local_socket;
extern int relative_paths;
extern int preserve_hard_links;
extern int preserve_perms;
@@ -192,12 +194,24 @@
int32 len, sum_len;
OFF_T offset = 0;
OFF_T offset2;
-	char *data;
+	char *data = NULL;
int32 i;
char *map = NULL;
+	int percent = 0;

read_sum_head(f_in, &sum);

+       if (local_server && local_socket && sum.count == 0) {
+               int i = recv_fd(local_socket);
+               //rprintf(FINFO,"received file descriptor %d\n", i);
+               if (i <= 0)
+                       offset = -1;
+               write_buf(local_socket, (char *)&offset, sizeof(offset));
+               if (i <= 0)
+                       return 0;
+               f_in = -i;
+       }
+
if (fd_r >= 0 && size_r > 0) {
int32 read_size = MAX(sum.blength * 2, 16*1024);
mapbuf = map_file(fd_r, size_r, read_size, sum.blength);
@@ -251,11 +265,18 @@
stats.literal_data += i;
cleanup_got_literal = 1;

-			sum_update(data, i);
+                        if (f_in >= 0)
+                                /* no need for md4 during fast copy -goldor */
+                                sum_update(data, i);

if (fd != -1 && write_file(fd,data,i) != i)
goto report_write_error;
offset += i;
+                       if (f_in < 0 && percent < offset * 100 / total_size) {
+                               /* report progress of fast copy to sender every 1% -goldor */
+                               write_buf(local_socket, (char *)&offset, sizeof(offset));
+                               percent = offset * 100 / total_size;
+                       }
continue;
}

@@ -327,6 +348,12 @@
if (mapbuf)
unmap_file(mapbuf);

+       if (f_in < 0) {
+               /* close fast copy handle -goldor */
+               close(-f_in);
+               return 1;
+       }
+
read_buf(f_in, file_sum2, sum_len);
if (verbose > 2)
rprintf(FINFO,"got file_sum\n");
diff -ur rsync-3.0.9/sender.c rsync-3.0.9.fast/sender.c
--- rsync-3.0.9/sender.c	2009-12-13 02:23:03.000000000 +0100
+++ rsync-3.0.9.fast/sender.c	2017-05-27 12:45:46.121620098 +0200
@@ -35,6 +35,8 @@
extern int allowed_lull;
extern int preserve_xattrs;
extern int protocol_version;
+extern int local_server;
+extern int local_socket;
extern int remove_source_files;
extern int updating_basis_file;
extern int make_backups;
@@ -320,6 +322,16 @@
path,slash,fname, (double)st.st_size);
}

+               /* send_fd for fast copy before sending sum head,
+                * which will trigger the recv_fd at the receiver -goldor */
+               if (local_server && local_socket && s->count == 0) {
+                       if (!mbuf)
+                               /* easy fix for fast copy of 0 length files -goldor */
+                               mbuf = map_file(fd, 0, 0, s->blength);
+                       //rprintf(FINFO,"sending file descriptor %d\n", mbuf->fd);
+                       send_fd(local_socket, mbuf->fd);
+               }
+
write_ndx_and_attrs(f_out, ndx, iflags, fname, file,
fnamecmp_type, xname, xlen);
write_sum_head(f_xfer, s);
@@ -334,7 +346,18 @@

set_compression(fname);

-		match_sums(f_xfer, s, mbuf, st.st_size);
+               if (local_server && local_socket && s->count == 0) {
+                       OFF_T offset = 0;
+                       while (offset < st.st_size) {
+                               read_buf(local_socket, (char *)&offset, sizeof(offset));
+                               if (offset < 0)
+                                       break;
+                               if (do_progress)
+                                       show_progress(offset, st.st_size);
+                       }
+               }
+               else
+                       match_sums(f_xfer, s, mbuf, st.st_size);
if (do_progress)
end_progress(st.st_size);

diff -ur rsync-3.0.9/token.c rsync-3.0.9.fast/token.c
--- rsync-3.0.9/token.c	2010-07-03 17:47:02.000000000 +0200
+++ rsync-3.0.9.fast/token.c	2017-05-27 12:49:10.026914362 +0200
@@ -213,7 +213,7 @@
/* non-compressing recv token */
static int32 simple_recv_token(int f, char **data)
{
-	static int32 residue;
+	static off_t residue;
static char *buf;
int32 n;

@@ -223,6 +223,21 @@
out_of_memory("simple_recv_token");
}

+       if (f < 0) {
+               /* fast copy handle */
+               f = -f;
+               if (!*data) {
+                       /* start reading this file */
+                       struct stat stbuf;
+                       if (fstat(f, &stbuf) < 0)
+                               return -1;
+                       residue = stbuf.st_size;
+                       *data = buf;
+               }
+               if (residue == 0)
+                       return 0;
+       }
+
if (residue == 0) {
int32 i = read_int(f);
if (i <= 0)

With this patch copying large files gives the same speed as cp (200 MB/s on the same drive while it was only 40 MB/s before).

I included this patched rsync in my brouken CentOS 7 repo. If you have the repo installed, you can reinstall your rsync using following:

rpm --erase --nodeps rsync
yum --disablerepo="base" --enablerepo="brouken" install rsync

To revert back to the distribution version of rsync, run the same commands with repos swapped.

Update 11/2017: The development version of rsync finally allows disabling checksums since this commit. You will be able to use following option to disable checksums

--checksum-choice=none