Go问答 一个并发查询 A 记录的工具导致 cpu 大量占用的问题

yulinsec · 2020年02月23日 · 最后由 yulinsec 回复于 2020年02月23日 · 1179 次阅读

如题,最近在写一个高并发查询大量域名 A 记录的工具,使用的是 github.com/miekg/dns 这个包,基本思路是给定一个域名列表和 dns 服务器列表,将 dns 服务器做成循环链表,将 dns 服务器与域名一一对应存入 map 然后交给 worker 解析,当域名大约在 10 万以上的,goroutine 在 2000 左右就会出现大量占用 cpu 的情况。请问各位有没有写过类似程序的经验,请教一下应该是哪里出问题了。代码如下。

package main

import (
    "bufio"
    "container/ring"
    "errors"
    "fmt"
    "github.com/miekg/dns"
    cmap "github.com/orcaman/concurrent-map"
    "log"
    "os"
    "time"
)

var (
    client = new(dns.Client)

)
var (
    signalsnum = 0
)

func init()  {
    client.Timeout = time.Duration(500*time.Millisecond)
}
func lookupA(fqdn, serverAddr string) ([]string, error) {

    var ips []string
    m1 := new(dns.Msg)
    m1.Id = dns.Id()
    m1.RecursionDesired = true
    m1.Question = make([]dns.Question, 1)
    m1.Question[0] = dns.Question{dns.Fqdn(fqdn), dns.TypeA, dns.ClassINET}
    in, _, err := client.Exchange(m1, serverAddr+":53")
    if err != nil {
        return ips,err
    }
    if len(in.Answer) < 1 {
        return ips, errors.New("no answer")
    }
    for _, answer := range in.Answer {
        if a, ok := answer.(*dns.A); ok {
            ips = append(ips, a.A.String())
            return ips,nil
        }
    }
    return ips, nil
}

func worker(dnsmapkey chan string,gather chan string,m cmap.ConcurrentMap) {
    for  {
        select {
        case name := <-dnsmapkey:
            dnskey, _ := m.Get(name)
            dnserver := fmt.Sprintf("%s", dnskey)
            //fmt.Println(name,"========",dnserver)
            result, err := lookupA(name+".lenovo.com", dnserver)
            if err != nil {
                //有err就认为解析失败,加入失败队列。
            }
            if len(result) > 0 {
                signalsnum++
                gather <- name + "===" + result[0] + "===" + dnserver
            }
            m.Remove(name)
        }


    }
}
func filelines(name string) int {
    line := 0
    file,err := os.Open(name)
    if err != nil{
        log.Fatal(err)
    }
    defer file.Close()
    scanner := bufio.NewScanner(file)
    for scanner.Scan(){
        line++
    }
    return line
}

var (
    hashMapSize = 3000
)
func main() {
    //profile.Start().Stop()
    r := ring.New(filelines("../dns.txt"))
    //var results []string
    dnsmapkey := make(chan string, hashMapSize)
    m := cmap.New()
    gather := make(chan string)
    fh, err := os.Open("./hostname.txt")
    if err != nil {
        panic(err)
    }
    defer fh.Close()
    dnsfile, err := os.Open("../dns.txt")
    if err != nil {
        panic(err)
    }
    defer dnsfile.Close()
    namescanner := bufio.NewScanner(fh)
    dnscanner := bufio.NewScanner(dnsfile)
    for dnscanner.Scan(){
        r.Value = dnscanner.Text()
        r = r.Next()
    }

    for i := 0; i < hashMapSize; i++ {
        go worker(dnsmapkey,gather,m)
    }

    for namescanner.Scan() {
        name := namescanner.Text()
        dns := fmt.Sprintf("%s",r.Value)
        m.Set(name,dns)
        dnsmapkey <- name
        r = r.Next()
    }
    close(dnsmapkey)
    for i:=0;i<signalsnum;i++{
        fmt.Println(<-gather)
    }


}
更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio

试一下 pprof 看一下情况,怀疑是并发 map 的问题

2楼 已删除
astaxie 回复

谢答。已经查过啦,发现占用最多的就是调 lookup 的时候。然后 map 的话加上去掉没啥区别。

yulinsec 回复

也试过单 dns,直接传域名,但是只要 goroutine 上去了,cpu 就会上来。

yulinsec 回复

goroutine 上来不怕的,主要是看 goroutine 之间是不是有竞争等待关系

astaxie 回复

emmm,您可以详细说说吗。我自己的理解是,查询 dns 的话应该是一个 io 为主的程序,有没有可能是等待时间太短,一次查询结束的太快的话,就导致 io 其实是上不去的。

7楼 已删除
yulinsec 回复

可以试试 Scheduler Trace

GODEBUG=schedtrace=1000 ./myserver
astaxie 回复

这是开 6000 goroutine 的结果,这是不是意味着,很多 goroutine 都没被用到,一直在饥饿。

yulinsec 回复

可以参考一下这个看看,从你的输出来看就用到了一个 core,实际上必然会有阻塞 https://github.com/golang/go/wiki/Performance#scheduler-trace

astaxie 回复

是用单核服务器跑的,github 其实是有一个叫 massdns 的工具做类似的事情,他是用 c 的写,速度特别快,但是 cpu 占用却很少。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册