Nginx长连接学习之二

发布时间 2024-01-12 11:55:01作者: 济南小老虎

Nginx长连接学习之二


背景

距离最开始学习Nginx的长连接已经一年半;
距离最开始学习Linux的TCP内核参数也已经过去了一年.

最近产品再次出现了TCP链接相关的问题. 
因为一开始不知道部署模式已经变更, 我先排除了内存参数的问题. 

结果很打脸, 还是内核参数问题导致的, 但是可能比较隐晦. 
因为Nginx服务器使用的CentOS9_Stream 并且进行了内核升级. 

这里想还是继续总结一下, 因为我已经忘记了一年半以前学习的内容. 
参数都已经还给了互联网. 

问题场景描述

本次压测场景出现了 CPU忽高忽低的问题. 
最开始不清楚部署已经增加了nginx, 一直以为是直连springboot应用
所以一开始一直怀疑是 类似于有进程执行了强制safepoint导致的停顿. 
所以这个教训告诉大家, 一定要详细了解系统的部署拓扑图, 没有拓扑图前不要进行任何推测. 

增加了nginx的error log等信息后发现了大量的
cannot assign port 等的提示, 基本上实锤了与2023年的问题完全相同. 

TCP四元组端口号不足导致的问题. 

基础知识说明

第一: tcp内核参数
tcp内核参数其实是一个很重要的点. 
ulimit里面的nofile nproc等参数 其实属于安全部分的设置, 他的数值受限制于内核参数的上限.
除此之外还有类似于tcp内核参数, 文件数, 内存数相关的内核参数. 

第二: nginx
nginx 可以作为web服务器, 也可以作为反向代理服务器. 
作为web服务器时比较单纯, 只跟客户端沟通就可以了. 
但是作为proxy代理服务器时就比较复杂: 
一方面要作为 链接 客户端的 web服务器进行交互,
另一方面要作为 客户端与backend的应用服务器进行交互. 
所以代理服务器时 他不仅仅是服务器, 同时还是客户端. 

第三: linux内核
Linux内核是Linux操作系统的基础. 很多参数其实会作用于Linux内核上面
但是也有很多是无法通过参数进行修改的. 比如今天想讨论的 time_wait持续时间的问题. 
如果无法通过参数进行修改, 那么可能需要重新编译内核. 但是如果没有足够的技术储备,不建议如此使用. 
会导致运维起来难度增加, 并且存在后续的升级和维护困难. 
这一点参照了 解bug之路 公众号里面的Linux源码学习相关

问题现象

通过如下命令在linux服务器上面查询

netstat -ano |grep ^tcp |awk '{print $6}'|sort |uniq -c|sort -k1hr

发现有较多的 time_wait 相关的信息
114229 TIME _WAIT

因为后面有四到五台应用服务器 平均米格应用服务器已经快3万个 time wait的链接对应了. 
所以nginx 会出现 cannot assign port的问题. 

解决问题的方式-修改内核参数

1. 修改linux内核参数
这里很早之前总结过: 
最新核的参数其实是:
net.ipv4.tcp_max_tw_buckets = 5000
net.ipv4.ip_local_port_range = 2048 65500
net.ipv4.ip_local_reserved_ports = 5200,6379,7005,8001-8100

需要注意 tcp_max_tw_buckets 是针对全局 整个系统的. 
一般设置到 5000 时 新增的 time_wait的 端口就会将旧的端口冲掉.
有一定很小的概率出现 四元组冲突. 需要注意. 

另外注意 ip_local_port_range 是针对每个四元组来的, 不同的IP会选择 整个range进行port分配
所以建议增加一个 reserved的参数, 保证重启服务器时不会出现因为端口号占用 导致启动失败. 

需要注意 这三个参数的修改并非解决问题, 而是避开了问题, 将能够支撑的QPS/TPS进行了增加. 
彻底解决应该不能依靠这类参数修改类解决

解决问题的方式-重新编译内核

