11 . Nginx核心原理讲解

应用场景优缺点

应用场景

11 . Nginx核心原理讲解

// 1.静态请求
// 2.反向代理
// 3.负载均衡
// 4.资源缓存
// 5.安全防护
// 6.访问限制IP
// 7.访问认证 /*
核心主要是以下三个应用:
静态资源服务: 通过本地文件系统提供服务
反向代理服务: Nginx的强大性能,缓存,负载均衡
API: OpenResty
*/
优点
/* 1.功能模块少
2.代码模块少
3.高可靠,热部署,可扩展.
4.BSD许可证.
是一个给于使用者很大*的协议,BSD 代码鼓励代码共享,但需要尊重代码作者的著作权。
BSD由于允许使用者修改和重新发布代码,也允许使用或在BSD代码上开发商业软件发布和销售,
因此是对商业集成很友好的协议。而很多的公司企业在选用开源产品的时候都首选BSD协议,
因为可以完全控制这些第三方的代码,在必要的时候可以修改或者二次开发。 5.CPU亲和
CPU亲和是一种把CPU核心跟nginx工作进程绑定在一起,把每个worker进程固定在一个CPU上执行,减少切换CPU的cache miss,获得更好的性能。
6.事件驱动模型.(此处很有可能被细问,最好研究清楚)
*/

Nginx同类型产品

Tengine
/*
Tengine是由淘宝网发起的Web服务器项目,他在Nginx的基础上,针对大量访问网站的需求,
增加了很多高级功能和特性,Tengine的性能和稳定性已经在大型网站如淘宝网,天猫商城得到了很好的检验,
他的目标就是打造一个高效,稳定,安全易用的平台.
*/
OpenResty
/*
这个开源Web平台主要由章亦春维护,2011年之前由淘宝赞助,后来12-16由美国的CloudFlare公司提供支持,
目前由OpenResty软件基金会和OpenRest lnc公司提供支持. 因为大部分Nginx模块都是由软件包的维护者开发,所以可以确保这些模块及其他组件可以很好的一起工作.
因为Nginx模块开发非常难,而他把nginx的事件驱动,非以lua语言开发然后提供给开发者,兼具高性能开发效率提升特点.
*/

Nginx原理

Nginx组成
/*
Nginx二进制可执行文件
由各模块源码编译出的一个文件 Nginx配置文件
控制Nginx的行为 Access.log访问日志
记录每一条http请求信息 Error.log错误日志
定位问题
*/
Nginx进程结构

11 . Nginx核心原理讲解

/*
有两种进程结构,一种是单进程的,一般测试,调试,开发用,生产环境一般都是多进程,因为生产环境要保证Nginx足够健壮,发挥多核特性,一般默认是是多进程.
他的进程架构一般是有一个父进程, 叫Master进程,他有很多子进程,分为两类,一个是Work进程,一个是Cache进程.
nginx做他的进程设计也是考虑到高可用高可靠,让第三方模块不会在master加入功能设计,虽然nginx允许第三方模块,
但一般不会这样做,master一般做worker进程管理,所有的worker进程是处理真正请求的,
master是监控worker是不是在工作,热部署,重新载入配置文件,缓存就是在多个worker共享,
还要在CacheManager,CacheLoader共享,为后端代理动态请求缓存使用的,CacheLoaer做缓存载入,缓存管理
进程间通信是通过共享内存解决的
*/

nginx之所以用多进程不用单进程因为:

要保证高可靠性,高可用性,当nginx启用了多线程时候,因为线程之间是共享一个内存地址空间的,所以当某一个第三方模块引发一个地址空间导致的段错误,在地址越界错误时,整个nginx会挂掉,多进程则不会.

Nginx请求处理流程

11 . Nginx核心原理讲解

/*
从Nginx内部看,有Web,EMAIL,TCP流量,分别有三种状态机,传输层状态机,HTTP状态机,MAIL状态机,
之所以叫状态机是因为他使用的非阻塞事件驱动处理,epoll,一般使用异步处理引擎要使用状态机才能将请求正确识别和处理,
所以我们在解析请求的时候分别走需要访问静态资源他就会找到静态资源,但会出现当内存不足以完全缓存所有文件信息,sedfile,io会退化成阻塞的IO操作,所以这里会有一个线程池处理,
反向代理就会做磁盘缓存,请求完成会将日志记录在access,error,也可以远程到其他服务器.
更多的时候,Nginx作为反向代理,负载均衡的,通过协议级传输到后面服务器,也可以通过应用层,fastcgi,uwsgi代理到应用服务器.
*/
信号管理Nginx的父子进程
/*
Master进程
监控worker进程
CHLD
管理worker进程
接受信号
TERM,INT 立刻停止Nginx
QUIT 优雅停止Nginx,但是不要对用户发送立刻结束连接向TCP reset的报文
HUP 重载配置文件
USR1 重新打开日志文件 USR2 这两个信号需要kill找到master的pid才能发送,上面可以直接nginx 发送信号
WINCH
Worker进程
接受信号
TERM,INT
QUIT
USR1
WINCH
nginx命令行
reload: HUP
reopen: USR1
stop: TERM
quit: QUIT
*/
Nginx reload的原理

