Nginx反向代理性能调优

2,805 阅读7分钟

本文已参与「新人创作礼」活动.一起开启掘金创作之路。

全局配置

worker_processes  auto;
worker_cpu_affinity auto;
worker_rlimit_nofile 204800
events {
    worker_connections  102400;
}
  • worker_processes: 工作进程数量,一般建议配置成cpu核数,auto会自动检测并配置成cpu核数。
  • worker_cpu_affinity:让每一个工作进程绑定到特定cpu,避免cpu切换带来的性能损失。auto实现自动绑定。
  • worker_connections:每个工作进程能处理的并发连接数上限,根据实际情况设置合理值,这里需要注意的是:即使超过了限制,客户端连接请求进来时,仍然可以建立链接,并且能收到http请求,nginx发现超过连接数限制,才关闭链接(这是因为建链处理在操作系统层面)。另一方面,如果nginx已经超过该连接数限制,则不会向后端发起建链请求。
  • worker_rlimit_nofile :每个工作进程能打开的文件描述符上限,包括tcp连接和文件操作引起的文件描述符。设置的值要高于worker_connections的值,因为nginx是中间方,既要维持和客户端的链接,也要维持和后端服务之间的链接。该值乘以worker进程数还要略低于操作系统能打开文件描述符限制,避免nginx占用了所有文件描述符导致其它系统进程无法打开文件描述符导致系统错误。

限制连接

限制客户端连接和请求

http段配置:

limit_req_zone $binary_remote_addr zone=perip_req:10m rate=200r/s;
limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
  • limit_req_zone: 定义所有worker进程共享的内存区域,以某个变量名为key统计请求数量保存在内存中,该数量也称为状态。本例中定义了10m大小的内存区,存放每个ip的请求数,rate参数限制了请求数为每秒200个(令牌桶算法)。内存区存储的状态在64位机器上固定大小为128字节,因此10m大小的内存区可以存的状态个数为:10*2^20 / 128 = 80*2^10,大概8万个状态,即可以同时保存8万个ip每秒请求数量。如果存储满了,又有新的客户端请求到来,则会先删除最久未使用的ip对应的状态,再存储最新ip对应的状态。对于用户特别多的场景,可以根据实际情况设置更大的值。
  • limit_conn_zone: 定义共享内存区。本例中以ip维度保存连接数,内存区大小为10M。

http段、server段或location段配置:

limit_req zone=perip_req burst=100 nodelay;
limit_conn perip_conn 100;
  • limit_req: 利用之前http段中定义的内存区perip_req中的状态,限制单位时间内的请求数为200,超过限制数的请求需排队处理,burst的值为队列长度。如果队列满了,新的请求将直接拒绝。队列中的请求的处理行为取决后面的参数,nodelay:队列中请求可以马上处理。而delay 10的意思是队列中的请求,超过10的部分需要启timer延时处理,而10个以内的排队请求可以马上处理。
  • limit_conn: 利用之前http段中定义的内存区perip_conn中的状态,限制每个客户端的连接数为100.
  • 定义在http段则对所有请求起作用,定义在server段则只对改server起作用,定义在location段则只对某些url请求起作用。

限制后端连接和请求

http段配置:

limit_req_zone $server_name zone=perserver_req:10m rate=2000r/s;
limit_conn_zone $server_name zone=perserver_conn:10m;

server段配置:

limit_req zone=perserver_req burst=1000;
limit_conn perserver_conn 400;
  • 后端的连接数和请求数限制一般要大于客户端的连接数和请求数,具体应该对后端服务做性能压测后做设置。具体思路是:压测找出多个请求的可接受平均响应时间内的最大并发请求数,如平均响应时间200ms,最大并发请求数为2000,则连接数可以设置为:2000/(1000/200) = 400. 据此最终可以设置连接限制200,请求限制2000.

