美文网首页
golang并发ssh执行远程命令

golang并发ssh执行远程命令

作者: yiduyangyi | 来源:发表于2018-02-01 21:27 被阅读142次

    需求

    在kubernetes/docker容器化应用中,业务应用由大量容器组成,由于生产环境中出于安全考虑,一般不会允许用户直接登入集群机器,然后登入机器上的容器。况且数量之多,也没有效率。因此设计了一个命令行工具,以权限受控的账号ssh远程连接到容器所在宿主机,然后docker exec到容器内执行命令。而且该过程必须能够批量化的进行。

    实现

    下面是并发执行远程ssh命令的核心实现

        jobs := make(chan *model.Command, len(instanceList))
        results := make(chan *model.CommandResult, len(instanceList))
    
        // 开启多个goroutine去远程登入容器,执行命令
        for e := 1; e <= parallelism; e++ {
            go service.Executor(e, jobs, results)
        }
        
        for _, ins := range instanceList {
            jobs <- &model.Command{
                Host:         ins.Host,
                ContainerId:  ins.ContainerId,
                Command:      cmd,
            }
        }
        close(jobs)
    
        failCount := 0
        size := len(instanceList)
        for j := 1; j <= size; j++ {
            rst := <-results
            success := "Success"
            if rst.CmdError != nil {
                success = "Fail"
                failCount++
            }
            fmt.Printf("[%d/%d] - [%s]\t", j, size, success)
            fmt.Printf("Host = %s, ContainerId = %s, rst.Host, rst.ContainerId)
            fmt.Println(rst.Output)
            if rst.CmdError != nil {
                if ee, ok := rst.CmdError.(*exec.ExitError); ok {
                    waitStatus := ee.Sys().(syscall.WaitStatus)
                    fmt.Printf("%d\n", waitStatus.ExitStatus())
                }
                fmt.Printf("%s\n", rst.CmdError.Error())
            }
        }
        //结果汇总
        fmt.Printf("[INFO] Total = %d, Success = %d, Fail = %d", size, size-failCount, failCount)
    

    下面是service.Executor的关键代码

    func Executor(jobs <-chan *model.Command, jobResults chan<- *model.CommandResult) {
        for job := range jobs {
            out, err := ExecuteCommandInContainer(job.Host, job.ContainerId, job.Command)
            jobResults <- &model.CommandResult{
                CmdError:     err,
                ContainerId:  job.ContainerId,
                Host:         job.Host,
                Output:       out,
            }
        }
    }
    
    // 登录容器,执行一个具体的命令
    func ExecuteCommandInContainer(host string, containerId string, command string) (out string, err error) {
        err = AddRsafile()
        if err != nil {
            return
        }
        homeDir := os.Getenv("HOME")
        dockerHost := fmt.Sprintf(`rd@%s`, host)
        containerLoginCmd := fmt.Sprintf("sudo docker exec -it -u rd %s bash -c \"%s\"", containerId, command)
        cmd := exec.Command("ssh", "-i", homeDir+"/.ssh/.id_rsa",
            "-oUserKnownHostsFile=/dev/null", "-oStrictHostKeyChecking=no",
            "-t", "-t", dockerHost, containerLoginCmd)
    
        cmd.Stdin = os.Stdin
    
        b, err := cmd.Output()
        if err != nil {
            return
        }
        out = string(b)
        return
    }
    
    

    问题

    上述代码,编译成二进制可执行文件后,在shell终端里执行,当并发度大于1时,终端会被打乱,同时执行完了之后,终端已经假死,必须reset才能继续使用。但是,放到crontab里执行时,并无该问题,这是为什么?

    追踪

    初步怀疑是ssh并发写终端stdout问题,但是代码中明明是串行写的。于是去查ssh相关参数的用法。
    注意到,上面ssh命令,带有2个-t参数,这是做什么的?参见ssh的帮助

    -T      Disable pseudo-tty allocation.
    
    -t      Force pseudo-tty allocation.  This can be used to execute arbitrary screen-based programs on a remote machine, which can be very useful, e.g., when implementing menu services.  Multiple -t options force tty allocation,
                 even if ssh has no local tty.
    

    两个-t是强制ssh分配tty,尝试去掉一个,我们发现,在命令行里执行并没有什么问题,但是在crontab里就有问题了,会提示

    Pseudo-terminal will not be allocated because stdin is not a terminal. 
    

    首先crontab是非登录式shell的环境,分配伪终端时,无法将stdin分配为一个terminal,也就是上面提示的含义。使用2个-t,强制分配。完美解决了crontab里无法正确执行的问题。但是并发执行是什么问题呢?

    受到这个启发,由初期怀疑是并发写到控制台导致的,转入怀疑是多个ssh的线程公用了同一个stdin导致的,因为上述代码中,设定了cmd.Stdin = os.Stdin,于是将cmd.Stdin = nil, 本来这个工具也无需输入,调整之后,并发执行问题完美解决。

    参考

    有关如何在golang中执行shell命令,可参考这篇文章 Shelled-out Commands In Golang

    相关文章

      网友评论

          本文标题:golang并发ssh执行远程命令

          本文链接:https://www.haomeiwen.com/subject/qzujzxtx.html