nginx-学习2-架构基础

181 阅读6分钟

1.进程架构

  • master 管理多个woker
  • worker 实际处理请求
  • cache manager 管理多个work的共享缓存信息
  • cache loader 加载到共享缓存里
ps -ef | grep nginx
kill -SIGHUP 9170 # 等价于 nginx -s reload  热更新配置
kill -SIGTERM 6982 # 关闭某个worker 

2.信号 管理 父子进程

配置热更新

  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进程发送QUIT信号,关闭老master进程
  6. 回滚:向老master发送HUP,向新master发送QUIT

worker进程:优雅的关闭

  1. 设置定时器 worker shutdown timeout
  2. 关闭监听句柄
  3. 关闭空闲连接
  4. 在循环中等待全部连接关闭 (当超过指定时间就不在等待)
  5. 退出进程

3.架构原理

nginx处理流程

由于nginx使用epoll来实现多路复用io,也叫非阻塞事件驱动处理引擎。

所以每次外部流量进入要区分每次请求的数据是具体是给谁操作。需要引入状态机的概念区分模块。

外部流量  >  状态机        >   分发处理的地方
web      >   http状态机    >   
email    >   email状态机   >   
tcp      >   传输层状态机  > 

状态机再根据配置,转发给不同的处理

  • 静态资源
  • 反向代理
  • api服务(fastCGI memroycahe等)

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

  • 应用层 -> 传输层 -> 网络层 -> 链路层 -> 物理层
  • 物理层 使用 以太网 和广域网 传输数据
  • 应用层 <- 传输层 <- 网络层 <- 链路层 <- 物理层

TCP/IP协议层级

  • 应用层 :只保存基础的数据 data (http,DNS,SSH)
  • 传输层TCP : 源端口+目的端口 + data (TCP UDP)
  • 网络层IP :源ip+目的ip + 源端口+目的端口 + data (IP,ARP,DHCP)
  • 数据链路层 :源mac+目的mac + 源ip+目的ip + 源端口+目的端口 + data (ADSL,wifi)

Reactor事件驱动模型

Nginx的Worker进程首先会调用Nginx的Core核心模块。在Reactor模型中会维护一个运行循环(Run-Loop),主要包括事件收集、事件分发、事件处理,这个工作在Nginx中由Core核心模块负责。

通过统一接收事件,并根据请求的类型转发给不用的消费者处理

  • 事件:如:必须是完整的tcp 3次握手,即客户端syn,服务器syn+ack,客户端ack 后才算一个事件。 Nginx对高并发IO的处理使用了Reactor事件驱动模型,实现了多路IO复用
  • 事件收集器:负责收集Worker进程的各种I/O请求。
  • 事件发送器:负责将I/O事件发送到事件处理器。
  • 事件处理器:负责各种事件的响应工作。

事件分类

可以使用 Wireshark 进行抓包

读事件

  • Accept建立连接
  • Read读消息

写事件

Write写消息,

TCP协议与非阻塞接口

  • 阻塞操作:是指在执行操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。被挂起的进程进入睡眠状态,被从调度器的运行队列移走,直到等待的条件被满足。

  • 非阻塞操作:在不能进行操作时, 并不挂起,它要么放弃,要么不停地查询,直至可以进行操作为止。

  • 等待队列: 阻塞访问最大的好处是当设备不可操作时进程可以挂起进入睡眠,让出CPU。 linux中可以使用等待队列(Wait Queue) 来实现阻塞进程的唤醒。等待队列作为Linux内核中的一个基本单位,以队列为数据结构,与调度机制紧密结合,可以用来同步对系统资源的访问。

4.nginx事件循环流程

image.png

  1. 客户端发起请求
  2. tcp 3次握手结束后,把消息内容加入等待队列
  3. 同时通知并唤醒nginx
  4. nginx的master进程通过Reactor事件系统,去循环读取等待队列
  5. 这时候把内核态的数据复制到用户态
  6. 读取一个就转发给work进程处理
  7. 处理的时候可能产生新的消息内容,直接也插到等待队列
  8. 等待队列的内容都处理完后,nginx又恢复到 休眠模式

5.epoll

网络IO

网络IO 一般选用 多路复用IO

linux有3种,nginx 选择的epoll

  • select
  • poll
  • epoll

问题

传统的等待队列是数组,连接数有时候可能高达数百万的连接,每次遍历效率很低。

解决方案

nginx 选用epoll的数据结构存储活跃的连接数。因为在高并发连接中,每次处理的活跃连接数量占比很小。

