Nginx学习笔记三:Nginx架构基础(二)

目录

1、Nginx的请求切换

2、Nginx的模块

3、Nginx模块的分类

4、Nginx通过连接池处理网络请求

5、Nginx内存池

6、worker进程的协同工作

7、内存管理器Slab

      Openresty如何使用 ngx_slab_stat 模块

8、哈希表的max_size与bucket_size如何配置

9、添加动态模块


1、Nginx的请求切换

传统服务器:一线程仅处理一连接

  • 不做连接切换
  • 依赖OS的进程调度实现并发

Nginx Worker:一线程同时处理多连接

  • 用户态代码完成连接切换
  • 尽量减少OS进程切换

2、Nginx的模块

编译进Nginx、提供哪些配置项、模块何时被使用、提供哪些变量?

  • 模块ngx_http_core_module
  • 打开 objs 目录下的 ngx_modules.c 文件,可以看到 ngx_modules[] 数组包含了所有编译进Nginx的模块。
  • 在 src / http / modules  目录下 查看http具体某个模块的源代码,如 ngx_http_gzip_filter_module.c 。
  • 如果是第三方模块,也会有.c文件,ngx_command_t 是每个模块必须具备的结构体(唯一),这个结构体是一个数组,数组中的成员是其支持的指令名。如static  ngx_command_t ngx_http_gzip_filter_commands[] 里面有一个成员是 "gzip"。
  • ngx_module_t 是通用的模块,其中的 index 定义了所有模块的顺序,这个顺序是很重要的,决定了有些模块发生冲突时,先生效的模块会阻碍后生效的模块发挥作用。
  • 模块细分为子模块,都遵循同样的规则,每类子模块会定义各自新的规则。
  • 高内聚,相应独立的功能是在同一个模块中
  • 抽象

Nginx学习笔记三:Nginx架构基础(二)_第1张图片

3、Nginx模块的分类

  • 每个模块必须有的数据结构ngx_module_t,其中的成员 type 定义了这个模块是属于哪个类型的模块
  • core:event、http、mail、stream

Nginx学习笔记三:Nginx架构基础(二)_第2张图片

4、Nginx通过连接池处理网络请求

  • 每个连接自动地对应一个读事件与一个写事件,所以在ngx_cycle_t结构体中有 connections 、 read_events 、 write_events三个数组。数组的大小跟 worker_connections(可配置) 是对应的,默认是 512 。三个数组是通过序号对应起来的。
  • 在使用时,我们配置 worker_connections 成足够使用的,但其指向的数组同时影响了打开的内存,其数越大意味着Nginx使用了更大的内存。

Nginx学习笔记三:Nginx架构基础(二)_第3张图片

每个connection连接到底使用了多大的内存呢?

  • 每个 connection 就是 ngx_connection_s 结构体,其在 64 位系统中大约占 232 字节,每个connection对应着两个事件,读事件与写事件,每个事件所对应的结构体是 ngx_event_s,大约占 96 字节,所以每个连接大约消耗了 424 字节。
  • worker_conection 配置得越大,初始化所消耗的内存就更多
  • ngx_event_s 结构体中的 handler 是一个回调方法,timer 就是 Nginx 实现超时定时器的结构体,当我们对http请求作读超时/写超时时,其实是在操作它的读事件写事件中的 timer,handler 和 timer 都是可配置的
  • ngx_connection_s 成员: readwrite 是读写事件,recvsend 是抽象的操作系统的底层方法,怎样进行发送和接收。sent,表示在 http请求中的字节数,bytes_sent

Nginx学习笔记三:Nginx架构基础(二)_第4张图片

5、Nginx内存池

内存池概述:

  • 内存池是在真正使用内存之前,预先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够用时,再继续申请新的内存。
  • 内存池的好处是:减少向系统申请和释放内存的时间开销,解决内存频繁分配产生的碎片,提高程序性能。

Nginx内存池中有一个头部,头部里又包含一个数据部,数据部主要用来为用户分配小块内存。数据部之外主要为用户分配大块内存、管理外部资源、日志信息以及内存池的一些其他信息。

