命令行

nginx命令参数

1
2
3
4
5
-c file 指定配置文件
-g 指令 指定配置项(高于配置文件),例如 nginx -g "daemon off;worker_process 2;"
-p prefix 设置工作路径
-s signal 向master进程发送信号
-t 测试配置文件

信号

可以通过向nginx的master进程发送信号来触发相应操作

1
2
3
4
5
6
7
8
9
Kill -HUP pid
HUP 等同于 -s reload 平滑重启
USR1 重新打开日志文件
USR2 平滑升级nginx
QUIT 优雅地关闭
WINCH 优雅地停止worker进程
扩展: 子进程结束时会向父进程发送CHLD信号

应用

静态资源服务器

gzip静态资源压缩

1
2
3
4
gzip on;
gzip_min_length 1; # 单位为字节 表示对超过1字节大小的内容数据进行压缩
gzip_comp_level 2; # 表示开启2级压缩 压缩等级越高,CPU消耗越高
gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; # 指定需要压缩的内容类型

开启后响应头出现Content-Encoding:gzip

20190317155281732155344.png

响应速率限制

1
set $limit_rate 1k; # 限制响应 速率为1KB/s

$limit_rate 是http核心模块的内置变量,在nginx中很多模块都有自己的内置变量,在配置文件中,你可以直接使用这些变量 http模块内置变量

日志格式

nginx支持定义多种日志格式,并可自定义命名

1
2
3
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

反向代理服务器

upstream

调度策略:

  • 轮询(默认) 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除
  • weight 指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况
  • ip_hash(不可开启weight) 每个请求按访问ip的hash结果分配,每个访客固定访问一个后端服务器
  • fair (需开启upstream_fair模块) 按响应时间分配请求,响应时间短的优先分配
  • url_hash(需开启hash模块) 每个url定向到同一台服务器

设备状态

  • down表示当前server暂时不参与负载
  • weight默认为1,weight越大,负载的权重就越大
  • max_fails:一定时间内允许请求失败的次数,默认为1。当超过该次数后,返回proxy_next_upstream模块定义的错误
  • fail_timeout: 失败/超时时间,默认10秒。2个作用: 1.在该时间内max_fails次失败后,服务器不可用 2.不可用的时间
  • backup: 备机,nginx不会给它转发任何请求。只有当其他节点全部无法连接的时候,nginx才会启用这个节点。 一旦有可用的节点恢复服务,该节点则不再使用,又进入后备状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
upstream local {
ip_hash;
server 127.0.0.1:8080 weight=4 max_fails=2 fail_timeout=30s; //30秒内失败超过2次,则服务停止30秒 //ip
server unix:/tmp/backend3; //unix domain socket
server xxx.example.com weight=5; //域名
}
server {
listen 80;
server_name test.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://local;
}

代理缓存

对于后端服务内容不经常变动的情况,我们可以在反向代理上加代理缓存来减少后端访问次数,减小后端压力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
proxy_cache_path /tmp/nginxcache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
upstream local {
ip_hash;
server 127.0.0.1:8080;
}
server {
listen 80;
server_name test.com;
...
proxy_cache my_cache;
proxy_cache_key $host$uri$is_args$args;
proxy_cache_valid 200 304 302 1d;
...
proxy_pass http://local;
}

Nginx架构基础

Nginx的进程结构

20190317155281730679159.png

多进程: 利用多核优势

多进程-多线程: 处于高可用、高可靠的目的,多线程共享同一块地址空间,当某个第三方模块引发了一个地址错误导致的段错误时会导致整个Nginx进程挂掉

Master子进程分为两类: worker进程和cache相关进程

缓存要在多个worker进程间共享,且不光要被worker进程使用,还要被cache manager和cache loader进程使用,这些进程间的通讯都是使用共享内存解决

master进程和子进程通讯是通过信号进行管理的

绑核的好处?nginx采用事件驱动模型后,每个worker进程从头到尾占用一个核,绑核可以更好的使用每颗CPU核上的CPU缓存,减少缓存失效的情况

多进程间通讯可以使用共享内存、信号等,但我们做进程间的管理时,通常只使用信号

子进程终止时会向父进程发送CHLD信号

20190317155281729825991.png

Nginx Reload流程

子进程会继承所有父进程已经打开的文件描述符

20190317155281728454760.png

20190317155281727195755.png

Nginx 热升级流程

20190317155281723591528.png

Worker进程优雅的关闭

20190317155281722670705.png

对于HTTP可以做到优雅关闭,TCP/UDP等由于无法判定是否传输完毕,所以无法实现优雅关闭

网络收发与Nginx事件间的关系

MTU与MSS

MTU和MSS功能基本一致,都是根据对应包大小进行分片,但是实现效果却不太一样。

网络层提供的是一个不可靠的传输方式,如果任何一个包在传输中丢失了,网络层是无法发现的,需要靠上层应用来保证,也就是说如果一个大IP包分片传输时丢失了其中的一部分,之后无法组合出完整的IP包,即使上层应用发现了传输失败,也无法做到仅重传丢失的分片,只能对IP包整个重传,IP报越大的话重传的代价也就越高

传输层提供是一个可靠的传输方式,与网络层不同的是,TCP自身实现了重传机制,丢失任何一篇数据包都能单独重传,所以TCP为了高效传输,需要极力避免被网络层分片,所以就有个MSS标志,并且MSS的值就是根据MTU计算得出的,即避免了网络层上的分片,又保证了最大的传输效率。

MSS=MTU-IP头-TCP头,当在MTU为1500的网络上传输时,MSS为1460(1500-20字节IP头-20字节TCP头)

TCP协议与非阻塞接口

事件分发消费器

20190317155281721128553.png

Nginx事件循环

20190317155281719837051.png

Epoll

20190408155472328936288.png

前提: 高并发连接中,每次处理的活跃连接占比其实是很小的

仅仅遍历活跃连接链表

一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。

epoll的内部实现

Nginx的请求切换

201903171552817188905.png

在传统服务器中,一个进程只处理一个连接,当网络事件不满足或时间片用完时,比如就会切换到其他进程,如此往复,每次进程切换都有开销,在大并发情况下,这类开销很客观。

而Nginx在用户态进行连接切换,这样就没有了进程切换的成本,进程时间片通常是5ms~800ms,我们通常将nginx进程优先级调整为-19,这样操作系统分配的时间片会更大。

同步&异步、阻塞&非阻塞

阻塞&非阻塞主要指的是对于操作系统或者底层C库提供的方法或系统调用,我们调用这个方法时,这个方法可能会导致我的进程进入sleep状态,为什么会进入sleep状态呢?当前的条件不满足时,操作系统主动把我的进程切换为另外一个进程,让另外一个进程在使用这个cpu,这样就是一个阻塞方法。非阻塞方法就是我们调用该方法永远不会当我们时间片未用完时把我们进程主动切换掉。

20190317155281717880747.png
20190317155281713671902.png
20190317155281716536794.png

Nginx 模块

如何加入指定模块

在编译时通过--add-module参数指定模块位置,这样编译时会将该模块一起编译

如何查看模块是否被成功被编译进去了呢?

编译后在objs/ngx_modules.c中查看ngx_modules数组,里面是所有编译进nginx中的模块

如何了解某模块的具体使用方式

每个官方模块都有非常详细的文档说明,举例HTTP_LOG模块

http_log模块文档描述比较简单,仅有一个Directives(指令)单元,我们看一下access_log指令

Syntax: **access_log** *path* [*format* [buffer=*size*] [gzip[=*level*]] [flush=*time*] [if=*condition*]]; **access_log** off;
Default: access_log logs/access.log combined;
Context: http, server, location, if in location, limit_except

context:可以使用的上下文环境

另一种方法是去看模块的源代码,比如gzip模块,我们打开src/http/modules/ngx_http_gzip_filter_module.c文件,其中有一个唯一的ngx_command_t类型数组,其中定义了该模块的可用的指令

Nginx模块是如何定义的

201903171552817065212.png

Ngx_module_t中的成员字段type定了子类型模块属于哪个模块

index表示了顺序

