Golang中net/http包导致too many open files问题如何解决

Golang中net/http包导致too many open files问题如何解决 我正在开发一个基于SAAS的项目,该项目使用REST API构建,可以拥有多个商户账户。我们需要为每个商户每10分钟运行一次定时任务。这些定时任务的URL是我们服务器自身的API。因此,我使用了Golang的net/http包来满足这个需求。

我遇到的错误: http: Accept error: accept tcp [::]:8080: accept4: too many open files; retrying in 1s 以及 connection() error occured during connection handshake: dial tcp IP:27017: socket: too many open files.

我已经尝试过的: 我尝试过使用 connection: close 请求头,移除 defer 并显式调用 req.Body.Close(),以及复用HTTP客户端来避免连接数超限。

请看一下代码:

package main

import (
	"gopkg.in/robfig/cron.v3"
)

func main() {
	RunCron()
}

func RunCron() {
	c := cron.New()
	c.AddFunc("@every 0h10m0s", SendMsg)
	c.Start()
}

func SendMsg() {
	RunCronBatch()
}

func RunCronBatch() {
	businessNames := []string{"bsName1", "bsName2","bsName3","bsName4"}
	client := &http.Client{}
	for _, businessName := range businessNames {
		url := "http://" + businessName + ".maim-domain.com"
		go func(client *http.Client, url string) {
			CallHttpRqstScheduler(client, "GET", url, false)
		}(client, url)
	}
}

func CallHttpRqstScheduler(client *http.Client, method, url string, checkResp bool, body ...io.Reader) (error, interface{}) {
	var reqBody io.Reader
	if len(body) > 0 {
		reqBody = body[0]
	} else {
		reqBody = nil
	}

	req, err := http.NewRequest(method, url, reqBody)
	if err != nil {
		fmt.Print(err.Error())
		return err, nil
	}
	req.Header.Set("Connection", "close")
	if method == "POST" {
		req.Header.Add("Content-Type", "application/json")
	}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Print(err.Error())
		return err, nil
	}
	if checkResp {
		bodyBytes, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			fmt.Print(err.Error())
			req.Close = true
			resp.Body.Close()
			return err, nil
		}
		var response APIResponseObj
		err = json.Unmarshal(bodyBytes, &response)
		if err != nil {
			fmt.Print(err.Error())
			req.Close = true
			resp.Body.Close()
			return err, nil
		}
		req.Close = true
		resp.Body.Close()
		return nil, response.Response.Data
	}
	req.Close = true
	resp.Body.Close()
	return nil, nil
}

但是这种结构仍然给我带来了上述错误。

历史情况: 在实现这种结构之前,我也曾将curl与sh命令结合使用。但那种结构同样会耗尽资源,因此遇到了资源暂时不可用的问题。

    package main

func main() {
    businessNames := []string{"bsName1", "bsName2","bsName3","bsName4"}
    for _, businessName := range businessNames {
        url := "http://" + businessName + ".main-domain.com"
        command := "curl '"+url+"'"
        ExecuteCommand(command)
    }
}

func ExecuteCommand(command string) error {
    cmd := exec.Command("sh", "-c", command)
    var out bytes.Buffer
    var stderr bytes.Buffer
    cmd.Stdout = &out
    cmd.Stderr = &stderr

    err := cmd.Run()

    if err != nil {
        fmt.Println(fmt.Sprint(err) + ": " + stderr.String())
        return err
    }
    fmt.Println("Result: " + out.String())
    return nil
}

有人能指导我一下,在HTTP方法中我做错了什么或者遗漏了什么吗?


更多关于Golang中net/http包导致too many open files问题如何解决的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

是的,企业名称可能有成千上万个。我在每个返回语句前都使用了 resp.Close()(也尝试过使用 defer)。同样尝试过使用工作池,但这个错误似乎仍未解决。https://play.golang.org/p/pMePF8zDvSH。

更多关于Golang中net/http包导致too many open files问题如何解决的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我猜这个错误来自操作系统。也许你是在一个默认的Linux安装环境下,理论上任何连接都会使用一个来自65535的文件描述符。在这种情况下,即使你正确地关闭了连接,系统也需要一段时间来释放描述符(请记住连接处于僵尸状态)。 所以,如果发生这种情况,你可以限制你的请求,或者调整你的操作系统以接受更多打开的文件(查找’ulimit’命令)。