Nginx学习笔记三:Nginx架构基础(二)_第5张图片

ngx_pool_s的ngx_pool_data_t中:

  • last:是一个unsigned char 类型的指针,保存的是当前内存池分配的末位地址,即下一次分配从此处开始
  • end:内存池结束位置
  • next:内存池里面有很多块内存,这些内存块就是通过该指针连成链表的,next指向下一块内存
  • failed:内存池分配失败次数

ngx_pool_s:

  • max:内存池数据块的最大值
  • current:指向当前内存池
  • chain:该指针挂接一个 ngx_chain_t 结构
  • large:大块内存链表,即分配空间超过 max 的情况使用
  • cleanup:释放内存池的callback
  • log:日志信息

Nginx内存池的创建:

  • Nginx为每一个层级都会创建一个内存池,进行内存的管理,比如一个模块,tcp连接,http请求等,在对应的生命周期结束的时候会摧毁整个内存池,把分配的内存一次性归还给操作系统。
  • 在分配的内存上,nginx有小块内存和大块内存的概念,小块内存Nginx在分配时候会尝试在当前的内存池节点中分配,而大块内存会直接调用系统函数 alloc 向操作系统申请。

内存池分配内存有三种情况:

  1. 申请的是小块内存,内存池的空间足够:直接移动起始指针
  2. 申请的是小块内存,内存池的空间不够:建立同等大小的内存池并分配size大小(用户需要申请的空间size)的空间
  3. 申请的是大块内存:在堆中申请大块内存,并用链表管理
  • 开辟内存为什么要考虑内存对齐?因为内存取整可以降低CPU读取内存的次数,提高性能。主要为减少内存的I/O操作,由于此操作性能太低。

大部分参考资料:

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
  • nginx内存池的大块内存和小块内存的界限是max即一页内容=4096字节=4k
  • 小块内存为什么不提供释放函数,只有一个重置函数ngx_reset_pool什么时候调用呢 ?因为http协议本身就是一个短连接,无状态的协议。处理完请求,服务器断开。在处理完http请求完成之后,就可以发起内存池的重置函数。

内存池的重置和清理:

  • 在释放内存时,nginx没有专门提供针对释放小块内存的函数,小块内存会在 ngx_destory_pool(销毁内存池)ngx_reset_pool(重置内存池) 的时候一起释放
  • 内存池清理时,只释放大块内存
  • 内存池重置时,释放大块内存,重置小块内存 

6、worker进程的协同工作

Nginx是一个多进程程序,不同的worker进程之间通过共享内存进行通信。

  • 所谓共享内存,也就是我们打开了一块共享内存,比如说10M,多个worker进程之间可以同时访问它,包括读取和写入。
  • 解决竞争关系:早期是基于信号量的锁,现在大多数是自旋锁,要求所有的Nginx模块快速地使用共享内存,快速地取得锁,再快速地释放,避免出现死锁、性能下降等问题。

Nginx进程间的通讯方式

  • 基础同步工具:信号 、共享内存
  • 高级通讯方式:锁(自旋锁) 、 Slab内存管理器

红黑树:其插入、删除、查找操作的时间复杂度都是O(logN)。应用如限速、流控、反向代理、ssl等

单链表:将需要共享的元素串联起来。

Ngx_http_lua_api:OpenResty的核心模块。

Nginx学习笔记三:Nginx架构基础(二)_第6张图片

OpenResty共享内存代码示例

Nginx学习笔记三:Nginx架构基础(二)_第7张图片

7、内存管理器Slab