nginx和后端服务之间http长链接

  • nginx和后端服务之间的默认处理方式:建链,发请求,返回响应,关闭链接。
  • 每一个请求的处理都是这样的过程,而建链需要经过3次tcp握手,关闭链接则需要4次握手,非常耗费系统cpu和内存资源。
  • 短链接还有一个坏处:关闭链接方一般是nginx,这样链接将进入TIME_WAIT状态,并等待2倍MSL时间(一般是30sx2=60s)才能真正把链接回收,在高并发时很容易导致端口耗尽(端口资源总共最多6万多个),影响nginx后续向后端服务发送请求(没有可用端口)。
  • 因此改成长链接方式是最重要,也是最需要优化的,更改成长链接后建链一次,可以处理多次http请求。需要注意在一个链接中的http请求是串行处理的。
upstream yhzxtest {
        server 192.168.x.xxxx:8080;
        keepalive_requests 1000000;
        keepalive 400;
}
server {
        listen 80;
        server_name  yhzxtest.zhuojian;
        limit_req zone=perip_req burst=100 nodelay;
        limit_req zone=perserver_req burst=1000;

        limit_conn perip_conn 100;
        limit_conn perserver_conn 400;

        location / {
                proxy_pass      http://yhzxtest/;
                proxy_http_version 1.1;
                proxy_set_header Connection "";
        }
   }
  • keepalive: 表示空闲链接的最大数量,不要小于后端连接数限制,否则会出现虽然还未到连接数限制,但仍然频繁关闭链接的情况。
  • keepalive_requests:为了避免资源泄漏,一个链接处理了这个参数定义的请求数后将关闭。后续请求将通过别的链接或新建的链接来处理。繁忙系统为了避免频繁关闭造成性能影响可以设置更大的数值。这里有两个坑:1. 低于1.15.3的版本无法设置这个参数 2. 默认值是100,在高并发的场景,100个请求可能只持续1s,也就是说如果不设置这个参数或者设太小,nginx还是会频繁关闭连接、新建连接。这个数可以设得相对大一点,确保长链接至少可以持续1天。
  • proxy_http_version: 指定http版本1.1,因1.0http版本默认不支持http长链接
  • proxy_set_header: nginx会透传客户端的http头部,为了避免客户端头部影响,这里要把Connection头部清掉,以便支持http长链接。

黑名单

常规配置,略

http2.0

客户端http2

  • 前提条件:有证书,使用https。
  • 启用http2优点:可以配置更少的单客户端链接限制,能同时更高效处理更多的客户端链接和请求(多路复用)。
  • 启用http2缺点:cpu要求更高(加解密计算)
  • 总的来说,开启http2,能提升一定客户端性能,但提升nginx性能有限,甚至对cpu要求会更高。特别是对于每个客户端少量请求,频繁关闭的场景,提升意义不大。

后端http2

  • 不建议在nginx和后端之间采用http2,官方讨论在这里,核心意思是因为nginx和后端之间的链接和请求可以有很多,用多路复用意义不大,由于更复杂的ssl握手和加解密反而影响性能。

独立nginx还是合用nginx

有条件(有机器和带宽),还是应该让有高并发需求或者容易招人攻击的后端服务用独立的nginx做反向代理,避免相互影响。

其它思考

限制连接和请求以及启用黑名单功能都不能在高并发或恶意刷请求的场景中使nginx完全保持健康,原因是nginx的工作方式:

  • 建链握手和拆链握手都是操作系统内核的工作,nginx本身无法拒绝客户端建链,而建链本身是消耗cpu和带宽的(还消耗系统文件描述符)。
  • nginx对黑名单和限制连接及请求的处理,都是在http应用层协议返回错误信息(如502,服务临时不可用),维持这些连接,发送这些数据也是耗cpu和带宽的。所以这里还有一个优化:极简化nginx错误返回数据,比如从100个字节的html数据改成10+个字节的文本数据,乘以被拒的并发请求数,将节省一个数量级的可观流量,同时降低cpu损耗。

如果需要在tcp握手阶段就断开连接以便节省系统资源,需要在网络传输层而不是应用层做工作,比如:添加防火墙黑名单(linux的iptables或者专业防火墙网络设备等)