每次新增和删除都只是链表上操作,查询和删除都非常的快。

实现:

红黑树+ 链表

实际结构

eventpoll对象

  • lock
  • mtx
  • wq
  • poll_wait
  • rdllist epitem对象的集合,红黑树中每个结点都是某于epitem结构中的rdllink成员,
  • rbr 对应 单个 epitem对象
  • ovflist
  • user

epitem对象 红黑树中每个结点都是基于epitem结构中的rbn成员

  • rbn
  • rdllink
  • next
  • ffdnwait
  • pwqlisted
  • fllinktevent

使用

  1. 创建
  2. 操作: 添加/修改/删除
  3. 获取句柄
  4. 关闭

6.nginx中的请求切换

传统容器处理线程方案

  • 每一线程仅处理一连接。
  • 当操作系统发现阻塞时,会自动切换到另外一个进程去执行。等其他执行完又再次切回来。
  • 依赖操作系统的进程调度实现并发,每次切换成本很高。

nginx容器处理线程方案

  • 一条线程同时处理多连接
  • 用户态代码完成连接切换,即直接用单个线程通过代码判断,实现处理多个连接的处理。
  • 尽量减少操作系统进程切换

7.阻塞&非阻塞 同步&异步之间的区别

  1. 阻塞调用:如 调用accept建立连接时,会等待队列插入accept信息,等队列有了信息后才继续执行,如果过程中有阻塞,会进入等待,会触发操作系统的进程切换。导致延迟。利用的还是操作系统自动切换。
  2. 非阻塞:通过代码可以知道当前是否队列有空或不为空,无需等待,直接返回对应的状态码,省去了系统的进程切换。

非阻塞调用下的同步与异步

核心是解决地狱回调问题。

异步代码:下面代码的逻辑被拆分了

rc = ngx_http_read_request_body ( r, ngx_http_upstream_init) ;
if( rc > = NGX_HTTP_SPECIAL_RESPONSE){
    return rc ;
}

# 这里定义了 ngx_http_read_request_body 方法
ngx_int_t
ngx_http_read_request_body (ngx_http_request_t * r ,ngx_http_client_body_handler pt post_handler)

# 这是 回调方法
ngx_http_upstream_init(ngx_http_request_t * r){

}

同步代码:符合人的阅读习惯,之上而下

local client = redis:new()
client:set timeout(30000)
local ok,err = client:connect(ip,port)
if not ok then
    ngx.say("failed: ",err)
    return
end

8.nginx模块原理与使用

流程

  1. 前提编译进Nginx
  2. 提供哪些配置项
  3. 模块何被使用
  4. 提供哪些变量

nginx.org/en/docs/htt…

 # 主模块
ngx_module_t
# 包含子模块
commands :ngx_command_t
ctx: 待每个模块将具体化上下文  

# ctx 上下文
ngx_core_module_t # 核心
ngx_http_module_t # http
ngx_event_module_t # event 
ngx_mail_conf_ctx_t # mail

# ngx_core_module_t 核心
core 
errlog 
thread_pool
openssl
events 
http
stream

# ngx_http_module_t
upstream模块
响应过滤模块
请求处理模块
ngx_http_core_module模块

# ngx_event_module_t # event 
event_core
epoll

源码分析

cd /src/ # 源码位置
core  
event  
http  
mail  
misc  
os  
stream

# http 模块
cd /src/core/http

9.连接池

构成包含:上游服务器的连接和下游的客户端的连接

struct ngx_cycle_t { 
    # 包含下面打个主要属性 
    free_connections
    connections # 预分配的cannection n个连接
    read_events  # 预分配的connection n个读事件
    write_events # 预分配的connection n个写事件
}

一般长度一样,可以实现下标统一检索数据。

connections指向的连接池中,每个连接所需要的读写事件都以相同的数组序号对应着read_event, write_events读写事件数组,所以相同序号下这三个数组中的元素是配合使用的

核心数据结构

struct ngx_connection_s {
    ngx_event_t *read; # 读事件
    ngx_event_t *write; # 写事件
    ngx_pool_t *pool; # <= 初始 connection pool size配置
}

# 读写事件结构
struct ngx_event_t {
    void *data;
}

限制长度

events {
    worker_connections  1024;
}

内存池对性能的影响

  1. 连接内存池
  2. 请求内存池
ngx_pool_s # 内存池
ngx_pool_data_t # 数据块
ngx_pool_large_s # 大块内存
ngx_pool_cleanup_s  # 需要清理的外部资源