成员变量commands类型是ngx_command_t,意思是可用的指令

Nginx 模块分类

20190317155281751922612.png

Nginx模块分为以下几个大类

  • Ngx_core_module
  • Ngx_conf_module
  • Ngx_event_module
  • Ngx_http_module
    • 请求处理模块
    • 响应过滤模块 关键字filter
    • upstream相关模块
  • Ngx_stream_module
  • Ngx_mail_module

我们会在每类模块的第一个模块中定义该类模块通用的逻辑,比如event_core、ngx_http_core_module、ngx_mail_core_module等

在拿到一个第三方模块时,可以根据模块命名简单判断其工作用途

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

每个worker进程中都有一个独立的ngx_cycle_t数据结构,其中connections就是连接池。

配置项worker_conntections表示每个worker进程同时可以打开的最大连接数,对于高性能条件下需要调高配置。

需要注意如果用于反向代理服务器,一个客户端会消耗2个连接,即对下游客户端的连接和对上游服务器的连接

每个连接自动对应一个读事件,一个写事件,即ngx_cycle_t中的read_events、write_events

20190317155282581770429.png

当你调整worker_connections配置项后,同时也会消耗更多的内存,每个connection连接使用了多大的内存呢?

每个connection即ngx_connection_s结构体,在64位操作系统中占用大约232字节。另外刚才也说到每个conenction对应两个事件,事件的结构体是ngx_event_s,占用96字节

所以我们使用一个连接的时候,占用的内存是232+96*2=424字节。

接着我们来看下ngx_event_s中有哪些成员

这里我们比较关注的是handler,这是一个回调方法,很多第三方模块会把handler设置为自己的实现

timer字段,当我们对http请求做读超时、写超时等设置时,其实是在操作读事件和写事件中的timer,比如client_header_timeout等配置项

我们看下ngx_connection_s中的其他成员

1
2
3
4
5
ngx_event_t *read; 读事件
ngx_event_t *write; 写事件
ngx_recv_pt recv; 抽象解耦底层方法
ngx_send_pt send; 抽象解耦底层方法
off_t send; 该连接上已经发送了的字节数,也就是bytes_send变量

20190317155282636412868.png

内存池

如果你开发过nginx第三方模块,虽然我们在写C源代码,但是我们不需要关心内存资源的释放。官方推荐通常不需要做修改

继续说上节的ngx_connection_s结构体,其中有个成员变量pool,它对应的就是这个连接所使用的内存池。这个内存池可以通过配置项connection_pool_size去配置。

这里有个问题,我们为什么需要内存池?

内存池会提前分配好一块内存,这样可以减少申请内存的次数,而且当我们使用小块内存的时候,通过内存池内部连接机制,可以大大减少内存碎片。

  • 连接内存池: 每个连接都会分配对应的连接内存池,大小由connection_pool_size控制,默认是256|512
  • 请求内存池: 每个http请求都会分配对应的请求内存池,大小由request_pool_size控制,默认是4k

请求结束或连接结束后请求内存池/连接内存池会自动销毁,这样第三方模块开发者只需要关注我是从请求内存池中申请分配的内存还是从连接内存池申请分配的内存,无需关心释放,大大加快了第三方模块开发速度

20190317155283460654907.png

所有worker进程协同工作的关键:共享内存

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

  • 信号
  • 共享内存

如果要做数据同步,只能通过共享内存。所谓共享内存,也就是我们打开了一块内存,多个worker可以同时访问它,包括读取和写入。

由于涉及同时操作,所以这里引入了锁。

现在Nginx中使用的锁都是自旋锁。使用自旋锁要求所有的Nginx模块必须快速的使用共享内存(即快速拿锁->快速释放锁)。

在模块中手动编写分配把共享内存给到不同的对象是非常繁琐的,所以这里使用了Slab内存管理器来解决这个问题。

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

20190317155283565219820.png

Ngx_http_lua_api模块是openresty的核心模块,openresty在该模块中定义了lua_shared_dict指令。

lua_shared_dict使用红黑树来保存一个key-value,当内存占用超过分配大小时,lua_shared_dict会使用lru策略,进行淘汰,在此使用链表存储数据用于淘汰数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
http{
lua_shared_dict dogs 10m;
server {
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"))
}
}
}
}

Slab内存管理

之前我们谈到Nginx不同worker进程间需要共享信息时只能通过共享内存,我们也谈到共享内存上可以使用链表、红黑树这样的数据结构。但是每个红黑树上有许多结点,每个结点都需要分配内存去存放,那么怎样把一整块共享内存切割成小块内存供给红黑树使用呢?下面我们看下Slab内存管理是怎样应用于共享内存上的

20190318155287817354100.png

20190424155604009092462.png

哈希表的max_size与bucket_size如何配置

Nginx容器

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

数组(ngx_array_t),这里的数组和平时理解的数组还有有所不同的,它是多块连续内存,其中每块连续内存中可以存放许多元素,这样一种数据结构

链表(ngx_list_t)

队列(ngx_queue_t)

基数树是自平衡排序二叉树的一种,不过它的key只能是整型

其中最主要的两种容器是哈希表和红黑树

哈希表

20190323155333481943534.png

nginx的哈希表和正常见到的哈希表相比,还是稍有不同的。

我们从实现层面上来看,与正常哈希表相似,也就是每个元素会顺序地放在一块连续的内存中,每个元素它的key同样是通过哈希函数来映射的。每个元素就是一个ngx_hash_elt_t结构,name就是它的key,value就是指向实际内容的指针。

不同处在于nginx中哈希表的应用场景不同,nginx中的哈希表仅仅应用于静态不变的内容,也就是在运行过程中,这个哈希表通常是不会出现插入和删除的,nginx刚启动时就可以确定哈希表中一共有多少个元素。nginx中使用哈希表的模块通常会暴露出两个参数—bucket_size、max_size,max_size仅仅控制了最大的哈希表bucket的个数,而不是实际bucket的个数,bucket_size需要注意的是对齐问题,会主动向上对齐,且最好不要超过64字节

20190323155333583734753.png

Nginx中最长用的容器: 红黑树

20190323155333610734603.png

红黑树是一棵自平衡二叉查找树,其有以下特点:

  • 高度不会超过2倍log(n)
  • 增删改查算法复杂度O(log(n))
  • 遍历复杂度O(n)

20190323155333623164824.png

详解HTTP模块

指令的context

指令的合并

20190320155306443125355.png

处理HTTP请求头部的流程

20190320155306447814760.png

三次握手用户ACK后,内核认为连接已经建立成功,操作系统会根据自身的负载均衡算法选中某个worker进程,这个worker进程会通过epoll_wait方法拿到刚刚这个连接的句柄,刚刚的ACK其实是一个读事件,根据这个读事件我们就可以调用accept方法,到accpet方法时我们需要分配连接内存池,然后我们的http模块开始从事件模块手中接入请求的处理过程,http模块在启动时定义了一个回调方法ngx_http_init_connection,这时需要将读事件加入到epoll中,并添加超时定时器(client_header_timeout:60s).

接收数据时,通过回调方法ngx_http_wait_request_handler从连接内存中中分配内存,client_header_buffer_size:1k

ps: connection_pool_size只是初始512K,还可以扩

2019032015530644941631.png

处理请求和处理连接是不一样的,处理连接我只要把它收到我的nginx内存中就ok了,但处理请求时,可能要做大量的请求分析,包括协议、每个header,这里会分配一个请求内存池。

有时请求url特别大,超过了1K内存,这是我们回分配更大的内存large_client_header_buffers:4 8k (最大32k)

HTTP请求的11个阶段

20190320155306425189674.png

image-20190321015117716

postread阶段

realip模块: 获取真实的客户端ip地址

remote_addr、remote_port会显示真实客户端ip及端口

该模块默认没有被编译进nginx,需编译时手动—with-http_realip_module指定开启

rewrite阶段

rewrite按顺序分为两个rewrite,依次是server rewrite和普通rewrite

301永久重定向 302临时重定向

rewrite_log 开启后,rewrite的每一步都会在error_log中记录

freturn 返回信息

rewrite

