1. 背景:
希望调优ingress-nginx性能,承载更高并发。
因此学习了Linux网络知识和nginx优化配置,这里记录一下。
2. Linux网络模型
先要了解Linux网络模型,清楚请求流量处理的全过程,才能有针对性的在每个处理步骤进行调优。
在linux中使用的是四层网络模型,即TCP/IP网络模型。TCP/IP 模型,把网络互联的框架分为应用层、传输层、网络层、网络接口层等四层,其中,
应用层,负责向用户提供一组应用程序,比如 HTTP、FTP、DNS 等。
传输层,负责端到端的通信,比如 TCP、UDP 等。
网络层,负责网络包的封装、寻址和路由,比如 IP、ICMP 等。
网络接口层,负责网络包在物理网络中的传输,比如 MAC 寻址、错误侦测以及通过网卡传输网络帧等。
在进行网络传输时,数据包就会按照协议栈,对上一层发来的数据进行逐层处理;然后封装上该层的协议头,再发送给下一层。

四层网络模型
Linux内核中的网络协议栈也是类似TCP/IP的四层结构,但多出了一个抽象的套接字层,它是应用程序与网络协议栈之间的接口。
应用程序使用socket函数创建一个Socket时,该函数会返回一个文件描述符,该文件描述符可以被用于后续的Socket操作,例如发送和接收数据。在这种情况下,Socket就被视为一个文件,它可以使用read()和write()等函数进行读写操作。
Socket为实现网络通信提供了一种标准的接口,使得应用程序可以通过这个接口来进行网络通信,而不用关心底层协议的实现细节。这样就使得应用程序的开发变得更加简单、方便和高效。

Linux中的网络包处理流程
当一个网络帧到达网卡后,网卡会通过 DMA 方式,把这个网络包放到收包队列中;然后通过硬中断,告诉中断处理程序已经收到了网络包。
接着,网卡中断处理程序会为网络帧分配内核数据结构(sk_buff),并将其拷贝到 sk_buff 缓冲区中;然后再通过软中断,通知内核收到了新的网络帧。
接下来,内核协议栈从缓冲区中取出网络帧,并通过网络协议栈,从下到上逐层处理这个网络帧。比如,
在链路层检查报文的合法性,找出上层协议的类型(比如 IPv4 还是 IPv6),再去掉帧头、帧尾,然后交给网络层。
网络层取出 IP 头,判断网络包下一步的走向,比如是交给上层处理还是转发。当网络层确认这个包是要发送到本机后,就会取出上层协议的类型(比如 TCP 还是 UDP),去掉 IP 头,再交给传输层处理。
传输层取出 TCP 头或者 UDP 头后,根据 < 源 IP、源端口、目的 IP、目的端口 > 四元组作为标识,找出对应的 Socket,并把数据拷贝到 Socket 的接收缓存中。
最后,应用程序就可以使用 Socket 接口,读取到新接收到的数据了。
4. 性能调优的思考
在网络性能调优前,我先需要了解本机的网络性能,下面列出了查看 Linux 的网络性能(状态)的常用命令。

Linux网络性能指标
Linux中对应用服务进行高并发调优可分两部分:
Linux系统方面的优化(网络协议栈参数调优)
应用服务配置调优
5. Linux系统参数调优
5.1 半连接队列与全连接队列
什么是 TCP 半连接队列和全连接队列?
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
半连接队列,也称 SYN 队列。
全连接队列,也称 accepet 队列。
服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。

半连接队列与全连接队列
半连接队列和全连接队列都有最大长度限制,超过限制时,请求会被丢弃,并返回RST。