11 . Nginx核心原理讲解

/*
1. 向master进程发送HUP信号(reload命令)
2. master进程检验配置与法是否正确.
3. master进程打开新的监听端口.
4. master进程用新配置启动新的worker子进程.
5. master进程向老worker子进程发送QUIT信号.
6. 老worker进程关闭监听句柄,处理完当前连接后结束进程.
*/
Nginx热升级流程
/*
1. 将旧Nginx文件换成新Nginx文件(注意备份)
2. 向master进程发送USR2信号
3. master进程修改pid文件名,加后缀.oldbin
4. master进程用新Nginx文件启动新Master进程
5. 向老master进程发送WINCH信号,关闭老worker
6. 回滚: 向老master发送HUP,向新master发送QUIT
*/

不停机载入新配置文件

11 . Nginx核心原理讲解

worker进程优雅的关闭

/*
1. 设置定时器: worker_shutdown_timeout
2. 关闭监听句柄
3. 关闭空闲连接
4. 在循环中等待全部连接关闭
5. 退出进程
*/

Nginx平滑升级流程

# 在上面源码编译安装Nginx1.14的基础上升级到1.16
wget http://nginx.org/download/nginx-1.16.0.tar.gz
tar xf nginx-1.16.0.tar.gz -C /usr/local/src/
cd /usr/local/src/nginx-1.16.0/
./configure --prefix=/usr/local/nginx116 --with-http_stub_status_module --with-http_ssl_module --user=nginx --group=nginx
make && make install # 备份原来老的Nginx文件,主要是为了回退
cp ./nginx/sbin/nginx ./nginx/sbin/nginx.bak # 将新版的Nginx二进制文件替换已安装的Nginx的二进制文件
cp nginx116/sbin/nginx nginx/sbin/nginx -rf ./nginx/sbin/nginx -v
# nginx version: nginx/1.16.0 # 给Nginx旧的主进程发送一个USR2信号,让新主进程和旧进程同时工作.
# 再发一个Winch给旧的主进程号让子进程退出,如果主进程还在,方便回滚
kill -USR2 17522

版本回滚

cp nginx.bak  ./nginx/sbin/nginx -rf

# 发送HUP信号唤醒旧版本
kill -HUP `cat /usr/local/nginx/logs/nginx.pid.oldbin ` # 关闭新版本的主进程和Worker进程
kill -USR2 24148
kill -WINCH 24148 [root@test1 local]# ./nginx/sbin/nginx -v
# nginx version: nginx/1.14.2
网络收发与Nginx事件间对应关系

11 . Nginx核心原理讲解

接下来看上面这张图,比如主机 A 就是一台家里的笔记本电脑,那么主机 B 就是一台服务器,上面跑着 Nginx 服务。从主机 A 发送一个 HTTP 的 GET 请求到主机 B,这样的一个过程中主要经历了哪些事件?通过上图数据流部分可以看出:

/*
应用层发送了一个GET请求 -> 到了传输层,这一步主要做一件事,就是浏览器打开一个端口,在windows的任务管理器可以看到这一点,
他会把这个端口记下来以及把Nginx打开的端口如80或者443记到传输层--> 然后网络层会记下我们主机所在的IP和目标主机,也就是Nginx所在服务器公网IP-->
到链路层以后--> 经过以太网-->到达家里的路由器(网络层),家里的路由器会记录所在运营商下一段的IP --> 通过广域网 --> 跳转到主机B所在的机器中 -->
报文会经过链路层 --> 网络层 --> 到传输层, 在传输层操作系统就知道给那个打开了80或者443进程,
这个进程自然就是Nginx --> 那么Nginx在他的HTTP状态机里面(应用层)就会处理这个请求.
*/

TCP流与报文

/*
数据链路层会在数据的前面Header部分和Footer部分添加上源MAC地址和源目的地址 -->
到了网络层则是Nginx的公网地址(目的IP地址)和浏览器的公网地址(源IP地址) -->
到了TCP层(传输层),指定了Nginx打开的端口(目的端口)和浏览器打开的端口(源端口) -->
然后应用层就是HTTP协议了. 这就是一个报文,也就是说我们发送的HTTP协议会被切割成很多小的报文,在网络层切割叫MTU,以太网的每个MTU是1500字节;
在TCP层(传输层)会考虑中间每个环节最大的一个MTU值,这个时候往往每个报文只有几百字节,
这个报文我们称为MSS,所以每收到一个MSS小于这么大小的一个报文其实就是一个网络事件.
这个时候,我们来看下TCP协议是怎样和我们日常调用的一些接口(比如Accept,Read,Write,Close)是怎样关联在一起的?
*/

