k8s的 Nginx Ingress 调优
内核参数调优
我们先看看通过内核的哪些参数能够提高Ingress的性能。保证在高并发环境下,发挥Ingress的最大性能。
调大全连接队列的大小
TCP 全连接队列的最大值取决于 somaxconn 和 backlog 之间的最小值,也就是 min(somaxconn, backlog)。在高并发环境下,如果队列过小,可能导致队列溢出,使得连接部分连接无法建立。要调大 Nginx Ingress 的连接队列,只需要调整 somaxconn 内核参数的值即可,但我想跟你分享下这背后的相关原理。Nginx 监听 socket 时没有读取 somaxconn,而是有自己单独的参数配置。在 nginx.conf 中 listen 端口的位置,还有个叫 backlog 参数可以设置,它会决定 nginx listen 的端口的连接队列大小。
server { listen 80 backlog=1024; ...
backlog 是 listen(int sockfd, int backlog) 函数中的 backlog 大小,Nginx 默认值是 511,可以通过修改配置文件设置其长度;还有 Go 程序标准库在 listen 时,默认直接读取 somaxconn 作为队列大小。就是说,即便你的 somaxconn 配的很高,nginx 所监听端口的连接队列最大却也只有 511,高并发场景下可能导致连接队列溢出。所以在这个在 Nginx Ingress 中, Nginx Ingress Controller 会自动读取 somaxconn 的值作为 backlog 参数写到生成的 nginx.conf 中: https://github.com/kubernetes/ingress-nginx/blob/controller-v0.34.1/internal/ingress/controller/nginx.go#L592 也就是说,Nginx Ingress 的连接队列大小只取决于 somaxconn 的大小,这个值在 Nginx Ingress 默认为 4096,建议给 Nginx Ingress 设为 65535:
sysctl -w net.core.somaxconn=65535
扩大源端口范围
根据《linux中TCP三次握手与四次挥手介绍及调优》的介绍,我们知道客户端会占用端口。在高并发场景会导致 Nginx Ingress 使用大量源端口与upstream建立连接。源端口范围是在内核参数 net.ipv4.ip_local_port_range 中调整的。在高并发环境下,端口范围过小容易导致源端口耗尽,使得部分连接异常。Nginx Ingress 创建的 Pod 源端口范围默认是 32768-60999,建议将其扩大,调整为 1024-65535:
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
TIME_WAIT
根据《linux中TCP三次握手与四次挥手介绍及调优》的介绍,我们知道客户端会占用端口。当在 netns 中 TIME_WAIT 状态的连接就比较多的时候,源端口就会被长时间占用。因为而 TIME_WAIT 连接默认要等 2MSL 时长才释放,当这种状态连接数量累积到超过一定量之后可能会导致无法新建连接。所以建议给 Nginx Ingress 开启 TIME_WAIT 复用,即允许将 TIME_WAIT 连接重新用于新的 TCP 连接:
sysctl -w net.ipv4.tcp_tw_reuse=1
减小FIN_WAIT2状态的参数 net.ipv4.tcp_fin_timeout 的时间和减小TIME_WAIT 状态的参数net.netfilter.nf_conntrack_tcp_timeout_time_wait的时间 ,让系统尽快释放它们所占用的资源。
sysctl -w net.ipv4.tcp_fin_timeout=15 sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
调大增大处于 TIME_WAIT 状态的连接数
Nginx一定要关注这个值,因为它对你的系统起到一个保护的作用,一旦端口全部被占用,服务就异常了。tcp_max_tw_buckets 能帮你降低这种情况的发生概率,争取补救时间。在只有 60000 多个端口可用的情况下,配置为:
sysctl -w net.ipv4.tcp_max_tw_buckets = 55000
调大最大文件句柄数
Nginx 作为反向代理,对于每个请求,它会与 client 和 upstream server 分别建立一个连接,即占据两个文件句柄,所以理论上来说 Nginx 能同时处理的连接数最多是系统最大文件句柄数限制的一半。系统最大文件句柄数由 fs.file-max 这个内核参数来控制,默认值为 838860,建议调大:
sysctl -w fs.file-max=1048576
配置示例
给 Nginx Ingress Controller 的 Pod 添加 initContainers 来设置内核参数:
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="1024 65535" sysctl -w net.ipv4.tcp_max_tw_buckets = 55000 sysctl -w net.ipv4.tcp_tw_reuse=1 sysctl -w fs.file-max=1048576 sysctl -w net.ipv4.tcp_fin_timeout=15 sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
应用层配置调优
除了内核参数需要调优,Nginx 本身的一些配置也需要进行调优,下面我们来详细看下。
调高 keepalive 连接最大请求数
keepalive_requests指令用于设置一个keep-alive连接上可以服务的请求的最大数量,当最大请求数量达到时,连接被关闭。默认是100。这个参数的真实含义,是指一个keep alive建立之后,nginx就会为这个连接设置一个计数器,记录这个keep alive的长连接上已经接收并处理的客户端请求的数量。如果达到这个参数设置的最大值时,则nginx会强行关闭这个长连接,逼迫客户端不得不重新建立新的长连接。
简单解释一下:QPS=10000时,客户端每秒发送10000个请求(通常建立有多个长连接),每个连接只能最多跑100次请求,意味着平均每秒钟就会有100个长连接因此被nginx关闭。同样意味着为了保持QPS,客户端不得不每秒重新新建100个连接。因此,就会发现有大量的TIME_WAIT的socket连接(即使此时keep alive已经在client和nginx之间生效)。因此对于QPS较高的场景,非常有必要加大这个参数,以避免出现大量连接被生成再抛弃的情况,减少TIME_WAIT。
如果是内网 Ingress,单个 client 的 QPS 可能较大,比如达到 10000 QPS,Nginx 就可能频繁断开跟 client 建立的 keepalive 连接,然后就会产生大量 TIME_WAIT 状态连接。我们应该尽量避免产生大量 TIME_WAIT 连接,所以,建议这种高并发场景应该增大 Nginx 与 client 的 keepalive 连接的最大请求数量,在 Nginx Ingress 的配置对应 keep-alive-requests,可以设置为 10000,参考: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#keep-alive-requests 同样的,Nginx 与 upstream 的 keepalive 连接的请求数量的配置是 upstream-keepalive-requests,参考: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#upstream-keepalive-requests
但是,一般情况应该不必配此参数,如果将其调高,可能导致负载不均,因为 Nginx 与 upstream 保持的 keepalive 连接过久,导致连接发生调度的次数就少了,连接就过于 "固化",使得流量的负载不均衡。
调高 keepalive 最大空闲连接数
Nginx 针对 upstream 有个叫 keepalive 的配置,它不是 keepalive 超时时间,也不是 keepalive 最大连接数,而是 keepalive 最大空闲连接数。当这个数量被突破时,最近使用最少的连接将被关闭。
简单解释一下:有一个HTTP服务,作为upstream服务器接收请求,响应时间为100毫秒。如果要达到10000 QPS的性能,就需要在nginx和upstream服务器之间建立大约1000条HTTP连接。nginx为此建立连接池,然后请求过来时为每个请求分配一个连接,请求结束时回收连接放入连接池中,连接的状态也就更改为idle。我们再假设这个upstream服务器的keepalive参数设置比较小,比如常见的10. A、假设请求和响应是均匀而平稳的,那么这1000条连接应该都是一放回连接池就立即被后续请求申请使用,线程池中的idle线程会非常的少,趋近于零,不会造成连接数量反复震荡。B、显示中请求和响应不可能平稳,我们以10毫秒为一个单位,来看连接的情况(注意场景是1000个线程+100毫秒响应时间,每秒有10000个请求完成),我们假设应答始终都是平稳的,只是请求不平稳,第一个10毫秒只有50,第二个10毫秒有150:
- 下一个10毫秒,有100个连接结束请求回收连接到连接池,但是假设此时请求不均匀10毫秒内没有预计的100个请求进来,而是只有50个请求。注意此时连接池回收了100个连接又分配出去50个连接,因此连接池内有50个空闲连接。
- 然后注意看keepalive=10的设置,这意味着连接池中最多容许保留有10个空闲连接。因此nginx不得不将这50个空闲连接中的40个关闭,只留下10个。
- 再下一个10个毫秒,有150个请求进来,有100个请求结束任务释放连接。150 - 100 = 50,空缺了50个连接,减掉前面连接池保留的10个空闲连接,nginx不得不新建40个新连接来满足要求。
C、同样,如果假设相应不均衡也会出现上面的连接数波动情况。
它的默认值为 32,在高并发下场景下会产生大量请求和连接,而现实世界中请求并不是完全均匀的,有些建立的连接可能会短暂空闲,而空闲连接数多了之后关闭空闲连接,就可能导致 Nginx 与 upstream 频繁断连和建连,引发 TIME_WAIT 飙升。在高并发场景下可以调到 1000,参考: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#upstream-keepalive-connections
网关超时
ingress nginx 与 upstream pod 建立 TCP 连接并进行通信,其中涉及 3 个超时配置,我们也相应进行调优。proxy-connect-timeout 选项 设置 nginx 与 upstream pod 连接建立的超时时间,ingress nginx 默认设置为 5s,由于在nginx 和业务均在内网同机房通信,我们将此超时时间缩短一些,比如3秒。参考:https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#proxy-connect-timeout
proxy-read-timeout 选项设置 nginx 与 upstream pod 之间读操作的超时时间,ingress nginx 默认设置为 60s,当业务方服务异常导致响应耗时飙涨时,异常请求会长时间夯住 ingress 网关,我们在拉取所有服务正常请求的 P99.99 耗时之后,将网关与 upstream pod 之间读写超时均缩短到 3s,使得 nginx 可以及时掐断异常请求,避免长时间被夯住。参考:https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#proxy-read-timeout
https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#proxy-send-timeout
调高单个 worker 最大连接数
max-worker-connections 控制每个 worker 进程可以打开的最大连接数,默认配置是 16384。在高并发环境建议调高,比如设置到 65536,这样可以让 nginx 拥有处理更多连接的能力,参考: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#max-worker-connections
优化重试机制
nginx 提供了默认的 upstream 请求重试机制,默认情况下,当 upstream 服务返回 error 或者超时,nginx 会自动重试异常请求,并且没有重试次数限制。由于接入层 nginx 和 ingress nginx 本质都是 nginx,两层 nginx 都启用了默认的重试机制,异常请求时会出现大量重试,最差情况下会导致集群网关雪崩。接入层 nginx 一起解决了这个问题:接入层 nginx 必须使用 proxy_next_upstream_tries 严格限制重试次数,ingress nginx 则使用 proxy-next-upstream="off"直接关闭默认的重试机制。参考:https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#proxy-next-upstream
开启 brotli 压缩
参考: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#enable-brotli
压缩是时间换空间的通用方法。用cpu时间来换取大量的网络带宽,增大吞吐量。Brotli是Google开发的一种压缩方法,于2015年发布。我们常用的压缩算法是 gzip(Ingress-nginx也是默认使用gzip),据说brotli要比gzip高出20%至30%的压缩率。默认的压缩算法是gzip,压缩级别为1,如需要启用brotli,需要配置以下三个参数:
- enable-brotli: true 或 false,是否启用brotli压缩算法
- brotli-level: 压缩级别,范围1~11,默认为4,级别越高,越消耗CPU性能。
- brotli-types: 由brotli即时压缩的MIME类型
配置示例
Nginx 全局配置通过 configmap 配置(Nginx Ingress Controller 会 watch 并自动 reload 配置):
apiVersion: v1 kind: ConfigMap metadata: name: nginx-ingress-controller data: keep-alive-requests: "10000" upstream-keepalive-connections: "200" max-worker-connections: "65536" proxy-connect-timeout: "3" proxy-read-timeout: "3" proxy-send-timeout: "3" proxy-next-upstream: "off" enable-brotli: "true" brotli-level: "6" brotli-types: "text/xml image/svg+xml application/x-font-ttf image/vnd.microsoft.icon application/x-font-opentype application/json font/eot application/vnd.ms-fontobject application/javascript font/otf application/xml application/xhtml+xml text/javascript application/x-javascript text/plain application/x-font-truetype application/xml+rss image/x-icon font/opentype text/css image/x-win-bitmap"
参考: