part.5.1 Nginx的系统层优化

硬件优化

从软件层面提示硬件使用效率
  • 增大 CPU 的利用率
  • 增大内存的利用率
  • 增大磁盘 IO 的利用率
  • 增大网络带宽的利用率
提升硬件规格
  • 网卡 -- 万兆网卡,例如 10G25G40G 等
  • 磁盘 -- 固态硬盘,关注 IOPS 和 BPS 指标
  • CPU -- 更快的主频,更多的核心,更大的缓存,更优的架构
  • 内存 -- 更快的访问速度
超出硬件性能上限后使用 DNS

高效使用 CPU

问:如何增大 Nginx 使用 CPU 的有戏时长
  • 能够使用全部 CPU 资源
    • master-worker 多进程架构
    • worker 进程数量应当大于等于 CPU 核数
  • Nginx 进程间不做无用功浪费 CPU 资源
    • worker 进程不应在繁忙时,主动让出 CPU
      • worker 进程间不应由于争抢造成资源耗散
        • worker 进程数量应当等于 CPU 核数
      • worker 进程不应调用一些 API 导致主动让出 CPU
        • 拒绝类似的第三方模块
  • 不被其他进程争抢资源
    • 提升优先级占用 CPU 更长的时间
    • 减少操作系统商耗资源的非 Nginx 进程
设置 worker 进程的数量
Syntax: worker_processes number | auto
Default: worker_processes 1;
Context: main
问:为何一个 CPU 就可以同时运行多个进程?
  • 宏观上并行,微观上串行
    • 把进程的运行时间分为一段段的时间片
    • os 调度系统依次选择每个进程,最多执行时间片指定的时长
  • 阻塞 APC 引发的时间片主动让出 CPU
    • 速度不一致引发的阻塞 API
      • 硬件执行速度不一致,例如 CPU 和磁盘
    • 业务场景产生的阻塞 API
      • 例如同步读网络报文
问:什么决定 CPU 时间片的大小
  • Nice 静态优先级 -20~19 (数字越大越友好)
  • Priority 动态优先级 0~139
    • 使用 O1 调度算法,根据 CPU 消耗型进程和 IO 消耗型进程调整幅度,幅度+-5
确保进程在运行态
  • R 运行 -- 正在运行或在运行队列中等待
  • S 中断 -- 休眠中,受阻,在等待某个条件的形成或接受到信号
  • D 不可中断 -- 收到信号不唤醒和不可运行,进程必须等待直到有中断发生
  • Z 僵死 -- 进程已终止,但进程描述符存在,直到父进程调用 wait4() 系统调用后释放
  • T 停止 -- 进程收到 SIGSTOPSIGSTPSIGTINSIGTOU 信号后停止运行
减少进程间切换
  • Nginx worker 尽可能的处于 R 状态
    • R 状态的进程数量大于 CPU 核心时,负载急速增高
  • 尽可能的减少进程间切换
    • 何为进程间切换
      • 是指 CPU 从一个进程或线程切换到另一个进程或线程
      • 类别
        • 主动切换
        • 被动切换:时间片耗尽
      • Cost<5us
    • 减少主动切换
    • 减少被动切换
      • 增大进程优先级
  • 绑定 CPU
延时处理新连接
使用 TCP_DEFER_ACCEPT 延迟处理新连接
Syntax: listen address[:port] [deferred];
Default: listen *:80|*:8000;
Context: server
如何查看上下文切换次数
  • vmstat -- 在 system 中的 cs 列中
  • dstat -- 在 csw 列中
  • pidstat -w -p 进程号 -- 在 cswch/s 和 nvcswch/s 列中
设置 worker 进程的静态优先级
Syntax: worker_priority number
Default: worker_priority
Context: main

多核间的负载均衡

在 nginx 早期的版本中有一个 accept_mutex 指令,现在这个指令默认是关闭的。这个指令是为了解决精群问题。精群问题就是当多个 worker 进程空闲时有一个请求处理,避免多个 worker 进程都去处理这个请求。accept_mutex 指令加里一把锁,保证在同一时间只有一个 worker 进程在处理请求。

