文章目录
- 1 Nginx原理
- 1.1 Nginx怎么处理请求
- 1.2 Nginx是如何实现高并发
- 1.3 Nginx高可用性怎么配置
- 1.4 为什么Nginx不使用多线程
- 1.5 Nginx动态资源、静态资源
- 1.5.1 什么是动态资源、静态资源
- 1.5.2 为什么要做动、静分离
- 1.5.3 什么叫 CDN 服务
- 1.5.4 Nginx怎么做的动静分离
- 1.6 Nginx怎么限流
- 1.6.1 限制请求速率
- 1.6.1.1 正常限制访问频率(正常流量)
- 1.6.1.2 突发限制访问频率(突发流量)
- 1.6.1.3 设置白名单
- 1.6.1.4 limit_req重复
- 1.6.2 限制并发连接数
- 1.6.3 上传/下载速率限制
- 1.6.3.1 limit_rate
- 1.6.3.2 limit_rate_after
- 1.6.3.3 proxy_limit_rate
- 1.6.4 动态限速
- 1.6.4.1 基于时间动态限速
- 1.6.4.2 基于变量动态限速
- 1.6.5 限流算法
- 1.6.5.1 漏桶算法
- 1.6.5.2 令牌桶算法
- 1.6.5.3 固定窗口计数法
- 1.6.5.4 滑动窗口计数法
- 1.7 Nginx如何实现后端服务健康检查
- 1.8 Nginx 如何开启压缩
- 1.9 如何在Nginx中获得当前时间
- 1.10 惊群效应
- 1.10.1 简介
- 1.10.2 Nginx 架构
- 1.10.3 nginx 使用 epoll
- 1.10.3.1 master 的工作
- 1.10.3.2 worker 的工作
- 1.10.4 解决方案
- 1.10.4.1 accept_mutex(应用层的解决方案)
- 1.10.4.2 EPOLLEXCLUSIVE(内核层的解决方案)
- 1.10.4.3 SO_REUSEPORT(内核层的解决方案)
1 Nginx原理
1.1 Nginx怎么处理请求
server { # 第一个Server区块开始,表示一个独立的虚拟主机站点
listen 80;# 提供服务的端口,默认80
server_name localhost; # 提供服务的域名主机名
location / { # 第一个location区块开始
root html; # 站点的根目录,相当于Nginx的安装目录
index index.html index.html; # 默认的首页文件,多个用空格分开
} # 第一个location区块结果
-
Nginx
在启动时,会解析配置文件,得到需要监听的端口
与IP 地址
,然后在Nginx
的Master
进程里面先初始化好这个监控的Socket
(创建Socket
,设置addr
、reuse
等选项,绑定到指定的ip
地址端口,再listen
监听)。
然后,再fork
(一个现有进程可以调用fork
函数创建一个新进程。由fork
创建的新进程被称为子进程 )出多个子进程出来。 - 子进程会竞争
accept
新的连接。此时,客户端就可以向nginx
发起连接了。当客户端与nginx
进行三次握手,与nginx
建立好一个连接后。此时,某一个子进程会accept
成功,得到这个建立好的连接的Socket
,然后创建nginx
对连接的封装,即ngx_connection_t
结构体。
接着,设置读写事件处理函数,并添加读写事件来与客户端进行数据的交换。 -
Nginx
或客户端来主动关掉连接,到此,一个连接就寿终正寝了。
1.2 Nginx是如何实现高并发
如果一个 server
采用一个进程(或者线程)负责一个request
的方式,那么进程数就是并发数。那么显而易见的,就是会有很多进程在等待中。等什么?最多的应该是等待网络传输。
而 Nginx
的异步非阻塞
工作方式正是利用了这点等待的时间。在需要等待的时候,这些进程就空闲出来待命了。因此表现为少数几个进程就解决了大量的并发问题。Nginx
是如何利用的呢,简单来说:同样的 4 个进程,如果采用一个进程负责一个 request 的方式,那么,同时进来 4 个 request 之后,每个进程就负责其中一个,直至会话关闭。期间,如果有第 5 个request进来了。就无法及时反应了,因为 4 个进程都没干完活呢,因此,一般有个调度进程,每当新进来了一个 request ,就新开个进程来处理。
回想下,BIO
是不是存在这样问题?Nginx
不这样,每进来一个request
,会有一个worker
进程去处理。但不是全程的处理,处理到什么程度呢?处理到可能发生阻塞的地方,比如向上游(后端)服务器转发request
,并等待请求返回。那么,这个处理的worker
不会这么傻等着,他会在发送完请求后,注册一个事件: “如果 upstream 返回了,告诉我一声,我再接着干” 。于是他就休息去了。此时,如果再有 request
进来,就可以很快再按这种方式处理。而一旦上游服务器返回了,就会触发这个事件,worker
才会来接手,这个request
才会接着往下走。
这就是为什么说,Nginx
基于事件模型。
由于web server
的工作性质决定了每个request
的大部份生命都是在网络传输中,实际上花费在server
机器上的时间片不多。这是几个进程就解决高并发的秘密所在。即:webserver
刚好属于网络 IO 密集型应用,不算是计算密集型。异步,非阻塞,使用 epoll ,和大量细节处的优化。也正是 Nginx 之所以然的技术基石
1.3 Nginx高可用性怎么配置
当上游服务器(真实访问服务器),一旦出现故障或者是没有及时相应的话,应该直接轮训到下一台服务器,保证服务器的高可用
Nginx配置代码:
server {
listen 80;
server_name www.lijie.com;
location / {
### 指定上游服务器负载均衡服务器
proxy_pass http://backServer;
### nginx与上游服务器(真实访问的服务器)超时时间 后端服务器连接的超时时间_发起握手等候响应超时时间
proxy_connect_timeout 1s;
###nginx发送给上游服务器(真实访问的服务器)超时时间
proxy_send_timeout 1s;
### nginx接受上游服务器(真实访问的服务器)超时时间
proxy_read_timeout 1s;
index index.html index.htm;
}
}
1.4 为什么Nginx不使用多线程
Apache
: 创建多个进程或线程,而每个进程或线程都会为其分配cpu
和内存(线程要比进程小的多,所以worker
支持比perfork
高的并发),并发过大会榨干服务器资源。Nginx
: 采用单线程来异步非阻塞处理请求(管理员可以配置 Nginx
主进程的工作进程的数量)(epoll
),不会为每个请求分配 cpu
和内存资源,节省了大量资源,同时也减少了大量的 CPU
的上下文切换。所以才使得 Nginx
支持更高的并发。
nginx
和apache
的区别 :
- 轻量级,同样起
web
服务,比apache
占用更少的内存和资源。
抗并发,nginx
处理请求是异步非阻塞的,而apache
则是阻塞性的,在高并发下nginx
能保持低资源,低消耗高性能。 - 高度模块化的设计,编写模块相对简单。
- 最核心的区别在于
apache
是同步多进程模型,一个连接对应一个进程,nginx
是异步的,多个连接可以对应一个进程。
Nginx | Apache |
nginx是一个基于事件的web服务器 | apache是一个基于流程的服务器 |
所有请求都由一个线程处理 | 单个线程处理单个请求 |
nginx避免子进程的感念 | apache是基于子进程的 |
nginx类似于速度 | apache类似于功率 |
nginx在内存消耗和连接方面比较好 | apache在内存消耗和连接上没有提高 |
nginx在负载均衡方面表现较好 | 当流量到达进程极限时,apache将拒绝新的连接 |
nginx只具有核心功能 | apache提供了比nginx更多的功能 |
nginx的性能和可伸缩性不依赖于硬件 | apache依赖于cpu和内存等硬件组件 |
1.5 Nginx动态资源、静态资源
1.5.1 什么是动态资源、静态资源
动态资源、静态资源分离,是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。
动态资源、静态资源分离简单的概括是:动态文件
与静态文件的分离
1.5.2 为什么要做动、静分离
在我们的软件开发中,有些请求是需要后台处理的(如:.jsp,.do
等等),有些请求是不需要经过后台处理的(如:css、html、jpg、js
等等文件),这些不需要经过后台处理的文件称为静态文件,否则动态文件。
因此我们后台处理忽略静态文件。
这会有人又说那我后台忽略静态文件不就完了吗?当然这是可以的,但是这样后台的请求次数就明显增多了。在我们对资源的响应速度有要求的时候,我们应该使用这种动静分离的策略去解决动、静分离将网站静态资源(HTML,JavaScript,CSS,img
等文件)与后台应用分开部署,提高用户访问静态代码的速度,降低对后台应用访问
这里我们将静态资源放到 Nginx
中,动态资源转发到 Tomcat
服务器中去。
当然,因为现在七牛、阿里云等 CDN 服务已经很成熟,主流的做法,是把静态资源缓存到 CDN
服务中,从而提升访问速度。
相比本地的 Nginx
来说,CDN
服务器由于在国内有更多的节点,可以实现用户的就近访问。并且,CDN 服务可以提供更大的带宽,不像我们自己的应用服务,提供的带宽是有限的
1.5.3 什么叫 CDN 服务
CDN
,即内容分发网络。
其目的是,通过在现有的 Internet
中增加一层新的网络架构,将网站的内容发布到最接近用户的网络边缘,使用户可就近取得所需的内容,提高用户访问网站的速度。
一般来说,因为现在 CDN
服务比较大众,所以基本所有公司都会使用 CDN 服务
1.5.4 Nginx怎么做的动静分离
只需要指定路径对应的目录。location /
可以使用正则表达式匹配。并指定对应的硬盘中的目录。如下:(操作都是在Linux上)
location /image/ {
root /usr/local/static/;
autoindex on;
}
步骤:
# 创建目录
mkdir /usr/local/static/image
# 进入目录
cd /usr/local/static/image
# 上传照片
photo.jpg
# 重启nginx
sudo nginx -s reload
打开浏览器 输入 server_name/image/1.jpg 就可以访问该静态图片了
1.6 Nginx怎么限流
Nginx
限流就是限制用户请求速度,防止服务器受不了
限流有3种 :
- 正常限制访问频率(正常流量)
- 突发限制访问频率(突发流量)
- 限制并发连接数
Nginx
的限流都是基于漏桶流
算法
实现三种限流算法
1.6.1 限制请求速率
nginx
的 ngx_http_limit_req_module
模块提供限制请求处理速率的能力,使用了漏桶算法(leaky bucket algorithm
)。我们可以想像有一只上面进水、下面匀速出水的桶,如果桶里面有水,那刚进去的水就要存在桶里等下面的水流完之后才会流出,如果进水的速度大于水流出的速度,桶里的水就会满,这时水就不会进到桶里,而是直接从桶的上面溢出。
对应到处理网络请求,水代表从客户端来的请求,而桶代表一个队列,请求在该队列中依据先进先出(FIFO)算法等待被处理。漏的水代表请求离开缓冲区并被服务器处理,溢出代表了请求被丢弃并且永不被服务。
1.6.1.1 正常限制访问频率(正常流量)
Nginx
中使用ngx_http_limit_req_module
模块来限制的访问频率,限制的原理实质是基于漏桶算法
原理来实现的。在nginx.conf
配置文件中可以使用limit_req_zone
命令及limit_req
命令限制单个IP
的请求处理频率
# 定义限流维度,一个用户一分钟一个请求进来,多余的全部漏掉
limit_req_zone $binary_remote_addr zone=test:10m rate=2r/s;
# 绑定限流维度
server{
location /seckill.html {
limit_req zone=test;
proxy_pass http://lj_seckill;
}
}
1r/s
代表1秒一个请求,1r/m
一分钟接收一个请求, 如果Nginx
这时还有别人的请求没有处理完,Nginx
就会拒绝处理该用户请求。limit_req_zone
:用于设置限流和共享内存区域的参数,格式为:limit_req_zone key zone rate
-
key
: 定义限流对象,$binary_remote_addr
是nginx
中的变量,表示基于remote_addr
(客户端IP) 来做限流,binary_
是二进制存储。使用$binary_remote_addr
而不是$remote_addr
是因为二进制存储可以压缩内存占用量。$remote_addr
变量的大小从7到15个字节不等,而$binary_remote_addr
变量的大小对于 IPv4 始终为4个字节,对于 IPv6 地址则为16个字节。 -
zone
: 定义共享内存区来存储访问信息,访问信息包括每个 IP 地址状态和访问受限请求 URL 的频率等。zone
的定义又分为两个部分:由zone= 关键字标识的区域名称,以及冒号后面的区域大小
。test:10m
表示一个大小为10M,名字为test
的内存区域。1M 能存储16000个 IP 地址的访问信息,test 大概可以存储约160000个地址。nginx
创建新记录的时候,会移除前60秒内没有被使用的记录,如果释放的空间还是存储不了新的记录,会返回503的状态码。 -
rate
: 设置最大的访问速率。rate=2r/s
(为了好模拟,rate 设置的值比较小),表示每秒最多处理 2个请求。事实上 nginx 是以毫秒为粒度追踪请求的,rate=2r/s
实际上是每500毫秒1个请求,也就是说,上一个请求完成后,如果500毫秒内还有请求到达,这些请求会被拒绝(默认返回503,如果想修改返回值,可以设置limit_req_status
)
limit_req_zone
只是设置限流参数,如果要生效的话,必须和 limit_req
配合使用。limit_req
的格式为:limit_req zone=name [burst=number] [nodelay]
上面的例子只简单指定了 zone=test
,表示使用 test 这个区域的配置,在请求 html 文件时进行限流。我们可以理解为这个桶目前没有任何储存水滴的能力,到达的所有不能立即漏出的请求都会被拒绝。如果我1秒内发送了10次请求,其中前500毫秒1次,后500毫秒9次,那么只有前500毫秒的请求和后500毫秒的第一次请求会响应,其余请求都会被拒绝。
1.6.1.2 突发限制访问频率(突发流量)
上面的配置一定程度可以限制访问频率,但是也存在着一个问题:如果突发流量超出请求被拒绝处理,无法处理活动时候的突发流量,这时候应该如何进一步处理呢?Nginx
提供burst
参数结合nodelay
参数可以解决流量突发的问题,可以设置能处理的超过设置的请求数外能额外处理的请求数。我们可以将之前的例子添加burst
参数以及nodelay
参数:
# 定义限流维度,一个用户一分钟一个请求进来,多余的全部漏掉
limit_req_zone $binary_remote_addr zone=test:10m rate=2r/s;
# 绑定限流维度
server{
location/seckill.html{
limit_req zone=test burst=5 nodelay;
proxy_pass http://lj_seckill;
}
}
为什么就多了一个 burst=5 nodelay;
呢,多了这个可以代表Nginx
对于一个用户的请求会立即处理前五个,多余的就慢慢来落,没有其他用户的请求我就处理你的,有其他的请求的话我Nginx
就漏掉不接受你的请求
burst
表示在超过设定的访问速率后能额外处理的请求数。当 rate=2r/s
时,表示每500ms 可以处理一个请求。burst=5时,如果同时有10个请求到达,nginx 会处理第1个请求,剩余9个请求中,会有5个被放入队列,剩余的4个请求会直接被拒绝。然后每隔500ms从队列中获取一个请求进行处理,此时如果后面继续有请求进来,如果队列中的请求数目超过了5,会被拒绝,不足5的时候会添加到队列中进行等待
配置 burst
之后,虽然同时到达的请求不会全部被拒绝,但是仍需要等待500ms
一次的处理时间,放入桶中的第5个请求需要等待500ms * 4
的时间才能被处理,更长的等待时间意味着用户的流失,在许多场景下,这个等待时间是不可接受的。此时我们需要增加 nodelay
参数,和 burst
配合使用。nodelay
表示不延迟。设置 nodelay
后,第一个到达的请求和队列中的请求会立即进行处理,不会出现等待的请求。
需要注意的是,虽然队列中的5个请求立即被处理了,但是队列中的位置依旧是按照500ms 的速度依次被释放的。后面的4个请求依旧是被拒绝的,长期来看并不会提高吞吐量的上限,长期吞吐量的上限是由设置的 rate 决定的。
1.6.1.3 设置白名单
如果遇到不需要限流的情况,比如测试要压测,可以通过配置白名单,取消限流的设置。白名单要用到 nginx
的 ngx_http_geo_module
和 ngx_http_map_module
模块。
geo $limit {
default 1;
10.0.0.0/8 0;
192.168.0.0/24 0;
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=mylimit:10m rate=2r/s;
geo
指令可以根据 IP
创建变量 $limit
。$limit
的默认值是1,如果匹配到了下面的 IP,则返回对应的值(这里返回的是0)。
之后通过 map
指令,将 $limit
的值映射为$limit_key
,在白名单内的,$limit_key
为空字符串,不在白名单内的,则为 $binary_remote_addr
。当 limit_req_zone
指令的第一个参数是一个空字符串,限制不起作用,因此白名单的 IP 地址(在10.0.0.0/8和192.168.0.0/24子网中)没有被限制,其它 IP 地址都被限制为 2r/s
1.6.1.4 limit_req重复
如果同一个 location
下配置了多条 limit_req
的指令,这些指令所定义的限制都会被使用。
geo $limit {
default 1;
10.0.0.0/8 0;
192.168.0.0/24 0;
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=mylimit:10m rate=2r/s;
limit_req_zone $binary_remote_addr zone=myLimit2:10m rate=10r/s;
server {
location ~* \.(html)$ {
limit_req zone=mylimit burst=5 nodelay;
limit_req zone=myLimit2 burst=5 nodelay;
}
}
上面的例子配置了两条规则,myLimit
和 myLimit2
。白名单用户虽然没有匹配到mylimit的规则,但是根据规则 mylimit2
,被限制为10r/s。对于不在白名单的用户,则需要同时匹配mylimit 和 mylimit2,两者中最严格的条件 2r/s 会起作用
1.6.2 限制并发连接数
Nginx
中的ngx_http_limit_conn_module
模块提供了限制并发连接数的功能,可以使用limit_conn_zone
指令以及limit_conn
执行进行配置。接下来我们可以通过一个简单的例子来看下:
limit_conn_zone $binary_remote_addr zone=myip:10m;
limit_conn_zone $server_name zone=myServerName:10m;
server {
location / {
limit_conn myip 10;
limit_conn myServerName 100;
rewrite / http://www.lijie.net permanent;
}
}
上面配置了单个IP
同时并发连接数最多只能10
个连接,并且设置了整个虚拟服务器同时最大并发数最多只能100
个链接。当然,只有当请求的header
被服务器处理后,虚拟服务器的连接数才会计数。刚才有提到过Nginx
是基于漏桶算法原理实现的,实际上限流一般都是基于漏桶算法和令牌桶算法实现的。
其中:
-
limit_conn myip 10
:key
是$binary_remote_addr
,表示限制单个IP同时最多能持有10个连接。 -
limit_conn myServerName 100
:key
是$server_name
,表示虚拟主机(server) 同时能处理并发连接的总数为100。
注意
:只有当 request header
被后端server
处理后,这个连接才进行计数
1.6.3 上传/下载速率限制
limit_rate
主要用于限制用户和服务器之间传输的字节数,最常用的场景可能就是下载/上传限速。limit_rate
并没有单独的一个模块,而是在ngx_http_core_module
中,同时它的相关指令也比较少,只有limit_rate
和limit_rate_after
这两个指令。
1.6.3.1 limit_rate
server {
location / {
limit_rate 4k;
}
}
limit_rate
的用法非常简单,后面跟随的rate
就是具体限速的阈值,rate
可以设置为变量,从而可以实现动态限速,限速指令的生效范围是根据每个连接
确定的,例如上面限定每个连接的速率为4k,也就是当客户端发起两个连接的时候,速率就可以变为8k注意
默认的单位是bytes/s
,也就是每秒传输的字节数Bytes
而不是比特数bits
1.6.3.2 limit_rate_after
server {
location / {
limit_rate_after 500k;
limit_rate 4k;
}
}
limit_rate_after
允许在传输了一部分数据之后再进行限速,例如上面的配置中就是传输的前500k
数据不限速,500k
之后再进行限速。比较常见的应用场景如分段下载限速,超过指定大小的部分再进行限速;又或者是流媒体视频网站一般为了保证用户体验而不会对第一个画面进行限速,确保其能够尽快加载出来,等用户开始观看视频之后,再把带宽限制在合理的范围内,从而降低因客户端网速过快导致提前加载过多内容带来的额外成本。
1.6.3.3 proxy_limit_rate
proxy_limit_rate
的基本原理和用法与limit_rate
几乎一样,唯一不同的是proxy_limit_rate
是限制nginx
和后端upstream
服务器之间的连接速率而limit_rate
限制的是nginx
和客户端
之间的连接速率。需要注意的是proxy_limit_rate
需要开启了proxy_buffering
这个指令才会生效。
Syntax: proxy_limit_rate rate;
Default: proxy_limit_rate 0;
Context: http, server, location
This directive appeared in version 1.7.7.
1.6.4 动态限速
limit_rate
的一大特点就是能够使用变量,这就意味着和map
指令之类的进行组合就可以实现动态限速功能,这里只列几个简单的示范
1.6.4.1 基于时间动态限速
这里引入了nginx
内置的一个ssi模块
,这个模块有两个比较有意思的时间变量:$date_local
和$date_gmt
,分别对应当前时间
和GMT时间
这里使用变量和map指令
组合的方式,利用正则表达式匹配不同的时间段,再结合map变量将不同时间段和不同的限速对应起来。
map $date_local $limit_rate_time {
default 4K;
~(00:|01:|02:|03:|04:|05:|06:|07:).*:.* 16K;
~(08:|12:|13:|18:).*:.* 8K;
~(19:|20:|21:|22:|23:).*:.* 16K;
}
limit_rate $limit_rate_time
1.6.4.2 基于变量动态限速
有些服务可能会对不用的用户进行不同的限速,例如VIP用户的速度要更快一些等,例如下面可以针对不同的cookie进行限速
map $cookie_User $limit_rate_cookie {
gold 64K;
silver 32K;
copper 16K;
iron 8K;
}
limit_rate $limit_rate_cookie
1.6.5 限流算法
1.6.5.1 漏桶算法
漏桶算法
思路很简单,我们把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。
简易理解图:
一个固定容量的桶,有水流进来,也有水流出去。对于流进来的水来说,我们无法预计一共有多少水会流进来,也无法预计水流的速度。但是对于流出去的水来说,这个桶可以固定水流出的速率(处理速度),从而达到流量整形和流量控制的效果
漏桶限制的是常量流出速率(即流出速率是一个固定常量值),所以最大的速率就是出水的速率,不能出现突发流量
实际请求图示:
1.6.5.2 令牌桶算法
令牌桶算法的原理也比较简单,我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。
系统会维护一个令牌(token
)桶,以一个恒定的速度往桶里放入令牌(token
),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token
),当桶里没有令牌(token
)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。
令牌桶算法(Token Bucket
)是网络流量整形(Traffic Shaping
)和速率限制(Rate Limiting
)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送
我们有一个固定的桶,桶里存放着令牌(token
)。一开始桶是空的,系统按固定的时间(rate
)往桶里添加令牌,直到桶里的令牌数满,多余的请求会被丢弃。当请求来的时候,从桶里移除一个令牌,如果桶是空的则拒绝请求或者阻塞。
令牌桶有以下特点:
- 令牌按固定的速率被放入令牌桶中
- 桶中最多存放 B 个令牌,当桶满时,新添加的令牌被丢弃或拒绝
- 如果桶中的令牌不足 N 个,则不会删除令牌,且请求将被限流(丢弃或阻塞等待)
令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌…),并允许一定程度突发流量,所以也是非常常用的限流算法
1.6.5.3 固定窗口计数法
固定窗口计数法的思路是:
- 将时间划分为固定的窗口大小,例如1s
- 在窗口时间段内,每来一个请求,对计数器加1。
- 当计数器达到设定限制后,该窗口时间内的之后的请求都被丢弃处理。
- 该窗口时间结束后,计数器清零,从新开始计数。
固定窗口计数法是最简单、也最直观的限流算法。但在最坏的情况下,会让通过的请求量是限制数量的两倍,例如,假设限制的是每个窗口5个请求:
- T窗口的前1/2时间 无流量进入,后1/2时间通过5个请求;
- T+1窗口的前 1/2时间 通过5个请求,后1/2时间因达到限制丢弃请求。
因此在 T的后1/2
和(T+1)
的前1/2
时间组成的完整窗口内,通过了10个请求。
1.6.5.4 滑动窗口计数法
滑动窗口计数法的思路是:
- 将时间划分为
细粒度
的区间,每个区间维持一个计数器,每进入一个请求则将计数器加一 - 多个区间组成一个时间窗口,每流逝一个区间时间后,则抛弃最老的一个区间,纳入新区间。如图中示例的窗口T1 变为 窗口T2
- 若当前窗口的区间计数器总和超过设定的限制数量,则本窗口内的后续请求都被丢弃。
滑动窗口其实也是一种计算器算法,它有一个显著特点,当时间窗口的跨度越长时,限流效果就越平滑
。打个比方,如果当前时间窗口只有两秒,而访问请求全部集中在第一秒的时候,当时间向后滑动一秒后,当前窗口的计数量将发生较大的变化,拉长时间窗口可以降低这种情况的发生概率
1.7 Nginx如何实现后端服务健康检查
方式一,利用 nginx
自带模块 ngx_http_proxy_module
和 ngx_http_upstream_module
对后端节点做健康检查。
方式二(推荐),利用 nginx_upstream_check_module
模块对后端节点做健康检查。
1.8 Nginx 如何开启压缩
开启nginx gzip
压缩后,网页、css、js等静态资源的大小会大大的减少,从而可以节约大量的带宽,提高传输效率,给用户快的体验。虽然会消耗cpu资源,但是为了给用户更好的体验是值得的。
开启的配置如下:
将以上配置放到nginx.conf
的http{ … }
节点中
http {
# 开启gzip
gzip on;
# 启用gzip压缩的最小文件;小于设置值的文件将不会被压缩
gzip_min_length 1k;
# gzip 压缩级别 1-10
gzip_comp_level 2;
# 进行压缩的文件类型。
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
# 是否在http header中添加Vary: Accept-Encoding,建议开启
gzip_vary on;
}
gzip
虽然好用,但是以下类型的资源不建议启用 :
- 图片类型
原因:图片如jpg、png本身就会有压缩,所以就算开启gzip
后,压缩前和压缩后大小没有多大区别,所以开启了反而会白白的浪费资源。(Tips:可以试试将一张jpg图片压缩为zip,观察大小并没有多大的变化。虽然zip和gzip算法不一样,但是可以看出压缩图片的价值并不大) - 大文件
原因:会消耗大量的cpu
资源,且不一定有明显的效果。
1.9 如何在Nginx中获得当前时间
要获得Nginx
的当前时间,必须使用SSI
模块、和date_local
的变量。
Proxy_set_header THE-TIME $date_gmt;
1.10 惊群效应
1.10.1 简介
惊群效应(thundering herd
)是指多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只能有一个进程(线程)获得这个时间的控制权
,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群效应
。
原因&问题
说起来其实也简单,多数时候为了提高应用的请求处理能力,会使用多进程(多线程)去监听请求,当请求来时,因为都有能力处理,所以就都被唤醒了。
而问题就是,最终还是只能有一个进程能来处理。当请求多了,不停地唤醒、休眠、唤醒、休眠,做了很多的无用功,上下文切换又累。
1.10.2 Nginx 架构
第一点我们需要了解 nginx 大致的架构是怎么样的。nginx 将进程分为 master
和 worker
两类,非常常见的一种 M-S
策略,也就是 master 负责统筹管理 worker,当然它也负责如:启动、读取配置文件,监听处理各种信号等工作。
但是,第一个要注意的问题就出现了,master
的工作有且只有这些,对于请求来说它是不管的,就如同图中所示,请求是直接被 worker
处理的。如此一来,请求应该被哪个 worker 处理呢?worker 内部又是如何处理请求的呢?
1.10.3 nginx 使用 epoll
接下来我们就要知道 nginx 是如何使用 epoll
来处理请求的。下面可能会涉及到一些源码的内容,但不用担心,不需要全部理解,只需要知道它们的作用就可以了。
点击了解Linux 中epoll原理
1.10.3.1 master 的工作
其实 master 并不是毫无作为,至少端口是它来占的。
ngx_open_listening_sockets(ngx_cycle_t *cycle)
{
.....
for (i = 0; i < cycle->listening.nelts; i++) {
.....
if (bind(s, ls[i].sockaddr, ls[i].socklen) == -1) {
if (listen(s, ls[i].backlog) == -1) {
}
那么,根据我们 nginx.conf
的配置文件,看需要监听哪个端口,于是就去 bind 的了,这里没问题。
这里是直接在代码里面搜 bind
方法去找的,因为不管怎么样,总是要绑定端口的
然后是创建 worker 的,虽不起眼,但很关键。
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
char *name, ngx_int_t respawn)
{
....
pid = fork();
这里直接搜 fork,整个项目里面需要 fork 的情况只有两个地方,很快就找到了 worker
由于是 fork 创建的,也就是复制了一份 task_struct 结构。所以 master 的几乎全部它都有。
1.10.3.2 worker 的工作
nginx
有一个分模块的思想,它将不同功能分成了不同的模块,而 epoll 自然就是在 ngx_epoll_module.c
中了
ngx_epoll_init(ngx_cycle_t *cycle, ngx_msec_t timer)
{
ngx_epoll_conf_t *epcf;
epcf = ngx_event_get_conf(cycle->conf_ctx, ngx_epoll_module);
if (ep == -1) {
ep = epoll_create(cycle->connection_n / 2);
其他不重要,就连 epoll_ctl
和 epoll_wait
也不重要了,这里你需要知道的就是,从调用链路来看,是 worker 创建的 epoll 对象,也就是每个 worker 都有自己的 epoll 对象,而监听的sokcet 是一样的!
1.10.4 解决方案
此时问题的关键基本就能了解了,每个 worker 都有处理能力,请求来了此时应该唤醒谁呢?讲道理那不是所有 epoll 都会有事件,所有 worker 都 accept 请求?显然这样是不行的。那么 nginx 是如何解决的呢?
解决方式一共有三种,下面我们一个个来看:
1.10.4.1 accept_mutex(应用层的解决方案)
mutex 就是锁,这也是对于高并发处理的 一般操作,没错,加锁肯定能解决问题。
https://github.com/nginx/nginx/blob/b489ba83e9be446923facfe1a2fe392be3095d1f/src/event/ngx_event_accept.c#L328 具体代码就不展示了,其中细节很多,但本质很容易理解,就是当请求来了,谁拿到了这个锁,谁就去处理。没拿到的就不管了。锁的问题很直接,除了慢没啥不好的,但至少很公平。
1.10.4.2 EPOLLEXCLUSIVE(内核层的解决方案)
EPOLLEXCLUSIVE
是 2016 年 4.5+ 内核新添加的一个 epoll 的标识。它降低了多个进程/线程通过 epoll_ctl
添加共享 fd 引发的惊群概率,使得一个事件发生时,只唤醒一个正在 epoll_wait
阻塞等待唤醒的进程(而不是全部唤醒)。
关键是:每次内核只唤醒一个睡眠的进程处理资源,但这个方案不是完美的解决了,它仅是降低了概率哦。
为什么这样说呢?相比于原来全部唤醒,那肯定是好了不少,降低了冲突。但由于本质来说 socket 是共享的,当前进程处理完成的时间不确定,在后面被唤醒的进程可能会发现当前的 socket 已经被之前唤醒的进程处理掉了。
1.10.4.3 SO_REUSEPORT(内核层的解决方案)
nginx 在 1.9.1 版本加入了这个功能:https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ 其本质是利用了 Linux
的 reuseport
的特性,使用 reuseport
内核允许多个进程 listening socket
到同一个端口上,而从内核层面做了负载均衡,每次唤醒其中一个进程。
反应到 nginx
上就是,每个 worker
进程都创建独立的 listening socket
,监听相同的端口,accept 时只有一个进程会获得连接。效果就和下图所示一样。
而使用方式则是:
http {
server {
listen 80 reuseport;
server_name localhost;
# ...
}
}
从官方的测试情况来看确实是厉害
但是这个方案的问题在于内核是不知道你忙还是不忙的。只会无脑的丢给你。与之前的抢锁对比,抢锁的进程一定是不忙的,现在手上的工作都已经忙不过来了,没机会去抢锁了;而这个方案可能导致,如果当前进程忙不过来了,还是会只要根据 reuseport
的负载规则轮到你了就会发送给你,所以会导致有的请求被前面慢的请求卡住了。