ITPub博客

首页 > > > N年老司机的一次探索--NGX构架改动的干货手动奉上!

N年老司机的一次探索--NGX构架改动的干货手动奉上!

作者:u_453e1a12781dea 时间:2017-07-28 18:52:54 0 删除 编辑
点击蓝色字关注"CU技术社区" 一起玩耍哦~


1本文目的


截止目前,NGX和BGN融合后在线上运行稳定,尚未发现重大问题。


本文分析和总结NGX和BGN融合的原因、遇到的问题以及解决途径,希望对基于NGX开发的同仁们有所启发和借鉴。


业内尚未发现类似NGX和BGN融合的先例,因此我们的技术路线是一次探索,未必适合您的胃口;同时,我们的技术路线也未必符合主流NGX社区的思路,我们修改了NGX,但是又希望以最小的代价修改、最大的可能性扩展NGX,可能会伤到纯粹派的NGX拥护者。对此,我们深表歉意,但请您尊重我们的选择。 


我们在LUA上做了多年的业务开发,使用了章亦春先生的NGX LUA模块。在NGX和BGN融合过程中,我们对NGX LUA中的部分接口做了使用上的限制、甚至修改、替换,由于这些变更并非纯粹针对NGX LUA的改良,更确切地说,是为了适配的目的,因此无法提交给社区,敬请章亦春先生谅解,同时,我们对NGX LUA的认识可能会引起章亦春先生及社区的不适,也一并请求谅解,我们所做的一切都是希望从业务、架构、性能角度来审视技术路线。


在NGX和BGN融合过程中,曾经出现巨大的障碍,我们无法跨过NGX LUA的协程调度引起的异常core dump。在这里我们要特别鸣谢今日头条的黄龙先生,通过对LuaJIT源码的分析,最终从原理上指引我们走上正确的解决之道。


2 名词解释


NGX:即nginx,本文中简写。


BGN:这是本文作者开发的一个分布式/并行计算平台,以任务的形式驱动,每个任务可以包含一个或多个子任务,每个子任务看作是一次远程函数调用,核心思想是分布式模块概念,任务在模块与模块之间、非模块向模块传递。目前维护的是协程版。NGX与BGN融合过程中,主要用到了BGN的协程池机制和BGN的基础框架,对任务以及私有通讯协议涉及很少。


3 融合的目的


NGX和BGN融合的目的是为了充分发挥NGX和BGN各自的优势、搭建一套便于开发的高性能基础服务平台。


从降低开发成本、特别是业务逻辑实现成本的角度来说,章亦春先生的NGX LUA模块已经完全解决了,为什么我们还要做NGX和BGN的融合呢?


答案就在于“高性能”,这是核心痛点。举例:NGX + LUA后,随便set一个变量,性能较NGX下降10倍。线上众多的高性能物理服务器,不能提供令人满意的性能指标。


分析业务逻辑、LUA流程和NGX LUA协程,可以看到LUA的部分瓶颈:

  1. LUA是脚本语言,性能比原生NGX低

  2. 从NGX流程进入LUA脚本执行,整个“进入”过程长

  3. 变量的get/set操作,导致频繁查表、频繁在NGX和LUA之间穿梭

  4. 数据从NGX进LUA,以及从LUA进NGX,伴随着内存拷贝

  5. LUA脚本的灵活性和业务的复杂性,放纵了开发者,导致代码的执行效率低

  6. NGX LUA协程处于流程“中间”位置,导致暴露给LUA的接口存在性能上的缺陷


关于第(6)点举例,NGX LUA没有暴露epoll事件的接口,在处理网络IO时,不能做到被动式,即事件触发再去处理,只能通过timer事件轮询,这是性能低的重要原因之一。理论上,通过回调LUA以及修改NGX LUA应该可以做到,但代价太大,LUA业务逻辑要比现在复杂太多,可行性受到质疑。反观NGX和BGN融合,将整个局部流程,比如HTTP回源、与存储的交互、DNS解析等整体封装进BGN,对LUA暴露一个接口,调用接口时流程挂起,直到交互完成再返回的方式则简单、清晰很多。


