Golang中如何处理"too many open files"错误

Golang中如何处理"too many open files"错误 你好,

我目前在一家公司工作,我们的一个网站遇到了一些小麻烦。该网站是用 Go 语言构建的。我遇到这个特定问题已经超过三个月了。无论我尝试了什么解决方案,最终都失败了。网站本身运行正常,但由于代码中的某些问题,存在某种泄漏,导致打开的文件没有被关闭,从而使服务器崩溃,网站宕机,直到 IT 部门重启服务器。到目前为止,我已经尝试处理了 “defer” 并移除了代码中所有没有返回类型的 defer。我们没有未关闭的资源。同时,我也提高了服务器的软硬文件 ulimit 限制。但网站仍然会随机崩溃,出现 “too many open file” 错误或 “broken pipe” 错误。

以下是 go env 信息:

GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/juhi/Library/Caches/go-build"
GOENV="/Users/juhi/Library/Application Support/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/Users/juhi/go/"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GCCGO="gccgo"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/z9/hr670btx0_gdq81bgc3q42w40000gn/T/go-build387581625=/tmp/go-build -gno-record-gcc-switches -fno-common"

附注:已附上日志截图,Go 版本为:go version go1.13.4 darwin/amd64

我也尝试过添加 “时间戳”,但这导致了更多错误,所以我们移除了它。


更多关于Golang中如何处理"too many open files"错误的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中如何处理"too many open files"错误的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


处理"too many open files"错误的关键是系统性地识别和关闭所有打开的资源。以下是一些具体的排查方法和代码示例:

1. 使用pprof监控文件描述符

import (
    _ "net/http/pprof"
    "net/http"
    "os"
    "syscall"
)

func main() {
    // 启动pprof监控
    go func() {
        http.ListenAndServe("localhost:6060", nil)
    }()
    
    // 定期检查文件描述符数量
    go func() {
        for {
            checkFileDescriptors()
            time.Sleep(30 * time.Second)
        }
    }()
}

func checkFileDescriptors() {
    pid := os.Getpid()
    fdDir := fmt.Sprintf("/proc/%d/fd", pid)
    
    if fds, err := os.ReadDir(fdDir); err == nil {
        fmt.Printf("当前打开文件数: %d\n", len(fds))
    }
}

2. 使用net/http/httptrace追踪HTTP连接

import (
    "net/http"
    "net/http/httptrace"
    "context"
)

func makeRequestWithTrace() {
    req, _ := http.NewRequest("GET", "http://example.com", nil)
    
    trace := &httptrace.ClientTrace{
        GotConn: func(connInfo httptrace.GotConnInfo) {
            fmt.Printf("获取连接: %+v\n", connInfo)
        },
        PutIdleConn: func(err error) {
            fmt.Printf("放回空闲连接: %v\n", err)
        },
    }
    
    req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return
    }
    defer resp.Body.Close()
}

3. 确保数据库连接正确关闭

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "time"
)

func setupDatabase() *sql.DB {
    db, err := sql.Open("mysql", "user:pass@/dbname")
    if err != nil {
        panic(err)
    }
    
    // 设置连接池参数
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(25)
    db.SetConnMaxLifetime(5 * time.Minute)
    
    return db
}

func queryWithDefer(db *sql.DB) {
    rows, err := db.Query("SELECT * FROM users")
    if err != nil {
        return
    }
    defer rows.Close() // 必须关闭rows
    
    for rows.Next() {
        // 处理数据
    }
}

4. 使用lsof命令实时监控

在服务器上运行:

# 监控Go进程打开的文件
watch -n 1 "lsof -p $(pgrep your-app-name) | wc -l"

# 查看具体打开的文件
lsof -p $(pgrep your-app-name) | head -20

5. 实现自定义的HTTP客户端超时控制

import (
    "net/http"
    "time"
)

var httpClient = &http.Client{
    Timeout: time.Second * 30,
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 100,
        IdleConnTimeout:     90 * time.Second,
        TLSHandshakeTimeout: 10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
    },
}

func makeRequest(url string) (*http.Response, error) {
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return nil, err
    }
    
    resp, err := httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    
    return resp, nil
}

6. 使用runtime.SetFinalizer追踪资源泄漏

import (
    "runtime"
    "sync/atomic"
    "fmt"
)

var fileCounter int64

type trackedFile struct {
    *os.File
    id int64
}

func openTrackedFile(name string) (*trackedFile, error) {
    f, err := os.Open(name)
    if err != nil {
        return nil, err
    }
    
    id := atomic.AddInt64(&fileCounter, 1)
    tf := &trackedFile{File: f, id: id}
    
    runtime.SetFinalizer(tf, func(tf *trackedFile) {
        fmt.Printf("文件 %d 被垃圾回收\n", tf.id)
    })
    
    return tf, nil
}

7. 检查常见的资源泄漏点

// 检查是否忘记关闭的常见模式
func checkCommonLeaks() {
    // 1. HTTP响应体必须关闭
    resp, err := http.Get("http://example.com")
    if err == nil {
        defer resp.Body.Close() // 必须要有
        io.Copy(ioutil.Discard, resp.Body) // 读取完响应体
    }
    
    // 2. 文件操作
    f, err := os.Open("file.txt")
    if err == nil {
        defer f.Close() // 必须要有
    }
    
    // 3. 网络连接
    conn, err := net.Dial("tcp", "localhost:8080")
    if err == nil {
        defer conn.Close() // 必须要有
    }
}

8. 使用go test进行压力测试

func TestFileDescriptorLeak(t *testing.T) {
    startFds := countOpenFiles()
    
    for i := 0; i < 1000; i++ {
        // 执行可能泄漏的操作
        resp, err := http.Get("http://localhost:8080/test")
        if err == nil {
            resp.Body.Close()
        }
    }
    
    endFds := countOpenFiles()
    
    if endFds > startFds+10 {
        t.Errorf("检测到文件描述符泄漏: 开始=%d, 结束=%d", startFds, endFds)
    }
}

func countOpenFiles() int {
    pid := os.Getpid()
    fdDir := fmt.Sprintf("/proc/%d/fd", pid)
    fds, err := os.ReadDir(fdDir)
    if err != nil {
        return -1
    }
    return len(fds)
}

运行这些监控和测试代码可以帮助你定位具体的泄漏点。重点检查HTTP客户端、数据库连接、文件操作和网络连接等资源密集型操作。

回到顶部