Linux Tcp 端口耗尽

June 08, 2019

Linux Tcp 端口耗尽

Tcp连接中如果并发占用的端口过多,就会报socket: too many open files。我们一个正常的service可以提供多大并发的连接呢?

端口数量的理论限制

每个TCP连接在操作系统中由4元组唯一标识,包括:

(local ip, local port, peer ip, peer port)

如果客户端和服务端在统一机器的话local ippeer ip是一样的,并且peer port是固定的,就是服务端提供的端口,唯一变化的就是local port这个变量,这个值是一个16字节的数,所以最大值为65536,这个是理论上的限制。

实际中端口限制

1、当前用户进程可打开的文件数限制

# 通过ulimit 这个命令可以查看操作系统分配给每个进程可以拥有的文件具柄数量。
$ ulimit -n
1024

2、ip范围

# 这个配置文件中规定了最小端口和最大端口,理论上可以分配的最多端口就是最大端口减去最小端口。
$ cat /proc/sys/net/ipv4/ip_local_port_range
32768	60999

3、linux 硬件上支持可以打开的最大文件连接数

这个参数的默认值是跟内存大小有关系的,增加物理内存以后重启机器,这个值会增大。大约1G内存10万个句柄的线性关系。这个值是可以修改的,但是不要轻易修改。

# 查看最大文件具柄数
$ cat /proc/sys/fs/file-max 
791038
# 查看内存信息
$ cat /proc/meminfo
MemTotal:        8009808 kB
# 查看当前使用的文件具柄
$ cat /proc/sys/fs/file-nr 
5568	0	791038
5568:已分配文件句柄的数目    
0:   分配了但没有使用的句柄数  
791038:文件句柄最大数目

端口数量可分配的限制是由最小值限制的,比如上面的三种情况中的1,用户进程可分配的文件句柄为1024,那么就只可以创建这么多的。

修改可支持的文件具柄数

1、修改用户进程可支持的最大文件具柄数。

如果单个进程想要支持更多的文件具柄可以调大,但是对于tcp来说最多就只支持65535个端口数。

# 编辑如下的文件,并加入下入的内容
$ vim /etc/security/limits.conf
* soft nofile 65535
* hard nofile 65535

上述的配置修改之后当前的会话并不能马上生效,现在使用ulimit -n查看,值仍然是1024,可以使用ulimit -n 65535这样的方式让这次的会话生效。

2、ip范围端口也可更改范围

$ vim /proc/sys/net/ipv4/ip_local_port_range
1024	65535

把可分配的端口范围调大一些。端口分配有上限,就是65535,只能把下限往下调整,一般情况1024一下的端口分配的操作系统使用,所以不要调整到1024一下去。

测试

服务端代码

创建一个service,并监听8899端口,暴露一个/test请求,这个请求里面有一个休眠,休眠时间为一个小时,我了模拟并发,把所有的请求都阻塞,不返回。

package main

import (
	"fmt"
	"net/http"
	"time"
)

func test(w http.ResponseWriter, r *http.Request) {
	fmt.Println(r.RemoteAddr,r.RequestURI)
	time.Sleep(60*time.Minute)
	fmt.Fprint(w, "this is test https service")
}



func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/test", test)
	//http.ListenAndServeTLS(":8899", "server.crt", "server.key", nil)
	fmt.Println(http.ListenAndServe(":8899", mux))
}

客户端代码

客户端通过添加请求的数量和请求地址来进行模拟并发请求,保证每个请求都不被释放端口占用。这样可以用于检验上述的配置是否有效。

package main

import (
	"errors"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	_ "net/http/pprof"
	"sync"
)

var (
	num *int
	url *string
)


var (
	client = http.DefaultClient
)

func init()  {
	num = flag.Int("num",1,"http request number")
	url = flag.String("url","","http request url")
	flag.Parse()
	if *url == "" {
		panic(errors.New("request url is not nil"))
	}
}

func gorequest(wait *sync.WaitGroup)  {
	resp, err := http.Get(*url)
	if err != nil  {
		fmt.Println(err)
		return
	}
	defer resp.Body.Close()
	bytes, _ := ioutil.ReadAll(resp.Body)
	fmt.Println("response:",string(bytes))
	defer wait.Done()
}

func main() {
	var wait sync.WaitGroup
	wait.Add(*num)
	for i := 0; i < *num ; i++ {
		go gorequest(&wait)
	}
	wait.Wait()
}

参考文章


LRF 记录学习、生活的点滴