不可否认,基于LUA的开发带来的便捷性,是NGX原生开发所不能比拟的,但对于CDN Cache这样的业务场景而言,性能损耗的累积带来的麻烦不容忽视。我们无意完全去掉LUA,但是有必要对LUA瘦身。在平衡开发效率和代码执行效率过程中,一个基本的思想是“微服务”:将流程控制交给LUA,将耗时的、逻辑不经常发生变化的通用部分交给C,并提供LUA API,供LUA脚本中调用。


微服务提供LUA API,最直接的方式就是在NGX LUA模块中实现,但这些微服务本身逻辑和LUA没有任何关系,从解耦的角度来说,不应该放在NGX LUA模块中。


微服务可以放在NGX中实现,以符合NGX模块理念的形式存在,但NGX的开发并不容易,诸多限制。微服务放在NGX中实现,不是最自由的选择。


那么微服务放在哪里实现呢? BGN具备实现微服务的全部技术基础,那么,把LUA作为胶水语言,在同一个nginx worker进程内粘合NGX和BGN,看起来是个不错的选择。


还有一个思路,将微服务彻底独立出来,成为一个真正的server,这需要将业务的逻辑一起打包带走,也是个不错的选择,由于增加通讯开销,性能上有所欠缺。


综合考虑,还是决定尝试NGX和BGN的融合。

 

4 融合的技术路线


NGX和BGN分别是两套独立的系统、框架,放在同一个进程里,就需要有一个彼此都认同的东西来统一。我们的选择是:用协程统一。


原生的NGX是事件驱动模型,没有协程,章亦春先生的NGX LUA模块第一次引入协程,进而引入LUA脚本。


那么,为什么不直接动用NGX LUA模块中的协程技术呢?这主要是因为在NGX体系中所处的位置不同:NGX LUA中的协程(也称为LUA线程)是在HTTP请求中使用的,而我们要求的是整个NGX的运行由协程负责,也就是说,我们需要通过协程在顶层统一NGX和BGN。


为什么会选择协程的方式呢?

  1. BGN是协程方式,所有执行和调度都由协程处理,协程池采用master/slave模型;

  2. NGX是事件驱动,将事件的执行交给协程,是举手之劳,顺理成章;

  3. 在单个进程中,协程是理想的选择:无锁,就绪、执行、挂起、取消状态转移清晰可控,拥有独立的函数堆栈空间(依赖协程实现),无阻塞条件下执行效率高,并发度高。


NGX的事件主要分为三种:

4.1 epoll事件

特别地,我们考虑网络IO事件。NGX系统运转的初始动能来源于一次网络IO事件:HTTP请求的接入。

epoll_wait获取监听端口的读事件后,将该事件交给某个协程执行,完成请求的接入。


4.2 timer事件

在HTTP处理过程中,经常有“过一段时间再看看”的需求,此时NGX流程抛出timer事件,超时后被触发。被触发时,Timer事件被提交给某个协程执行。


4.3 post事件

在HTTP处理过程中,如果需要分心去做某件事,NGX流程就会抛出post事件,排队等待执行。在NGX主循环中,会依次执行队列中的post事件,此时将post事件加载到某个协程执行。


NGX是典型的“流式处理”,即整个HTTP的处理流程被切成一小块一小块的,称之为“split phase”(注:split phase和NGX的11个处理阶段没有半毛钱关系)。一个小块的处理过程中,会准备好下一个小块的handler,处理过程中有异常、有不同流程的选择,所以handler会非常丰富。当一个小块处理完成后,“流式”地进入下一个小块的处理过程。如果流程不能这么“任性”地“流”下去,怎么办?比如回源、访问外部资源、等待某个条件具备等。如果是等待外部资源就位,那好办,把下一个小块的处理挂在网络IO事件后即可;如果不需要网络交互、仅仅等待某个条件具备,NGX其实没有什么好手段,它只能通过timer事件轮询。


所以,NGX的策略就是尽可能将一切转化为网络IO事件,实在不行就轮询!每个小块的handler构成了一张纵横交错的逻辑网,怪复杂的。


这就是“流式处理”的毛病:复杂度太高。


