nginx设计中的性能优化点
一些指标
- 10000个非活跃 http keep alive 连接在nginx消耗2.5MB内存
- 单机支持10万以上并非连接
和nginx有关的内核调优:
可以修改/etc/sysctl.conf来更改内核参数调优
- file-max: 一个进程最大可以打开的文件句柄数, 这个参数直接限制了nginx的最大并发连接数
- tcp-tw-reuse: 设置为1时表示允许将time-wait状态的socket重新新的tcp连接。 ps. 内存方面现在linux已经优化的很好, 处于time-wait状态的socket只占用很少的内存, 释放/复用端口最重要, 毕竟端口数就就那些。
- tcp-keepalive-time: 当keep-alive启用时,tcp发送keep-alive消息的频率, 默认两小时, 设置的低一些可以更快清理无效连接。
- tcp_fin_timeout: 服务器主动断开连接时, socket保持在fin-wait-2状态的最大时间
- tcp_max_sync_backlog: syn队列最大长度
- ip_local_port_range: tcp/udp端口取值范围(是设置本地, 不能设置远端)
- net.ipv4.tcp_rmem/net.ipv4.tcp_wmem: tcp接受/发送滑动窗口的最小值, 最大值, 默认值
- netdev_max_backlog: 网卡接收数据包的速度大于内核处理速度时, 有一个队列保存这些数据包, 这个参数表示队列最大值
- rmem_default/wmwm_default: 内核套接字接收/发送缓存区的默认大小
- rmem_max/wmem_max: 内核套接字接收/发送缓存区的最大大小
nginx为访问第三方服务做的优化
在用nginx开发模块时,如果需要访问第三方服务, 不能自己简单的用套接字编程, 会破坏nginx全异步架构。nginx提供了两种全异步通信方式:
upstream: 它把nginx定义成代理服务器, 首要功能时透传。 nginx反向代理功能就是基于此。
subrequest用于处理请求派生出的子请求
两者区别: 比如实现频控模块要去redis读写数据, 这就是一个子请求, 在异步收到响应后, 如果通过频控, 就需要将请求透传给上游, 此时用upstream
配置搜索优化
server 配置通过hash表搜索
location块通过二叉树搜索, 因为是静态的,不会变, 所以不用红黑树, 用的完全平衡二叉树
内存池:
用于减少系统调用次数;
如何避免gc机制: 开发者申请一个内存池后可以从中任意申请内存, 最后用完销毁内存池即可。 nginx是一个web服务器, 每一个请求的生命周期很短, 也决定了每个内存池的生命周期短, 因此不会累积过多没有销毁的内存池。而在开发模块时,直接使用ngx_poll_t即可, 模块结束后自动释放。 ps. 感觉rust的生命周期, 所有权也是类似思想, 超过生命周期就销毁,只不过rust是语言去检查生命周期, nginx这里需要自己检查, 不用像go java需要另外线程/协程 去gc。
申请大内存和小内存的不同处理: 当nginx判断申请内存大(x86 大于4095字节)的时候, 直接从堆上申请内存,挂到一个大内存链表上,大内存的生命周期可能远远小于请求的生命周期的情况就需要nginx考虑了, nginx提供了方法提前释放大类存而不用等到内存池销毁。
小块内存niginx会一次申请比较大的一块, 后面复用。内存对齐,碎片整理: p281 ~ p283 重点看p283流程图
事件:
- 网络事件: epoll
- 定时事件器: 红黑树实现, 按照每个任务的时间来排序, 每次取最左边的节点的时间和当前时间判断即可。调用ngx_event_expire_timers方法触发所有超时事件。
- 磁盘事件: linux 真.异步io,书里面吐槽了下glic里的异步io是多用户线程实现的,假异步io, 而nginx的读操作用的是linux内核实现, 只有在内核进程中完成了磁盘操作, 内核才会通知nginx进程。 这样的好处一方面是可以让nginx更充分占用cpu, 另一方面当有大量读磁盘的事件堆积到内核的时候,内核可以通过“电梯算法”降低随机读磁盘的成本。
为啥是读操作采用异步而写操作不采用呢? 因为linux异步磁盘io写操作不支持缓存, 也就是每次用户进程都要写到磁盘上去, 这样更慢。
进程模型:
- master-woker: nginx启动时master进程进行初始化读取配置文件后fork出worker进程来监听处理网络事件, 因为时fork出来的, 所以和master监听的是同意个端口, accept事件到来时会有争抢。
- master进程不负责网络事件的监听和处理, 主要是负责启动服务, 平滑升级, 更换日志文件, 热更新配置等, 管理woker等功能。 是通过信号来管理woker进程。(说白了就是设置标志位)
- woker进程监听和处理网络事件, 一个woker挂了时master会快速拉起一个新woker
- 一个优化, nginx通常线程数和核心数相当, 可以配置进程绑定cpu, 减少进程在cpu间的越迁, 高效利用cpu cache
- 惊群问题: nginx 会fork 多个子进程监听同一个端口, 子进程在accept建立新连接时会争抢,进程数量多时会有性能下降。
开accept_mutex锁(默认打开)解决。 nginx通过一个进程间同步锁accept_mutex, 保证同一时刻只能由一个woker在监听web端口。释放锁通过定时器事件来实现。 - 进程间负载均衡问题:多个woker抢一个事件时, 会有一个成功, 但如果这个woker本来就已经堆积了大量事件,另外有空闲进程或者任务少的进程抢不到事件, 子进程间就出现了负载不均衡。
开accept_mutex锁解决(默认打开), 每个woker有个变量ngx_accept_disabled, 初始为负值,绝对值为配置的总连接数的7/8, 每次使用一个连接就加1。这个值为正时触发负载均衡,此时来accept事件时此woker不会去抢accept_mutex, 而是将ngx_accept_disabled 减一。 疑问, 为何是7/8?
TCP与ngixn(nginx基于tcp上的优化):
p342 全节重点
- 基础知识:
- 三次握手时的syn, accept队列
- 用户进程在调用内核send方法时的的流程图 9-9
- recv方法的流程图3. recv方法的流程图
- nginx的优化:
用户可调整内核内存缓存的上限, p345 下面一段
nginx也提供了内存自动调整功能, p347 图, 相当于多了几个阈值
再看upstream
- client 到nginx和nginx到server间的网速是极不对称的, 反向代理的一个重要功能就是, 提供大内存和磁盘文件缓存包, 按照一定策略,接受完一定大小的包后, 再由nginx发给client或server。 这样做减少了server需要维持的连接和缓存的占用时间。
- 与server:
- 建立tcp连接三次握手是异步的, 发送syn后epoll注册事件监听, 设置回调函数
- 向上游发送请求或者接收下游请求时, 请求大小未知,所以发送请求的方法需要被epoll调度多次后才能发/收完请求的全部内容。
- 包头大小有一个合理范围, 而包体不一定,并且nginx不关心包体内容, 因此nginx将包头全部用内存缓存, 包体则根据情况而定
-透传请求: nginx将client的请求包体全部接收完后才发给server - 透传响应: 可与配置三种模式:
- 1.不转发响应(不实现反向代理)
- 2.转发响应时上游网速优先: 若下游网速快于上游网速, 或者相差不大, 不需要nginx开辟大块内存或磁盘文件来缓存上游响应
- 3.转发响应时下游网速优先: 开辟内存会磁盘缓存响应