超过队列限制会被丢弃
TCP 全连接队列的最大值取决于 min(somaxconn, backlog)。
TCP 半连接队列最大值取决于 max_syn_backlog、somaxconn、backlog 。
somaxconn tcp_max_syn_backlog 是 Linux 内核的参数,默认值都是 128。
sysctl -w net.core.somaxconn=65530
sysctl -w net.ipv4.tcp_max_syn_backlog=10240
backlog 是 listen(int sockfd, int backlog) 函数中的 backlog 大小,Nginx 默认值是 511。
可以通过nginx.conf配置文件设置其长度,需重启nginx生效。
server {
listen 80 default backlog=10240;
servarname localhost;
...
} 使用 netstat -s | grep "overflowed" 查看全连接队列是否有溢出。
使用 netstat -s | grep "SYNs to LISTEN" 查看半连接队列溢出的情况。(累计值,多次查看)
5.2 可用端口范围
高并发环境将导致 Nginx Ingress 使用大量源端口与 upstream 建立连接,源端口范围从 net.ipv4.ip_local_port_range 内核参数中定义的区间随机选取。在高并发环境下,端口范围小容易导致源端口耗尽,使得部分连接异常。
开放端口范围
sysctl -w net.ipv4.ip_local_port_range=&quot;1024 65535&quot;
5.3 文件句柄数
每个tcp连接都要创建一个socket,每个socket就是一个文件,会占用1个文件句柄数。
调大文件句柄数限制,可以让 Nginx Ingress 建立更多连接。
vim /etc/security/limits.conf #文件句柄限制配置文件
# 系统全局性修改, *代表所有用户
* soft nofile 25530
* hard nofile 25530
# 针对root用户,soft仅提醒,hard限制,nofile打开最大文件数
root soft nofile 65530
root hard nofile 65530
5.4 tcp 长连接参数
长连接的好处是显而易见的,多个请求可以复用一条连接,省去连接建立和释放的时间开销和系统调用。
长连接的环境下,进行一次数据交互后,很长一段时间内无数据交互时,客户端可能意外断电、死机、崩溃,客户端的异常会使得客户端不能正常发送连接断开请求,影响TCP长连接正常释放。这时服务端并不知道对端的情况,它会一直维护这个连接,长时间的积累会导致非常多的无效长连接,造成Server端系统资源的消耗和浪费。
于是这就有了TCP的Keepalive(保活探测)机制,通过三个内核参数设置,默认的探活效率较低建议进行调整。
tcp_keepalive_time:在一定时间内在链路上没有数据传送的情况下,TCP 层将发送相应的KeepAlive探针以确定连接可用性,默认值为7200s
tcp_keepalive_intvl:探测失败(探测包无响应)后多久进行下次重试探测,默认值为75s
tcp_keepalive_probes:探测失败后重试发送保活探测包次数,默认值为9次
HTTP的长连接是基于TCP长连接实现的,因为HTTP是基于TCP的上层网络协议。
5.5 socket参数
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.rmem_default = 16777216
net.core.wmem_default = 16777216
net.core.optmem_max = 40960
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
6 容器场景下配置内核参数
Nginx Ingress 是以Pod的形式运行,在修改Pod里的内核参数前,需要先了解linux内核参数。
6.1 Linux 内核参数分类
linux kernel 参数可分为两类:
命名空间级:这些可以在Kubernetes集群的每个POD中设置,且pod间互不影响。
全局(主机)级:影响宿主机节点,及节点上的Pod。
Kubernetes又将命名空间级的参数分类为安全(safe)和 非安全(unsafe)。
那么如何修改Pod里的内核参数呢?
对于全局级的参数,笔者建议直接在宿主机上进行修改。
对于命名空间级别参数,笔者建议你在deployment.yaml中使用securityContext。

linux内核参数分类
6.2 如何判断参数类型
运行一个具有privileged权限的容器,然后在容器中修改该参数,在Pod所在节点上使用sysctl -a 查看。如果节点上的参数也被修改那就是主机级的,否则不是。
可参考以下代码示例,添加privileged权限的initContainers,在容器中修改内核参数。
spec:
template:
spec:
initContainers:
- name: setsysctl
image: busybox
securityContext:
privileged: true
command:
- sh
- -c
- |
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.ip_local_port_range=&quot;1024 65535&quot; 在确认是namespace级别后,只有以下几种参数是安全的,其余的都是非安全的。
全部安全参数如下: kernel.shm_rmid_forced; net.ipv4.ip_local_port_range; net.ipv4.tcp_syncookies; net.ipv4.ping_group_range(从 Kubernetes 1.18 开始); net.ipv4.ip_unprivileged_port_start(从 Kubernetes 1.22 开始); net.ipv4.ip_local_reserved_ports(从 Kubernetes 1.27 开始,需要 kernel 3.16+); net.ipv4.tcp_keepalive_time(从 Kubernetes 1.29 开始,需要 kernel 4.5+); net.ipv4.tcp_fin_timeout(从 Kubernetes 1.29 开始,需要 kernel 4.6+); net.ipv4.tcp_keepalive_intvl(从 Kubernetes 1.29 开始,需要 kernel 4.5+); net.ipv4.tcp_keepalive_probes(从 Kubernetes 1.29 开始,需要 kernel 4.5+)。
6.3 securityContext配置说明
如果是安全参数,直接配置securityContext就可以。
是非安全参数的话,还需在kubelet启动参数中添加--allowed-unsafe-sysctls。
以下示例是如何修改net.core.somaxconn参数:
在kubelet启动参数中添加
--allowed-unsafe-sysctls=net.core.somaxconn
然后重启kubelet
systemctl restart kubelet 示例yaml:
kind: Deployment
apiVersion: apps/v1
metadata:
name: test-kernel
namespace: default
labels:
appgroup: &#39;&#39;
version: v1
spec:
replicas: 1
selector:
matchLabels:
app: test-kernel
version: v1
template:
metadata:
labels:
app: test-kernel
version: v1
spec:
containers:
- name: container-1
image: centos:7.6.1810
command:
- /bin/bash
args:
- &#39;-c&#39;
- while true; do echo hello; sleep 10; done
resources:
limits:
cpu: 250m
memory: 256Mi
requests:
cpu: 250m
memory: 256Mi
imagePullPolicy: IfNotPresent
restartPolicy: Always
terminationGracePeriodSeconds: 30
dnsPolicy: ClusterFirst
securityContext:
sysctls:
- name: net.core.somaxconn
value: &#39;3000&#39;
imagePullSecrets:
- name: default-secret
schedulerName: default-scheduler
6.4 为何不推荐使用Initcontainer修改Pod中的内核参数
在6.2的示例中,使用Initcontainer修改Pod中的内核参数,但笔者不推荐这种用法。
原因如下:
这种方式Init容器必须开启特权,不在生产环境中使用特权容器,这应该是铁律。
这种方式可能会造成全局参数的覆盖,因为Pod调度的节点是随机的,可能会出现与预期不符的情况。
7 Nginx应用配置优化
7.1 nginx工作模型
nginx有下面两种不同的工作模型。
第一种:主进程 + 多个 worker 子进程。在这种方式下,主进程执行 bind() + listen() 后,创建多个子进程;然后,在每个子进程中,都通过 accept() 或 epoll_wait() ,来处理相同的套接字。主进程主要用来初始化套接字,并管理子进程的生命周期;而 worker 进程,则负责实际的请求处理。
第二种:监听到相同端口的多进程模型。在这种方式下,所有的进程都监听相同的接口,并且开启 SO_REUSEPORT 选项。由内核负责将请求负载均衡到这些监听进程中去,相当于每个进程/线程独享一个 listener 的全链接队列,不需要多个进程/线程竞争某个公共资源,减少竞争的资源消耗,处理效率自然提高了。

