Golang - 与多个节点的并发SSH连接

时间:2015-05-28 00:54:40

标签: ssh concurrency go channel

我有一组服务器,我正在尝试与之建立SSH连接,并且我正在为我必须建立的每个新SSH连接产生一个新的goroutine。然后,我将该连接的结果(以及错误(如果有的话))发送到通道,然后从通道读取。这个程序很有效,但即使我关闭了频道,它也会冻结。

这是我到目前为止所做的:

package main

import (
    "fmt"
    "net"
    "sync"

    "github.com/awslabs/aws-sdk-go/aws"
    "github.com/awslabs/aws-sdk-go/service/ec2"
)

// ConnectionResult container
type ConnectionResult struct {
    host    string
    message string
}

func main() {
    cnres := make(chan ConnectionResult)
    ec2svc := ec2.New(&aws.Config{Region: "us-east-1"})
    wg := sync.WaitGroup{}

    params := &ec2.DescribeInstancesInput{
        Filters: []*ec2.Filter{
            &ec2.Filter{
                Name: aws.String("instance-state-name"),
                Values: []*string{
                    aws.String("running"),
                },
            },
        },
    }

    resp, err := ec2svc.DescribeInstances(params)
    if err != nil {
        panic(err)
    }

    for _, res := range resp.Reservations {
        for _, inst := range res.Instances {
            for _, tag := range inst.Tags {
                if *tag.Key == "Name" {
                    host := *tag.Value
                    wg.Add(1)
                    go func(hostname string, cr chan ConnectionResult) {
                        defer wg.Done()
                        _, err := net.Dial("tcp", host+":22")
                        if err != nil {
                            cr <- ConnectionResult{host, "failed"}
                        } else {
                            cr <- ConnectionResult{host, "succeeded"}
                        }
                    }(host, cnres)
                }
            }
        }
    }

    for cr := range cnres {
        fmt.Println("Connection to " + cr.host + " " + cr.message)
    }

    close(cnres)

    defer wg.Wait()
}

我做错了什么?是否有更好的方法在Go中进行并发SSH连接?

3 个答案:

答案 0 :(得分:3)

上面的代码卡在range cnres for循环中。正如优秀的'Go by Example'所指出的那样,range只会在封闭的频道上退出。

解决这个难题的一种方法是在另一个goroutine中运行range cnres迭代。然后,您可以wg.Wait(),然后close()这个频道:

...
go func() {
        for cr := range cnres {
                fmt.Println("Connection to " + cr.host + " " + cr.message)
        }   
}() 
wg.Wait()
close(cnres)

在切线上(与被卡住的代码无关),我认为目的是在hostname函数和后续频道写入中使用Dial(),而不是host

答案 1 :(得分:1)

感谢Frederik,我能够成功运行:

package main

import (
    "fmt"
    "net"
    "sync"

    "github.com/awslabs/aws-sdk-go/aws"
    "github.com/awslabs/aws-sdk-go/service/ec2"
)

// ConnectionResult container
type ConnectionResult struct {
    host    string
    message string
}

func main() {
    cnres := make(chan ConnectionResult)
    ec2svc := ec2.New(&aws.Config{Region: "us-east-1"})
    wg := sync.WaitGroup{}

    params := &ec2.DescribeInstancesInput{
        Filters: []*ec2.Filter{
            &ec2.Filter{
                Name: aws.String("instance-state-name"),
                Values: []*string{
                    aws.String("running"),
                },
            },
        },
    }

    resp, err := ec2svc.DescribeInstances(params)
    if err != nil {
        panic(err)
    }

    for _, res := range resp.Reservations {
        for _, inst := range res.Instances {
            for _, tag := range inst.Tags {
                if *tag.Key == "Name" {
                    host := *tag.Value
                    publicdnsname := *inst.PublicDNSName
                    wg.Add(1)
                    go func(ec2name, cbname string, cr chan ConnectionResult) {
                        defer wg.Done()
                        _, err := net.Dial("tcp", ec2name+":22")
                        if err != nil {
                            cr <- ConnectionResult{cbname, "failed"}
                        } else {
                            cr <- ConnectionResult{cbname, "succeeded"}
                        }
                    }(publicdnsname, host, cnres)
                }
            }
        }
    }

    go func() {
        for cr := range cnres {
            fmt.Println("Connection to " + cr.host + " " + cr.message)
        }
    }()

    wg.Wait()
}

答案 2 :(得分:0)

Frederik的解决方案工作正常,但有一些例外。如果命令组例程(从写入通道的循环)执行命令的响应时间更长,则处理例程(Frederik的提示)将在最后一个命令例程完成之前处理并关闭通道,因此可能会发生数据丢失。

就我而言,我正在使用它对多个服务器执行远程SSH命令并打印响应。对我来说,有效的解决方案是使用2个单独的WaitGroup,一个用于命令组例程,另一个用于处理例程。这样,处理例程将等待所有命令例程完成,然后处理响应并关闭通道以退出循环:

// Create waitgroup, channel and execute command with concurrency (goroutine)
outchan := make(chan CommandResult)
var wg_command sync.WaitGroup
var wg_processing sync.WaitGroup
for _, t := range validNodes {
    wg_command.Add(1)
    target := t + " (" + user + "@" + nodes[t] + ")"
    go func(dst, user, ip, command string, out chan CommandResult) {
        defer wg_command.Done()
        result := remoteExec(user, ip, cmdCommand)
        out <- CommandResult{dst, result}
    }(target, user, nodes[t], cmdCommand, outchan)
}

wg_processing.Add(1)
go func() {
    defer wg_processing.Done()
    for o := range outchan {
        bBlue.Println(o.target, "=>", cmdCommand)
        fmt.Println(o.cmdout)
    }
}()

// wait untill all goroutines to finish and close the channel
wg_command.Wait()
close(outchan)
wg_processing.Wait()