11 . Nginx核心原理讲解

请求建立TCP连接事件实际上是发送了一个TCP报文,通过上面第二部分讲解的那样的一个流程到达了Nginx,对应的是读事件,因为对于Nginx来说,我读到了一个报文,所以就是Accept建立链接事件.

如果是TCP链接可读事件,就是发送了一个消息,对于Nginx也是一个读事件,就是Read读消息.

如果是对端(也就是浏览器)主动地关掉了,相当于 windows 操作系统会去发送一个要求关闭链接的一个事件,对于 Nginx 来说还是一个读事件,因为他只是去读取一个报文。

那什么是写事件呢?当我们的浏览器需要向浏览器发送响应的时候,需要把消息写到操作系统中,要求操作系统发送到网络中,这就是一个写事件。

像这样的一些网络读写事件,通常在 Nginx 中或者任何一个异步事件的处理框架中,他会有个东西叫事件收集、分发器。会定义每类事件处理的消费者,也就是说事件是一个生产者,是通过网络中自动的生产到我们的 Nginx 中的,我们要对每种事件建立一个消费者。比如连接建立事件消费者,就是对 Accept 调用,HTTP 模块就会去建立一个新的连接。还有很多读消息或者写消息,在 HTTP 状态机中不同的时间段会调用不同的方法也就是每个消费者处理。

以上就是一个事件分发、消费器,包括 AIO 像异步读写磁盘事件,还有定时器事件,比如是否超时(workershutdowntimeout)。

11 . Nginx核心原理讲解

Nginx事件循环
/*
当Nginx刚刚启动时,在等待事件部分,也就是打开了80或443端口,这个时候在等待新的时间进来,比如新的客户端连上了nginx向我们发起了连接,
此步往往对应epoll的epoll wait方法,这个时候的Nginx其实是处于sleep这样一个进程状态的,当操作系统收到了一个建立TCP连接的握手报文时并且处理完握手流程后,
操作系统就会通知epoll wait这个阻塞方法,告诉他可以往下走了,同时唤醒Nginx worker进程.
接着往下走之后,会去找操作系统索要事件,操作系统会把他准备好的事件,放到事件队列中,从这个事件队列中可以获取到需要处理的事件,
比如建立连接或者收到一个TCP请求报文
*/

11 . Nginx核心原理讲解

11 . Nginx核心原理讲解

取出以后就会进行循环处理事件,如上就是处理事件的一个循环:当发现队列中不为空,就把事件取出来开始处理事件;在处理事件的过程中,可能又生成新的事件,比如说发现一个连接新建立了,可能要添加一个超时时间,比如默认的 60 秒,也就是说 60 秒之内如果浏览器不向 Nginx 发送请求的话,Nginx 就会把这个连接关掉;又比如说当 Nginx 发现已经收完了完整的 HTTP 请求以后,可以生成 HTTP 响应了,那么这个生成响应是需要 Nginx 可以向操作系统的写缓存中心里面去把响应写进去,要求操作系统尽快的把这样一段响应内容发到浏览器上,也就是说可能在处理过程中可能会产生新的事件,就是循环处理事件部分指向的事件队列部分,等待下一次来处理。

如果所有的事件都处理完成以后呢,又会返回到等待事件部分。

在学习了 Nginx 事件循环后,我们再去理解,有时候使用一些第三方模块,这些第三方模块可能会做大量的 CPU 运算,这样的计算任务会导致处理一个事件的时间非常的长;在上面的一个流程图中,可以看到会导致队列中的大量事件会长时间得不到处理,从而引发恶性循环,也就是他们的超时时间可能到了;大量的 CPU、Nginx 的任务都消耗在处理连接不正常的断开,所以 Nginx 不能容忍有些第三方模块长时间的消耗大量的 CPU 进行计算任务就是这样一个原因。我们可以看到像 GZIP 这样的模块,他们都不会在一次使用大量的 CPU 而是分段使用,这些都与 Nginx 的事件循环有关的

Epoll的优劣与原理

epoll与poll比较
/*
epoll存储活跃的连接,每次只处理活跃的连接数量占比很小
poll是每次将所有的连接交给操作系统去遍历,找出活跃的连接,因此连接越多,耗时越长
*/
epoll如何实现只处理活跃连接
/*
epoll实现了eventpoll数据结构
数据结构中rdlist将活跃连接存储在链表中,当网卡发送报文时,增加节点,当读取一个事件后,链表删除节点,需要得到活跃连接就只需要遍历链表
数据结构中rdr使用红黑树(自平衡二叉树)将事件存储,例如:当有读事件时,就新增节点,事件复杂度为log
*/

