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
更多关于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客户端、数据库连接、文件操作和网络连接等资源密集型操作。

