【参考资料】
【1】https://colobu.com/2019/02/23/1m-go-tcp-connection/
【2】https://www.cnblogs.com/lojunren/p/3856290.html
感谢下smallnest的这三篇博文,这是我最近几年看过最好的技术博文,几乎没有之一:)
一、前置知识点
linux参数调优
- linux下每个socket都是一个文件描述符,因此系统配置的最大文件打开数量和一个进程能够打开的数量就决定了socket的数目上限;
- limits.conf文件是pam_limits.so的配置文件, pam_limits是对用户会话的资源进行限制;
2.1 文件路径 cat /etc/security/limits.conf
2.2 limix.conf的主要内容如下:
1)<domain>: 表示限制的作用范围,可以是用户也可以是组
2)<type>: hard和soft分别代表硬限制和软限制,这里的限制是为了防止失控的进程影响系统的性能。
其中硬限制表示由超级管理员发起的无论如何不可以超过的限额。
而软限额是一个小于硬限额的值,允许普通进程在一定时间范围内超过(这个时间由ufs_quota.h设置)
3)<item>: 限制的内容,主要包括core(内容文件大小)、data(最大文件大小)、fszie(最大文件大小)、nofile(单个进程打开最大文件数目)、noproc(进程最到数目)等
4)<value>: 设置的值
2.3 在调优中我们主要将nofile改大
* soft nofile 2065535
* hard nofile 2065535
- sysctl.conf是对整个系统的内核参数。通常我们采用sysctl命令时临时且立即生效的,但重启后会失效,永久修改的话就需要修改sysctl.conf文件。
3.1 文件路径: cat /etc/sysctl.conf
3.2 sysctl.conf 主要有如下一些参数可以调优:
1)fs.file-max=2000500 : 表示所有进程可以打开文件描述符数量
2)sysctl -w fs.nr_open=2000500 : 是limits.conf里nofile的上限
3)net.nf_conntrack_max=2000500 : 允许的最大跟踪连接条目,
是在内核内存中netfilter可以同时处理的任务,如果我们开启了iptable那么这个值也会限制最大连接数
4)net.ipv4.tcp_mem='131072 262144 524288' :
内核分配给tcp连接的内存,单位是page,一个page为4096 Bytes;
第一个值表示小于该值内核不做干预
第二个值表示超过后,内核进入压力模式
第三个值表示超过后,内核报Out of socket memory,
对于8G内存的服务器,那么这个值一般为4G,即1048576,
建议net.ipv4.tcp_mem = 524288 699050 1048576
5)net.ipv4.tcp_rmem='8760 256960 4088000' 和
net.ipv4.tcp_wmem='8760 256960 4088000' :
为每个TCP连接的读写缓存,单位是Byte
三个数值分别表示最小内存、缺省内存和最大内存
上面这个例子就是读写内存都是250K(按缺省值算),这样每个TCP连接消耗500KB
因此100万连接消耗内存约476G内存,这个值文章的配置是不是有问题,看其他文章一般缺省值在8K,这样的百万连接约15G内存
6)net.core.rmem_max=16384 和 net.core.wmem_max=16384 :
TCP接收和发送缓冲区的大小,单位是字节
7)net.core.somaxconn=2048 : 表示socket监听(listen)的backlog上限
backlog就是socket的监听队列,当一个请求尚未被处理或建立时会进入backlog。
而socket server处理后的请求不再位于监听队列中。
当server处理请求较慢,监听队列满后,新来的请求会被拒绝
8)net.ipv4.tcp_max_syn_backlog=2048 : 指定所能接受SYN同步包的最大客户端数量
备注: tcp_max_syn_backlog限制的是半连接数,而somaxconn是连接数目。在tcp协议栈中先收到SYN queue中,再到Accept queue
实际上这两个队列的大小就对应上面的配置参数
9)/proc/sys/net/core/netdev_max_backlog=2048 :
这是网卡的backlog设置,通常可以设置的比内核的要大
10) net.ipv4.tcp_tw_recycle=1 : 开启TCP连接中TIME-WAIT sockets的快速回收
11) net.ipv4.tcp_tw_reuse=1 : 减少处于FIN-WAIT-2连接状态的时间,使系统可以处理更多的连接
FIN-WAIT-2表示服务端强制客户端断开,客户端没有回ACK
1.2 Linux Epoll 原理
基于《Linux 网络编程(epoll)》笔记补充
- epoll原理
1.1. 当epoll_create被调用时,内核会在epoll文件系统里创建一个虚拟的file节点;
1.2. epoll利用mmap来映射一块连续的内存地址从内核态到用户态;
1.3. 在创建出来的这个地址上,epoll利用红黑树存储全部epoll要监听的套接字
1.4. 同时会创建一个rlist的双向链表存储准备就绪的事件;原理在于所有的事件都会有一个回调函数和网卡绑定,当有时间发生响应的事件则会被加入这个rlist;
1.5. 因此每次epoll_wait实际上就是查询这个rlist就可以;
备注: epoll适合的场景是连接数多,但活跃连接数不多的场景;因为大量的回调也会影响epoll的性能;
- linux下epoll下的主要的系统调用
2.1. epoll_create : 创建一个epoll句柄,size表示监听的数目
2.2. epoll_ctl : 将某个句柄新增或从epoll句柄中删除,或者修改epoll中某个句柄(诸如accept后的socket)与监听事件的绑定关系
2.3. epoll_wait : 等待并返回要处理的事件
2.4. 整个epoll的基本流程
epoll描述符 = epoll_create()
epoll_ctrl(epoll描述符,添加或者删除所有待监控的连接以及监听的事件)
返回活跃的连接以及要处理的事件 = epoll_wait( epoll描述符 )
1.3 性能指标查看
dstat(监测CPU、内存等)
- 安装 yum install dstat
- 监控结果
###### ss(网络连接情况)
- 全部tcp或udp 连接 ss -t-u
pprof(服务器性能)
- pprof是go语言中的原生工具链,支持远程查看服务器的性能,主要是内存和CPU等;
- 在程序中插入代码如下:
go func() {
if err := http.ListenAndServe(":6060", nil); err != nil {
fmt.Errorf("pprof failed: %v", err)
}
}()
- 可以在ip:6060网页查看服务器相关数据
- 也可以通过go tool命令拉取对应数据,例如:
go tool pprof http://localhost:6060/debug/pprof/goroutine
go tool pprof http://localhost:6060/debug/pprof/heap
二、goroutine-per-conn
服务端代码
func main() {
ln, err := net.Listen("tcp", ":3000")
if err != nil {
log.Printf("Listen err: %v", err)
return
}
var conections []net.Conn
defer func() {
for _, conn := range conections {
conn.Close()
}
}()
for {
conn, e := ln.Accept()
if e != nil {
if ne, ok := e.(net.Error); ok && ne.Temporary() {
log.Printf("accept temp err: %v", ne)
continue
}
return
}
go handleConn(conn)
conections = append(conections, conn)
}
}
func handleConn(conn net.Conn) {
buffer := make([]byte, 1024)
length, _ := conn.Read(buffer)
fmt.Println(string(buffer[:length]))
}
客户端代码
//flag是go的默认模块,主要用来获取程序执行时的参数
var (
ip = flag.String("ip", "47.110.89.134", "server IP")
connections = flag.Int("conn", 50000, "number of tcp connections")
)
func main() {
flag.Parse()
addr := *ip + ":3000"
log.Printf("连接到 %s", addr)
var conns []net.Conn
for i := 0; i < *connections; i++ {
//设置建立连接超时
c, err := net.DialTimeout("tcp", addr, 10*time.Second)
if err != nil {
fmt.Println("failed to connect", i, err)
i--
continue
}
conns = append(conns, c)
time.Sleep(time.Millisecond)
}
defer func() {
for _, c := range conns {
c.Close()
}
}()
log.Printf("完成初始化 %d 连接", len(conns))
tts := time.Second
if *connections > 100 {
tts = time.Millisecond * 5
}
for {
for i := 0; i < len(conns); i++ {
time.Sleep(tts)
conn := conns[i]
conn.Write([]byte("hello world\r\n"))
}
}
}
测试结果(5万并发)
- 出现问题:
failed to connect 28227 dial tcp 47.110.89.134:3000: connect: cannot assign requested address
原因:
由于客户端频繁的连服务器,由于每次连接都在很短的时间内结束,导致很多的TIME_WAIT,以至于用光了可用的端口。
TIME_WAIT是客户端主动发起关闭,但还没有得到答复
解决办法1:
在前置条件中已有说明,其他快速方案如下:
sysctl -w net.ipv4.tcp_timestamps=1
sysctl -w net.ipv4.tcp_tw_recycle=1
解决办法2:(当解决办法1仍然无效)
cat /proc/sys/net/ipv4/ip_local_port_range
发现32768 60999,表示可用端口在32768和60999之间,因此修改文件/etc/sysctl.conf 并用 sysctl -p使其生效
注意客户端和服务端都要修改
- 查看tcp连接数量
netstat -an | awk ‘/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}’
- dstat查看内存及CPU占用
CPU占用极少,内存占用约370M,即基本上一个socket连接占用约7K内存。
三、goroutine-epoll
服务端代码(server.go)
var epoller *epoll
func main() {
ln, err := net.Listen("tcp", ":3000")
if err != nil {
panic(err)
}
epoller, err = MkEpoll()
if err != nil {
panic(err)
}
go start()
for {
conn, e := ln.Accept()
if e != nil {
if ne, ok := e.(net.Error); ok && ne.Temporary() {
log.Printf("accept temp err: %v", ne)
continue
}
log.Printf("accept err: %v", e)
return
}
if err := epoller.Add(conn); err != nil {
log.Printf("failed to add connection %v", err)
conn.Close()
}
}
}
func start() {
var buf = make([]byte, 8)
for {
connections, err := epoller.Wait()
if err != nil {
log.Printf("failed to epoll wait %v", err)
continue
}
for _, conn := range connections {
if conn == nil {
break
}
if _, err := conn.Read(buf); err != nil {
if err := epoller.Remove(conn); err != nil {
log.Printf("failed to remove %v", err)
}
conn.Close()
}
}
}
}
服务端代码(对linux下epoll的封装)
// +build linux
package main
import (
"log"
"net"
"reflect"
"sync"
"syscall"
"golang.org/x/sys/unix"
)
type epoll struct {
fd int
connections map[int]net.Conn
lock *sync.RWMutex
}
func MkEpoll() (*epoll, error) {
fd, err := unix.EpollCreate1(0)
if err != nil {
return nil, err
}
return &epoll{
fd: fd,
lock: &sync.RWMutex{},
connections: make(map[int]net.Conn),
}, nil
}
func (e *epoll) Add(conn net.Conn) error {
// Extract file descriptor associated with the connection
fd := socketFD(conn)
err := unix.EpollCtl(e.fd, syscall.EPOLL_CTL_ADD, fd, &unix.EpollEvent{Events: unix.POLLIN | unix.POLLHUP, Fd: int32(fd)})
if err != nil {
return err
}
e.lock.Lock()
defer e.lock.Unlock()
e.connections[fd] = conn
if len(e.connections)%100 == 0 {
log.Printf("total number of connections: %v", len(e.connections))
}
return nil
}
func (e *epoll) Remove(conn net.Conn) error {
fd := socketFD(conn)
err := unix.EpollCtl(e.fd, syscall.EPOLL_CTL_DEL, fd, nil)
if err != nil {
return err
}
e.lock.Lock()
defer e.lock.Unlock()
delete(e.connections, fd)
if len(e.connections)%100 == 0 {
log.Printf("total number of connections: %v", len(e.connections))
}
return nil
}
func (e *epoll) Wait() ([]net.Conn, error) {
events := make([]unix.EpollEvent, 100)
n, err := unix.EpollWait(e.fd, events, 100)
if err != nil {
return nil, err
}
e.lock.RLock()
defer e.lock.RUnlock()
var connections []net.Conn
for i := 0; i < n; i++ {
conn := e.connections[int(events[i].Fd)]
connections = append(connections, conn)
}
return connections, nil
}
func socketFD(conn net.Conn) int {
//tls := reflect.TypeOf(conn.UnderlyingConn()) == reflect.TypeOf(&tls.Conn{})
// Extract the file descriptor associated with the connection
//connVal := reflect.Indirect(reflect.ValueOf(conn)).FieldByName("conn").Elem()
tcpConn := reflect.Indirect(reflect.ValueOf(conn)).FieldByName("conn")
//if tls {
// tcpConn = reflect.Indirect(tcpConn.Elem())
//}
fdVal := tcpConn.FieldByName("fd")
pfdVal := reflect.Indirect(fdVal).FieldByName("pfd")
return int(pfdVal.FieldByName("Sysfd").Int())
}