11 . Nginx核心原理讲解

Nginx模块

模块分类

11 . Nginx核心原理讲解

/*
核心模块:
HTTP模块: 用来发布http web服务网站的模块
event模块: 用来处理nginx,访问请求,并进行恢复.
mail模块: 负责邮箱处理和发布的. 基础模块:
HTTP Access模块: 用来进行虚拟主机发布访问模块,起到记录访问日志;
HTTP FastCGI模块: 用于和PHP程序进行交互的模块,负责将来访问nginx的php请求转发到后端的PHP上.
HTTP Proxy模块: 配置反向代理转发的模块,负责向后端传递参数.
HTTP Rewrite模块: 支持Rewrite规则重写,支持域名跳转. 第三方模块:
HTTP Upstream Request Hash模块: 利用hash算法进行负载均衡的模块.
HTTP Access Key模块: http请求访问校验模块
Limit_req模块: http请求限制模块
Upstream check module: 检测后端负载转发的模块.
*/

Nginx如何通过连接池处理网络请求

11 . Nginx核心原理讲解

/*
每一个worker进程里面都有一个独立的ngx_cycle_t这样一个数据结构;
现在不要对他里面的细节来纠结,这里三个主要的数组要看一下:
*/

11 . Nginx核心原理讲解

/*
其中connections这就是我们所谓的连接池,他指向我们这个数组有多大尼,可以看下有一个配置项:
*/

11 . Nginx核心原理讲解

默认会有一个512大小的数组,这里的每一个数组就是一个连接,可以看到512其实是非常小的,因为我们nginx动则处理万,十万,百万级的计算,所以我们往往是需要去修改的;而且官方提示很清楚,这个连接不止去用于客户端的连接,也用于面向上有服务器的连接,所以如果我们做反向代理的时候,每个客户端意味着消耗我们两个connections;

11 . Nginx核心原理讲解

  每一个连接自动的对应一个读事件和一个写事件,所以在ngx_cycle_t中还有个write_event_s;它们指向的数组的大小也跟worker_connections这个配置是一样的;所以connections连接事件,读事件,写事件是通过序号对应起来的;所以我们在考虑nginx能够释放多大性能的时候,首先要保证worker_connections足够我们使用;但是worker_connections所指向的数组,同时影响了我们所打开的内存,当我们配置了更大的worker_connections的时候,也就意味着nginx使用了更大的内存;所以每一个connections连接到底使用了多大的内存尼?

    我们可以看一下,每一个ngx_connection_s这样的一个结构体在64位操作系统中它占用的字节数大约是232字节;具体会因nginx版本不同,可能会有微小的差异;

    每一个ngx_connection_s这样的一个结构体它对应着两个事件,一个读事件,一个写事件,我们之前谈到网络事件谈到了它的许多特性;

    那么在nginx中每一个事件对应一个结构体叫做ngx_event_s;每个事件对应的结构体它所对应的字节数是96字节;

    所以我们在使用一个连接的时候它大概消耗的字节为(232+96)*2;我们的worker_connections配置的越大,那么初始化的时候就会预分配这么多的内存;

ngx_event_s里面有哪些成员尼?

这里我们比较关注的是handler这是一个回调方法;也就是说很多第三方模块会把这个handler设置为自己的实现;这里还有个timer,也就是说当我们对http请求做读超时和写超时时候等等设置的时候,其实是在操作读事件和写事件中的tmer;这个timer其实就是nginx实现超时定时器,也就是基于rbtree中的红黑树去实现的结构体;这里定时器其实也是可配的,这里我们看下它的配置;

11 . Nginx核心原理讲解

默认设置为60s,其实这个60s也就是在我们刚刚某个连接上,在准备读它的header时,我们在它的读事件上添加个60s的定时器;

当多个事件形成队列的时候我们可以使用ngx_queue_t;

我们再来看下ngx_connections_s 每个连接有些什么样的成员?

  read 和 write分别是它的读写事件;

  recv和send是它的一个抽象的操作系统的底层方法;怎么样发送和接受;

  这里还有一个变量叫sent (off_t:表示无符号的整型)它表示这个连接上已经发送了多少字节,也就是在配置中经常使用到的$bytes_sent

  还是在ngx_http_core_module 模块中 我们先找到它的内置变量;

11 . Nginx核心原理讲解

 $bytes_sent:它表示向客户端发送了多少字节;

 通常在access.log记录nginx处理了哪些请求中我们可以记录这么一个变量,我们在ngixn.conf配置中添加了这么一个变量

小结