现在的 nginx 在内核层面提供了一个负载均衡的机制,叫 reuseport。它在 KERNEL层面上, 让每一个worker进程都在监听,它做负载均衡。

多队列网卡对多核 CPU 的优化
  • RSS -- Receive Side Scaling 硬中断负载均衡
  • RPS -- Receive Packet Steering 软中断负载均衡
  • RFS -- Receive Flow Steering
提升 CPU 缓存命中率:worker_cpu_affinity

CPU 中有寄存机和多级缓存,CPU 是不能直接进内存中读取数据的,要通过一级一级的缓存来读取。如果经常换核处理,则前一个核中的缓存就失效了,这就是所谓的 CPU 缓存命中率。

绑定 worker 到指定 CPU
syntax: worker_cpu_affinity cpumask ...;
        worker_cpu_afinity auto [cpumask];
Default: --
Context: main
NUMA 架构

随着 CPU 核数越来越多,内存总线访问的时候多并发情况下就会导致冲突。让一半的核心通过一根内存总线访问一半的内存,另一半的核心通过另一根内存总线访问另一半的内存。

查看 cpu 信息
numactl --hardware

查看命中率
numastat

慢启动和拥塞窗口

  • 拥塞窗口 -- 发送方主动限制流量
  • 通过窗口(对端接收窗口) -- 接收方限制流量(如果过小,会导致发送方长时间接收不到响应,从而重复发送请求)
  • 实际流量 -- 拥塞窗口与通告窗口的最小值
拥塞处理
  • 慢启动
    • 指数扩展拥塞窗口(cwnd 为拥塞窗口大小)
      • 每收到1个 ACKcwnd = cwnd + 1
      • 每过一个RTTcwnd = cwnd * 2
  • 拥塞避免:窗口大于 threshold
    • 线性扩展拥塞窗口
      • 每收到1一个 ACKcwnd = cwnd + 1/cwnd
      • 每过一个RTT,窗口加1
  • 拥塞发生
    • 急速降低拥塞窗口
      • RTO超时,threshold = cwnd/2,cwnd = 1 -Fast Retransmit,收到3个 duplicate ACKcwnd = cwnd/2threshold = cwnd
  • 快速恢复
    • Fast Retransmit 出现时,cwnd 调整为 threshold + 3*MSS
RTT 与 RTO
  • RTT -- Round Trip Time
    • 时刻变化
    • 组成
      • 物理链路传输时间
      • 末端处理时间
      • 路由器排队处理时间
    • 指导 RTO
  • RTO -- Retransmission TimeOut
    • 正确的应对丢包

应用层协议的优化

TLS/SSL 优化握手性能
Syntax: ssl_session_cache off|none|[builtin[:size][shared:name:size]];
Default: ssl_session_cache none;
Context: http, server
  • off -- 不使用 Session 缓存,且 nginx 在协议中明确告诉客户端 session 缓存不被使用
  • none -- 不使用 session 缓存
  • builtin -- 使用 Openssl 的 session 缓存,由于在内存中使用,所以仅当同一客户端的两次连接都命中到同一个 worker 进程时,session 缓存才会生效
  • shared:name:size -- 定义共享内存,为所有 worker 进程提供 session 缓存服务。1MB大约可用于 4000 个 session
TLS/SSL 中的会话票证 tickets

nginx 将会话 session 中的信息作为 tickets 加密发给客户端,放客户端下次发起 TLS 连接时带上 tickets,由 nginx 解密验证后复用会话 session

会话票证虽然更易在 nginx 集群中使用,但破坏了 TLS/SSL 的安全机制,有安全风险,必须频繁更换 tickets 密钥。

#是否开启会话票证服务
Syntax: ssl_session_tickets on|off
Default: ssl_session_tickets on
Context: http, server

#使用会话票证时加密 tickets 的密钥文件
Syntax: ssl_session_ticket_key file
Default: --
Context: http, server
HTTP 长连接
  • 可减少握手次数
  • 通过减少并发连接数减少了服务器资源的消耗
  • 降低 TCP 拥塞控制的影响