NGX LUA很好地解决了这个问题:将异步回调转化为顺序调用的形式。那么,NGX LUA是如何做到在顺序执行的过程中,分身去做另一件事,然后回来继续顺序执行的呢?就是挂起!通过lua的yield切换出去,再通过resume切换回来。切换出去意味着当前流程被挂起,切换回来意味着该流程又可以恢复执行。


BGN从诞生第一天起就是为了实现这个目标:将异步转为为顺序,让并行与分布更自然。


异步转化为顺序,流程挂起,给开发人员卸下了沉重的负担。这是NGX LUA、BGN带来的红利,协程的挂起能力赋予了流程的挂起能力。


理想的方案应该是:一个HTTP请求的完整生命周期都发生在同一个协程中,通过流程的挂起完成整个请求的处理过程。可惜事实上做不到,NGX的流式处理,抛出事件的这种玩法,限制了理想的实现,我们需要向NGX妥协。


NGX和BGN作为独立的系统,分别有自己的主循环,我们将BGN的主循环嵌入到NGX主循环中,形成一个更大的、唯一的主循环。


NGX和BGN都可以做为独立的服务器,有自己的epoll机制,我们保持它们的独立性,承担正交的网络IO功能。


综上种种,用一句话来概括NGX和BGN融合的技术路线:


用协程在顶层统一NGX和BGN,形成双epoll的服务器,使NGX具备流程挂起能力。


5 融合的技术障碍


在NGX和BGN融合过程中,或者说我们用协程“改造”NGX的过程中,遇到过各种各样的障碍,根本的原因有两个:(1)NGX的流式处理不会考虑事件被调度时的中间态 (2)NGX LUA的协程干扰。


举例:

在NGX中,如果一个事件被执行,执行方式形如:

ev->handler(ev)

一行代码搞定。

在融合环境下,事件的执行要分三步走:

S1. 分配一个协程给handler

S2. 协程排队,等待被调度执行

S3. 协程被调度,执行handler,即ev->handler(ev)


中间态在事件的状态迁移中必须被小心对待,留意时序的不确定性。幸运的是,NGX代码的实现相当优异,思路清晰,本身也是按照事件的“并行”处理设计的,因此解决起来问题不大。


对于NGX LUA的问题,就没那么幸运了:LUA协程(线程)的切换极有可能从一个BGN协程切到了另一个BGN协程,这是绝对不允许出现的。为什么呢? LUA协程和BGN协程实现不同:

(1)LUA协程的堆栈严格意义上来说,它不会保存当前的函数堆栈,比如参数表、局部变量、当前指令位置等,它仅保存了一个函数返回地址(注:很像流式处理,对不对?),这可以从切出函数lua_yield的调用点看出来,总是发生在return处,如

return lua_yield(L, 0);

这不是巧合,是LUA协程被设计成必须这么干: LUA协程的函数调用堆栈,形象地说,存的是回调,而不是类似C语言中的函数堆栈空间,这是LUA协程要支持不同平台和语言做出的抽象与牺牲。


而BGN的协程则保留着完整的函数调用堆栈信息,可以从任意点切出,切回来恢复继续往下执行。


 (2)BGN的协程处于顶端,意味着每个LUA协程(线程)的运行,必然发生在某个BGN协程中(沙箱)。如果从一个LUA协程切换到另一个LUA协程时,是从一个BGN协程跳进了另一个BGN协程,就会导致BGN协程的函数调用堆栈被破坏,引起core dump。


好在章亦春先生的NGX LUA的LUA协程切换一般发生在同一个HTTP请求的多个LUA协程之间,这给NGX和BGN融合、同时兼容NGX LUA带来了一线生机。应该说,章亦春先生的NGX LUA在设计、组织和调度LUA协程时,是非常合理的。


我们要付出的代价是,改造NGX LUA,使LUA协程的切换限定在同一个BGN协程内;如果做不到,就重构NGX LUA接口;如果还做不到,对不起,那就禁止使用该NGX LUA接口。


代价相当惨烈。初期NGX和BGN融合后的不稳定因素,全部与NGX LUA有关。最安全的做法是,所有在LUA脚本中会发生异步行为的接口,统统换成BGN的接口,由BGN负责异步处理,LUA脚本中严格使用最基本的语法和接口,创建用户线程啦、异步socket通讯等高端玩法,全部废止,这可以从根上断掉LUA协程的调度。