businessNames 有多长?如果它有数千个或更多,你可能会耗尽可同时打开的连接数。如果是这种情况,不要为每个请求创建新的 goroutine,而是创建一个工作池(可能是 10 个、100 个等),通过通道发送业务名称,然后让一个消费者 goroutine 将结果取回。

我还建议在 client.Do(req) 之后检查 err != nil 后立即使用 defer resp.Close,以确保响应体不会泄漏并保持连接打开。

问题出在HTTP客户端没有正确复用和连接管理上。虽然你创建了单个http.Client,但在并发请求时没有控制并发数量,且没有配置连接池参数。以下是解决方案:

package main

import (
    "net/http"
    "time"
    "golang.org/x/net/http2"
    "gopkg.in/robfig/cron.v3"
)

func main() {
    RunCron()
}

func RunCron() {
    c := cron.New()
    c.AddFunc("@every 10m", SendMsg)
    c.Start()
    select {} // 保持程序运行
}

func SendMsg() {
    RunCronBatch()
}

func RunCronBatch() {
    businessNames := []string{"bsName1", "bsName2", "bsName3", "bsName4"}
    
    // 创建配置了连接池的HTTP客户端
    client := &http.Client{
        Transport: &http.Transport{
            MaxIdleConns:        100,
            MaxIdleConnsPerHost: 100,
            MaxConnsPerHost:     100,
            IdleConnTimeout:     90 * time.Second,
            DisableKeepAlives:   false, // 保持连接复用
        },
        Timeout: 30 * time.Second,
    }
    
    // 使用HTTP/2提升性能
    http2.ConfigureTransport(client.Transport.(*http.Transport))
    
    // 使用带缓冲的channel控制并发
    semaphore := make(chan struct{}, 50) // 限制最大50个并发
    
    for _, businessName := range businessNames {
        url := "http://" + businessName + ".maim-domain.com"
        semaphore <- struct{}{}
        go func(url string) {
            defer func() { <-semaphore }()
            CallHttpRqstScheduler(client, "GET", url, false)
        }(url)
    }
    
    // 等待所有goroutine完成
    for i := 0; i < cap(semaphore); i++ {
        semaphore <- struct{}{}
    }
}

func CallHttpRqstScheduler(client *http.Client, method, url string, checkResp bool, body ...io.Reader) (error, interface{}) {
    var reqBody io.Reader
    if len(body) > 0 {
        reqBody = body[0]
    }

    req, err := http.NewRequest(method, url, reqBody)
    if err != nil {
        return err, nil
    }
    
    // 不需要手动设置Connection头,Transport会处理
    if method == "POST" {
        req.Header.Set("Content-Type", "application/json")
    }
    
    resp, err := client.Do(req)
    if err != nil {
        return err, nil
    }
    defer resp.Body.Close() // 确保body被关闭
    
    if !checkResp {
        // 即使不检查响应也需要读取并关闭body
        io.Copy(io.Discard, resp.Body)
        return nil, nil
    }
    
    bodyBytes, err := io.ReadAll(resp.Body)
    if err != nil {
        return err, nil
    }
    
    var response APIResponseObj
    if err := json.Unmarshal(bodyBytes, &response); err != nil {
        return err, nil
    }
    
    return nil, response.Response.Data
}

同时需要调整系统文件描述符限制:

# 查看当前限制
ulimit -n

# 临时提高限制
ulimit -n 65536

# 永久修改(Linux)
# 在/etc/security/limits.conf中添加:
# * soft nofile 65536
# * hard nofile 65536

对于MongoDB连接问题,同样需要配置连接池:

// MongoDB连接示例
clientOptions := options.Client().
    ApplyURI("mongodb://localhost:27017").
    SetMaxPoolSize(100).          // 最大连接数
    SetMinPoolSize(10).           // 最小连接数
    SetMaxConnIdleTime(30 * time.Second)

client, err := mongo.Connect(context.Background(), clientOptions)

关键点:

  1. 正确配置http.Transport的连接池参数
  2. 使用defer resp.Body.Close()确保资源释放
  3. 通过channel控制并发数量
  4. 即使不读取响应体也需要消耗并关闭
  5. 调整系统文件描述符限制
回到顶部