Syntax: keepalive_requeests number
Default: keepalive_requests 100
Context: http, server, location

Syntax: keepalive_requests number
Default: keepalive_requests 100
Context: upstream
gzip 压缩

通过实时压缩 http 包体,提升网络传输效率。全称为 ngx_http_gzip_module,通过 --without-http_gzip_module 禁用模块。

Syntax: gzip on|off
Default: gzip off
Context: http, server, location, if in location

#压缩的请求类型
Syntax: gzip_types mime-type ...
Default: gzip_types text/html
Context: http, server, location

#压缩的最小文件大小
Syntax: gzip_min_length length
Default: gzip_min_length 20
Context: http, server, location

#通过正则筛选出不压缩的响应
Syntax: gzip_disable regex ...
Default: --
Context: http, server, location

#可压缩的 http 版本
Syntax: gzip_http_version 1.0|1.1
Default: gzip_http_version 1.1
Context: http, server, location


#是否压缩上游的响应
Syntax: gzip_proxicd off|expired|no-cache|no-store|private|no_last_modified|no_etag|auth|any ...;
Default: gzip_proxied off
Context: http, server, location

- off -- 不压缩来自上游的响应
- expired -- 如果上游响应中含有 Expires 头部,且其值中的时间与系统时间比较后确定不会缓存,则压缩响应
- no-cache -- 如果上游响应中含有"Cache-Control"头部,且其值含有"no-cache"值,则压缩响应
- no-store -- 如果上游响应中含有"Cache-Control"头部,且其值含有"no-store"值,则压缩响应
- private -- 如果上游响应中含有"Cache-Control"头部,且其值含有"private"值,则压缩响应
- no_last_modified -- 如果上游响应中没有"Last-Modified"头部,则压缩响应
- no_etag -- 如果上游响应中没有"ETag"头部,则压缩响应
- auth -- 如果客户端请求中含有"Authorization"头部,则压缩响应
- any -- 压缩所有来自上游的响应


#其他压缩参数
Syntax: gzip_comp_level level
Default: gzip_comp_level 1
Context: http, server, location

Syntax: gzip_buffers number size
Default: gzip_buffers 32 4k| 16 8k
Context: http, server, location

Syntax: gzip_vary on|off
Default: gzip_vary off
Context: http, server, location

磁盘 IO 的优化

直接 IO 绕开磁盘高速缓存

正常情况下,磁盘会先读到内核存储的高速缓存中,再读取到用户存储中,进行了两次读,如果命中了高速缓存中的内容就不需要去磁盘中读取内容,这是高速缓存的特点。

直接IO绕开了高速缓存,直接在磁盘中读取内容。适用于大文件的读取。

当磁盘上的文件大小超过size后,启用 directIO 功能,避免 Buffered IO 模式下磁盘也缓存中的拷贝消耗
Syntax: directio size | off
Default: directio off
Context: http, server, location

Syntax: directio_alignment size
Default: directio_alignment 512
Context: http, server, location
异步 IO

在正常情况下,读写磁盘时用户请求是被阻塞的,异步 IO 在读写磁盘时可以去处理其他任务。

Syntax: aio on | off | threads[=pool]
Default: aio off
Context: http, server, location

Syntax: aio_write on | off
Default: aio_write off
Context: http, server, location

#将磁盘文件读入缓存中待处理,例如 gzio 模块会使用
Syntax: output_buffers number size
Default: output_buffers 2 32k
Context: http, server, location
定义线程池
#仅能在静态资源文件站中定义
Syntax: thread_pool name threads=number [max_queue=number]
Default: htread_pool default threads=32 max_queue=65536
Context: main

减少磁盘读写次数

empty_gif 模块

全称为 ngx_http_empty_gif_module 模块,通过 --without-http_empry_gif_module 禁用模块。