/*
当我们需要配置高并发的nginx的时候,必须把connections的数目配置的足够大,而每个connections将对应两个event,
都会消耗一定的内存,还有nginx的许多结构体中,它们的一些成员和我们的内置变量是可以对应起来的;
*/

内存池对性能的影响

如果你开发过nginx的第三方模块,虽然我们在写C语言代码,但是不需要关心内存的释放,如果你现在在配置一些罕见场景的nginx的时候,你可能会需要去修改nginx在请求和连接上初始分配的内存池大小,但是nginx官方上推荐通常不需要修改这样的配置,那么我们究竟要不要修改这些内存池的大小尼?

  下面我们来看下 内存池 究竟是怎么运转的?

  在上讲中我们看到一个结构体ngx_connection_s也就是每一个连接需要这么一个结构体;而这个结构体中,有一个成员变量pool;

  它对应着这个连接所使用的内存池

11 . Nginx核心原理讲解

这个内存池可以通过一个变量connection_pool_size配置来定义,那么为什么我们需要内存池尼?

11 . Nginx核心原理讲解

如果有一些工具的话,我们会发现,nginx它所产生的内存碎片是非常小的;这就是内存池的一个功劳,那么内存池尼它会把内存提前分配好一批,而且当我们使用小块内存的时候,每次我们使用的小块内存的时候,它会使用last,end,next,failed一个个连接在一起,每次我们使用的东西比较小的时候尼,在第二次再分配小块内存,它还会再进行连接一起,这样就大大减少了我们的内存碎片,当然当我们分配大块内存的时候尼,会走操作系统的 alloc模块.

对于Nginx有什么好处尼?

因为它主要在处理WEB请求,WEB请求特别是对http请求;它有两个非常明显的特点就是每当我们有个tcp连接的时候,这个tcp连接上面可能会有很多http请求;也就是所谓的http keepalive请求,连接没有关闭;执行完一条请求以后 还负责执行另外一条请求;那么有一些内存尼 我为连接分配一次就够了;比如说我去读取每一个请求的前1k字节;在连接内存池上我分配一次;只要这个连接不关闭,那么这个1k的内存我永远不要释放;什么时候需要释放尼?连接关闭的时候我再释放,没有任何问题;

请求内存池尼?每一个http请求我开始分配的时候尼,我不知道分配多大,但是http请求特别是http1.1而言,通常我们会分配4k大小的内存;因为我们的url或者header需要分配这么多;如果没有内存池尼,我们可能需要频繁的分配(小块的分配);而分配内存池尼,我们是有代价的;如果我们一次性的分配较多的内存尼,就没有这样的问题;而请求执行完毕以后,哪怕连接我们还可以复用;我们也可以把请求内存池给销毁;而这样所有的nginx第三方模块开发者就不需要去关注你村什么时候去释放;它只需要关注它是从请求内存池里面申请分配的内存;还是从连接内存池里申请分配的内存;只要这个逻辑讲的通;比如说请求结束以后,连接人想继续使用,你可以在连接内存池里分配;

可以看下具体例子: ngx_http_core_module这个模块

11 . Nginx核心原理讲解

里面有个connection_pool_size点开以后他默认是256或512

11 . Nginx核心原理讲解

这跟我们的操作系统位数有关的.

内存池配置512并不是代表这里我们只能分配512的字节.

当我们分配的内存超过预分配大小时候,还是可以继续分配的,这里只是说我们提前预分配了足够大小的空间可以减少我分配内存的次数.

那么我们再看另外一个配置叫request_pool_size

11 . Nginx核心原理讲解

11 . Nginx核心原理讲解

也就是每一个请求的内存池的大小,我们这里可以看到他的默认大小是4k; 为什么会差距这么大尼?

之所以会差距8倍,那是因为对连接而言,它需要保存的上下文信息非常的少;它只需要帮助后面的请求,读取最初一部分字节就行了,而对于请求而言,我们需要保存大量的上下文信息,比如说所有读取到的url或者header我需要保存下来,url通常还比较长,所以我们需要4k的大小;当然官方文档中说 它对性能的影响比较小,如果我们在极端场景下,如果你的url特别大,可以考虑把这个值分配的更大,通常来说你是很小内存的,url特别小,header也非常少,可以考虑将这个值降低一些,这样nginx消耗的内存会小一些;也意味着可以做更大并发量的请求;

总结

以上我们介绍了内存池的原理,以及请求内存池和连接内存池,它们的配置代表了怎么样的意义,内存池对减少我们内存碎片,对第三方模块的快速开发,是有很大意义的;可能有些第三方模块不当使用了内存池,比如本该在请求内存池里分配内存,却在连接内存池里连接内存;可能会导致内存的延期释放,导致nginx的内存无谓的增加;这需要我们注意;

nginx共享内存

11 . Nginx核心原理讲解

