张超:又拍云系统开发高级工程师,负责又拍云 CDN 平台相关组件的更新及维护。Github ID: tokers,活跃于 OpenResty 社区和 Nginx 邮件列表等开源社区,专注于服务端技术的研究;曾为 ngx_lua 贡献源码,在 Nginx、ngx_lua、CDN 性能优化、日志优化方面有较为深入的研究。

子请求、父请求和主请求

Nginx 所处理的大部分请求,都是在接收到客户端发来的 HTTP 请求报文后创建的,这些请求直接与客户端打交道,称之为主请求;与之相对的则是子请求,顾名思义,子请求是由另外的请求创建的,比如主请求(当然子请求本身也可以创建子请求),当一个请求创建一个子请求后,它就成了该子请求的父请求。从源码层面来说,当前请求的主请求通过 r->main 指针获取,父请求则通过 r->parent 指针获取。

使用子请求机制的意义在于,它能够分散原本集中在单个请求里的处理逻辑,简化任务,大大降低请求的复杂度。例如当既需要访问一个 MySQL 集群,又需要访问一个 Redis 集群时,我们就可以分别创建一个子请求负责和 MySQL 的交互,另外一个负责和 Redis 的交互,简化主请求的业务复杂度。而且创建子请求的过程不涉及任何的网络 I/O,仅仅是一些内存的分配,其代价非常可控,因此在笔者看来,子请求机制是 Nginx 里最为巧妙的设计之一。

子请求创建与驱动

通常需要创建子请求时,模块开发者们可以调用函数 ngx_http_subrequest 来实现,默认情况下,子请求会共享父请求的内存池,变量缓存,下游连接和 HTTP 请求头等数据。当子请求创建完毕后,它会被挂到 r->main->posted_requests 链表上,这个链表用以保存需要延迟处理的请求(不局限于子请求)。因此子请求会在父请求本地调度完毕后得到运行的机会,这通常是子请求获得首次运行机会的手段。

我们知道 Nginx 针对一个 HTTP 请求,将其处理逻辑分别划分到了 11 个不同的阶段。当一个子请求被创建出来后,它首先运行的是 find config 阶段,即寻找一个合适的 location,然后开始后续的逻辑处理。通常,如果一个子请求不涉及任何的网络 I/O 操作,或者定时器处理,一次调度即可完成当前的子请求;而如果子请求需要处理一些网络、定时器事件,那么后续该子请求的调度,都会由这些事件来驱动,这使得它的调度和普通的主请求变得无差别。

既然除第一次外,子请求的驱动可能是由网络事件来驱动的,那么子请求的调度就是乱序的了。假设当前主请求需要向后端请求一个大小 2MB 的资源,我们通过产生两个子请求,分别获取 0-1MB 和 1MB - 2MB 的部分,然后发往下游,因为网络的不确定性,很有可能后者(1MB - 2MB)先获取到并往下游传输。那么此时下游所得到的数据就成了脏数据了。

为了解决这个问题,Nginx 为子请求机制引入了另外一个称为 postpone_filter 的模块。该模块的目的在于,判断当前准备发送数据的请求,是否是“活跃的”,如果当前请求不是“活跃”的,则它期望发送的数据会被暂时保存起来,直到某一刻它“活跃”了,才能将这些数据发往下游。

怎么判断一个请求是否是“活跃”的?我们需要先了解父、子请求之间的保存形式。对于当前请求,它的子请求以链表的方式被维护起来,而前面提到,子请求也可以创建子请求,因此这些请求间完整的保存形式可以理解成一颗分层树,如下图所示。

 

上图中,每个红圈表示一个请求,每一层的请求分别是上一层请求的子请求。从树遍历的角度讲,在这样一棵树上,哪个节点应该最先被处理?结合子请求机制的实际意义来分析,子请求是为了分摊父请求的处理逻辑,降低业务复杂度。换而言之,父请求是依赖于子请求的。很大程度上父请求可能需要等到当前子请求运行完毕后根据子请求反馈的结果来做一些收尾工作。所以需要采用的是类似后序遍历的规则。即上图最右下角的请求是第一个“活跃”的请求。

从源码层面来说,这颗分层树的保存用到了两个数据结构,r->postponed 和 r->parent这两个指针,遍历 r->postponed 来按序访问当前请求的子请求(树中同层的兄弟节点);遍历 r->parent 访问到父请求(树中上一层的父节点)。