从前端页面做用户行为分析时,由于跨域等要求,前端打点的上报数据一般是 GET 请求,且考虑到浏览器解析 DOM 树的性能消耗,所以请求透明图片消耗最小,而 1*1 的 gif 图片体积最小(仅43字节),故通常请求 gif 图片,并且在请求中把用户行为信息上报服务器。

nginx 可以在 access 日志中获取到请求参数,进而统计用户行为。但若在磁盘中读取1*1的文件则有磁盘 IO 消耗,empty_gif 模块将图片放在内存中,加快了处理速度

Syntax: empty_gif
Default: --
Context: location
access 日志的压缩
#buffer 默认 64kb
#gzip 默认级别为 1
#通过 zcat 解压查看
Syntax: access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=confition]]
Default: access_log logs/access_log combined
Context: http, server, location, if in location, limit_except
error.log 日志输出内存

在开发环境下定位问题时,若需要打开 debug 级别日志,但对 debug 级别大量日志引发的性能问题不能容忍,可以将日志输出到内存中。

配置语法为: error_log memory:32m debug

查看内容中日志的方法:

  • gdb -p [worker进程id] -ex "source nginx.gdb" --batch
  • nginx.gdb 脚本内容
set $log = ngx_cycle->log
while $log->writer != ngx_log_memory_writer
    set $log = $log->next
end
set $buf = (ngx_log_memory_buf_t *) $log->wdata
dump binary memory debug_log.txt $buf->start $buf->end

零拷贝与 gzip_static 模块

正常情况下,应用程序 nginx 作为一个静态资源服务,需要从磁盘中读取文件,然后把这个文件通过网卡发送给客户端;sendfile0 零拷贝字节将数据从磁盘拷贝到网卡,然后通过网卡发送给客户端。

直接 IO 会自动禁用 sendfile

gzip_static 模块

模块全称为 ngx_http_gzip_static_module,通过 --with-http_gzip_static_module 启用模块。

如果需要压缩,那么就一定要先拷贝到 nginx 中进行压缩;那么 gzip_static 模块会把静态资源文件压缩一份,以 .gz 结尾。检测到同名 .gz 文件时,response 中以 gzip 相关 header 返回 .gz 文件的内容。

Syntax: gzip_static on | off | always
Default: gzip_static off
Context: http, server, location
gunzip 模块

模块全称为 ngx_http_gunzip_module,通过 --with-http_gunzip_module 启用模块。

当客户端不支持 gzip 时,且磁盘上仅有压缩文件,则实时解压缩并将其发送给客户端。

Syntax: gunzip on | off
Default: gunzip off
Context: http, server, location

Syntax: gunzip_buffers number size
Default: gunzip_buffers 32 4k| 16 8k
Context: http, server, location

滑动窗口与缓冲区

用于限制连接的网速,解决报文乱序和可靠传输问题;Nginx 中 limit_rate 等限速指令皆依赖它实现;由操作系统内核实现;连接两端各有发送窗口与接收窗口。

  • 发送窗口 -- 用于发送内容
  • 接收窗口 -- 用于接收内容
nginx 的超时指令与滑动窗口
#两次读操作间的超时
Syntax: client_body_timeout time
Default: client_body_timeout 60s
Context: http, server, location

#两次写操作间的超时
Syntax: send_timeout time
Default: send_timeout 60s
Context: http, server, location

#以上两者兼具
Syntax: proxy_timeout timeout
Default: proxy_timeout 10m
Context: stream, server
丢包重传
  • net.ipv4.tcp_retries1 = 3 -- 达到上限后,更新路由缓存
  • net.ipv4.tcp_retries2 = 15 -- 达到上限后,关闭 TCP 连接
  • 仅作近似理解,实际以超时时间为准,可能少于 retries 次数就认定达到上限

优化缓冲区域传输效率