nginx的进程间的通讯方式主要有两种

1 . 第一种是信号,之前我们在说如何管理Nginx的过程中已经有比较详细的介绍过了:

2 . 共享内存: 如果需要做数据的同步,只能通过共享内存,所谓共享内存,也就是我们打开了一块内存,比如说10M,一整块0到10M之间,多个worker进程之间可以同时的访问他; 包括读取和写入,那么为了使用好这样一个共享内存就会引入另外两个问题;第一个问题就是锁, 因为多个worker进程同时操作一块内存,一定会存在竞争关系;所以我们需要加锁,在Nginx的锁中,在早期,它还有基于信号量的锁,信号量是nginx比较久远的进程同步方式,它会导致你的进程进入休眠状态;也就是发送了主动切换;而现在大多数操作系统版本中,nginx所使用的锁都是自旋锁,而不会基于信号量;自旋锁也就是说当这个锁的条件没有满足比如说,这块内存现在被1号worker进程使用,2号worker进程需要去获取锁的时候,只要1号进程没有释放锁,2号进程会一直请求这把锁,就好像如果是基于信号量的早期的nginx锁,那么假设这把锁锁住了一扇门,如果worker进程1已经拿到了这把锁进到屋里,worker进程2是试图拿锁,敲门,发现里面已经有人了,那么worker进程2就会就地休息;等待worker进程1从门里出来以后通知它,而自旋锁不一样,那么worker进程2发现屋里已经有worker进程1了;它就会一直持续的去敲门,所以使用自旋锁要求所有的nginx模块,必须快速地使用共享内存,也就是快速的取得锁以后,快速的释放锁,一旦发现有第三方模块不遵守这样的规则,就可能会导致出现死锁或者说性能下降的问题;那么有了这样的一块共享内存;会引入第二个问题;因为一整块共享内存是往往是给许多对象同时使用的;如果我们在模块中手动的去编写,分配把这些内存给到不同的对象,这是非常繁琐的;所以这个时候 我们使用了Slab内存管理器;

那么Nginx那些模块使用了共享内存尼?

11 . Nginx核心原理讲解

使用共享内存主要使用了两种数据结构:

1 . Rbtree: 红黑树, 比如我们想做限速和流控等等场景时,我们是不能容忍在内存中做的, 否则一个worker进程对某一个用户触发了流控,而其他worker进程还不知道,所以我们只能在共享内存中做; 红黑树有一个特点: 就是他的插入删除非常的快,当然也可以做遍历,所以如下模块有一个特点: 需要做快速的插入和删除:

11 . Nginx核心原理讲解

比如我发现了一个客户端我对他限速,限速如果达到了,我需要他从我的数据结构容器中移除,都需要非常的快速.

那么第二个常用的数据结构是单链表,也就是说我只需要把这些共享的元素串联起来就可以了,比如:

11 . Nginx核心原理讲解

我们来看个非常复杂的例子就是: ngx_http_lua_api

ngx_http_lua_api 这个模块其实是openresty的核心模块;openresty在这个模块中定义了一个SDK,这个SDK叫lua_shared_dict;当这个指令出现的时候,它会分配一块共享内存;比如说这里我们分了10m;这个共享内存会有一个名称叫做 dogs;

11 . Nginx核心原理讲解

接下来我们在lua代码中,比如content_by_lua_block;对应着我们nginx收到了 set这个url的时候;需要做一些什么样的事情,我们首先从dogs共享内存中取出;然后设置了一个key-value; Jim-8;然后向客户端返回我已存储;

  然后在get请求中我们把Jim的值8取出来;返回给用户;

  那么在这一段代码中尼,我们同时使用了我们刚刚使用的红黑树和单链表 那么这个lua_shared_dict dogs 10m中使用红黑树来保存每一个key-value;红黑树中每一个节点就是Jim它的value就是8;那么为什么我还需要一个链表尼?是因为这个10m是有限的;当我们的Lua代码涉及到了我们的应用业务代码;很容易就超过了10m的限制;当我们出现10m限制的时候尼,会有很多种处理方法;比如让它写入失败;但是lua_shared_dict 采用了另外一种实现方式它用lru淘汰;也就是我最早set,最早get 长时间不用的那一个节点;比如前面还有Jim等于7或者等于6的节点;会优先被淘汰掉;当已经达到10m的最大值时;所以这个lua_shared_dict同时满足了红黑树和链表;

  共享内存是nginx跨worker进程通讯的最有效的手段;只要我们需要让一段业务逻辑在多个worker进程中同时生效;比如很多在做集群的流控上;那么必须使用共享内存;而不能在每一个worker内存中去操作;

Slab管理器