# 配置限制参数
request_pool_size size;
connection_pool_size size:

nginx.org/en/docs/htt…

worker共享内存

# 基础同步
1. 信号
2. 共享内存

# 高级通讯
1. 锁
2. Slab内存管理器

官方nginx模块使用了共享内存

  1. ngx_http_lua_api
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"))
            }
        }
    }
}
  1. rbtree
  • ngx_stream_limit_conn_module
  • ngx_http_limit_conn_module
  • ngx_stream_limit_req_module
  • http/cache : ngx_http_file_cache , ngx_http_proxy_module
  • ssl :ngx_http_ssl_module
  1. 单链表
  • ngx_http_upstream_zone_module
  • ngx_stream_upstream_zone_module

10.Slab内存管理器

  • ngx_slab_pool_t* pool 内存池 (包含多个页面)
  • ngx_slab_page_t 页面 (包含多个slot块)页面之间通过链表关联
  • slot 块

概念

  • 将内存按照页进行分配,每页的大小相同, 此处设为page_size。
  • 将内存块按照2的整数次幂进行划分, 最小为8bit, 最大为page_size/2。
  • 例如,假设每页大小为4Kb, 则将内存分为8, 16, 32, 64, 128, 256, 512, 1024, 2048共9种
  • 每种对应一个slot, 此时slots数组的大小n即为9

bestfit 最佳适应算法

选择最优的算法分配slot给页面

  • 最多两倍内存消耗
  • 适合小对象
  • 避免碎片化
  • 避免重复初始化

配置 ngx_slab_stat 查看状态

# 安装模块
modules/ngx_slab_stat

# 下载 tengine 版本
https://tengine.taobao.org/download.html

tengine 自带 ngx_slab_stat
# 编译运行
# 在当前的 openretry下 使用  tengine 的库,也可以复制过去
./configure --add-module=../tengine-2.3.3/modules/ngx_slab_stat/

# 配置 查询slab_stat 列表
http {  
    lua_shared_dict dogs 10m;  # 定义共享变量    
    server {
        server {
            listen 8080;
            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"))
                }
            }
    }
}

哈希表的max_size与 bucket_size如何配置

nginx 主要高级数据结构 哈希表 ngx_hash_t

  • buckets
  • size
  • ngx_hash_find()

每个哈希的节点结构 ngx_hash_elt_t

  • value
  • len
  • name

哈希表配置

对齐问题 Bucket size 和 Max size 保存一致长度

红黑树

红黑树 可以解决 节点一遍倒的问题 ,让节点均匀分布

是一种:自平衡二叉查找树

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

ngx_rbtree_t 结构体

  • root # 根节点
  • sentinel # 哨兵节点
  • insert
  • ngx_rbtree_init()
  • ngx_rbtree_insert()
  • ngx_rbtree_delete()

红黑树 使用的模块

  • ngx_conf_module config_dump_rbtree
  • ngx_event_timer_rbtree
  • ngx_http_file_cache
  • ngx_http_geo_module
  • ngx_http_limit_conn_module
  • ngx_http_limit_req_module
  • ngx_http_lua_shdict:ngx.shared.DICT LRU链表性质
  • ngx_resolver_tresolver
  • ngx_stream_geo_module
  • ngx_stream_limit_conn_module

11.nginx动态模块

有点像window的dll, 编译是指记录链接的指针, 调整新的库,只需要替换库即可,无需重新编译。

创建流程

  1. Configure加入动态模块
  2. 编译进binary
  3. 启动时初始化模块数组
  4. 读取load module配置
  5. 打开动态库并加入模块数组
  6. 基于模块数组开始初始化

压缩图片例子

# 方法1 直接编译
# 编译安装 
./configure --with-http_image_filter_module

# conf 配置 设置图片裁剪
location /{
    root /html
    image_filter resize 100 100; # 设置图片为 100*100
}
            
# 方法2 编译 生成动态库
./configure  --with-http_image_filter_module=dynamic

# 在objs/会生成 ngx_http_image_filter_module.so
cp objs/ngx_http_image_filter_module.so  /usr/local/nginx/modules #复制so文件 到 nginx安装目录里

# conf 配置
# 顶部添加 
load_module modules/ngx_http_image_filter_module.so;
# 对应server 正常使用 image_filter
location /{
    root /html
    image_filter resize 100 100; # 设置图片为 100*100
}

参考

time.geekbang.org/course/intr…