php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #56374 Slow file transfers using SFTP / SCP
Submitted: 2005-04-15 13:59 UTC Modified: 2012-06-18 20:19 UTC
From: felix at zango dot com Assigned: langemeijer (profile)
Status: Closed Package: ssh2 (PECL)
PHP Version: 5.0.3 OS: Slackware 10.1.0 Linux
Private report: No CVE-ID: None
View Add Comment Developer Edit
Anyone can comment on a bug. Have a simpler test case? Does it work for you on a different platform? Let us know!
Just going to say 'Me too!'? Don't clutter the database with that please !
Your email address:
MUST BE VALID
Solve the problem:
48 + 16 = ?
Subscribe to this entry?

 
 [2005-04-15 13:59 UTC] felix at zango dot com
Description:
------------
I am experiencing very slow file transfers when using ssh2_scp_send or php wrappers with sftp, usually around 30KB/sec on an internal network.  I am able to open a SSH connection, use SFTP / SCP without any problems using the PHP wrappers to mkdir, rmdir, unlink.

I was wondering if you knew a way to increase the buffer size or simply make it transfer faster.

FYI: I included part of stack trace of the PHP script trying to send a file:

[?]
send(3, "E-D:\246\346\25M\16\276\307\325\363\216\31*\334~\210\213"..., 1524, MSG_NOSIGNAL) = 1524
fcntl64(3, F_SETFL, O_RDONLY|O_NONBLOCK) = 0
recv(3, 0xbfff9370, 1, MSG_NOSIGNAL)    = -1 EAGAIN (Resource temporarily unavailable)
fcntl64(3, F_SETFL, O_RDONLY|O_NONBLOCK) = 0
recv(3, 0xbfff9370, 8, MSG_NOSIGNAL)    = -1 EAGAIN (Resource temporarily unavailable)
nanosleep({0, 250000000}, NULL)         = 0
recv(3, "3\365\21\311Ig\343\0", 8, MSG_NOSIGNAL) = 8
recv(3, "o\256\301NM\30\2175", 8, MSG_NOSIGNAL) = 8
recv(3, "\273\354}\265\275\271%a\225\310\tS\234\340\n\311M\32\242"..., 20, MSG_NOSIGNAL) = 20
fcntl64(3, F_SETFL, O_RDONLY|O_NONBLOCK) = 0
recv(3, 0xbfff9370, 1, MSG_NOSIGNAL)    = -1 EAGAIN (Resource temporarily unavailable)
fcntl64(3, F_SETFL, O_RDONLY|O_NONBLOCK) = 0
recv(3, 0xbfff9380, 1, MSG_NOSIGNAL)    = -1 EAGAIN (Resource temporarily unavailable)
fcntl64(3, F_SETFL, O_RDONLY)           = 0
getpid()                                = 10218
getpid()                                = 10218
send(3, "\254\367\246\25\234ho\262\344?z\212\205\"2\246Z\311\247"..., 36, MSG_NOSIGNAL) = 36
munmap(0xb7a6c000, 356352)              = 0
fcntl64(3, F_SETFL, O_RDONLY)           = 0
getpid()                                = 10218
getpid()                                = 10218
send(3, "\24s\306\313\227\356\17V\35=P\216\352\350\344\272\2\327"..., 52, MSG_NOSIGNAL) = 52
fcntl64(3, F_SETFL, O_RDONLY|O_NONBLOCK) = 0
recv(3, 0xbfffb3b0, 1, MSG_NOSIGNAL)    = -1 EAGAIN (Resource temporarily unavailable)
fcntl64(3, F_SETFL, O_RDONLY|O_NONBLOCK) = 0
recv(3, 0xbfffb3b0, 8, MSG_NOSIGNAL)    = -1 EAGAIN (Resource temporarily unavailable)
nanosleep({0, 250000000}, NULL)         = 0
recv(3, "\354EJl{\211}\343", 8, MSG_NOSIGNAL) = 8
recv(3, "\270\206Dl\253\202m\n", 8, MSG_NOSIGNAL) = 8
recv(3, "\4\316\34o!\351\333\357\204\216\250v\327g\262\231\37\23"..., 20, MSG_NOSIGNAL) = 20
fcntl64(3, F_SETFL, O_RDONLY|O_NONBLOCK) = 0
recv(3, 0xbfffb3b0, 1, MSG_NOSIGNAL)    = -1 EAGAIN (Resource temporarily unavailable)
fcntl64(3, F_SETFL, O_RDONLY|O_NONBLOCK) = 0
recv(3, 0xbfffb3c0, 1, MSG_NOSIGNAL)    = -1 EAGAIN (Resource temporarily unavailable)
fcntl64(3, F_SETFL, O_RDONLY)           = 0
getpid()                                = 10218
getpid()                                = 10218
send(3, "4\374KL\360\20e\205\314j-\2\25293r/r\367\367\253\325f\375"..., 36, MSG_NOSIGNAL) = 36
close(4)                                = 0
gettimeofday({1113586396, 438326}, NULL) = 0
lseek(1, 127, SEEK_SET)                 = -1 ESPIPE (Illegal seek)
write(1, "< done in 11sec (29.9516 KB/sec)"..., 38) = 38
msgrcv(
[?]


Reproduce code:
---------------
<?
define('SFTP_ADDR', '10.0.0.2');

$start_time = microtime(true);

$connection = ssh2_connect(SFTP_ADDR, 22);
$sftp = ssh2_sftp($connection);

$src_file = '/var/www/data.src';
$dst_sftp_file = 'ssh2.sftp://' . $sftp . '/www/data.dst';
$dst_scp_file = '/www/data.dst';

// using SFTP wrapper
copy($src_file, $dst_sftp_file);

// using SCP
ssh2_scp_send($connection, $src_file, $dst_scp_file);

print 'Total Time: ' . microtime(true) - $start_time;
?>

Expected result:
----------------
Expected: FAST file transfer, over 3MB/sec

Actual result:
--------------
Result: SLOW file transfer, around 30KB/sec

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2005-04-19 09:58 UTC] felix at cdtinc dot ca
I also tested using ssh2_exec to simply do a file tansfer using cat > filename, however I am limited to 128k of transfer per call (so multiple cat >> filename are necessary after closing & opening new streams after 128k are written)  With this method I achieved 45KB/sec connected on 127.0.0.1 compared to around 30KB/sec when using SFTP or SCP.