TCP缓冲区
  • net.ipv4.tcp_rmem = 4096 87380 6291456 -- 读缓存最小值、默认值、最大值,单位字节,覆盖 net.core.rmem_max
  • net.ipv4.tcp_wmem = 4096 16384 4194304 -- 写缓存最小值、默认值、最大值,单位字节,覆盖net.core.wmem_max`
  • net.ipv4.tcp_mem = 1511646 2055528 3083292 -- 系统无内存压力、启动压力模式阈值、最大值,单位为页的数量
  • net.ipv4.tcp_moderate_rcvbuf = 1 -- 开启自动调整缓存模式
Syntax: listen address[:port] [rcvbuf=size] [sndbuf=size]
Default: listen *:80 | *:8000
Context: server
调整接收窗口与应用缓存
net.ipv4.tcp_adv_win_scale = 1
应用缓存 = buffer / (2 ^ tcp_adv_win_scale)

BDP = 带宽 * 时延

吞吐量 = 窗口 / 时延

禁用 Nagle 算法

Nagle 算法避免一个连接上同时存在大量小报文(最多只存在一个小报文,合并多个小报文一起发送),提高带宽利用率。

  • 吞吐量有限:启用 Nagle算法,tcp_nodelay off
  • 低时延有限:禁用 Nagle 算法,tcp_nodelay on
#进针对HTTP keepAlive连接生效
Syntax: tpc_nodelay on | off
Default: tcp_nodelay on;
Context: http, server, lcoation

Syntax: tcp_nodelay on | off
Default: tpc_nodelay on
Context: stream, server
nginx 避免发送小报文
Syntax: postpone_output size
Default: postpone_output 1460
Context: http, server, location
启用 CORK算法

进针对 sendfile on 开启时有效,完全禁止小报文的发送,提升网络效率

Syntax: tcp_nopush on | off
Default: tcp_nopush off
Context: http, server, location

控制 TCP 三次握手

image

SYN_SENT 状态
  • net.ipv4.tcp_syn_retries = 6 -- 主动建立连接时,发 SYN 的重试次数
  • net.ipv4.ip_local_port_range = 32768 60999 -- 建立连接时的本地端口可用范围
主动建立连接时应用层超时时间
Syntax: proxy_connect_timeout time
Default: proxy_connect_timeout 60s
Context: http, server, location

Syntax: proxy_connect_timeout time
Default: proxy_connect_timeout 60s
Context: stream, server
SYN_RCVD状态
  • net.ipv4.tcp_max_syn_backlog -- SYN_RCVD状态连接的最大个数
  • net.ipv4.tcp_synack_retries -- 被动建立连接时,发SYN/ACK的重试次数
服务器端处理三次握手

SYN 网络分组发送 SYN 请求,服务器内核将此插入到 SYN 队列中,队列数量可控,同时发送 SYN/ACK ,这时这个请求就是半连接状态。当此客户端发送 ACK 请求时就将此从队列中取出,加入 到 ACCEST 队列中。这个队列中就是已经连接起连接的请求了。nginx 调用 accept 取出连接套接字。

建立TCP连接的优化

问:如何应对SYN攻击?

攻击者短时间伪造不同 IP 地址的SYN报文,快速占满backlog队列,使服务器不能为正常用户服务。

  • net.core.netdev_max_backlog -- 接收自网卡、但未被内核协议栈处理的报文队列长度
  • net.ipv4.tcp_max_syn_backlog -- SYN_RCVD状态连接的最大个数
  • net.ipv4.tcp_abort_on_overflow -- 超出处理能力时,对新来的 SYN 直接回包 `RST ,丢弃连接
tcp_syncookies
  • net.ipv4.tcp_syncookies = 1
    • 当 SYN队 列满后,新的 SYN不进入队列,计算出cookie 再以 SYN+ACK 中的序列号返回客户端,正常客户端发报文时,服务器根据报文中携带的 cookie 重新恢复连接。
      • 由于 cookie 占用序列号空间,导致此时所以TCP可选功能失效,例如扩充窗口、时间戳等。