这种削足适履的做法,是无奈的选择。理想的做法是:NGX和BGN直接交互,不带LUA玩。


您能想象NGX中HTTP请求和NGX LUA中的HTTP请求是貌合神离的吗?

根据配置,在HTTP请求处理的某个阶段,需要交给LUA脚本处理时,NGX LUA就会创建一个fake request。NGX LUA为了与NGX和谐相处,并没有改动过NGX代码和结构,严格遵循NGX模块开发理念,因此,我们可以从逻辑上审视request和fake request的关系: fake request可以找到request,反过来request并不知道fake request的存在。


这种单向的关系导致一个问题:当NGX走关断流程,释放request及connection时,NGX LUA并不知道,也即,不能及时撤销掉fake request及LUA协程。在NGX日志中会看到这样的现象:客户端已断链,LUA脚本还在执行,并试图向客户端发送数据。


这个问题对NGX LUA不是个事,因为在后面的LUA协程处理中,发现异常后就地卧倒,等NGX LUA调度器过来收尸。


但是在BGN协程接管NGX事件处理后,问题变得严重: 假如lua脚本中发起一个sleep(ngx.sleep),NGX LUA会创建一个LUA协程(线程)。在超时前,客户端断链,timer事件没有被取消;超时后,timer事件被BGN协程加载执行,访问request相关信息时,直接崩掉(此时NGX LUA还没有发现客户端断链,时序上timer事件被先执行了)。


咨询社区,得到如下回复,

 

根据社区回复,在客户端断链后,相应的LUA协程不会被撤销,而是通过注册abort回调lua,也就是说回到LUA协程进行abort操作。


但是,我们的期望是不进LUA协程,而是站在BGN协程的角度来取消掉LUA协程,包括timer事件。于是我们在NGX LUA模块中加了一行清场的代码,也许不是最好的解决办法,但稳定了局面。


LUA堆栈则是一个不能彻底解决的障碍。


LUA的堆栈主要是用来在LUA和其它语言(这里是C语言)之间交换数据的。对于回源、与后端存储的交互,NGX拿到数据后,通过堆栈拷贝给LUA,发送数据时,又通过堆栈拷贝给NGX。业务逻辑处理过程中的LUA变量,同样会占用堆栈。


LUA堆栈大小固定,LUA自动回收机制GC效率较差,而主动调用GC回收,执行效率又太差。内存占用超过堆栈上限,引起LUA崩溃,业务就受到影响了。


控制LUA内存量,有严格控制变量的生命周期、显式将无用的变量置空、控制数据收发的流量等措施,但只能减缓,不能彻底、完美解决。还有一个取巧的办法:不定时reload一下,推倒重来。


6 合并回源


NGX和BGN融合后,可以充分利用流程的挂起能力、BGN开发的便捷性,提供更复杂、更强大的功能。


合并回源就是一例。先提供流程示意图:


几个名词解释:

CLIENT:客户端,一般是普通网民

RFS:一种随机访问存储系统

ORIG:源站


特别提示:

  1. 图中CLIENT、NGX、BGN、RFS、ORIG是五种角色,而不是五个实例。在系统运行中,每种角色对应的实例有多个,实例可能在同一个进程,也可能在不同进程;可能在同一台物理服务器,也可能在不同物理服务器上。

  2. 示意图并非回源流程的全貌,而是一个切图,也不涵盖全部的交互过程,比如异常流程等,只能保证示意完整。

  3. 在融合的NGX和BGN中,NGX和BGN两者之间并未直接交互,而是通过LUA实现交互的,因此图中的确是示意图。实际数据流如下:

 

  1. 示意图中有红、蓝两个回源的流程,但经过合并后,只有蓝色流程在回源


来看看整个流程。

在某个相近的时间点上,两个客户端向NGX请求尚未缓存的资源,触发NGX的回源流程。


NGX选择合并回源流程,要求BGN遵照执行。


BGN向RFS发出对即将回源的资源上锁的指令,RFS对第一个到达的指令返回TOKEN,对后续到达的指令则不返回TOKEN。