Is there a way to increase the size of the write buffers or just do some magic to make it faster?

BTW, my reproduce code above is bugged, here is something that actually works:


#!/usr/local/php5/bin/php
<?php
$src_file = 'test/data.src';
$dst_file = 'mirrored/data.dst';

// get filesize
$filesize = filesize($src_file);

// establish connection to FTP over SSH
$connection = ssh2_connect('127.0.0.1', 22);
ssh2_auth_password($connection, 'user', 'password');

$time = microtime(true);

// ** SFTP ** 
/*
$sftp = ssh2_sftp($connection);
print "Connected to FTP over SSH\n";
copy($src_file, 'ssh2.sftp://' . $sftp . $dst_file);
*/

// ** SSH + cat **
/*
$fp_in = fopen($src_file, 'r');
$stream = ssh2_exec($connection, 'cat > ' . $dst_file);

$written_bytes = 0;
while ($written_bytes < $filesize) {
	$written_bytes += stream_copy_to_stream($fp_in, $stream, 131072);

	if ($written_bytes < $filesize) {
		fclose($stream);
		$stream = ssh2_exec($connection, 'cat >> ' . $dst_file);
	}
}
fclose($fp_in);
fclose($stream);
*/
$time = microtime(true) - $time;
print 'Time: ' . round($time, 2) . ' :: ' . round($filesize / 1024 / $time, 2) . "KB/sec\n";
?>
 [2005-04-19 10:27 UTC] pollita@php.net
Just a note so you know I'm not ignoring this.  A couple other people have reported similar problems.  It's the #1 boadblock to a stable release at this point.
 [2005-04-20 09:48 UTC] felix at cdtinc dot ca
As I was going through the PHP module code, I noticed the stream_close handling does a php_sleep(2).

I am unsure if it is really needed because it means closing a SFTP connection always takes more than 2 seconds.  For the purpose of testing transfer speed, I commented out this part and re-run my code with shell execute of 'cat >' and I reached transfer speed of around 350KB/sec.. which is  a lot better already.  

Now I would greatly prefer using a SFTP wrapper with the function 'copy', it is much cleaner.

Side note:  there should be a simple function to close a SSH / SFTP / SCP connection other than fclose() or unset()

Sorry for bugging you with my problems :)
 [2005-04-20 11:11 UTC] pollita@php.net
Yeah, I can't for the life of me remember why I put that sleep in there.  Probably leftover debugging cruft.

Of course as you noticed the transfer is still more than a little slow.  I've identified a few places in the library where the same buffer gets duplicated and shufled around unnecessarily, they'll be trimmed down.

As to a full fledged ssh2_sftp_close() / ssh2_disconnect() type function:  Yes that could be done.  It would mean registering each channel with the session resource and each file handle with their respective SFTP resource, then itterating through that array/subarray structure to close them all down.

The trouble with that approach is allowing the connection variable to fall out of scope without triggering the shutdown mechanism unless no children are active.  If children were active when it fell out of scope, but they all go inactive later then the cleanup process does have to be triggered.  Again, doable, but hairy from an internal standpoint.  

I took the approach that made the most sense to me, from a top-down model.  Create resources (connections, channels, handles), then destroy resources (handles, channels, connections).

If you'd like to supply a patch which provides this functionality I'd be willing to review and possibly include it.  I'm not personally planning on working on that functionality, but who knows...
 [2005-05-02 16:44 UTC] felix at cdtinc dot ca
Do you think the sleep command can be safely removed?

Also, have you figured out why data transfer is so slow, is it a PHP only issue?

Thanks a lot for your help
 [2005-05-02 16:51 UTC] felix at zango dot com
[ignore] updated my email address to something that actually works
 [2005-05-03 15:58 UTC] felix at zango dot com