网上很多资料都说 net.ipv4.tcp_fin_timeout 等内核参数可以修改 time_wait的时间
但是更多大佬都说了  是内核编译时就已经决定的一个参数
https://zhuanlan.zhihu.com/p/286537295
这个文章里面的就说的很清楚(需要去读原文,才能看懂下面这一段)
/* 这边TCP_TIMEWAIT_LEN 60 * HZ */
inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
                     TCP_TIMEWAIT_LEN);

他只受到 TCP_TIMEWAIT_LEN 参数的影响, 但是修改的话 需要重新编译内核. 
并且作者后面也说了, 在time_wait 的每个slot大于100个时有可能会加大每个slot内的time wait的处理时间.
导致一个time wait的链接最大处理时间超过 112秒((7.5s+7.5s)*7+7.5s)). 

并且他也发现某些内核, 会将等待事件从 60/8 的 7.5秒修改为 1秒, 
这样最大的等待事件会变成 68秒. (8.5s+7+7.5s) 

需要说明 这种处理方式跟内核参数修改是类似的, 也是你为了缓解问题, 并非最终解决问题. 

插播-长连接的优势

1)对响应时间要求较高;
2)服务走的是公网,客户端与服务端的TCP建立的三次握手和断开的四次挥手都需要40ms左右(真实数据包计算出来的),共需要80ms左右;
3)每个接入方使用的IP就若干个,需要建立的请求连接有限。
4)使用长连接技术,可以大幅减少TCP频繁握手的次数,极大提高响应时间;
   同时,即使使用长连接技术,也不需要消耗很多的系统资源用来缓存sockets会话信息。

来源: https://www.cnblogs.com/kevingrace/p/9364404.html

解决问题的方式-长连接

最后一种可能是较好的解决问题的方式是 长连接. 
知识背景里面也提到了. 
Nginx 主要是有 服务器端 和 客户端的双重角色. 
所以解决问题也需要两者都是进行考虑. 

但是跟之前解决问题的思路一样. 所有的问题都是环环相扣的
配置nginx只是问题解决的一部分, 并不能够将所有的问题全部解决. 
可能需要客户端浏览器以及后端应用服务器同步进行修改才可以. 

主要的参数有:
keepalive 
keepalive_timeout
keepalive_requests
proxy_timeout
等参数. 这里的配置其实比较繁琐, 需要仔细理解. 

解决问题的方式-长连接-nginx作为服务端

http {
  keepalive_timeout 120s;        #客户端链接超时时间。为0的时候禁用长连接。即长连接的timeout
  keepalive_requests 10000;      #在一个长连接上可以服务的最大请求数目。
                                 #当达到最大请求数目且所有已有请求结束后,连接被关闭。默认值为100。即每个连接的最大请求数
}

解决问题的方式-长连接-nginx作为客户端

http {
upstream backend {
  server 192.168.0.1:8080 weight=1 max_fails=2 fail_timeout=30s;
  server 192.168.0.2:8080 weight=1 max_fails=2 fail_timeout=30s;
  keepalive 300;       #这个很重要!
  keepalive_requests 10000;  
}  
 
server {
  listen 8080 default_server;
  server_name "";
 
location / {
  proxy_pass http://backend;
  proxy_http_version 1.1;                   #设置http版本为1.1
  proxy_set_header Connection "";           #设置Connection为长连接(默认为no)
  proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_read_timeout 600s;         #新增配置1
  proxy_send_timeout 120s;         #新增配置2
  }
 }
}

注意事项

需要注意, 如果采用websocket的话. 默认应该是长连接.
但是需要使用单点的站点进行升级处理.

Nginx支持WebSocket。
对于Nginx将升级请求从客户端发送到后台服务器,必须明确设置Upgrade和Connection标题。
这也算是上面情况所非常用的场景。
HTTP的Upgrade协议头机制用于将连接从HTTP连接升级到WebSocket连接,Upgrade机制使用了Upgrade协议头和Connection协议头。
为了让Nginx可以将来自客户端的Upgrade请求发送到后端服务器,Upgrade和Connection的头信息必须被显式的设置。