BGN侧获得TOKEN的流程(蓝方)有权发起回源请求,在回源前,把自己登记到RFS,告诉RFS:我正在等待该资源,资源就位后记得通知我。


BGN侧未获得TOKEN的流程(红方),把自己登记到RFS,告诉RFS:我正在等待该资源,资源就位后记得通知我。


BGN侧蓝方回源,获取资源后,将资源存进RFS。RFS将资源存储后,通知所有正在等待该资源的各方,过来取资源。


BGN侧蓝方和红方分别去RFS取资源。


BGN侧蓝方获取资源后,向RFS侧发出解锁指令,然后向NGX侧返回合并回源结果。

BGN侧红方获取资源后,向NGX侧返回合并回源结果。


NGX向客户端返回资源。流程结束。


合并回源流程有如下看点:

  1. 存储是最清楚资源是否已经缓存的,因此让存储参与到流程控制中是合理的。但是,如果存储不具备这种“参与”能力,怎么办?那就需要额外的角色来承担,如此一来,流程更长。

  2. 蓝方和红方在等待存储的资源就绪通知的过程中,流程被挂起,这是BGN协程赋予的能力。

  3. 存储在资源就绪时,通知等待各方,这是存储具备的反向通知能力。

  4. BGN接收并处理存储的资源就绪通知,这是融合后BGN的epoll赋予给NGX的能力:NGX的worker进程从“各向同性”被改造成“各向异性”,每个worker进程拥有独立的服务器(端口)。原生NGX从master进程继承全部服务器,worker进程无差异,请求打到任何一个worker都可以。BGN可以让每个worker进程具备独一无二的身份,从而支持反向通知处理能力。

如果保持NGX的worker进程的“各向同性”,能不能获得支持反向通知处理的能力呢?能!长连接!需要权衡为长连接付出的代价。


  1. 从BGN出发,所有与存储、源站的网络交互,走的是BGN的epoll。


从以上可以看到,BGN就是NGX的强大外挂。


7 融合后前景展望


为了融合NGX和BGN,我们对NGX的顶层架构进行了修改,这种修改是无害的、不会伤及NGX的处理流程、不会影响NGX版本升级;对NGX LUA的修改则是有害的,限制了LUA一些高级接口的使用,但所有LUA失去的接口功能,都可以从BGN中找回来,我们只需要克制欲望,使用基础的、无害的LUA接口即可。


目前,BGN的私有协议并未开启。私有协议的通讯效率要高于HTTP:私有协议可以在同一条连接上不断收发,没有请求-响应-请求-响应-请求这样的顺序要求,BGN内部自动匹配响应和请求。


BGN的任务驱动机制完善,提供比NGX的request和sub-request更强大的并行处理和分布式处理能力。


BGN有高效的内存块管理机制,功能在BGN实现,可以脱离NGX的生命周期结束再回收内存、以及LUA的GC机制,内存占用量控制很好,内存使用效率更高。


BGN有完善的(分布式)模块机制,完胜NGX的模块机制。


BGN的开发模式简单、成熟,基础设施完善,开发效率虽不如LUA,但远胜NGX,不涉及NGX原生流程的变动。


如果NGX和BGN直接交互,性能更好。




对文章思路方法有问题

欢迎在留言区评论~

我们会联系作者答疑解惑

周末愉快~


SACC2017

SACC2017

作为国内最受欢迎的架构师盛会,2017 第九届中国系统架构师大会 (SACC) 将于将于 2017 年 10 月 19-21 日在北京新云南皇冠假日酒店震撼来袭。


大会以 “云智未来” 为主题,云集国内外顶级专家,围绕云计算、人工智能、大数据、移动互联网、产业应用等热点领域展开技术探讨与交流。本届大会共设置 2 大主会场,18 个技术专场;邀请来自互联网、金融、制造业、电商等多个领域,100 余位技术专家及行业领袖来分享他们的经验;并将吸引 4000 + 人次的系统运维、架构师及 IT 决策人士参会,为他们提供最具价值的交流平台。

点击阅读原文立享6.8折购票优惠~

原文作者:;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2019-07-17

  • 博文量
    1982
  • 访问量
    17885