一切皆文件:句柄数的上限
  • 操作系统全局
    • fs.file-max
      • 操作系统可使用的最大句柄数
    • 使用fs.file-nr可以查看当前已分配、正使用、上限
      • fs.file-nr = 21632 0 40000500
  • 限制用户
    • /etc/security/limits.conf
      • root soft nofile 65535
      • root hard nofile 65535
  • 限制进程
    • Syntax: worker_rlimit_nofile number
    • Default: --
    • Context: main
  • 设置 worker 进程最大连接数
    • Syntax: worker_connections number
    • Default: worker_connections 512
    • Context: events
    • 包括 Nginx 与上游、下游间的连接
两个队列的长度
  • SYN 队列未完成握手
    • net.ipv4.tcp_max_syn_backlog = 262144
  • ACCEPT 队列已完成握手
    • net.core.somaxconn
      • 系统级最大backlog队列长度
Syntax: listen address[:port] [backlog=number]
Default: listen *:80 | *:8000
Context: server
Tcp Fast Open

客户端发送 SYN包,服务端计算出一个 cookie,并且通过 SYN+ACK 包发送给客户端,客户端发送 ACK 包回应,三次握手建立,发送数据。当第二次建立连接时,客户端发送 SYN+Cookie 包就可以直接与服务端建立连接,省去了一次数据来回的时间。

  • net.ipv4.tcp_fastopen --系统开启`TFO功能
    • 0-- 关闭
    • 1 -- 作为客户端时可以使用 TFO
    • 2 -- 作为服务器时可以使用 TFO
    • 3 -- 无论作为客户端还是服务器,都可以使用 TFO
Syntax: listen address[:port] [fastopen=number]
Default: listen *:80 | *:8000
Context: server
  • fastopen=number
    • 为防止带数据的 SYN 攻击,限制最大长度,指定 TFO 连接队列的最大长度。

TCP 协议的 keepalive 功能

TCP 的 keepalive 功能主要用来检测实际断掉的连接和用于维持与客户端间的防火墙有活跃网络包

功能
  • linux 的 tcp keepalive
    • 发送心跳周期
      • net.ipv4.tcp_keepalive_time = 7200
    • 探测包发送间隔
      • net.ipv4.tcp_keepalive_intvl = 75
    • 探测包重试次数
      • net.ipv4.tcp_keepalive_probes = 9
  • nginx 的 tcp keepalive
    • so_keepalive = 30m::10
    • keepidle, keepintvl, keepcnt

减少关闭连接时的 time_wait 端口数量

被动关闭连接端的状态
  • CLOSE_WAIT 状态 -- 应用进程没有及时响应对端关闭连接
  • LAST_ACK 状态 -- 等待接收主动关闭端操作系统发来的针对 FIN 的 ACK 报文
主动关闭连接端的状态
  • fin_wait1 状态 -- net.ipv4.tcp_orphan_retries = 0 -- 发送FIN报文的重试次数,0相当于8
  • fin_wait2 状态 -- net.ipv4.tcp_fin_timeout = 60 -- 保持在 FINFIN_WAIT_2 状态的时间

lingering_close 延迟关闭TCP连接

当 nginx 处理完成调用 close 关闭连接后,若接收缓冲区仍然收到客户端发来的内容,则服务器会向客户端发送 RST包关闭连接,导致客户端由于收到RST而忽略了http response。

lingering 配置指令
#off -- 关闭功能
#on --  nginx 判断,当用户请求未接收完(根据 chunk 或者 Content-Length 头部等)时启用功能,否则及时关闭连接
#always -- 无条件启用功能
Syntax: lingering_close off | on | always
Default: lingering_close on
Context: http, server, location

#当功能启用时,最长的读取用户请求内容的时长,达到后立即关闭连接
Syntax: lingering_time time
Default: lingering_time 30s
Context: http, server, location

#当功能启用时,检测客户端是否仍然请求内容到达,若超时后仍没有数据到达,则立即关闭连接
Syntax: lingering_timeout time
Default: lingering_timeout 5s
Context: http, server, location
以 RST 代替正常的四次握手关闭连接
当其他读、写超时指令生效引发连接关闭时,通过发送 RST 立刻释放端口、内存等资源来关闭连接
Syntax: reset_timedout_connection on | off
Default: reset_timedout_connection off
Context: http, server, location