当阻塞读取连接关闭时,为什么此go例程随机无法退出?

时间:2019-04-03 02:05:08

标签: go udp exit goroutine

当连接关闭时,为什么此接收器转到例程拒绝终止

这将按预期运行,但是随后每调用20-10,000x次,接收器将无法关闭,从而导致go例程泄漏,从而导致100%cpu。

注意:如果记录所有错误,则如果conn.SetReadDeadline被注释掉,我将在关闭的通道上看到读取。使用时,我将I / O超时视为错误。

此过程运行了10k个周期,其中主进程启动11对发送/接收器,并在主进程发送关闭信号之前处理1000个作业。此设置运行了6个小时以上,没有任何问题,一夜之间达到了10k个周期,但是今天早上我不能让它运行20个以上的周期,而不会让接收器标记为不关闭并退出。

func sender(w worker, ch channel) {

    var j job
    for {
        select {
        case <-ch.quit: // shutdown broadcast, exit
            w.Close()
            ch.stopped <- w.id // debug, send stop confirmed
            return

        case j = <-w.job: // worker designated jobs
        case j = <-ch.spawner: // FCFS jobs
        }

        ... prepare job ...

        w.WriteToUDP(buf, w.addr)

}

func receiver(w worker, ch channel) {

    deadline := 100 * time.Millisecond
out:
    for {
        w.SetReadDeadline(time.Now().Add(deadline))
        // blocking read, should release on close (or deadline)
        n, err = w.c.Read(buf)

        select {
        case <-ch.quit: // shutdown broadcast, exit
            ch.stopped <- w.id+100 // debug, receiver stop confirmed
            return
        default:
        }

        if n == 0 || err != nil {
            continue
        }
        update := &update{id: w.id}

         ... process update logic ...

        select {
        case <-ch.quit: // shutting down
            break out
        case ch.update <- update
        }

}

我需要一种可靠的方法来使接收器在收到关闭广播或关闭conn时关闭。从功能上讲,关闭通道应该足够了,根据go软件包documentation,这是首选方法,请参见Conn接口。

我升级到最新版本,即1.12.1,没有更改。 在开发中的MacOS上运行,在生产中的CentOS上运行。

有人遇到这个问题吗? 如果是这样,您如何可靠地修复它?


可能的解决方案

我的冗长而棘手的解决方案似乎可以解决该问题:

1)像这样(在上面,不变)在go例程中启动发送方

2)像下面这样在go例程中启动接收器

func receive(w worker, ch channel) {

    request := make(chan []byte, 1)
    reader := make(chan []byte, 1)

    defer func() {
        close(request) // exit signaling
        w.c.Close()    // exit signaling
        //close(reader)
    }()

    go func() {

        // untried senario, for if we still have leaks -> 100% cpu
        // we may need to be totally reliant on closing request or ch.quit
        // defer w.c.Close()

        deadline := 100 * time.Millisecond
        var n int
        var err error

        for buf := range request {
            for {
                select {
                case <-ch.quit: // shutdown signal
                    return
                default:
                }
                w.c.SetReadDeadline(time.Now().Add(deadline))
                n, err = w.c.Read(buf)
                if err != nil { // timeout or close
                    continue
                }
                break
            }
            select {
            case <-ch.quit: // shutdown signal
                return
            case reader <- buf[:n]:
                //default:
            }
        }
    }()

    var buf []byte

out:
    for {

        request <- make([]byte, messageSize)

        select {
        case <-ch.quit: // shutting down
            break out
        case buf = <-reader:
        }

        update := &update{id: w.id}

      ... process update logic ...


        select {
        case <-ch.quit: // shutting down
            break out
        case ch.update <- update
        }

    }

我的问题是,为什么这个可怕的版本2产生了一个新的go例程以从阻塞c.Read(buf)读取,似乎更可靠地工作,这意味着当发送关闭信号时,它不会泄漏;何时第一个简单得多的版本没有...而且由于c.Read(buf)的阻塞,似乎基本上是同一回事。

当这是一个合法且可验证的可重复问题时,降级我的问题无济于事,该问题仍未得到答复。

1 个答案:

答案 0 :(得分:0)

感谢大家的答复。

所以。从来没有堆栈跟踪。实际上,我根本没有任何错误,没有种族检测或其他任何东西,并且没有死锁,执行例程不会关闭并退出,并且不能始终如一地再现。我已经运行了两个星期的相同数据。

当go例程无法报告正在退出的例程时,它将简单地失控并将CPU驱动到100%,但是只有在所有其他例程退出并且系统继续运行之后,才会执行。我从来没有看到内存增长。 CPU会逐渐升高到200%,300%,400%,此时必须重新引导系统。

我记录了发生泄漏的时间,它总是不同的,并且在380次先前的成功运行(23对不正常运行的go例程)成功运行之后,下一次1832在一个接收器之前发生了一次泄漏泄漏,下一次只有23次,并且在相同的起点上咀嚼了完全相同的数据。泄漏的接收器刚刚失控,但只有在22个其他同伴组全部关闭并成功退出并且系统移至下一批后,该接收器才失控。除了保证在某些时候泄漏之外,它不会始终失败。

经过很多天,大量的重写,以及每次操作之前/之后都有一百万条日志,这似乎最终是问题所在,并且在浏览库之后,我不确定为什么会这样,为什么也不会随机发生

无论出于何种原因,如果您解析并直接跳过问题而不先阅读问题,golang.org / x / net / dns / dnsmessage库将随机出现。不知道为什么这很重要,你好,跳过问题意味着您不关心该标头部分并将其标记为已处理,并且它实际上可以连续运行一百万次,但实际上并没有,所以您似乎您必须先阅读一个问题,然后才能跳过所有问题,因为这似乎已成为解决方案。我有18,525个批次,并添加该功能可以关闭泄漏。

var p dnsmessage.Parser
h, err := p.Start(buf[:n])
if err != nil {
    continue // what!?
}

switch {
case h.RCode == dnsmessage.RCodeSuccess:
    q, err := p.Question() // should only have one question
    if q.Type != w.Type || err != nil {
        continue // what!?, impossible
    }
    // Note: if you do NOT do the above first, you're asking for pain! (tr)
    if err := p.SkipAllQuestions(); err != nil {
        continue // what!?
    }
    // Do not count as "received" until we have passed above point and
    // validated that response had a question that we could skip...