Go协程并发4万次HTTP请求遇dial tcp地址分配失败问题求助
问题描述
使用Go协程并发发起HTTP请求时,迭代次数达到40000次会报错:dial tcp 127.0.0.1:3333: can't assign requested address,20000-30000次可正常运行,期望支持100万次并发迭代。
相关代码
func TestHttp(c *gin.Context) { limit, _ := strconv.Atoi(c.Param("limit")) for i := 1; i <= limit; i++ { url := "http://hostname/merchant" + strconv.Itoa(i) + "/print-test?p=" + strconv.Itoa(i) go MakeRequest2(url, i) } } /* * Function to hit url with get method and close the response body */ func MakeRequest2(url string, i int) { client := &http.Client{} req, err := http.NewRequest("GET", url, nil) if err != nil { fmt.Println(err) return } req.Close = true resp, err := client.Do(req) if err != nil { fmt.Println(err) return } defer resp.Body.Close() } func PrintTest(c *gin.Context) { a := make(map[string]int) for i := 1; i <= 10; i++ { a[strconv.Itoa(i)] = i } fmt.Println(a) }
报错信息
Get "http://hostname/merchant34096/print-test?p=34096": dial tcp hostname: connect: cannot assign requested address
解决方案
错误原因
这个报错是因为系统的临时端口耗尽。每个TCP客户端连接都会占用一个本地临时端口,默认情况下系统临时端口范围有限(比如Linux通常是32768-60999,共约2.8万个)。当并发发起4万次请求时,短时间内创建的大量TCP连接会占满所有临时端口,后续请求无法分配新端口,从而触发报错。
另外代码里的两个操作会加重问题:
- 每次请求都新建
http.Client,没有复用连接池,每个请求都创建全新TCP连接,加速端口消耗。 - 设置
req.Close = true,强制请求完成后关闭连接,无法利用HTTP长连接复用端口。
解决步骤
复用全局
http.Client连接池:
Go的http.Client默认自带连接池,全局只初始化一次,所有请求复用它,这样可以重复利用TCP连接,大幅减少端口占用。移除
req.Close = true:
该设置会让连接在请求结束后立即关闭,无法进入连接池复用,删除后连接会保持活跃,可被后续请求复用。控制并发数:
即使复用连接池,100万次直接并发也会对系统和目标服务造成极大压力,用带缓冲的通道做并发限流器,比如限制同时运行的协程数为1000(可根据服务器配置调整),避免瞬间请求量过载。调整系统临时端口参数(可选):
如果需要更大的并发量,可以修改系统参数扩大临时端口范围并加快连接回收:- Linux系统执行以下命令:
# 扩大临时端口范围 echo "net.ipv4.ip_local_port_range = 1024 65535" >> /etc/sysctl.conf # 加快TIME_WAIT状态连接回收 echo "net.ipv4.tcp_tw_reuse = 1" >> /etc/sysctl.conf echo "net.ipv4.tcp_tw_recycle = 1" >> /etc/sysctl.conf # 生效配置 sysctl -p
- Linux系统执行以下命令:
修改后的示例代码
import ( "net/http" "strconv" "sync" "time" ) // 全局复用的http.Client,仅初始化一次 var httpClient = &http.Client{ Transport: &http.Transport{ MaxIdleConns: 1000, // 全局最大空闲连接数 MaxIdleConnsPerHost: 100, // 每个主机的最大空闲连接数 IdleConnTimeout: 30 * time.Second, // 空闲连接超时时间 }, } func TestHttp(c *gin.Context) { limit, _ := strconv.Atoi(c.Param("limit")) // 设置并发限制,这里限制为1000个协程同时运行 sem := make(chan struct{}, 1000) var wg sync.WaitGroup wg.Add(limit) for i := 1; i <= limit; i++ { sem <- struct{}{} // 传入循环变量i的副本,避免协程引用同一变量 go func(i int) { defer func() { <-sem wg.Done() }() url := "http://hostname/merchant" + strconv.Itoa(i) + "/print-test?p=" + strconv.Itoa(i) MakeRequest2(url, i) }(i) } wg.Wait() } func MakeRequest2(url string, i int) { req, err := http.NewRequest("GET", url, nil) if err != nil { fmt.Println(err) return } // 移除req.Close = true,让连接进入连接池复用 resp, err := httpClient.Do(req) if err != nil { fmt.Println(err) return } defer resp.Body.Close() } func PrintTest(c *gin.Context) { a := make(map[string]int) for i := 1; i <= 10; i++ { a[strconv.Itoa(i)] = i } fmt.Println(a) }
内容的提问来源于stack exchange,提问作者sahil garg
相关产品推荐
相关产品推荐