Nginx 不同的worker之间需要共享信息的时候,只能通过共享内存。共享内存会使用链表,红黑树这样的数据结构。但是每个红黑树上有很多节点,每个节点上都需要分配内存去存放。怎样把一整块共享内存切割成一小块给红黑树上面的每一个节点使用呢?

  • Bestfit分配内存方式:最多两倍内存消耗、适合小对象、避免碎片、避免重复初始化
  • 整块的共享内存分为很多页面,每个页面4k,会切分为很多slot。比如32字节是一种slot,64字节是一种slot,128字节也是一种slot,这些slot是以乘2的方式向上增长。如果现在有一个51字节需要分配的内存,会存放在64字节。
  • slot指向的就是不同大小的块,这种数据结构会有内存的浪费。如上例就浪费了13字节。
  • 如果我们分配的内存非常小,比如小于一个页面的大小就非常合适,很少有碎片。 
  • 每分配一块内存就是沿着还没使用的空白内存继续使用。
  • 当一个页面使用满以后,再拿一个空闲页面给slab slot大小的内存继续使用。
  • 有时候分配在一段内存上的数据结构是固定的,需要初始化,用 Bestfit 这种方式原先的数据结构都还在。当重复使用的时候,避免了重复初始化。

slab内存,应用在Openresty的 lua_shared_dict 以及limit request 、limit connection。

Nginx学习笔记三:Nginx架构基础(二)_第8张图片

ngx_slab_stat:统计Slab使用状态。

Nginx学习笔记三:Nginx架构基础(二)_第9张图片

  • 可以看到不同的slot,分配了多少,使用了多少,有多少请求在访问,失败了多少次。

Openresty如何使用 ngx_slab_stat 模块

  1. 先下载编译 tengine ,该模块路径tengine-2.2.2/modules/ngx_slab_stat。这是一个标准的 Nginx 第三模块,每个第三方模块会通过 .c 文件定义好nginx_module_t这样的结构体、以及处理哪些配置项、提供哪些变量,并有一个config来帮助它编译到目标nginx中。
  2. 让Openresty编译时把 ngx_slab_stat 模块编译进去,然后使用 lua_shared_dict 分配内存,再用 slab_stat 查看共享内存的使用情况。

  • add_module 这个命令可以把一个目录下具备 config 这样配置项的目录添加到 Nginx 目录中。即让模块的源码让 ./configure 识别到。

slab_stat 如何使用: vim nginx.conf

Nginx学习笔记三:Nginx架构基础(二)_第10张图片

  • location 的 slab_stat 是一个 slab_stat 提供的配置项,会返回 slab_stat 统计状况。

Nginx学习笔记三:Nginx架构基础(二)_第11张图片

 

  • slab 内存分配使用了 Bestfit 思想,也是linux操作系统经常使用的内存分配方式。
  • 通常我们使用共享内存时都需要使用 slab 分配相应的内存给对象,再使用上层的数据结构维护这个对象。

8、哈希表的max_size与bucket_size如何配置

nginx的容器是很多nginx高级功能的实现基础。即使我们不需要编写第三方模块、查看nginx源代码,但我们可能需要变更nginx配置文件达到最大化的性能,因此需要了解nginx的容器是怎样使用的。

Nginx 六个容器

  • 数组 (ngx_array_t :和平常理解的数组是不同的,是多块连续内存,其中每一块连续内存中可以存放许多元素)
  • 链表(ngx_list_t)
  • 队列(ngx_q_t,很多nginx数据结构中都有相应的这样的数据结构,这些结构体实现的功能是类似的,只是操作方法不同)
  • 哈希表(常用)
  • 红黑树(常用)
  • 基数树(自平衡排序二叉树的一种,只不过key只能是整型,所以像geo模块在使用基数树,其他使用基数树的场景不多)

哈希表

  • name是key,value是一个指针,指向实际的内容

Nginx学习笔记三:Nginx架构基础(二)_第12张图片

  1. Nginx 哈希表应用场景不同,仅仅应用于静态不变的内容,即在运行过程中 hash表 通常不会出现插入和删除,即 nginx 刚启动的时候就能确定这个hash表中一共有多少个元素。
  2. 当使用hash表的这些模块通常会暴露出来 max_size、bucket_size 这两个参数给我们使用。当我们使用的时候,我们的 max_size 仅仅控制了最大的hash表bucket的个数而不是实际上hash表bucket的个数。
  3. 比如:max_size可能配置为100,但是实际上只有10个元素使用了哈希表,与实际上bucket_size不符。它的意义在于可以限制最大化的使用,因为消耗了内存。

使用hash表的模块有怎样的特点呢?

