Golang运行时:epollwait在fd 3上失败并返回错误码9,这是Go的bug吗?

Golang运行时:epollwait在fd 3上失败并返回错误码9,这是Go的bug吗? 朋友们好!我是论坛的新手,很高兴能接受关于如何参与并成为一名优秀会员的指导。我遇到了一个问题,想请教大家的建议。

我在 Ubuntu 18.04.5 LTS 上运行 go version go1.14.1 linux/amd64,内核版本为 5.8.0-41-generic。我使用普通的 go build 进行构建,没有特殊的环境设置。我的程序在运行时栈上引发了一个运行时错误,原因是 epollwait() 失败并返回 EBADF。我可能误读了回溯信息,但我认为这并非由程序代码中的 goroutine 引起。通常,在 panic() 的转储信息中,我甚至看不到 runtime stack 部分。

untime: epollwait on fd 3 failed with 9
fatal error: runtime: netpoll failed

runtime stack:
runtime.throw(0xa18a04, 0x17)
        /usr/local/go/src/runtime/panic.go:1114 +0x72
runtime.netpoll(0x0, 0x0)
        /usr/local/go/src/runtime/netpoll_epoll.go:123 +0x363
runtime.findrunnable(0xc000034000, 0x0)
        /usr/local/go/src/runtime/proc.go:2126 +0xc60
runtime.schedule()
        /usr/local/go/src/runtime/proc.go:2520 +0x2fc
runtime.park_m(0xc000001680)
        /usr/local/go/src/runtime/proc.go:2690 +0x9d
runtime.mcall(0x0)
        /usr/local/go/src/runtime/asm_amd64.s:318 +0x5b

goroutine 1 [select]:
net/http.(*Transport).getConn(0xc0000eb540, 0xc00012abd0, 0x0, 0xc0000d86c0, 0x5, 0xc0000d8700, 0x13, 0x0, 0x0, 0x0, ...)
        /usr/local/go/src/net/http/transport.go:1291 +0x57b
net/http.(*Transport).roundTrip(0xc0000eb540, 0xc0000c5300, 0xc0000a6d60, 0xc00011b1f0, 0x40e488)
        /usr/local/go/src/net/http/transport.go:552 +0x726
net/http.(*Transport).RoundTrip(0xc0000eb540, 0xc0000c5300, 0xc0000eb540, 0x0, 0x0)
        /usr/local/go/src/net/http/roundtrip.go:17 +0x35
net/http.send(0xc0000c5300, 0xad9e40, 0xc0000eb540, 0x0, 0x0, 0x0, 0xc0000b4140, 0xc, 0x1, 0x0)
        /usr/local/go/src/net/http/client.go:252 +0x43e
net/http.(*Client).send(0xc00012a900, 0xc0000c5300, 0x0, 0x0, 0x0, 0xc0000b4140, 0x0, 0x1, 0xd)
        /usr/local/go/src/net/http/client.go:176 +0xfa
[... down through Hashicorp Vault client and application code ...]
main.main()
        /home/jmarks1/projects/20200124-loadvac/src/load_vac/load_vac.go:31 +0x53

goroutine 19 [chan receive]:
net/http.(*persistConn).addTLS(0xc0000c7440, 0xc0000d8700, 0xf, 0x0, 0xc0000d8710, 0x3)
        /usr/local/go/src/net/http/transport.go:1459 +0x1d3
net/http.(*Transport).dialConn(0xc0000eb540, 0xae4d40, 0xc0000b6de0, 0x0, 0xc0000d86c0, 0x5, 0xc0000d8700, 0x13, 0x0, 0xc0000c7440, ...)
        /usr/local/go/src/net/http/transport.go:1529 +0x1c5d
net/http.(*Transport).dialConnFor(0xc0000eb540, 0xc0000e0580)
        /usr/local/go/src/net/http/transport.go:1365 +0xc6
created by net/http.(*Transport).queueForDial
        /usr/local/go/src/net/http/transport.go:1334 +0x3fe

goroutine 5 [runnable]:
encoding/base64.(*Encoding).Decode(0xc0000ba000, 0xc000397200, 0x5d6, 0x5d6, 0xc00046601c, 0x7ca, 0x9e5, 0x0, 0x200, 0x0)
        /usr/local/go/src/encoding/base64/base64.go:471 +0x744
encoding/pem.Decode(0xc000466000, 0x801, 0xa01, 0x801, 0xa01, 0x0, 0x0)
        /usr/local/go/src/encoding/pem/pem.go:168 +0x766
crypto/x509.(*CertPool).AppendCertsFromPEM(0xc00007e780, 0xc000466000, 0x801, 0xa01, 0xa01)
        /usr/local/go/src/crypto/x509/cert_pool.go:131 +0x64
crypto/x509.loadSystemRoots(0x0, 0x7f1f0936ca88, 0xc000117628)
        /usr/local/go/src/crypto/x509/root_unix.go:75 +0x504
[... through X509 and associated code ...]
crypto/tls.(*Conn).clientHandshake(0xc000050e00, 0x0, 0x0)
        /usr/local/go/src/crypto/tls/handshake_client.go:206 +0x5ef
