Paramiko 执行大 wget 命令时挂起

4 投票
2 回答
3967 浏览
提问于 2025-04-17 03:33

你好,我在一个Ubuntu 10的服务器上执行一个命令时遇到了问题,这个命令是用来下载一个100MB的文件。其他短一点的命令都能正常工作,只有这个不行。下面的代码块展示了我如何使用paramiko库,以及我尝试解决这个问题的不同方法(可以查看不同的run或exec方法)。在exec_cmd这个方法中,执行到这一行时就卡住了:

        out = self.in_buffer.read(nbytes, self.timeout)

这是来自paramiko库的channel.py模块中的recv方法。

而同样的wget命令在Mac上用普通的ssh工具在终端中执行时却完全没问题。

"""
Management of SSH connections
"""

import logging
import os
import paramiko
import socket
import time
import StringIO


class SSHClient():
    def __init__(self):
        self._ssh_client = paramiko.SSHClient()
        self._ssh_client.load_system_host_keys()
        self._ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self.time_out = 300
        self.wait = 5

    def connect(self, hostname, user, pkey):
        retry = self.time_out
        self.hostname = hostname
        logging.info("connecting to:%s user:%s key:%s" % (hostname, user, pkey))
        while retry > 0:
            try:
                self._ssh_client.connect(hostname,
                                         username=user,
                                         key_filename=os.path.expanduser(pkey),
                                         timeout=self.time_out)
                return
            except socket.error, (value,message):
                if value == 61 or value == 111:
                    logging.warning('SSH Connection refused, will retry in 5 seconds')
                    time.sleep(self.wait)
                    retry -= self.wait
                else:
                    raise
            except paramiko.BadHostKeyException:
                logging.warning("%s has an entry in ~/.ssh/known_hosts and it doesn't match" % self.server.hostname)
                logging.warning('Edit that file to remove the entry and then try again')
                retry = 0
            except EOFError:
                logging.warning('Unexpected Error from SSH Connection, retry in 5 seconds')
                time.sleep(self.wait)
                retry -= self.wait
        logging.error('Could not establish SSH connection')

    def exists(self, path):
        status = self.run('[ -a %s ] || echo "FALSE"' % path)
        if status[1].startswith('FALSE'):
            return 0
        return 1

    def shell(self):
        """
        Start an interactive shell session on the remote host.
        """
        channel = self._ssh_client.invoke_shell()
        interactive_shell(channel)

    def run(self, command):
        """
        Execute a command on the remote host.  Return a tuple containing
        an integer status and a string containing all output from the command.
        """
        logging.info('running:%s on %s' % (command, self.hostname))
        log_fp = StringIO.StringIO()
        status = 0
        try:
            t = self._ssh_client.exec_command(command)
        except paramiko.SSHException:
            logging.error("Error executing command: " + command)
            status = 1
        log_fp.write(t[1].read())
        log_fp.write(t[2].read())
        t[0].close()
        t[1].close()
        t[2].close()
        logging.info('output: %s' % log_fp.getvalue())
        return (status, log_fp.getvalue())

    def run_pty(self, command):
        """
        Execute a command on the remote host with a pseudo-terminal.
        Returns a string containing the output of the command.
        """
        logging.info('running:%s on %s' % (command, self.hostname))
        channel = self._ssh_client.get_transport().open_session()
        channel.get_pty()
        status = 0
        try:
            channel.exec_command(command)
        except:
            logging.error("Error executing command: " + command)
            status = 1
        return status, channel.recv(1024)

    def close(self):
        transport = self._ssh_client.get_transport()
        transport.close()

    def run_remote(self, cmd, check_exit_status=True, verbose=True, use_sudo=False):
        logging.info('running:%s on %s' % (cmd, self.hostname))
        ssh = self._ssh_client
        chan = ssh.get_transport().open_session()
        stdin = chan.makefile('wb')
        stdout = chan.makefile('rb')
        stderr = chan.makefile_stderr('rb')
        processed_cmd = cmd
        if use_sudo:
            processed_cmd = 'sudo -S bash -c "%s"' % cmd.replace('"', '\\"')
        chan.exec_command(processed_cmd)
        result = {
            'stdout': [],
            'stderr': [],
        }
        exit_status = chan.recv_exit_status()
        result['exit_status'] = exit_status

        def print_output():
            for line in stdout:
                result['stdout'].append(line)
                logging.info(line)
            for line in stderr:
                result['stderr'].append(line)
                logging.info(line)
        if verbose:
            print processed_cmd
            print_output()
        return exit_status,result 

    def exec_cmd(self, cmd):
        import select
        ssh = self._ssh_client
        channel = ssh.get_transport().open_session()
        END = "CMD_EPILOGqwkjidksjk58754dskhjdksjKDSL"
        cmd += ";echo " + END
        logging.info('running:%s on %s' % (cmd, self.hostname))
        channel.exec_command(cmd)
        out = ""
        buf = ""
        while END not in buf:
          rl, wl, xl = select.select([channel],[],[],0.0)
          if len(rl) > 0:
              # Must be stdout
              buf = channel.recv(1024)
              logging.info(buf)
              out += buf
        return 0, out

2 个回答

2
  1. 在这种情况下,我建议使用列表追加然后再合并。为什么呢?因为在Python中,字符串是不可变的。这意味着每次你使用 += 时,实际上是在创建两个新字符串,并且还要读取一个第三个字符串。而如果你先创建一个列表并往里面添加内容,就能减少创建字符串的数量。
  2. 你真的需要多次调用select吗?我的理解是,你并不太在乎这个过程是否会阻塞线程。因为 select 基本上是对同名C方法的一个封装:

    select() 和 pselect() 允许程序监控多个文件描述符,等待其中一个或多个文件描述符变得“准备好”进行某种输入输出操作(比如可以输入)。如果可以在不阻塞的情况下执行相应的输入输出操作(例如,read(2)),那么这个文件描述符就被认为是准备好的。

  3. 你的代码中并没有监听 socket.timeout 异常。
  4. 写入标准输出或文件系统可能会很耗资源,但你却在记录每一行 recv 返回的内容。你能把日志记录的那行代码移动一下吗?
  5. 你有没有考虑手动处理通道的读取?实际上,你只需要的代码是:
try:
    out = self.in_buffer.read(nbytes, self.timeout)
except PipeTimeout, e:
    # do something with error

虽然不能保证,但这样可以减少额外的处理。

4

我也遇到过同样的问题,我的Python脚本在远程SSH客户端运行一个shell脚本时卡住了,因为那个脚本在下载一个400MB的文件。

我发现给wget命令加上一个超时时间解决了这个问题。

最开始我用的是:

wget http://blah:8888/file.zip

现在我改成了:

wget -q -T90 http://blah:8888/file.zip

这样就顺利多了!

希望这对你有帮助。

撰写回答