Nginx学习笔记三:Nginx架构基础(二)_第13张图片

           比如在stream、http的核心模块里,对所有的变量(variables)使用了hash表,因为变量在我们模块编译进去的时候就已经定义清楚了。还有像 map(stream/http)反向代理(因为反向代理中,我们需要对很多header,在配置文件中定义好的header做hash提升性能,后面的referer、ssi等等也是一样的道理,因为hash表访问复杂度O(1))。

           哈希表中有一个bucket size ,往往有默认值,这个默认值在Nginx配置文档中说是cpu cache line ,会对齐到这样一个值。有什么意义呢?这个值实际上影响了我们怎样去配置bucket size。现在主流的cpu有L1、L2、L3缓存的,它在取主存的数据时,并不是按照所谓的64位或32位取数据。现在主流的cpu一次取主存取的字节数就是cpu cache line ,现在是64字节。

           哈希表为什么要向64字节对齐呢?假设我们每个哈希表的bucket 是59字节,如果是紧密排在一起的,取第一个哈希表元素只需要访问一次。但取第二个元素的时候需要访问主存两次,即第1个比 cacheline 少的话,总有一个会占有2个 cacheline 。为了避免取两次的问题,nginx在代码中会自动的向上对齐

          所以在配置bucket_size 的时候需要注意两个问题:

  1. 如果配置的bucket size 不是cpu cache line,例如配置了70字节,实际上是分配每个元素128字节。
  2. 尽量不要超过64字节,以减少cpu访问每个hash表元素的次数。

实际上还有很多第三方模块使用了hash表,使用hash表需要注意:

  1. 它只为静态不变的内容服务。
  2. hash表的 bucket size 需要考虑 cpu cache line 的对齐问题。

9、添加动态模块

这里以安装第三方ngx_http_google_filter_module模块为例

1. 下载第三方扩展模块ngx_http_google_filter_module

 cd /data/software/
 git clone https://github.com/cuber/ngx_http_google_filter_module

2. 查看nginx编译安装时安装了哪些模块
将命令行切换到nginx执行程序所在的目录并输入 ./nginx -V,具体如下:

[root@liuyazhuang121 sbin]# ./nginx -V
nginx version: nginx/1.9.3
built by gcc 4.4.7 20120313 (Red Hat 4.4.7-17) (GCC) 
built with OpenSSL 1.0.2 22 Jan 2015
TLS SNI support enabled
configure arguments: --prefix=/usr/local/nginx-1.9.3 --with-openssl=/usr/local/src/openssl-1.0.2 --with-pcre=/usr/local/src/pcre-8.37 --with-zlib=/usr/local/src/zlib-1.2.8 --with-http_ssl_module
[root@liuyazhuang121 sbin]# 

可以看出编译安装使用了 --prefix=/usr/local/nginx-1.9.3 --with-openssl=/usr/local/src/openssl-1.0.2 --with-pcre=/usr/local/src/pcre-8.37 --with-zlib=/usr/local/src/zlib-1.2.8 --with-http_ssl_module 这些参数。
3. 加入需要安装的模块,重新编译
这里添加 -–add-module=/data/software/ngx_http_google_filter_module
具体如下:

./configure  --prefix=/usr/local/nginx-1.9.3 --with-openssl=/usr/local/src/openssl-1.0.2 --with-pcre=/usr/local/src/pcre-8.37 --with-zlib=/usr/local/src/zlib-1.2.8 --with-http_ssl_module -–add-module=/data/software/ngx_http_google_filter_module
如上,将之前安装Nginx的参数全部加上,最后添加-–add-module=/data/software/ngx_http_google_filter_module

之后,我们要进行编译操作,如下:

 make    //千万不要make install,不然就真的覆盖

4. 替换nginx二进制文件

# 备份原来的nginx执行程序
 cp /usr/local/nginx-1.9.3/sbin/nginx /usr/local/nginx-1.9.3/sbin/nginx.bak
# 将新编译的nginx执行程序复制到/usr/local/nginx/sbin/目录下
 cp /opt/nginx/nginx /usr/local/nginx/sbin/

 

你可能感兴趣的:(Nginx)