刚刚我们谈到nginx不同的worker进程间需要共享信息的时候,需要通过共享内存;我们也谈到了共享内存上可以使用链表或者红黑树这样的数据结构;但是每一个红黑树上有许多节点;每一个节点你都需要分配内存去存放;那么怎么样把一整块共享内存切割成一小块给红黑树上的每一个节点使用尼?

  下面我们来看下Slab内存分配管理是怎么样应用于共享内存上的;首先我们来看下Slab内存管理是怎么样的一种形式;

11 . Nginx核心原理讲解

它首先会把整块的共享内存分为很多页面;那么每个页面例如4k;会切分为很多slot;比如32字节是一种slot;64字节又是一种slot;128字节又是一种slot;那么这些slot是以乘2的方式向上增长的;如果现在有一个51字节需要分配的内存会放到哪里尼?会放于小于它最大的一个slot的一个环节;比如说64字节;所以上图中slot就是指向不同大小的块;所以这样的一种数据结构尼 它有一个特点;会有内存的浪费的;就像我们刚刚所说的;51字节它会用64字节来存放;那么其它的13字节就浪费了;那么最多会有多少内存消耗尼?会有两倍;这种使用的方式叫做Bestfit;Bestfit这种分配内存的方式有什么好处尼?它适合小对象;如果我们要分配的对象的内存非常小,比如小于一个页面的大小,就非常合适;因为它很少有碎片,那么每分配一块内存,就会沿着还未分配的空白的地方继续使用就可以了;当一个页面使用满以后,我再拿一个空白的页面继续给此类slot大小的内存继续使用就可以.那么有时候我分配在某段内存上的数据结构它是固定的,甚至需要初始化;那么这样的话,原先的数据结构都还在;当我重复使用的话,也避免了初始化;Slab内存管理中,我们怎么做数据的监控和统计尼?

那么tng上有一个模块叫做slab_stat;slab_stat可以帮我们看不同的slot;

11 . Nginx核心原理讲解

比如说:8字节 16 字节 。。。。等等;

  一共目前分配了多少,使用了多少,有多少个请求在访问,失败了多少次,这个对我们来监控Slab是非常有用处的;

  下面我们来看下怎么样在openresty的场景下去使用tng上的slab_stat这个模块;

  首先我们打开tengine的页面 http://tengine.taobao.org/document/ngx_slab_stat.html

  但是会发现在这个模块上没有github的地址;也就是说它没有作为一个独立的模块提供出来;那这个时候该怎么办尼?

  那么tengine怎么下载下来?从download里;

[root@server ~]# wget http://tengine.taobao.org/download/tengine-2.2.3.tar.gz
[root@server ~]# tar xf tengine-2.2.3.tar.gz
[root@server ~]# cd openresty-1.13.6.2/
[root@server openresty-1.13.6.2]# ./configure --add-module=../tengine-2.2.3/modules/ngx_slab_stat/
[root@server openresty-1.13.6.2]# make
[root@server openresty-1.13.6.2]# cp build/nginx-1.13.6/objs/nginx /usr/local/openresty/nginx/sbin/nginx

nginx.conf

[root@server nginx]# cat conf/nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
lua_shared_dict dogs 10m;
server {
listen 8090;
server_name localhost; location = /slab_stat{
slab_stat;
} location /set {
content_by_lua_block {
local dogs = ngx.shared.dogs
dogs:set("Jim",8)
ngx.say("STORED");
}
} location /get {
content_by_lua_block {
local dogs = ngx.shared.dogs
ngx.say(dogs:get("Jim"));
}
} location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}

测试