nginx两种工作模式
现在最新版本的ingress-nginx会默认开启reuseport(第二种),如果你使用的是旧版本,建议手动开启reuseport。
7.2.进程级别的文件句柄数
设置每个工作进程可以打开的最大文件数。默认值时为表示“系统限制-1024”。
因为默认值较大且不太合理,建议根据实际情况调小 的值。
7.3 后端长连接最大请求数
Nginx 针对 client 和 upstream 的 keepalive 连接,具备 keepalive_requests 参数来控制单个 keepalive 连接的最大请求数,默认值均为100。当一个 keepalive 连接中请求次数超过默认值时,将断开并重新建立连接。这样的好处是避免产生大量的 TIME_WAIT 连接,建议您在 Nginx Ingress 的配置中将 keep-alive-requests 配置为5000,建议您在高并发环境中增大 Nginx 与 client 的 keepalive 连接的最大请求数量。
同样,Nginx 针对 upstream 的 keepalive 连接的请求数量的配置是 upstream-keepalive-requests,配置方法请参见 upstream-keepalive-requests。
注意:
在非高并发环境,不必配调高upstream-keepalive-requests参数。如果将其调高,Nginx 与 upstream 保持的 keepalive 连接主动断开的频率过慢,会导致流量负载不均。
7.4 后端连接超时
ingress nginx 与 upstream pod 建立 TCP 连接并进行通信,其中涉及 3 个超时配置,我们也相应进行调优。
proxy-connect-timeout:设置 nginx 与 upstream pod 连接建立的超时时间,ingress nginx 默认设置为 5s,建议将此超时时间缩短一些,比如3秒。
proxy-read-timeout 、proxy-send-timeout:设置 nginx 与 upstream pod 之间读写操作的超时时间,默认值为60s,当后端服务异常导致响应耗时飙涨时,异常请求会长时间夯住 ingress 网关。建议缩短到适当数值如30s,使得 nginx 可以及时掐断异常请求,避免长时间被夯住。
7.5 ingress nginx 全局配置方式
安装ingress-nginx时一般会在kube-system命名空间或ingress-nginx命名空间中自动创建一个configmap。我们可以修改它来进行ingress-nginx 的全局配置。
如果不确定configmap的名字,可以查看ingress-nginx.yaml启动参数中--configmap的值。

查看configmap名字和命名空间
下面是confimap示例:
kind: ConfigMap
apiVersion: v1
metadata:
name: cceaddon-nginx-ingress-controller
namespace: kube-system
data:
keep-alive-requests: &quot;5000&quot;
upstream-keepalive-connections: &quot;2000&quot;
max-worker-connections: &quot;10000&quot;
proxy-connect-timeout: &quot;3&quot;
proxy-read-timeout: &quot;30&quot;
proxy-send-timeout: &quot;30&quot;
max-worker-open-files: &quot;10240&quot;
reuse-port: &quot;true&quot;
proxy-body-size: &quot;50m&quot;