crypto/tls.(*Conn).Handshake(0xc000050e00, 0x0, 0x0)
        /usr/local/go/src/crypto/tls/conn.go:1340 +0xcc
net/http.(*persistConn).addTLS.func2(0x0, 0xc000050e00, 0xc0000200f0, 0xc0000740c0)
        /usr/local/go/src/net/http/transport.go:1453 +0x42
created by net/http.(*persistConn).addTLS
        /usr/local/go/src/net/http/transport.go:1449 +0x1aa

这是一个 Hashicorp Vault 客户端程序,这个问题似乎只在尝试向 Vault 发送特定数据时发生,并且仅针对某一个特定的 Vault 服务器。我推测可能是服务器版本不喜欢这个输入,或者……服务器线程/进程崩溃了……并在传输过程中断开了连接。(我不确定,也无法直接访问服务器。)

有趣的是,我无法从这个特定的 panic 中 recover()。在简单的主函数中,当 panic 发生时,延迟执行的函数没有被调用。如果我取消注释标记的那行 panic 代码(这绕过了 epollwait() 引起的 panic),这个 panic 可以被恢复的。

func main() {
        defer func() {
                fmt.Fprintln(os.Stderr, "DEFERRED FUNC RUNNING")

                if rval := recover(); rval != nil {
                        errQuit(errors.Errorf("%s", rval), "Failed")
                }
        }()

        // 如果我取消注释下面这行,延迟方法就会运行
        // panic("fooooo")

        if err := cmd.RootCmd.Execute(); err != nil {
                errQuit(err, "Failed")
        }
}

这种“无法恢复的魔法 panic”看起来像是 Golang 的 bug,还是仅仅是一种程序 bug?为什么这个 panic 无法恢复?从这些代码片段中,您能看出我是否做错了什么吗?

非常感谢您的帮助!


更多关于Golang运行时:epollwait在fd 3上失败并返回错误码9,这是Go的bug吗?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang运行时:epollwait在fd 3上失败并返回错误码9,这是Go的bug吗?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这不是Go语言的bug,而是你的程序在文件描述符管理上出现了问题。错误码9对应EBADF(Bad file descriptor),说明epoll正在监听一个无效的文件描述符。

问题通常发生在以下情况:

  1. 网络连接被意外关闭
  2. 文件描述符被错误地重用
  3. 并发访问导致竞态条件

从堆栈跟踪看,问题出现在net/http.(*Transport).getConn期间,这通常与HTTP连接管理有关。以下是一些可能的原因和解决方案:

1. 连接泄露或过早关闭

// 确保正确关闭响应体
resp, err := http.Get("http://example.com")
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close() // 必须关闭

body, err := io.ReadAll(resp.Body)
// 处理响应

2. 并发访问问题

// 使用sync.Once确保Transport只创建一次
var (
    transport *http.Transport
    once      sync.Once
)

func getTransport() *http.Transport {
    once.Do(func() {
        transport = &http.Transport{
            MaxIdleConns:        100,
            IdleConnTimeout:     90 * time.Second,
            TLSHandshakeTimeout: 10 * time.Second,
        }
    })
    return transport
}

3. 自定义Transport配置

transport := &http.Transport{
    DialContext: (&net.Dialer{
        Timeout:   30 * time.Second,
        KeepAlive: 30 * time.Second,
    }).DialContext,
    MaxIdleConns:          100,
    IdleConnTimeout:       90 * time.Second,
    TLSHandshakeTimeout:   10 * time.Second,
    ExpectContinueTimeout: 1 * time.Second,
}

client := &http.Client{
    Transport: transport,
    Timeout:   30 * time.Second,
}

4. 关于panic无法恢复的原因 运行时错误(如throw触发的panic)无法通过recover()捕获。这是Go的预期行为:

func main() {
    defer func() {
        // 这无法捕获runtime.throw()产生的panic
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
    
    // 这种panic可以被恢复
    panic("normal panic")
    
    // 但runtime.throw()产生的panic无法被恢复
}

5. 诊断文件描述符问题

// 检查当前进程打开的文件描述符
func checkFDs() {
    pid := os.Getpid()
    dir := fmt.Sprintf("/proc/%d/fd", pid)
    
    files, err := os.ReadDir(dir)
    if err != nil {
        log.Printf("无法读取fd目录: %v", err)
        return
    }
    
    log.Printf("进程 %d 打开了 %d 个文件描述符", pid, len(files))
}

建议的排查步骤:

  1. 升级到更新的Go版本(1.14.1较旧)
  2. 检查是否有并发访问共享的http.Client
  3. 确保所有响应体都被正确关闭
  4. 使用ulimit -n检查文件描述符限制
  5. 考虑使用连接池管理HTTP客户端

这个问题最可能的原因是HTTP连接在epoll仍在监听时被意外关闭,导致文件描述符无效。检查你的Vault客户端代码,确保连接管理正确,特别是处理错误和清理资源的部分。

回到顶部