I was wondering why I saw so many
nanosleep({0, 250000000}, NULL)         = 0
in my stack trace.  After some investigating, I found this is caused by this:
libssh2/src/packet.c: libssh2_blocking_read()
...
ret = recv(session->socket_fd, buf + bytes_read, count - bytes_read, LIBSSH2_SOCKET_RECV_FLAGS(session));
... this line very often return -1 because of socket error: EAGAIN (normal non blocking msg)

for test purposes, I modified the function to use select() calls and the speed increased from 30k/sec to 150k/sec.  Basically I avoid all the 0.25 sec sleep that occures everytime a EAGAIN occures.  If you add this up for a normal buffer length of 8k, it means a maximum of 32kb/sec if each read returns -1 once.

Here is the new beginning I modified:

/* {{{ libssh2_blocking_read
 * Force a blocking read, regardless of socket settings
 */
static int libssh2_blocking_read(LIBSSH2_SESSION *session, unsigned char *buf, size_t count)
{
        size_t bytes_read = 0;
        int polls = 0;

        /* added socket select code */
        struct timeval timeout;
        timeout.tv_sec = 30;
        timeout.tv_usec = 0;
        int socket_count;

        while (bytes_read < count) {
                int ret;


                /* added socket select code */
                fd_set read_socket;
                FD_ZERO(&read_socket);
                FD_SET(session->socket_fd, &read_socket);
                socket_count = select(session->socket_fd+1, &read_socket, NULL, NULL, &timeout);

                if (socket_count <= 0) {
                        return -1;
                }


That's that.  I realize that your socket handling is not quite optimal and I suspect a similar problem also occures when establishing a connection.  In my debugging, I found thousands of recv returning EAGAIN before the connection was succesfully completed.

I was wondering if your streams support the set_buffer function as this would also help reduce the problem.

I hope this helps you complete your debugging and cleans the way for a stable package!
 [2005-05-18 00:50 UTC] pollita@php.net
Please try out libssh2 0.10 and pecl/ssh2 0.8
 [2005-10-23 09:19 UTC] mike@php.net
No feedback was provided. The bug is being suspended because
we assume that you are no longer experiencing the problem.
If this is not the case and you are able to provide the
information that was requested earlier, please do so and
change the status of the bug back to "Open". Thank you.


 [2008-05-14 09:06 UTC] volkirik at yahoo dot com dot tr
if you dont want to use fclose($stream), you can patch it!

for closing SFTP subsystems, Publickey Subsystems and Tunnels; use ssh2_subsys_close()

open ssh2.c file.

1) Add following lines before " PHP_FUNCTION(ssh2_fingerprint) " and after " /* }}} */ "


/* {{{ proto stream ssh2_subsys_close(stream channel)
 * Close the SFTP subsystem/Publickey Subsystem or a Listener and return a true on success, false on error
 */
PHP_FUNCTION(ssh2_subsys_close)
{
	zval *zsubsys;

	/* check & parse parameters */
	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &zsubsys) == FAILURE) {
		RETURN_FALSE;
	}

	/* decrease the refcount and call destructor if needed */
	if (zend_list_delete(Z_LVAL_P(zsubsys)) == FAILURE) {
		RETURN_FALSE;
	}

	/* destruct pointer */
	zval_ptr_dtor(&zsubsys);

	RETURN_TRUE;
}
/* }}} */


2) Declare the ssh2_subsys_close() function again by adding a line after the existing PHP_FE() line.

find following line :

	PHP_FE(ssh2_connect,						NULL)

after add:

	PHP_FE(ssh2_subsys_close,					NULL)


----

for closing channels (ssh2_exec, ssh2_shell) use ssh2_channel_close()

1) Add following lines to the end of "ssh2_fopen_wrappers.c" file


/* ******************
   * Channel Closer *
   ****************** */

/* {{{ proto stream ssh2_channel_close(stream channel)
 * Close a SSH2 Channel
 */
PHP_FUNCTION(ssh2_channel_close)
{
	php_stream *channel;
	zval *zchannel;

	/* check & parse parameters */
	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &zchannel) == FAILURE) {
        RETURN_FALSE;
    }

	php_stream_from_zval(channel, &zchannel);

	/* close php stream */
	php_stream_close(channel);

	/* destruct pointer */
	zval_ptr_dtor(&zchannel);

	RETURN_TRUE;
}
/* }}} */


2) Declare the ssh2_channel_close() function again by adding a line after the existing PHP_FE() line.

find following line (in ssh2.c) :

	PHP_FE(ssh2_fetch_stream,					NULL)

after add:

	PHP_FE(ssh2_channel_close,					NULL)


find following line (in php_ssh2.h) :

PHP_FUNCTION(ssh2_fetch_stream);

after add:

PHP_FUNCTION(ssh2_channel_close);
 [2012-06-13 21:43 UTC] langemeijer@php.net
-Status: No Feedback +Status: Open
 [2012-06-18 20:19 UTC] langemeijer@php.net
-Status: Open +Status: Closed -Assigned To: +Assigned To: langemeijer
 
PHP Copyright © 2001-2019 The PHP Group
All rights reserved.
Last updated: Wed Dec 11 13:01:24 2019 UTC