postpone_filter 模块会判断当前请求是否“活跃”,如果不“活跃”,则把将要发送的数据临时拦截到它自己的 r->postponed链表上(所以这个链表上其实既有数据也有请求);如果是活跃的,则遍历它的 r->postponed 链表,要么把被临时拦截下来的数据发送出去,要么找到第一个子请求,将其标记为 “活跃”,然后返回。等到该子请求处理结束,重新将其父请求标记为“活跃”,这样一来,当父请求再一次运行到 postpone_filter 模块的时候,又可以遍历 r->postponed 链表,循环往复直到所有请求或者数据处理完毕。感兴趣的同学可以自行阅读相关源码(http://hg.nginx.org/nginx/file/tip/src/http/ngx_http_postpone_filter_module.c)。

使用了子请求机制的模块

目前整个 Nginx 生态圈,有很多使用子请求的例子,最著名的便是 ngx_lua 的子请求和 Nginx 官方的 slice_filter 模块了。

ngx_lua 提供给用户的 API (ngx.location.capture)灵活性非常大。 包括针对是否共享变量也可自行选择。特别地,ngx_lua 的子请求运行时,会阻塞父请求(挂起其对应的 Lua 协程)。直到子请求运行完毕,子请求的响应头、响应体(所以如果响应体比较大,则会消耗很多内存)等信息都会返回给父请求。ngx_lua 的子请求是不经过 postpone_filter模块的,它在一个较早的 filter 模块(ngx_http_lua_capture_filter) 里就完成了对子请求响应体的拦截。

Nginx 官方提供的 slice_filter模块,可以将一个资源下载,拆分成若干个 HTTP Range 请求,这样做最大的好处是分散热点。这个模块允许我们设置一个指令 slice_size,用以设置后续 Range 请求的区间大小。该模块会陆续创建子请求(在前一个完成后),直到所需资源下载完毕。

另外, Nginx/1.13.1 也引入了一个称为 Background subrequests 的机制(用以更新缓存)。基于这个机制,Nginx/1.13.4 引入了一个 mirror 模块,通过创建子请求,可以让用户自定义一些后台任务。比如预热一些资源,直接将它们放入 Nginx 自身的 proxy_cache 缓存中。

陷阱与缺陷

前文说到,子请求创建出来时,复用了父请求的一些数据,这无形中引入了一些坑点。

比如变量缓存,如果在子请求中访问并缓存了某个变量,当后续在父请求中使用时,我们就会得到之前的缓存数据,这可能造成工程师们花费大量的时间和精力去调试这个问题。

另外笔者认为一个非常重大的缺陷是,子请求复用了父请求的内存池,以 slice_filter 模块举例,它将一个 HTTP 请求划分成若干个的子请求,每个子请求向后端发起 HTTP Range 请求,在资源非常大 ,而配置的 slice_size 相对比较小的时候,会造成有大量的子请求的创建,整个资源下载过程可能会持续很长一段时间,这导致父请求的内存池在一段时间内没有释放,加之如果并发数比较大,可能会造成进程内存使用率变得很高,严重时可能会 OOM,影响到服务。因此在考虑使用的时候,需要权衡这些问题,有必要的话可能需要自行修改源码,以满足业务上的需要。

虽然一些缺点是在所难免的,但是子请求机制很大程度上简化了请求的处理逻辑,它分而治之的处理思想非常值得我们去学习和借鉴,无论如何,子请求机制也将是后续进行系统设计时的一大参考范例。

《我眼中的 Nginx》系列:

我眼中的 Nginx(一):Nginx 和位运算
我眼中的 Nginx(二):HTTP/2 dynamic table size update
我眼中的 Nginx(三):Nginx 变量和变量插值​
我眼中的 Nginx(四):是什么让你的 Nginx 服务退出这么慢?

我眼中的 Nginx(五):Nginx — 子请求设计之道的更多相关文章

  1. nginx源代码分析--读请求主体(1)

    首先,读取请求体已进入HTTP要求11相,我们需要做的请求正文部分处理一些模块,所以这个模块需要注册功能在这个阶段,在阅读功能要求的身体ngx_http_read_client_request_bod ...

  2. nginx限制请求之三:Nginx+Lua+Redis 对请求进行限制

    相关文章: <高可用服务设计之二:Rate limiting 限流与降级> <nginx限制请求之一:(ngx_http_limit_conn_module)模块> <n ...

  3. 微信内嵌浏览器sessionid丢失问题,nginx ip_hash将所有请求转发到一台机器

    现象微信中打开网页,图形验证码填写后,经常提示错误,即使填写正确也会提示错误,并且是间歇性出现. 系统前期,用户使用主要集中在pc浏览器中,一直没有出现这样的问题.近期有部分用户是在微信中访问的,才出 ...

  4. Nginx+Lua+Redis 对请求进行限制

    Nginx+Lua+Redis 对请求进行限制 一.概述 需求:所有访问/myapi/**的请求必须是POST请求,而且根据请求参数过滤不符合规则的非法请求(黑名单), 这些请求一律不转发到后端服务器 ...

  5. 利用nginx 虚拟主机、请求转发实现不同端口web访问

    一个服务器上挂一个网站实在是有点浪费:一个服务器上可以放多个网站:可以开启nginx的虚拟主机功能:利用访问的路径或者域名不同访问不同的文件夹:例如: 1.一台服务器上放多个网站使用nginx的配置文 ...

  6. 记一次nginx强制将https请求重定向http

    公司要做小程序,但是发现小程序只允许https请求 所以查了查资料使用nginx重定向请求得方式做 以下是过程: 阿里云ssl证书管理控制台申请ssl证书 下载nginx 证书: 解压后得到后缀为ke ...

  7. nginx反向代理导致请求header头信息丢失

    背景:前端与后端调试接口,后端拿不到前段发过去的请求头信息,导致接口不通.(但是在本地是可以拿到的) 原因:nginx做了反向代理,没有请求时候加头信息的配置 报错如下: 解决方法: 方法一:NGIN ...

  8. Nginx 流量带宽等请求状态统计( ngx_req_status)

    Nginx 流量带宽等请求状态统计 ( ngx_req_status)  插件下载地址: wget http://nginx.org/download/nginx-1.4.2.tar.gz git c ...

  9. Nginx教程(五) Nginx配置文件详解

    一. Nginx配置文件nginx.conf中文详解 #定义Nginx运行的用户和用户组 user www www; #nginx进程数,建议设置为等于CPU总核心数. worker_processe ...

随机推荐

  1. PHP中的 $_SERVER 函数说明详解

    用php在开发软件的时候,我们经常用到 $_SERVER[]这个函数,今天就来讲下这个数组的值,方便以后使用: A: $_SERVER['ARGC'] #包含传递给程序的 命令行参数的个数(如果运行在 ...

  2. 基于DP的LCS(最长公共子序列)问题

    最长公共子序列,即给出两个序列,给出最长的公共序列,例如: 序列1 understand 序列2 underground 最长公共序列undernd,长度为7 一般这类问题很适合使用动态规划,其动态规 ...

  3. vue组件的生命周期

    先来张组件生命周期的示意图: 文档里是这样描述的:你不需要立马弄明白所有的东西,不过以后它会有帮助.传送门. Vue2.0的生命周期钩子一共有10个,同样结合官方文档作出了下表 生命周期钩子 详细 b ...

  4. Java面试官最常问的volatile关键字

    在Java相关的职位面试中,很多Java面试官都喜欢考察应聘者对Java并发的了解程度,以volatile关键字为切入点,往往会问到底,Java内存模型(JMM)和Java并发编程的一些特点都会被牵扯 ...

  5. idea快捷

    IntelliJ Idea 常用快捷键 列表(实战终极总结!!!!) 1. -----------自动代码-------- 常用的有fori/sout/psvm+Tab即可生成循环.System.ou ...

  6. 【Web】一个非常简单的移动web消息框

    适用:h5+jquery,移动网页最佳 最近在写个简单的公众号页面,前端验证时有些信息要提示,很简单的需求实在不想找啥现成的轮子,又不至于用alert这么粗暴,遂写了个非常简单的消息框,效果如图: 特 ...

  7. 聊聊 Spring Boot 2.x 那些事儿

    本文目录: 即将的 Spring 2.0 - Spring 2.0 是什么 - 开发环境和 IDE - 使用 Spring Initializr 快速入门 Starter 组件 - Web:REST ...

  8. 一个js小游戏----总结

    花了大概一天左右的功夫实现了一个js小游戏的基本功能,类似于“雷电”那样的小游戏,实现了随即怪物发生器,碰撞检测,运动等等都实现了,下一个功能是子弹轨迹,还有其他一些扩展功能,没有用库,也没有用web ...

  9. Python爬虫利器五之Selenium的用法

    1.简介 Selenium 是什么?一句话,自动化测试工具.它支持各种浏览器,包括 Chrome,Safari,Firefox 等主流界面式浏览器,如果你在这些浏览器里面安装一个 Selenium 的 ...

  10. Python简介之输入和输出

    输出 输入 输出 用print()在括号中加上字符串就可以向屏幕上输出指定的文字.比如输出'hello,world!',用代码实现如下:print('hello world!'). print()函数 ...