延伸理解之一

Chrome浏览器以及Springboot的服务器端其实也有类似的设置.

浏览器对并发请求的数目限制是针对域名的,即针对同一域名(包括二级域名)在同一时间支持的并发请求数量的限制。
如果请求数目超出限制,则会阻塞。因此,网站中对一些静态资源,使用不同的一级域名,可以提升浏览器并行请求的数目,加速界面资源的获取速度。

HTTP/1.1中,单个TCP连接,在同一时间只能处理一个http请求,虽然存在Pipelining技术支持多个请求同时发送
    但由于实践中存在很多问题无法解决,所以浏览器默认是关闭,所以可以认为是不支持同时多个请求。
HTTP2 提供了多路传输功能,多个http请求,可以同时在同一个TCP连接中进行传输。

Chrome浏览器在http1.1协议时可以对同一个站点使用六个tcp链接进行访问. 
页面资源请求时,浏览器会同时和服务器建立多个TCP连接,在同一个TCP连接上顺序处理多个HTTP请求。
所以浏览器的并发性就体现在可以建立多个TCP连接,来支持多个http同时请求。

Chrome浏览器最多允许对同一个域名Host建立6个TCP连接,不同的浏览器有所区别。

如果都是HTTPS的连接,并且在同一域名下,浏览器会先和服务器协商使用HTTP2的Multiplexing功能进行多路传输,
不过未必所有的挂在这个域名下的资源都会使用同一个TCP连接。如果用不了HTTPS或者HTTP2(HTTP2是在HTTPS上实现的),
那么浏览器会就在同一个host建立多个TCP连接,每一个TCP连接进行顺序请求资源。

来源: https://cloud.tencent.com/developer/article/1518678

延伸理解之二

Springboot 开发的应用默认内嵌的tomcat
tomcat 里面有thread的线程池, 数据库连接池, 以及 http的链接池信息
需要注意 springboot 默认的 max-keep-alive-requests 就是100 如果并发量很大, 可以适当调高一下这个数值. 
建议跟nginx 里面的 upstream 里面的 keepalive 的参数值尽量保持一致, 可以尽可能的提高并发能力. (此观点待压测证明)

另外需要说明 threads.max基本上是 http-nio-port 的线程数量限制, 可以理解为是一个比较核心的工作线程数量限制.
其他的内部的线程池一般是通过 max 数值单独指定. 
需要注意, 线程也会消耗内存, 默认的linux上面的 线程的栈大小是1MB. 但是一般请款下也是延迟分配的. 
创建线程后可能会分配 200K左右的内存, 并不会完整使用1MB左右. 

也有很多配置信息: 
server:
  tomcat:
    # 当所有可能的请求处理线程都在使用中时,传入连接请求的最大队列长度
    accept-count: 1000
    # 服务器在任何给定时间接受和处理的最大连接数。一旦达到限制,操作系统仍然可以接受基于“acceptCount”属性的连接。
    max-connections: 20000
    threads:
      # 工作线程的最小数量,初始化时创建的线程数
      min-spare: 10
      # 工作线程的最大数量 io密集型建议10倍的cpu数,cpu密集型建议cpu数+1,绝大部分应用都是io密集型
      max: 500
    # 连接器在接受连接后等待显示请求 URI 行的时间。
    connection-timeout: 60000
    # 在关闭连接之前等待另一个 HTTP 请求的时间。如果未设置,则使用 connectionTimeout。设置为 -1 时不会超时。
    keep-alive-timeout: 60000
    # 在连接关闭之前可以进行流水线处理的最大HTTP请求数量。当设置为0或1时,禁用keep-alive和流水线处理。当设置为-1时,允许无限数量的流水线处理或keep-alive请求。 
    max-keep-alive-requests: 1000