1
2
3
4
5
6
7
rewrite regex replacement [flag]
flag
last 以replacement重新匹配
break 跳出,不再向下匹配
redirect 临时重定向
permanent 永久重定向

preaccess阶段

对请求做限制 limit_req

漏斗法: 速率恒定,削峰

burst 漏斗大小

1
2
3
4
5
6
7
8
9
10
11
12
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
...
server {
...
location /search/ {
limit_req zone=one burst=5;
}
对连接做限制 limit_conn
1
2
3
4
5
6
7
8
9
10
11
12
http {
limit_conn_zone $binary_remote_addr zone=addr:10m;
...
server {
...
location /download/ {
limit_conn addr 1;
}

access阶段

限制ip访问 access模块
1
2
3
4
5
6
allow address|CIDR|unix:|all;
deny address|CIDR|unix:|all
顺序执行 当满足一条后便不再向下执行
allow 192.168.1.1;
deny all;
对用户名密码校验 auth_basic模块
使用第三方做授权控制的auth_request模块

precontent阶段

try_files
1
2
try_files file .... uri;
try_files file .... code;
实时流量拷贝 mirror模块
1
2
mirror uri|off
mirror_request_body on|off

content阶段

static模块
1
2
3
4
5
6
7
8
9
10
11
root
location /i/ {
root /data/w3/images/;
}
访问"/i/test.jpg" 等同于访问 /data/w3/images/i/test.jpg
alias
location /i/ {
alias /data/w3/images/;
}
访问"/i/test.jpg" 等同于访问 /data/w3/images/test.jpg
concat

当页面需要访问多个小文件时,把他们的内容合并到一次http响应中返回,提升性能

Https://github.com/alibaba/nginx-http-concat

log阶段

access_log模块
1
log_format name string...;

HTTP过滤模块的调用流程

20190323155333308883817.png

2019032315533333428886.png

sub模块

功能: 将响应中指定的字符串,替换成新的字符串

默认没有编译进nginx

水电费三分

addition模块

功能: 在响应前或响应后增加内容,而增加内容的方式是通过新增自请求的响应完成

默认没有编译进nginx

Nginx变量

运行原理

20190323155333370098019.png

变量的特性

  • 惰性求值
  • 变量值可以时刻变化,其值为使用的那一刻的值

存放变量的哈希表

1
2
3
4
5
6
7
syntax: variables_hash_bucket_size size;
default: variables_hash_bucket_size 64;
context: http
syntax: variables_hash_max_size size;
default: variables_hash_max_size 1024;
context: http

HTTP框架提供变量

除了许多http模块会提供变量,http框架本身也会提供一些变量,包括以下几类

  • http请求相关的变量
  • TCP连接相关的变量
  • Nginx处理请求过程中产生的变量
  • 发送HTTP响应时相关的变量
  • Nginx系统变量
HTTP请求相关的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arg_参数名 url中某个具体参数的值
query_string 与args变量完全相同
args 全部URL参数
is_args 如果请求URL中有参数则返回?,否则返回空
conent_length http请求中标识包体长度的content-length头部的值
content_type 标识请求包体类型的content-type头部的值
uri 请求的uri(不包括?后的参数)
document_uri 与uri完全相同
request_uri 请求的URL(包括URI以及完整的参数)
scheme 协议名
request_method 请求方法
request_length 所有请求内容的大小,包括请求头、头部、包体等
remote_user 由http basic auth协议传入的用户名
request_body_file 临时存放请求包体的文件,如果包体非常小则不会存文件
request_body 请求中的包体,这个变量当且仅当使用反向代理,且使用内存暂存包体时才有效
request 原始的url请求,含有方法与协议版本,例如GET/?a=1&b=22 HTTP/1.1

host 先从请求行中获取,如果含有host头部,则用其值替换掉请求行中的主机名,如果前两者都取不到,则使用匹配上的server_name

TCP连接相关的变量
1
2
3
4
5
6
7
8
9
10
11
binary_remote_addr 客户端地址的整型格式,对于IPv4是4字节,对于IPv6是16字节
connection 递增的连接序号
connection_request 当前连接上执行过的请求数,对keepalive连接有意义
remote_addr 客户端地址
remote_port 客户端端口
proxy_protocol_addr 若使用了proxy_protocol协议则返回协议中的地址,否则返回空
proxy_protocol_port 若使用proxy_protocol协议则返回协议中的端口,否则返回空
server_addr 服务器端地址
server_port 服务器端口
TCP_INFO tcp内核层参数,包括$tcpinfo_rtt,$tcpinfo_rttvar
server_protocol 服务器端协议,例如HTTP/1.1
Nginx处理请求过程中产生的变量
1
2
3
4
5
6
7
8
9
request_time 请求处理到现在的耗时,精确到毫秒
server_name 匹配请求的server_name
https 如果开启了TLS/SSL,则返回on,否则返回空
request_completion 若请求处理完则返回OK,否则返回空
request_id 以16进制输出的请求标识id,该id共含有16个字节,是随机生成的
request_filename 待访问文件的完整路径
document_root 由URI和root/alias规则生成的文件夹路径
realpath_root 将document_root中的软连接等换成真实路径
limit_rate 返回客户端响应时的速度上限,单位为每秒字节数。可以通过set指令对请求产生效果
发送HTTP响应时相关的变量
1
2
3
4
body_bytes_sent 响应中body包体的长度
bytes_sent 全部http响应的长度
status http响应的返回码
send_trailer_名字 把响应结尾内容里值返回
Nginx系统变量
1
2
3
4
5
6
7
time_local 以本地时间标准输出当前时间 例如14/Nov/2018:15:55:37 +0800
time_iso8601 使用ISO 8601标准输出当前时间,例如2018-11-14T15:55:37+08:00
nginx_version Nginx版本号
pid 所属worker进程的进程id
pipe 使用了管道则返回p,否则返回.
hostname 所在服务器的主机名,与hostname命令输出一致
msec 197011日到现在的时间,单位为秒,小数点精确到毫秒

防盗链手段

简单的防盗链手段referer 模块

思路: 用invalid_referer变量根据配置判断referer头部是否合法

secure_link模块

通过验证URL中哈希值的方式防盗链

默认没有编译进nginx

过程

由某服务器(也可以是nginx)生成加密后的安全连接url,返回给客户端

客户端使用安全url访问nginx,由nginx的secure_link变量判断是否验证通过

原理
  • 哈希算法是不可逆的
  • 客户端只能拿到执行过哈希算法的URL
  • 仅生成URL的服务器,验证URL是否安全的nginx这两者,才保存执行哈希算法前的原始字符串
  • 原始字符串通常由一下部分有序构成
    • 资源位置,例如HTTP中指定资源的URI,防止攻击者拿到一个安全URLL后可以访问任意资源
    • 用户信息,例如用户IP地址,限制其他用户盗用安全URL
    • 时间戳,使安全URL及时过期
    • 密钥,仅服务器端拥有,增加攻击者猜测出原始字符串的难度

为复杂业务生成新的变量:map 模块

地理位置

geo模块

geoip模块

对客户端您使用keepalive提升连接效率

这里指的是HTTP中的keepalive

功能

多个HTTP请求通过复用TCP连接,实现以下功能

  • 减少握手次数
  • 通过减少并发连接数减少服务器资源的消耗
  • 降低TCP拥塞控制的影响

协议

1
2
3
4
5
6
7
8
9
Connection头部: 取值为close或者keepalive,前者表示请求处理完即关闭连接,后者表示复用连接处理吓一跳请求
Keep-Alive头部:其值为timeout=n,后面的数字n单位是秒,告诉客户端至少保留n秒
指令:
keepalive_disable none|browser
keepalive_request number; #表示一个TCP连接上最多执行多少个请求
keepalive_timeout timeout [header_timeout]; # 连接保留超过timeout秒后断开

反向代理与负载均衡

原理

负载均衡原理

2019032415534078939946.png

Nginx可扩展性

20190324155340793367129.png

x轴是把完全相同的工作分给上游,x轴扩展特点是服务是无状态的,无论其多少个服务他们是通等地对用户提供服务的,也就是我们通常理解的水平扩展。

y轴是按业务分割好再分给上游。

z轴是按用户的相关属性(价值、位置等)分给上游

负载均衡策略

round-robin

1
2
3
upstream name {...}
server address [paramters]

指定上游服务地址的upstream与server指令

功能: 置顶一组上游服务器地址,其中,地址可以是域名、IP地址或unix socket地址。可以再域名或IP地址后加端口,如果不加端口,那么默认使用80端口。

通过用参数:

  • backup: 指定当前server为备份服务,仅当非备份server不可用时,请求才会转发到该server
  • down 标识某台服务已经下线,不在服务
加权Round-Robin负载均衡算法
1
2
3
4
5
6
7
weight 服务访问的权重,默认1
max_conns server的最大并发连接数,仅作用于单个worker进程。默认是0,表示没有限制
max_fails 在fail_timeout时间段内,最大的失败次数。当达到最大失败时,会在fail_timeout秒内这台server不允许再次被选择
fail_timeout
单位是秒,默认为10秒,具有两个功能
指定一段时间内,最大的失败次数max_fails
到max_fails后,该server不能访问的时间
对上游服务使用keepalive长连接

功能: 通过复用连接,降低nginx与上游服务器建立、关闭连接的消耗,提升吞吐量的同时降低时延。

模块: ngx_http_upstream_keepalive_module

对上游连接的http头部设定
1
2
proxy_http_version: 1.1
proxy_set_header Connection "";
Upstream_keepalive的指令
1
2
3
synctax: keepalive connections; 设置到upstream服务器的空闲keepalive连接最大数量,当这个数量被超过时,最近使用最少的连接将被关闭,keepalive指令不会限制一个worker进程到upstream服务器连接的总数量
default: -
context: upstream
指定上游服务域名解析的resover指令
1
2
resolver address ... [valid=time]
resolver_timeout

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
upstream http_backend {
server 127.0.0.1:8081 weight=2;
server 127.0.0.1:8082;
keepalive 16;
}
server {
location /http {
proxy_pass http://http_backend;
proxy_http_version 1.1; # 代理时默认使用http1.0,这里需要调整为1.1
proxy_set_header Connection ""; # proxy_pass时,默认设定Connection close;我们通过置空来禁止发送该条header
}
}

哈希算法

Ip_hash

以客户端IP地址作为hash算法的关键字,映射到特定的上游服务器

1
2
3
Syntax: ip_hash;
Default: -
Context: upstream
hash

指定任意关键字作为hash key,基于hash算法映射到置顶的上游服务器

1
2
3
Syntax: hash key [consistent];
Default: -
Context: upstream

当upstream中一台服务器出现问题时,如果直接移除该机器,导致机器数量减少,这种行为影响到hash算法

一致性哈希算法可以缓解这个问题

一致性哈希算法(upstream_hash模块提供)

宕机或扩容时,hash算法引发大量路由变更,可能导致上游缓存大范围失效。

upstream使用一致性哈希算法非常简单,只需要加上consistent参数

2019032415534121027081.png

最少连接算法

优先选择连接最少的上游服务器:upstream_least_conn模块

功能: 从所有上游服务器中,找出当前并发连接数最少的一个,将请求抓饭到它

1
Syntax: least_conn;
使用共享内存使负载均衡策略对所有worker进程生效: upstream_zone模块

分配出共享内存,将其他upstream模块定义的负载均衡策略数据、运行时每个上游服务的状态数据存放在共享内存上,,以对所有nginx worker进程生效

1
2
3
Syntax: zone name [size];
Default: -
Context: upstream

20190324155341250188677.png

举个例子,默认round robin时是在worker各自的内存中存放计数的,多worker的情况下,由于每次访问时处理请求的worker不同,于是计数就打在了不同的地方,导致轮训没有按设定的方式执行,如图设置8081与8082权重是3和1,但是却出现了非3比1的情况,使用zone 可以解决这个问题。

20190324155341411497908.png

upstream模块提供的变量

1
2
3
4
5
6
7
8
9
10
upstream_addr 上游服务器的ip地址,格式为客都的字符串,例如127.0.0.1:8088
upstream_connect_time 与上游服务建立连接消耗的时间,单位为秒,精确到毫秒
upstream_header_time 接收上游服务发挥响应中http头部所消耗的时间,单位为秒,精确到毫秒
upstream_response_time 接收完整上游服务响应所消耗的时间,单位为秒,精确到毫秒
upstream_http_名称 从上游服务返回的响应头部的值
upstream_bytes_received 从上有服务接收到的响应长度,单位为字节
upstream_response_length 从上游服务返回的响应包体长度,单位为字节
upstream_status 上有服务返回的HTTP响应中的状态码。如果未连接上,该变量为502
upstream_cookie_名称 从上游服务中返回的响应头Set-Cookie中取到的cookie
upstream_trailer_名称 从上游服务的响应尾部取到的值

反向代理

http反向代理流程

20190324155341483555202.png

接收请求优化逻辑:默认接收请求时先接收完客户端信息,然后再去连接上游服务器

响应优化逻辑:默认接收完上游响应后再向客户端发送响应

对http协议的反向代理: proxy模块

功能: 对上游服务使用http/https协议进行反向代理

1
2
3
4
5
6
7
8
9
10
11
Syntax: proxy_pass URL;
Default: -
Context: location,if in locaion,limit_except
URL参数规则:
URL必须以http://或https://开头,接下来是域名、IP、UNIX SOCKET地址或upstream的名字,前两者可以在域名或IP后加端口。最后是可选的URI
URL参数中鞋带URI与否,会导致发向上游请求的URL不同:
* 不携带URI,则将客户端请求中的URL直接转发给上游
* location后使用正则表达式、@名字时,应采用这种方式
* 携带URI,则对用户请求中的URL作如下操作
* 将location参数中匹配上的一段替换为该URI
URL参数中可以携带变量

proxy_pass 后面接域名时会DNS缓存,导致这个域名背后的机器负载不均衡,官方推荐是使用变量解决

生成发往上游的请求头部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Syntax: proxy_method method;
Default: -
Context: http,server,location
Syntax: proxy_http_version 1.0|1.1;
Default: proxy_http_version 1.0;
Context: http,server,location
Syntax: proxy_set_header field value;
Default: proxy_set_header Host $proxy_host;
proxy_set_header Connection close;
Context: http,server,location
注意: 若value的值为空字符串,则整个header都不会向上游发送
Syntax: proxy_pass_request_headers on|off;
Default: proxy_pass_request_headers on;
Context: http,server,location
Syntax: proxy_pass_request_body on|off;
Default: proxy_pass_request_body on;
Context: http,server,location
Syntax: proxy_set_body value;
Default: -;
Context: http,server,location

接受用户请求包体的方式

用户请求包体是由http 请求框架所处理的,但其实这部分主要是被反向代理使用

1
2
3
4
5
6
7
8
9
10
11
12
接收客户端请求包体,是接受完再转发还是边收边转发,默认接受完再转发
Syntax: proxy_request_buffering on|off;
Default: proxy_request_buffering on;
Context: http,server,location
on:
客户端网速较慢
上游服务并发处理能力低
适合高吞吐量场景
off:
更及时的响应
降低nginx读写磁盘的消耗
一旦开始发送内容,proxy_next_upstream功能失败

客户端包体的接受

1
2
3
4
5
6
7
8
9
10
设定读取客户端请求包体的缓冲大小。
Syntax: client_body_buffer_size size;
Default: client_body_buffer_size 8k|16k;
Context: http,server,location
如果请求包体大于缓冲大小,则整个包体或其部分被写到临时文件中。默认缓冲大小等于两杯的内存页大小。X86上32位是8K64位是16K
将请求体全部放到buffer中
Syntax: client_body_in_single_buffer on|off;
Default: client_body_in_single_buffer off;
Context: http,server,location

存在包体时,接受包体所分配的内存:

  • 若接收头部时已经接受完全部包体,则不分配
  • 若剩余待接收包体长度小于client_body_buffer_size,则仅分配所需大小
  • 分配client_body_buffer_size大小内存接收包体

最大包体的长度限制

1
2
3
4
5
Syntax: client_max_body_size size;
Default: client_max_body_size 1m;
Context: http,server,location
仅对请求头部中含有Content-Length有效超出最大场后,返回413错误

临时文件路径格式

1
2
3
4
5
6
7
8
Syntax: client_body_temp_path path[level1 [level2[level3]]];
Default: client_body_temp_path client_body_temp;
Context: http,server,location
包体必须存放到包体中,通常用于定位问题时使用
Syntax: client_body_in_file_only on|clean|off;
Default: client_body_in_file_only off;
Context: http,server,location

与上游服务建立连接

1
2
3
4
5
6
7
8
9
# 建立连接的时间 握手时间
Syntax: proxy_connect_timeout time;
Default: proxy_connect_timeout 60s;
Context: http,server,location
超时后,回向客户端生成http响应,502
Syntax: proxy_next_upstream http_502|..;
Default: proxy_next_upstream error timeout;
Context: http,server,location
上游连接启用TCPkeepalive
1
2
3
Syntax: proxy_socket_keepalive on|off;
Default: proxy_socket_keepalive off;
Context: http,server,location

2019032415534175593714.png

修改TCP连接中的local address
1
2
3
4
5
Syntax: proxy_bind address [transparent]|off;
Default: -;
Context: http,server,location
如果adderss不是本机地址,需要添加transparent
当客户端关闭连接时
1
2
3
Syntax: proxy_ignore_client_abort on|off;
Default: proxy_ignore_client_abort off;
Context: http,server,location
向上游发送HTTP请求
1
2
3
4
设定向上游服务器传输的超时时间
Syntax: proxy_send_timeout time;
Default: proxy_send_timeout 60s;
Context: http,server,location

接收上游的响应

接收上游的HTTP响应头部
1
2
3
4
Syntax: proxy_buffer_size size;
Default: proxy_buffer_size 4k|8k;
Context: http,server,location
如果缓存不够用则会应发错误 error.log: upstream sent too big header
接收上游的HTTP包体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
设定代理缓冲大小
Syntax: proxy_buffers number size;
Default: proxy_buffers 8 4k|8k;
Context: http,server,location
缓存可以存放响应包体则放入缓存中,否则就需要放到磁盘中
开启代理缓冲
Syntax: proxy_buffering on|off;
Default: proxy_buffering on;
Context: http,server,location
限制写入磁盘文件的最大值
Syntax: proxy_max_temp_file_size size;
Default: proxy_max_temp_file_size 1024m;
Context: http,server,location
每次向磁盘文件中写入的字节数
Syntax: proxy_temp_file_write_size size;
Default: proxy_temp_file_write_size 8k|16k;
Context: http,server,location
磁盘文件的路径
Syntax: proxy_temp_path path [level1 [level2 [level3]]];
Default: proxy_temp_path proxy_temp;
Context: http,server,location
及时转发包体
1
2
3
4
虽然开启了响应缓冲,但是想更及时地转发包体,在接收xx大小数据后就开发转发
Syntax: proxy_busy_buffers_size size;
Default: proxy_busy_buffers_size 8k|16k;
Context: http,server,location
接收上游时网络速度相关指令
1
2
3
4
5
6
7
Syntax: proxy_read_timeout time;
Default: proxy_read_timeout 60s;
Context: http,server,location
Syntax: proxy_limit_rate rate;
Default: proxy_limit_rate 0;
Context: http,server,location
上游包体的持久化
1
2
3
4
5
6
7
Syntax: proxy_store_access users:permissions ...;
Default: proxy_store_access user:rw;
Context: http,server,location
Syntax: proxy_store on|off|string;
Default: proxy_store off;
Context: http,server,location

处理上游的响应头部

1
2
3
4
禁止向客户端发送某些header
Syntax: proxy_hide_header field;
Default: -;
Context: http,server,location
修改返回的Set-Cookie头部内容
1
2
3
4
5
6
7
8
9
Syntax: proxy_cookie_domain off;
proxy_cookie_domain domain replacement;
Default: proxy_cookie_domain off;
Context: http,server,location
Syntax: proxy_cookie_path off;
proxy_cookie_path path replacement;
Default: proxy_cookie_domain off;
Context: http,server,location
修改上游服务返回的Location头部内容
1
2
3
4
5
Syntax: proxy_redirect default;
proxy_redirect off;
proxy_redirect redirect replacement;
Default: proxy_redirect default;
Context: http,server,location

上游出现错误时的容错方案

proxy_next_upstream

当上游中某server返回的响应存在某种错误时,我们可以通过向该集群其他server重新发送请求,以此达到容错的目的

默认非幂等方法的请求不会重传

重新发送请求的前提的未向客户端发送任何内容

1
2
3
4
5
6
7
8
9
10
11
当请求某一个server时出现某种错误时重新选用其他server再次请求
Syntax: proxy_next_upstream error|timeout|invalid_header|http_500...;
Default: proxy_next_upstream error timeout;
Context: http,server,location
默认是对error 和 timeout的情况做重试
error 和上游服务器建立连接、发送请求或读取响应头时出现了错误
timeout 和上游服务器建立连接、传递请求或读取响应头时出现了超时
invalid_header 上游服务器返回空或无效的响应
http_xxx 上游服务器返回响应码xxx
non_idempotent 默认情况下,非幂等的方法(POST LOCK PATCH)不会触发next_upstream,如果你想对非幂等方法也启用next_upstream的话,加上non_idempotent
限制proxy_next_upstream的时间与次数
1
2
3
4
超过该时间,则传输给下一个server,默认无时间限制
Syntax: proxy_next_upstream_timeout time;
Default: proxy_next_upstream_timeout 0;
Context: http,server,location
1
2
3
4
限制传递下一个服务器的次数,默认无限制
Syntax: proxy_next_upstream_tries number;
Default: proxy_next_upstream_tries 0;
Context: http,server,location
对于错误码大于等于300的响应指定错误页面
1
2
3
4
对于错误码大于等于300的响应,开启指定错误页面,错误页面通过error_page指定
Syntax: proxy_intercept_errors on|off;
Default: proxy_intercept_errors off;
Context: http,server,location

及时清除缓存

第三方模块nginx_cache_purge

功能: 接收到指定HTTP请求后立即清除缓存

1
2
3
4
5
6
7
8
Syntax: proxy_cache_purge on|off|<method> [from all]|<ip>[..<ip>]];
Default: none;
Context: http,server,location
清除某共享内存空间的某个key对应缓存
Syntax: proxy_cache_purge zone_name key;
Default: none;
Context: location

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
http {
proxy_cache_path /tmp/cache keys_zone=tmpcache:10m;
server {
location / {
proxy_pass http://127.0.0.1:8000;
proxy_cache tmpcache;
proxy_cache_key $uri$is_args$args;
}
location ~ /purge(/.*) {
allow 127.0.0.1;
deny all;
proxy_cache_purge tmpcache $1$is_args$args;
}
}
}

uwsgi、fastcgi、scgi指令表

Uwsgi、fastcgi、scgi与proxy模块一样都是反向代理模块

面向下游使用http协议、面向上游使用独立协议

20190328155375093019256.png

2019032815537510417243.png

20190328155375113122351.png

20190328155375122794527.png

20190328155375130254893.png

20190328155375133725681.png

20190328155375137195863.png

20190328155375139838008.png

搭建websocket反向代理

服务端向客户端推送请求,长连接

websocket缺点:

  • 数据分片有序
  • 不支持多路复用 一个连接上不能同时进行多个未完成的请求
  • 不支持压缩
1
2
3
4
5
6
7
8
9
10
server {
listen 8888;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://echo.websocket.org;
}
}

用分片提升缓存效率

对于断点续传、大文件传输等含有range协议的场景时,slice模块非常有用

20190329155386358853983.png

1
2
3
4
5
6
7
8
9
10
location / {
proxy_cache three;
slice 1m;
proxy_cache_key $uri$is_args$args$slice_range;
proxy_set_header Range $slice_range;
proxy_cache_valid 200 206 1m;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://localhost:8012;
}

open file cache提升系统性能

对于打开的文件句柄优化

1
2
3
4
5
开启open file cache
Syntax: open_file_cache off;
open_file_cache max=N [inactive=time];
Default: open_file_cache_off;
Context: http,server,location

20190329155386392249121.png

性能优化方法论

20190331155403164898343.png

如何高效实用CPU

网络优化

TCP协议优化

滑动窗口

功能: 用于限制连接的网速,解决报文乱序和可靠性问题

nginx中limit_rate等限速指令皆依赖它实现

由操作内核实现

连接两端各有发送窗口和接收窗口

缓冲区
流量控制
拥塞处理
TCP关闭连接

20190404155435715679480.png

time_wait优化

如果服务器上有大量的CLOSE_WAIT,一定是应用程序有bug

1
2
3
4
5
6
7
8
表示是否允许将处于TIME-WAIT状态的socket(TIME-WAIT的端口)用于新的TCP连接
net.ipv4.tcp_tw_reuse = 1
TCP时间戳(会在TCP包头增加12个字节),以一种比超时更精确的方法来启动对RTT的计算,为是西那更好的性能应该启用该项
net.ipv4.tcp_timestamps = 1
time_wait状态连接的最大数量,超出后直接关闭连接
net.ipv4.tcp_max_tw_buckets = 262144

MSL(maximum segment lifetime): 报文最大生存时间,默认MSL为60秒
维持2MSL时长的TIME-WAIT状态: 保证至少一次报文的往返时间内端口是不可用的

lingering_close延迟关闭

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

应用层协议优化

TLS/SSL优化握手性能
1
2
3
4
5
6
7
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缓存,由于openssl的缓存是在内存中使用的,所以仅当统一客户端的两次连接都命中到同一个worker进程时,session缓存在生效
shared:name:size 定义共享内存,为所有worker进程提供session缓存服务,1MB大约可以存4000个session
TLS/SSL中的会话票证tickets

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

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

1
2
3
4
5
6
7
8
9
是否开启会话票证服务
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拥塞控制的影响

对下游及上游都有keepalive_requests指令

1
Syntax: keepalive_requests number;
gzip压缩
1
Syntax: gzip on|off;

nginx默认不会压缩来自上游的响应

1
2
3
4
5
6
Syntax: gzip_proxied off|expired|no-cache|private|no_last_modified|no_etag|auth|any ..;
Default: gzip_proxied off;
Context: http,server,location
off: 不压缩来自上游的响应
...
升级更高效的http2协议

向前兼容http/1.x协议
传输效率大幅度提升

IO优化

减少磁盘IO

优化读取

sendfile零拷贝
内存盘、ssd盘

减少写入

AIO
增大error_log级别
关闭access_log
压缩access_log
是否启用proxy_buffering
syslog替代本地IO

线程池thread pool

静态资源服务场景下应用IO线程池会有性能有较大幅度提升
20190404155436385324281.png

直接IO绕开磁盘高速缓存

对于大文件,不太可能在内存中缓存住,通过裸IO可以减少数据多次拷贝

减少磁盘读写次数

empty_gif模块

从前端页面做用户行为分析时,由于跨域等要求,前端打点的上报数据一般是GET请求,且考虑到浏览器解析DOM树的性能消耗,所以请求透明图片消耗最小,而11的gif图片体积最小(仅43字节),故通常请求gif图片,并在请求中把用户行为信息上报服务器
Nginx可以在access日志中获取到请求参数,进而统计用户行为。但若在磁盘汇总读取1
1的文件则有磁盘IO消耗,empty_gif模块将图片放在内存中,加快处理速度

1
2
3
Syntax: empty_gif;
Default: -
Context: location
access日志压缩

sendfile零拷贝提升性能

  • 减少了内存拷贝次数
  • 减少内存切换次数
    20190408155469330080884.png

使用sendfile与gzip配合时有点小问题,gzip压缩是动态压缩,一定要先拷贝到用户态再压缩,所以gzip会导致sendfile失效。这里可以借助gzip_static模块及gunzip模块进行相应优化。

对于静态文件可以使用gzip_static静态预先压缩(gzip),解决这个问题。
nginx离线压缩
如果服务器端只有压缩文件,但是客户端浏览器不支持接收压缩文件,可以利用gunzip进行响应时动态解压缩

linux默认的gzipC库分配内存能力不高,可以考虑google的tcmalloc

stub_status监控nginx

通过—with-http_stub_status_module启用模块

功能:
通过HTTP接口,实时监测nginx的连接状态。
统计数据存放于共享内存中,所以统计值包含所有worker进程,且执行reload不会导致数据清0,但热升级会导致数据清0

1
2
3
4
5
6
7
8
9
10
11
Syntax: stub_status;
Default: -
Context: server,location
Active connections: 当前活跃的客户端连接数(Reading+Writing+Waiting)
accepts: 自Nginx启动起,与客户端建立过的连接总数
handled: 自Nginx启动起,处理过的客户端连接总数。如果没有超出worker_connections配置,该值与accepts相同
requests: 自nginx启动起,处理过的客户端请求总数。由于存在HTTP Keep-Alive请求,故requests值会大于handled值
Reading: 正在读取HTTP请求头部的连接总数
Writing: 正在向客户端发送响应的连接总数
Waiting: 当前空闲的HTTP Keep-Alive连接总数

handled 小于accepts 一定是worker_connections配置小了

源码阅读

第三方模块源码的快速阅读方法

1.分析模块提供的config文件
2.分析ngx_module_t模块
3.分析ngx_command_t数组看看支持哪些配置指令
4.对于http模块,分析ngx_http_module_t中在http{}解析前后实现了哪些回调
5.根据第3,4步,找出该模式生效方式

  • 在11个阶段中哪个阶段处理请求
  • 在过滤响应中生效吗?
  • 在负载均衡中生效吗?
  • 是否提供了新的变量?

debug日志

—with-debug

1
2
3
4
针对特定客户端打印DEBUG级别日志,其他日志依然遵循error_log指令后设置的日志级别
Syntax: debug_connection address|CIDR|unix:;
Default: -
Context: events

20190408155472862470727.png

开启debug级别后访问nginx,在error_log日志中可以看到debug信息
2019040815547294907917.png

Openresty

Openresty概述

Openresty组成部分

20190409155474180814360.png
ngx_http_lua_module模块给予nginx各阶段嵌入lua的能力
ngx_stream_lua_module是在stream四层反向代理中加入lua

openresty的运行机制

20190409155474202852141.png

openresty中的SDK

  • cosocket通讯
    • udp
    • tcp
  • 基于共享内存的字典shared.DICT (跨worker进程)
  • 定时器
  • 基于协程的并发编程
  • 获取客户端请求与响应的信息
  • 修改客户端请求与响应,包括发送响应
  • 子请求
  • 工具类
    • 正则表达式
    • 日志
    • 系统配置
    • 编解码
    • 时间

Openresty使用要点

  • 不破坏nginx的事件驱动体系,不使用任何会阻塞nginx进程进行事件调度的方法
    • 不调用会导致nginx进程主动休眠的方法
      • 谨慎调用第三方库,若不是通过cosocket实现(例如Lua标准网络库或第三方服务提供的SDK库),就无法融入nginx的事件驱动体系中,那么对TCP消息的处理通常会导致nginx进程进入sleep状态
    • 不调用长时间占用CPU的方法
      • 避免lua代码块执行密集计算指令
  • 不破坏nginx的低内存消耗有点,避免对每个请求分配过大内存
    • 对会导致分配内存的lua SDK,谨慎分配内存,考虑用状态机多次分批处理
  • 理解lua代码块如何嵌入nginx中执行
  • 理解lua sdk详细用法,及他们如何继承在nginx中处理请求
  • 保持lua代码的高效

Openresty中的Nginx模块

核心模块

20190409155474265919960.png

反向代理模块

20190409155474274126591.png
该页面中模块都比较老,不建议使用

工具模块

20190409155474285447392.png
2019040915547429142010.png

Or官方Lua模块

20190409155474306126423.png

20190409155474310163513.png

20190409155474315185829.png

如何在Nginx中嵌入Lua代码

20190409155474335990137.png

在Nginx启动过程中嵌入Lua代码

20190409155474345393951.png

在11个HTTP阶段中嵌入Lua代码

20190409155474354590607.png

控制rewrite/access是否延迟lua代码

1
2
3
4
5
6
7
rewrite_by_lua_no_postpone
context: http
控制是否在rewrite阶段延迟至最后再执行Lua代码,默认关闭
access_by_lua_no_postpone
context:http
控制是否在access阶段延迟至最后再执行Lua代码,默认关闭

在过滤响应、负载均衡时嵌入lua代码

20190409155474382010042.png
2019040915547438469526.png
20190409155474388435138.png

在openssl处理ssl协议时嵌入lua代码

20190409155474393489629.png

在lua代码中获取当前阶段

20190409155474399534621.png

Openresty中Lua与C代码交互原理

20190409155474440651097.png

系统级配置指令

20190409155474460163020.png

取得配置参数SDK

20190409155474466775924.png

获取、修改请求与响应的SDK

20190409155474481473693.png

20190409155474489241961.png

20190409155474492687992.png

20190409155474497122999.png

20190409155474502397661.png

20190409155474509039209.png

20190409155474510782364.png

20190409155474512727436.png

20190409155474517215565.png

20190409155474520731927.png

20190409155474522896844.png

2019040915547452433967.png

20190409155474528054834.png

20190409155474530362969.png

20190409155474533438107.png

工具类型的SDK

sdk

cosocket

cosocket

并发编程

并发编程

FAQ

1.平滑重启时,某些旧worker由于某些原因一直不能关闭,导致旧worker越来越多

使用worker_shutdown_timeout配置项,设定worker优雅关闭超时时间,超过该时间后,会强制关闭worker进程

2.简略的请求处理流程

建立连接->处理请求头->请求处理11阶段

3.nginx master进程接收到HUP时的系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
strace: Process 29234 attached
00:18:32.311656 rt_sigsuspend([], 8) = ? ERESTARTNOHAND (To be restarted if no handler) <9.693532>
00:18:42.005383 --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=14630, si_uid=0} ---
00:18:42.005502 gettimeofday({1554826722, 5602}, NULL) = 0 <0.000011>
00:18:42.005734 rt_sigreturn({mask=[HUP INT QUIT USR1 USR2 ALRM TERM CHLD WINCH IO]}) = -1 EINTR (Interrupted system call) <0.000011>
00:18:42.005895 gettimeofday({1554826722, 5942}, NULL) = 0 <0.000010>
00:18:42.006044 stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=414, ...}) = 0 <0.000011>
00:18:42.006155 gettimeofday({1554826722, 6186}, NULL) = 0 <0.000008>
00:18:42.006253 uname({sysname="Linux", nodename="VM-74-227-ubuntu", ...}) = 0 <0.000009>
00:18:42.006361 open("/data1/nginx/conf/nginx.conf", O_RDONLY) = 11 <0.000015>
00:18:42.006491 fstat(11, {st_mode=S_IFREG|0644, st_size=5850, ...}) = 0 <0.000009>
00:18:42.006617 pread64(11, "\n#user nobody;\nworker_processes"..., 4096, 0) = 4096 <0.000011>
00:18:42.007446 epoll_create(100) = 12 <0.000012>
00:18:42.007563 close(12) = 0 <0.000013>
00:18:42.007765 open("/data1/nginx/conf/mime.types", O_RDONLY) = 12 <0.000013>
00:18:42.007873 fstat(12, {st_mode=S_IFREG|0644, st_size=5170, ...}) = 0 <0.000009>
00:18:42.007983 pread64(12, "\ntypes {\n text/html "..., 4096, 0) = 4096 <0.000009>
00:18:42.008174 pread64(12, "application/octet-stream "..., 1074, 4096) = 1074 <0.000049>
00:18:42.008354 close(12) = 0 <0.000009>
00:18:42.008524 brk(0x5572e6c50000) = 0x5572e6c50000 <0.000012>
00:18:42.008677 socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE) = 12 <0.000014>
00:18:42.008780 bind(12, {sa_family=AF_NETLINK, pid=0, groups=00000000}, 12) = 0 <0.000009>
00:18:42.008872 getsockname(12, {sa_family=AF_NETLINK, pid=29234, groups=00000000}, [12]) = 0 <0.000008>
00:18:42.008989 sendto(12, "\24\0\0\0\26\0\1\3\342\305\254\\\0\0\0\0\0\0\0\0", 20, 0, {sa_family=AF_NETLINK, pid=0, groups=00000000}, 12) = 20 <0.000022>
00:18:42.009103 recvmsg(12, {msg_name(12)={sa_family=AF_NETLINK, pid=0, groups=00000000}, msg_iov(1)=[{"L\0\0\0\24\0\2\0\342\305\254\\2r\0\0\2\10\200\376\1\0\0\0\10\0\1\0\177\0\0\1"..., 4096}], msg_controllen=0, msg_flags=0}, 0) = 508 <0.000016>
00:18:42.009207 recvmsg(12, {msg_name(12)={sa_family=AF_NETLINK, pid=0, groups=00000000}, msg_iov(1)=[{"\24\0\0\0\3\0\2\0\342\305\254\\2r\0\0\0\0\0\0", 4096}], msg_controllen=0, msg_flags=0}, 0) = 20 <0.000009>
00:18:42.009299 close(12) = 0 <0.000012>
00:18:42.009391 stat("/etc/resolv.conf", {st_mode=S_IFREG|0644, st_size=74, ...}) = 0 <0.000011>
00:18:42.009493 open("/etc/hosts", O_RDONLY|O_CLOEXEC) = 12 <0.000053>
00:18:42.009685 fstat(12, {st_mode=S_IFREG|0644, st_size=260, ...}) = 0 <0.000165>
00:18:42.009957 read(12, "127.0.0.1 localhost localhost."..., 4096) = 260 <0.000009>
00:18:42.010054 read(12, "", 4096) = 0 <0.000011>
00:18:42.010212 close(12) = 0 <0.000012>
00:18:42.010478 stat("/etc/resolv.conf", {st_mode=S_IFREG|0644, st_size=74, ...}) = 0 <0.000011>
00:18:42.010587 socket(PF_INET, SOCK_DGRAM|SOCK_NONBLOCK, IPPROTO_IP) = 12 <0.000012>
00:18:42.010675 connect(12, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.243.28.52")}, 16) = 0 <0.000015>
00:18:42.010786 gettimeofday({1554826722, 10823}, NULL) = 0 <0.000192>
00:18:42.011070 poll([{fd=12, events=POLLOUT}], 1, 0) = 1 ([{fd=12, revents=POLLOUT}]) <0.000009>
00:18:42.011165 sendto(12, "\330\376\1\0\0\1\0\0\0\0\0\0\3www\4cq67\3com\0\0\1\0\1", 30, MSG_NOSIGNAL, NULL, 0) = 30 <0.000038>
00:18:42.011288 poll([{fd=12, events=POLLIN}], 1, 1000) = 1 ([{fd=12, revents=POLLIN}]) <0.000268>
00:18:42.011650 ioctl(12, FIONREAD, [82]) = 0 <0.000010>
00:18:42.011741 recvfrom(12, "\330\376\201\200\0\1\0\2\0\0\0\0\3www\4cq67\3com\0\0\1\0\1\300\f"..., 1024, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.243.28.52")}, [16]) = 82 <0.000018>
00:18:42.011861 close(12) = 0 <0.000014>
00:18:42.012223 pread64(11, "\n \n\n #error_page "..., 1754, 4096) = 1754 <0.000011>
00:18:42.012397 brk(0x5572e6c71000) = 0x5572e6c71000 <0.000011>
00:18:42.012703 close(11) = 0 <0.000010>
00:18:42.012808 geteuid() = 0 <0.000009>
00:18:42.012908 open("/etc/passwd", O_RDONLY|O_CLOEXEC) = 11 <0.000014>
00:18:42.013020 lseek(11, 0, SEEK_CUR) = 0 <0.000009>
00:18:42.013107 fstat(11, {st_mode=S_IFREG|0644, st_size=1692, ...}) = 0 <0.000009>
00:18:42.013196 mmap(NULL, 1692, PROT_READ, MAP_SHARED, 11, 0) = 0x7f2e445e2000 <0.000012>
00:18:42.013285 lseek(11, 1692, SEEK_SET) = 1692 <0.000008>
00:18:42.013375 munmap(0x7f2e445e2000, 1692) = 0 <0.000014>
00:18:42.013463 close(11) = 0 <0.000009>
00:18:42.013549 open("/etc/group", O_RDONLY|O_CLOEXEC) = 11 <0.000012>
00:18:42.013715 lseek(11, 0, SEEK_CUR) = 0 <0.000014>
00:18:42.013831 fstat(11, {st_mode=S_IFREG|0644, st_size=915, ...}) = 0 <0.000009>
00:18:42.013933 mmap(NULL, 915, PROT_READ, MAP_SHARED, 11, 0) = 0x7f2e445e2000 <0.000015>
00:18:42.014052 lseek(11, 915, SEEK_SET) = 915 <0.000010>
00:18:42.014175 munmap(0x7f2e445e2000, 915) = 0 <0.000014>
00:18:42.014289 close(11) = 0 <0.000012>
00:18:42.014423 mkdir("/data1/nginx/client_body_temp", 0700) = -1 EEXIST (File exists) <0.000014>
00:18:42.014582 stat("/data1/nginx/client_body_temp", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0 <0.000013>
00:18:42.014703 mkdir("/data1/nginx/proxy_temp", 0700) = -1 EEXIST (File exists) <0.000013>
00:18:42.014822 stat("/data1/nginx/proxy_temp", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0 <0.000009>
00:18:42.014915 mkdir("/data1/nginx/fastcgi_temp", 0700) = -1 EEXIST (File exists) <0.000009>
00:18:42.015000 stat("/data1/nginx/fastcgi_temp", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0 <0.000009>
00:18:42.015087 mkdir("/data1/nginx/uwsgi_temp", 0700) = -1 EEXIST (File exists) <0.000009>
00:18:42.015171 stat("/data1/nginx/uwsgi_temp", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0 <0.000009>
00:18:42.015256 mkdir("/data1/nginx/scgi_temp", 0700) = -1 EEXIST (File exists) <0.000009>
00:18:42.015342 stat("/data1/nginx/scgi_temp", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0 <0.000009>
00:18:42.015429 open("/data1/nginx/logs/access.log", O_WRONLY|O_CREAT|O_APPEND, 0644) = 11 <0.000012>
00:18:42.015518 fcntl(11, F_SETFD, FD_CLOEXEC) = 0 <0.000008>
00:18:42.015603 open("/data1/nginx/logs/test.access.log", O_WRONLY|O_CREAT|O_APPEND, 0644) = 12 <0.000015>
00:18:42.015754 fcntl(12, F_SETFD, FD_CLOEXEC) = 0 <0.000009>
00:18:42.015848 open("/data1/nginx/logs/rewrite_error.log", O_WRONLY|O_CREAT|O_APPEND, 0644) = 13 <0.000013>
00:18:42.015943 fcntl(13, F_SETFD, FD_CLOEXEC) = 0 <0.000007>
00:18:42.016022 open("/data1/nginx/logs/error.log", O_WRONLY|O_CREAT|O_APPEND, 0644) = 14 <0.000012>
00:18:42.016110 fcntl(14, F_SETFD, FD_CLOEXEC) = 0 <0.000008>
00:18:42.016193 dup2(14, 2) = 2 <0.000008>
00:18:42.016274 getrlimit(RLIMIT_NOFILE, {rlim_cur=100000, rlim_max=100000}) = 0 <0.000008>
00:18:42.016358 close(4) = 0 <0.000008>
00:18:42.016436 close(5) = 0 <0.000008>
00:18:42.016513 close(6) = 0 <0.000008>
00:18:42.016594 close(7) = 0 <0.000007>
00:18:42.016740 socketpair(PF_LOCAL, SOCK_STREAM, 0, [4, 5]) = 0 <0.000017>
00:18:42.016846 ioctl(4, FIONBIO, [1]) = 0 <0.000009>
00:18:42.016949 ioctl(5, FIONBIO, [1]) = 0 <0.000008>
00:18:42.017034 ioctl(4, FIOASYNC, [1]) = 0 <0.000008>
00:18:42.017115 fcntl(4, F_SETOWN, 29234) = 0 <0.000008>
00:18:42.017194 fcntl(4, F_SETFD, FD_CLOEXEC) = 0 <0.000007>
00:18:42.017271 fcntl(5, F_SETFD, FD_CLOEXEC) = 0 <0.000008>
00:18:42.017350 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f2e445d59d0) = 29607 <0.000133>
00:18:42.017661 sendmsg(3, {msg_name(0)=NULL, msg_iov(1)=[{"\1\0\0\0\0\0\0\0\247s\0\0\0\0\0\0\1\0\0\0\0\0\0\0\4\0\0\0\0\0\0\0", 32}], msg_controllen=24, [{cmsg_len=20, cmsg_level=SOL_SOCKET, cmsg_type=SCM_RIGHTS, [4]}], msg_flags=0}, 0) = 32 <0.000058>
00:18:42.017827 nanosleep({0, 100000000}, NULL) = 0 <0.100080>
00:18:42.117970 sendmsg(3, {msg_name(0)=NULL, msg_iov(1)=[{"\3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\0\0\0\0", 32}], msg_controllen=0, msg_flags=0}, 0) = 32 <0.002582>
00:18:42.120653 rt_sigsuspend([], 8) = ? ERESTARTNOHAND (To be restarted if no handler) <0.000010>
00:18:42.120786 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=29237, si_uid=65534, si_status=0, si_utime=0, si_stime=1} ---
00:18:42.120914 gettimeofday({1554826722, 120933}, NULL) = 0 <0.000008>
00:18:42.120960 wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG, NULL) = 29237 <0.000022>
00:18:42.121098 wait4(-1, 0x7fff3ab4c93c, WNOHANG, NULL) = 0 <0.000008>
00:18:42.121170 rt_sigreturn({mask=[HUP INT QUIT USR1 USR2 ALRM TERM CHLD WINCH IO]}) = -1 EINTR (Interrupted system call) <0.000039>
00:18:42.121244 gettimeofday({1554826722, 121258}, NULL) = 0 <0.000037>
00:18:42.121319 close(3) = 0 <0.000010>
00:18:42.121386 close(10) = 0 <0.000014>
00:18:42.121470 sendmsg(4, {msg_name(0)=NULL, msg_iov(1)=[{"\2\0\0\0\0\0\0\0005r\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\0\0\0\0", 32}], msg_controllen=0, msg_flags=0}, 0) = 32 <0.000016>
00:18:42.121559 rt_sigsuspend([], 8) = ? ERESTARTNOHAND (To be restarted if no handler) <0.000009>
00:18:42.121628 --- SIGIO {si_signo=SIGIO, si_code=SI_KERNEL} ---
00:18:42.121668 gettimeofday({1554826722, 121698}, NULL) = 0 <0.000009>
00:18:42.121738 rt_sigreturn({mask=[HUP INT QUIT USR1 USR2 ALRM TERM CHLD WINCH IO]}) = -1 EINTR (Interrupted system call) <0.000012>
00:18:42.121841 gettimeofday({1554826722, 121889}, NULL) = 0 <0.000013>
00:18:42.121953 rt_sigsuspend([], 8

4.健康检查

借助upstream模块server指令的fail_timeout和max_fails以及proxy_next_upstream指令实现被动健康检查

某server在fail_timeout时间内失败max_fails次后,接下来的fail_timeout秒会被认为是不可用;fail_timeout后会默认为恢复,继续往该主机发送请求,往复循环

对于失败的请求会采用next_upstream策略请求下一个server

如何判定请求server失败呢?这是由*_next_upstream设定的

5.nginx内存池和连接池都是worker级别的