網卡速率變化致使paramiko模塊timeout的失效,多線程超時控制解決辦法。

原由:python

      上週給幾個集羣的機器升級軟件包,每一個集羣大概兩千臺服務器,可是在軟件發包和批量執行命令的過程當中有兩個集羣都遇到了問題,在批量執行命令的時候老是會在後面卡住久久不能退出,最後只好手動殺掉進程。
git

    以下圖是sshpt批量執行命令時,到最後卡住久久不動,好久之後報出一個TypeError的異常,由於返回值不是列表而是一個異常對象,這個異常並非關鍵,只是代碼對結果類型判斷不全。真正緣由應該是某臺機器執行命令久久沒有返回致使線程不能退出。github

image.png

經過增長debug日誌,我找到了這臺有問題的機器。而後ssh上去看看,在ssh登陸的過程當中,異常的緩慢卡頓。bash

經過ethtool命令,發現了問題,網卡變成百兆了,根據經驗,是因爲可能網卡或者網線老化致使的。
服務器


6900BBC1-FA1A-47B6-A3AD-AF3986F41446.png


這個的確是機器的問題,ssh很是慢,致使長時間的卡住,那麼如何解決這個問題呢。session

這時想到sshpt支持timeout參數,可否經過設置timeout,使執行超時的線程退出,而不是卡在那裏。
ssh

sshpt  —help 
-T <seconds>, --timeout=<seconds>
                Timeout (in seconds) before giving up on an SSH
                connection (default: 30)


然鵝,加上--timeout參數後並無什麼卵用。。。ide

看了一下sshpt關於timeout的代碼,發現這個timeout僅僅在ssh.connect中用到,而exec_command調用中並無傳入timeout。
函數

def paramikoConnect(host, username, password, timeout, port=22):     
    """Connects to 'host' and returns a Paramiko transport object to use in further communications"""
    # Uncomment this line to turn on Paramiko debugging (good for troubleshooting why some servers report connection failures)
    #paramiko.util.log_to_file('paramiko.log')
    ssh = paramiko.SSHClient()
    try:
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        #ssh.connect中傳入timeout
        ssh.connect(host, port=port, username=username, password=password, timeout=timeout)  
    except Exception, detail:
        # Connecting failed (for whatever reason)
        ssh = str(detail)
    return ssh

def sudoExecute(transport, command, password, run_as='root'):
    """Executes the given command via sudo as the specified user (run_as) using the given Paramiko transport object.
    Returns stdout, stderr (after command execution)"""
    #exec_command中並無timeout控制
    stdin, stdout, stderr = transport.exec_command("sudo -S -u %s %s" % (run_as, command))   
    if stdout.channel.closed is False: # If stdout is still open then sudo is asking us for a password
        stdin.write('%s\n' % password)
        stdin.flush()
    return stdout, stderr

測試發現,這臺100M網卡的機器在ssh.connect的過程當中並無超時,而是主要卡在命令執行的調用exec_command函數測試


其實paramiko的代碼中是支持對exec_command的timeout參數傳入

class SSHClient (ClosingContextManager):
    def exec_command(
        self,
        command,
        bufsize=-1,
        timeout=None,    #支持timeout
        get_pty=False,
        environment=None,
    ):
   
        chan = self._transport.open_session(timeout=timeout)
        if get_pty:
            chan.get_pty()
        chan.settimeout(timeout)
        if environment:
        chan.update_environment(environment)
        chan.exec_command(command)
        stdin = chan.makefile('wb', bufsize)
        stdout = chan.makefile('r', bufsize)
        stderr = chan.makefile_stderr('r', bufsize)
        return stdin, stdout, stderr


因而修改了sshpt的sudoExecute代碼,加上一個timeout=10測試一下

def sudoExecute(transport, command, password, run_as='root'):
    """Executes the given command via sudo as the specified user (run_as) using the given Paramiko transport object.
    Returns stdout, stderr (after command execution)"""
    #原來沒有timeout參數
    #stdin, stdout, stderr = transport.exec_command("sudo -S -u %s %s" % (run_as, command))
    #加入timeout=10秒,執行10秒超時
    stdin, stdout, stderr = transport.exec_command("sudo -S -u %s %s" % (run_as, command),timeout=10)   
    if stdout.channel.closed is False: # If stdout is still open then sudo is asking us for a password
        stdin.write('%s\n' % password)
        stdin.flush()
    return stdout, stderr


結果呢,依然仍是卡住了。。。


增長debug日誌。

     def exec_command(
        self,
        command,
        bufsize=-1,
        timeout=None
        get_pty=False,
        environment=None,
    ):
        #調用open_session()
        chan = self._transport.open_session(timeout=timeout)
        def open_session(
        self,
        window_size=None,
        max_packet_size=None,
        timeout=None,
      ):
        #調用open_channel()
        return self.open_channel('session',
                                 window_size=window_size,
                                 max_packet_size=max_packet_size,
                                 timeout=timeout)
def open_channel(self,
                     kind,
                     dest_addr=None,
                     src_addr=None,
                     window_size=None,
                     max_packet_size=None,
                     timeout=None):
        if not self.active:
            raise SSHException('SSH session not active')
        timeout = 3600 if timeout is None else timeout
        ....
        #timeout判斷代碼
        start_ts = time.time()
        while True:
            event.wait(0.1)
            if not self.active:
                e = self.get_exception()
                if e is None:
                    e = SSHException('Unable to open channel.')
                raise e
            if event.is_set():
                break    #經過加日誌發現,在這裏成功break跳出了timeout的判斷邏輯。
            elif start_ts + timeout < time.time():
                raise SSHException('Timeout opening channel.')
        chan = self._channels.get(chanid)
        # 可是執行_channels.get(chanid)的時候卡住沒有返回。
        # 這說明這種狀況下get(chanid)並無在timeout的判斷邏輯中,因此設置timeout並無起到做用
        if chan is not None:
            return chan
        e = self.get_exception()
        if e is None:
            e = SSHException('Unable to open channel.')
        raise e


未完待續



在github上提的issues

https://github.com/paramiko/paramiko/issues/1150

相關文章
相關標籤/搜索