[root@server nginx]# curl localhost:8090/set
STORED
[root@server nginx]# curl localhost:8090/get
8 [root@server nginx]# curl localhost:8090/slab_stat
* shared memory: dogs
total: 10240(KB) free: 10168(KB) size: 4(KB)
pages: 10168(KB) start:00007F7A4C7FE000 end:00007F7A4D1EE000
slot: 8(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 16(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 32(Bytes) total: 127 used: 1 reqs: 1 fails: 0
slot: 64(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 128(Bytes) total: 32 used: 2 reqs: 2 fails: 0
slot: 256(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 512(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 1024(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 2048(Bytes) total: 0 used: 0 reqs: 0 fails: 0 /*
配置项写完,把nginx启动看他的执行效果;
每一个slot及其slot对应的大小; 分配了多少个,使用了多少个,失败了多少个.
所谓分配就是10m是一个非常大的内存,他会划分很多歌页面; 对于比较小的比如32字节,一个页面可以有128个, 这里127可用,已经使用了一个.
*/

总结

/*
以上我们介绍了Slab内存的使用方法; slab使用了Bestfit思想,他也是Linux操作系统经常使用的内存分配方式;
那么通畅我们在使用共享内存时, 都需要使用slab_stat去分配相应的内存给对象,再使用上层的数据结构维护这些数据对象;
*/
哈希表的max_size与bucket_size如何配置

nginx容器

/*
数组
链表
队列
哈希表
红黑树
基数树
*/

哈希表

11 . Nginx核心原理讲解

11 . Nginx核心原理讲解

nginx哈希表仅用于静态不变的内容,nginx启动时候就能确定这个哈希表里面有多少个元素,所以,当使用哈希表这些模块会暴露出bucketsize,maxsize, 我们的bucketsize仅仅控制了最大哈希表bucketsize的个数,而不是实际个数, max size意义在于限制最大化的使用.

nginx红黑树

11 . Nginx核心原理讲解

11 . Nginx核心原理讲解

11 . Nginx核心原理讲解

/*
nginx内存中会大量使用红黑树,它是一个二叉树.同时是一个查找二叉树,他有可能退化为一个链表,
使用了红黑树的模块,再增删改查是非常快的.
*/

nginx动态模块提升运维效率

11 . Nginx核心原理讲解

我们在使用动态模块之前,先来看下在不适用动态模块的方法里,我们是怎么样使用Nginx;

  首先我们在下载完nginx的源代码,提供了一个叫config的脚本,以及在源代码中介绍的auto目录;这里都在帮助nginx在建立编译系统;那么nginx源代码中提供了很多官方模块,但我们也可能添加许多的第三方模块,不管是官方模块还是第三方模块,这些模块的源码都会和Nginx的框架源码放到一起,进行编译,最后编译出一个nginx可执行文件;那么这是不使用动态模块的一种方式;

那么使用了动态模块尼?

我们在编译的时候,指定了某些模块,使用动态模块的方式去编译;那么最后尼,除了生成nginx的二进制可执行文件,还会生成一个动态库,也就是我们指定了模块的那个动态库;

动态库和静态库有什么区别?

静态库: 是直接把所有源代码编译进最终的二进制可执行文件中;

动态库: 在nginx二进制可执行文件里,只保留了他的位置或者说地址,那么我们需要这个动态库的功能时尼, 由nginx的可执行文件去调用这个动态库,再去完成这样的功能, 所以这里好处就表现为仅仅需要修改某一个模块或者升级这个模块功能时,特别是我们nginx编译了大量第三方模块,那么这个时候,我们仅仅重新编译这个动态库,而不用去替换我的二进制文件,因为这里很有可能会漏了或者多编译进一些nginx模块或者参数使用了错误, 而我编译出新的动态库以后,我只要替换掉这个动态库,然后nginx -s reload一遍; 我就可以使用新的模块功能了.

具体使用时候,分为6个步骤

1 . 首先,要在nginx源代码加入configure加入动态模块必须指明这个模块是使用动态模块方式编译进nginx; 这里有一个潜台词,不是所有的nginx模块都可以以动态模块的方式加入到nginx中;只有一些模块才可以以动态模块的方式加入;

2 . 开始执行make,编译出binary;

3 . 到第三步的时候,也就是说我们开始启动nginx了;启动nginx的时候尼我们去读ngx_module里的数组;

4 . 读到模块数组中尼,我们发现了使用了一个动态模块,接下来我们会看到一个nginx的conf中加入的一个配置项,这个配置项叫load_module配置;指明了这个 动态模块所在的路径;

5 . 那么接下来我们就可以在nginx的进程中打开这个动态库加入模块数组,

6 . 最后再进行一个初始化的过程(基于模块数组进程初始化);

这是动态模块的一个工作流程,下面为一个简单演示:

11 . Nginx核心原理讲解

Example

[root@server nginx-1.14.2]# yum -y install libxml2 libxml2-dev yum -y install libxslt-devel gd-devel
[root@server nginx-1.14.2]# ./configure --prefix=/usr/local/nginx --with-http_image_filter_module=dynamic
[root@server nginx-1.14.2]# make
[root@server nginx-1.14.2]# mkdir /usr/local/nginx/modules
[root@server nginx-1.14.2]# cp objs/ngx_http_image_filter_module.so /usr/local/nginx/modules/ # 修改配置
[root@server nginx]# head -5 conf/nginx.conf
user root;
load_module modules/ngx_http_image_filter_module.so;
worker_processes 1;
events {
worker_connections 1024; server {
listen 80;
server_name localhost; location / {
root html;
image_filter resize 130 130;
}
} # 重启服务
[root@server nginx]# ./sbin/nginx -s reload # 图片变小了,说明image_filte模块已经生效了; # 使用了动态模块,不需要删除nginx 二进制文件,进行热升级了,可以减少我们出错的效率,但是并非所有的模块都支持动态模块的加载;

11 . Nginx核心原理讲解

上一篇:liunx 开机流程与模块管理


下一篇:【C#基础知识】静态构造函数,来源于一道面试题的理解