golang并发ssh执行远程命令

来源:互联网 发布:淘宝信息发布平台 编辑:程序博客网 时间:2024/05/29 19:28

需求

在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

原创粉丝点击