编程是门手艺,NGINX社区的经验分享 一文提过,专业的程序员擅长整体设计和细节处理能力。本文探讨整体设计,尤其是模块化这个技能。全能天才,Fabrice BellardFFmpeg,最强大的流媒体库 QEMU,硬件虚拟化的虚拟机 TCC,迷你CC编译器 QuickJS,100%支持JS语法的C引擎 等等,以上皆出自一人之手,法国天才。 去年QuickJS曾一度刷爆技术圈,NGINX社区的哥们第一时间推荐给我看,并以天才称他。 这软件开拓了我的视野。本文以它为引子探讨我认为非常重要的技能:如何组织代码。NJS,实现语言引擎真难私下问过Fabrice Bellard(给QJS提过patch)开发QJS的历程,答案令人惊叹,他只用了两年的业余时间。参与NJS这几年,才深知实现语言引擎有多复杂。 NJS从17年开始,现在差不多完成40%。但基础已经非常良好,后续功能开发会快速很多。而且常用功能都已经支持,其中我支持了模块化,箭头函数,等常用的。语言解析引入了LL(k)。 看似做了些不错的工作。然而跟QJS比,以台球打个比方。一个长距离很准的选手,90%的球都能打进,看似很厉害。但对一个发力非常厉害的人来说,他可能只需80%的准度,再加良好的走位,就能轻松一杆清台。 提QJS不是妄自菲薄,这样对比很不公平。QJS作者本身就是个JS专家,他都能用JS实现虚拟机。参与NJS的人员,包括Igor都不是真正的JS语法行家,JS的语法着实太庞大。我们平时开发过程中,有个社区外的JS行家对我们帮助非常大,简直就是JS活字典。因此在前期,只能靠着语法手册,然后实现,有些实现跟语法的本质有出入的话,又得重头再来。举个例子,早期实现的apply和call两个语法真是让人吃尽了苦头,这也是我最早参与的,因为修复它的bug,做了重构,然后发现社区的人非常接受这种重构的做法,有种碰到知音的感觉。QuickJS,五万行代码一个文件的软件我会解释这种做法是合理的。此时必须提出来,后面再详加解释。模块化,最好的代码组织方式我在参与NJS时,第一件事就是让它支持模块化编程。NJS刚出来时我就开始关注,后面挺长一段时间,用NJS写代码只能放在一个文件里,这对代码组织是极不友好的。先看下JS的模块化用法: main.js/* 自定义模块 */import foo from ‘foo.js‘;foo.inc();/* 内置模块 */import crypto from ‘crypto‘;var h = crypto.createHash(‘md5‘);var hash = h.update(‘AB‘).digest(‘hex‘);foo.jsvar state = {count:0}function inc() {state.count++;}function get() {return state.count;}export default {inc, get}支持模块化之后,变得非常好用。这个大功能也是NGINX作者Igor亲自帮review和调整的,收获良多。客观讲,JS语法比Lua实在好用太多,NJS目前已经非常稳定,只是功能没那么繁多,推荐轻量应用考虑用NJS,而且社区非常活跃,相信未来可期。 现在轻瞥一下QuickJS的源码。JSContext *JS_NewContext(JSRuntime *rt){JSContext *ctx;ctx = JS_NewContextRaw(rt);if (!ctx)return NULL;JS_AddIntrinsicBaseObjects(ctx);JS_AddIntrinsicDate(ctx);JS_AddIntrinsicEval(ctx);JS_AddIntrinsicStringNormalize(ctx);JS_AddIntrinsicRegExp(ctx);JS_AddIntrinsicJSON(ctx);JS_AddIntrinsicProxy(ctx);JS_AddIntrinsicMapSet(ctx);JS_AddIntrinsicTypedArrays(ctx);JS_AddIntrinsicPromise(ctx);return ctx;}void *JS_GetContextOpaque(JSContext *ctx){return ctx->user_opaque;}void JS_SetContextOpaque(JSContext *ctx, void *opaque){ctx->user_opaque = opaque;}所有源代码扔进一个文件里,我看过不少软件的源码,而且是比较完整的。NGINX, Unit, NJS, Lua等,以个人感观而言,QuickJS是最好的。初看有点凌乱,但细看的话(可能需要很熟悉JS语法),绝对的大师之作。 假如想删除某个语法功能,在QuickJS里可以连续的从某行一直删除到另一行,连续的一块。这在其它软件是不可能做到的,要么多个文件都要删除,要么在一个文件也要删除多个不同的地方。我认为这就是模块化的精髓:高内聚。 学过设计原则的同学想必都知道软件要高内聚,低耦合。我的理解是只要做到了高内聚,低耦合就是自然而然的事情。 举个例子,要实现nginx lua模块。有两个重要的功能:nginx模块相关函数,lua封装相关函数。 过度设计方式:ngx_http_lua_module.c/* nginx模块相关函数 */ngx_http_lua_request.c/* lua封装相关函数 */合理方式ngx_http_lua_module.c/* nginx模块相关函数 *//* lua封装相关函数 */https://github.com/hongzhidao/nginx-lua-module/blob/master/src/ngx_http_lua_module.c 过度设计是一种很容易踩进去的陷井。 讨论1: 如果有更多的功能,比如http subrequest这种功能进来时怎么办? 建议还是放在同一个文件里,不要被代码行数影响。 讨论2: 又有更多的功能,比如http share memory这种功能进来时怎么办? 是可以考虑独立到另一个文件了,原则就是要找到一个信服的理由,新的功能能独立成一个高内聚的模块。有个特征是它往往会有专门的API,比如共享内存操作的get, set等。 换另一个角度看,一个文件的引入本身也是一种成本,而且比函数级别更高。每次的重构都应该带来实质的价值。这是我坚持尽量放同一个文件的原因。我早期提过几次建议,想对njs做类似的事情,后来证明有些是过度设计的。而有些是正确的,比如把njs_vm.c分成njs_vm.c和njs_vmcode.c。一个负责虚拟机,一个负责字节码处理。总结一下: 高内聚是最高准则。 引入新文件成本高于函数,要有实质的价值才做。 不要被代码行数影响。 协作只是一种分工,不能做为破坏高内聚的理由。 再谈设计前面说QuickJS的代码质量非常高,是因为他的设计令人折服。整个QJS的代码行数不到5万,实现了100%的语法,其中还包括非常硬核的大数和正则,都自己造轮子。从整个引擎的实现方面它就做了高度的抽象,而且用的算法非常简单有效。举个例子,JS里对象的属性操作应该是最常用的,比如 a[‘name’]。a和name在语法解析时都是字符串,术语叫token。QJS用一个非常高效的hash实现,将所有JS用到字符串的都包括进去了,代码也很少。typedef struct JSShapeProperty {uint32_t hash_next : 26; /* 0 if last in list */uint32_t flags : 6; /* JS_PROP_XXX */JSAtom atom; /* JS_ATOM_NULL = free property entry */} JSShapeProperty;struct JSShape {uint32_t prop_hash_end[0]; /* hash table of size hash_mask + 1before the start of the structure. */JSGCObjectHeader header;/* true if the shape is inserted in the shape hash table. If not,JSShape.hash is not valid */uint8_t is_hashed;/* If true, the shape may have small array index properties ‘n‘ with 0
前言关于我:NJS核心开发者,Unit贡献者,NGINX专家编程是门手艺是一个系列,分享我在编程上的思考,尤其是在NGINX社区上学到的。编程是逻辑的创造,力求清晰这是我对编程的理解,代码体现的是业务的本质,也可以说成是一种抽象。既然是逻辑,清晰是最最重要的。以Linus在Ted的例子为例:删除列表里的元素。不好的逻辑:遍历列表,找到元素entry。如果entry有没前结点prev,将头部指向entry的next;如果有则将prev的next指向entry的next。好的逻辑:遍历列表,找到元素entry的引用,将引用指向entry的next。虽然两者都能达到目的,但是明显后者更清晰,代码更简洁。不清晰的逻辑往往有更多的特殊处理,反映为代码有更多的if分支。如果你想锻炼更好的逻辑,试试将代码里的if分支优化掉。前几年NGINX作者还在NJS项目时,跟他交集比现在更多。NJS是他亲自设计和实现的纯C JS引擎,它的代码跟NGINX没任何关系。两个软件都有红黑树的实现,NJS的版本是我见过最好红黑树实现,没有之一。我还给他提过一点小改进,我个人很喜欢红黑树这个数据结构。NJS的逻辑更清晰,如果看它实现的话。这种带来的好处是它的API更易用。比较下两者的使用:ngx_rbtree_init(&cache->sh->rbtree, &cache->sh->sentinel, ngx_http_file_cache_rbtree_insert_value);ngx_rbtree_insert(&cache->sh->rbtree, &fcn->node);njs_rbtree_init(&tree, rbtree_unit_test_comparison);njs_rbtree_insert(&tree, &items[i].node);唯一的区别是rbtree_init时,njs版本更简单,nginx版本需要一个叫sentinel的哨子节点。njs的实现逻辑更清晰,将那个特殊处理优化掉了。简而言之,代码应该做到逻辑清晰。也可以理解为更进一步的抽象。良好代码的判断标准@drdrxp提过一种方式,增加行数和删除行数比例为4:1。这个非常直观有效,尤其适用于稳定的项目。对于新项目,这个比例可以提高为3:1,因为新项目往往设计考虑的不够,需要更多的重构动作。以NJS为例,目前算稳定的项目了。xeioex#1xeioex805 commits 忽略数据,有大量的文件重命名重构igorsysoev#2igorsysoev313 commits 58,714 ++ 15,181 --lexborisov#3lexborisov124 commits 29,307 ++ 11,163 -- hongzhidao#4hongzhidao108 commits 12,864 ++ 10,028 --@Igor贡献了整个软件的基础库,实现了早期的JS语法。他的代码行数大约4:1。@xeioex绝对核心,实现了大量的JS功能。@lexborisov语言解析方面的专家,作了很多的改进。@hongzhidao我是第三个参与的,实现部分JS功能,并重构一些基础库。可以看出我跟@lexborisov因为做了更多的重构,增和删的比较很高。这方式是我印象比较深的一种,特别指出来,仅供参考。重构,提升能力最实用的习惯以下是Igor在NGINX上的一个重构例子,就像做了卫生打扫一样,让房间变的更干净了。重构就是这么一种技能,在不改变原来行为的前提,通过内部的优化让代码变的更具扩展性。一定要记住,不能改变原来的行为。重构的目的是让代码更具扩展性,就是为了添加新功能更简单。重构所做的事情可以很多,从小到大有这么几种常见的:1. 细小:改错字;重命名变量、函数和文件等,让代码更具可读性。2. 逻辑优化:比如Linus前面的那个例子,让逻辑更清晰。3. 设计改进:比如NGINX到NJS的红黑树实现。重构会有整一篇幅深入研究,尤其我在社区实践来的。测试用例,不可或缺这里只讨论针对代码的测试用例。以下是我实现Lua版本radix tree写的测试用例。function test_get() local tree = radix_tree.new(); tree:insert(‘foo‘, 1); tree:insert(‘bar‘, 2); tree:insert(‘zoo‘, 3); assert(tree:get(‘bar‘) == 2, ‘get bar‘); assert(tree:get(‘noop‘) == nil, ‘get non‘);endNJS比NGINX好一点的是,基础库比如红黑树之类的都有对应的测试用例,我认为这点非常重要,也是很多项目没做好的,一旦这些实现有问题,就是大规模的雪崩。当然也有很多将私下自己写测试用例的,没有开源出来。前面提的重构,如果没有测试用例就不可能进行。任何改的代码都有可能出问题,怎么保证没问题呢,跑一遍测试用例是最好的方式。在NJS项目里,我跟另外两个经常在修bug和增加功能时,会做一些重构。有时当天发现的bug,如果紧急当天修复,然后发布。敢做这样的事,得益于我们有大量的测试用例。测试用例也会有专门的篇幅继续深入介绍。命名,很大一门学问编程有两大难点,命名和缓存。命名这看起来简单的事情,要做好非常不容易。这课题放在此系列的另一篇中。设计,真正的功夫好的设计能减少代码的实现成本。NGINX代码最复杂的部分应该是buf那块。比如响应是一个流式内容,想对这内容做过滤处理。ssi那个模块就是干这事的。C跟其它语言不一样,几乎都在跟内存打交道。NGINX有内存池,但它只能统一分配和释放。上面举例的内容你需要保存,在NGINX里存放在buf这样的地方,这个buf本身又需要占内存。因此为了复用buf,NGINX做了非常复杂的实现。反观在NJS里,它实现了动态分配的内存池,既然内存可以动态分配和回收,就不需要buf复用了,我估计如果NGINX有这样的实现,代码至少可以简化30%。我以前说过,因为大量的第三方模块,导致NGINX没法引入这样的改进,有点可惜。我在自己的nginx-lua-module里,参考了njs不少好的设计。因此,好的设计可以节省很多的实现成本,也让软件更具健壮性和扩展性。这往往取决于实施者本身的综合能力,所谓天赋。如何真正提升编程能力我认为专业的程序员体现在两方面:整体设计能力和细节处理能力。我最早给社区提交代码时,大约60%多都会被改动调整,整体设计和细节处理的不好。现在90%多都会直接合并进去,甚至不用任何改动的。所以我想实践是最好的方式,有专家帮你review那再好不过了,他会重审你的设计,改进你的细节。实践是最好的方式,接着你要找到好代码或者好项目。自己实现,然后比对,或者找高手帮你看。好的代码或项目不用多,一个就足够了。找你兴趣的,或者用我推荐的。找基础库练手,这是最简单有效的。现在很多公司面试都要先做题,还有很多人在leetcode上刷题的。其实本质都是考核你的代码能力。举个例子,以下是我实现js_import "../.././aa/bb/cc/../d.js" 这样的语法时需要的函数。voidnjs_file_dirname(const njs_str_t *path, njs_str_t *name){ const u_char *p, *end; if (path->length == 0) { goto current_dir; } p = path->start + path->length - 1; /* Stripping basename. */ while (p >= path->start && *p != ‘/‘) { p--; } end = p + 1; if (end == path->start) { goto current_dir; } /* Stripping trailing slashes. */ while (p >= path->start && *p == ‘/‘) { p--; } p++; if (p == path->start) { p = end; } name->start = path->start; name->length = p - path->start; return;current_dir: *name = njs_str_value(".");}这个例子非常好,从命名,注释粒度,API设计都有涉及。这种就非常适合练手。另一种是自己学习和写项目。推荐utopia,1千行的API网关。即将开源。我会经常分享适合练手的好代码,欢迎关注公众号。编程是门手艺,一个分享编程的系列本文是个开篇。命名、重构、测试等更多编程技能在各自的篇幅里。[nginx-lua-module] https://github.com/hongzhidao/nginx-lua-module[utopia] https://github.com/hongzhidao/utopia
我的职业生涯大部分时间都在跟NGINX打交道,有足够的经验分享整个NGINX开发史的演进。本文以事后诸葛的角度揭示怎么形成现在这个生态。 此图展示了现如今活跃在NGINX生态的重要开源模块和产品。 一切从C开始 NGINX是纯C实现的软件,源码质量很高。即使不从事NGINX的人也可以将它作为很好的学习软件。作者Igor很早就有支持脚本语言的意图。所以问题就变成了C如何跟脚本语言引擎的结合了。这些主流脚本语言python, php, v8(js), perl, lua都有C的API,考虑到两方面:轻量级和性能,perl成为了当时的NGINX首选。甚至他还实现了一个迷你的SSI模块,自定义的脚本功能。但是从现在看,个人觉得Lua是和C交互最好的语言,天生为C设计的脚本语言,足够轻量,足够快。@agentzh将Lua引入NGINX,怎么看都是正确和极具工程实用的选择。估计NGINX社区也没料到Lua模块能如此成功。但是呢,Igor一直有脚本语言的情节,于是在2015年NJS诞生了,也是纯C的JS引擎。Lua天生能跟C交互,但不意味着它能天生跟服务器软件交互。这里最大的问题是语言虚拟机的实现。简单讲,一个请求会有个vm,非常轻量级,不用担心。对NGINX的每个请求,Lua的vm虽然做了些处理,但还是可以相互影响,如果你想做的话。但是Lua有GC,这也让这问题变的不是很严重。Igor的NJS是专门为NGINX量身定制的JS引擎,还考虑到JS有如此大的用户群体。NJS的每个请求的vm都是相互独立的,不会有任何影响,但是它没有GC。早期GC是列入NJS的计划的,但现在已经变的很遥远了,实现成本太大。这个在 HTTP请求也不是问题,NGINX对每个请求都是统一分配和销毁的。如果用户想在配置上增强NGINX,比如鉴权,NJS是个很好的选择,这也是它的设计初衷。而且NJS通过NGINX的subrequest能跟第三方交互,我们(我是NJS的开发者)最近还打算支持内置的HTTP库。所以想做应用的同学,NJS是个值得入手的选择。 模块化,鱼与熊掌NGINX的模块化机制从第一个版本就有了,但是当时Igor并不是为了第三方考虑的,只是为了方便自己的开发。用NGINX的人大都会为它的模块化机制感到惊叹,谁都可以不用改NGINX源码,只需加入自己的模块,以满足自己的需求。Lua就是最好的例子。 这问题也让Igor很头大,为什么呢?大量的第三方模块,质量参差不齐,它们严重依赖NGINX的API。NGINX是20年前的软件,当时的服务器架构跟如今已经不可同日而语。软件需要进化,就要做重构,但是API不能轻易改。关注NGINX社区的人知道,Igor亲自设计了另一个跟NGINX不同的软件Unit,这软件不会再支持模块化了,这是他们的选择。 所以从短期看,NGINX的模块化让它快速建立了整个生态。从长期看,整个生态也束缚在它的架构上。NGINX最大的问题不具备热加载,这种现在主流软件里已经不是问题,反而在它这里变成很棘手的问题,好在很多以Lua为主的应用可以解决这个问题。 OpenRestyNGINX的C实现和模块化机制,让Lua的引入变得顺其自然。虽然也有其它的语言已经支持,但如今证明只有Lua玩转的最好,或者说OpenResty做的最成功。严格来说,OpenResty里的Lua不能叫Lua。它是LuaJIT,以Lua5.1语法为主的另一个分支,而且看着没有跟进官方Lua的计划。Lua5.1以后的版本在语法上有了不少的改进,类似位运算,性能更是如此。对单纯使用Lua而言,非常推荐Lua5.4。OpenResty让很多开发者大幅提升了开发生产力,并且在它上面衍生了不少开源软件,尤其在API方面,比如Kong和APISIX。很多公司也有内部的自研尝试。 看懂设计不管你是多么熟悉NGINX源码的开发者,还是只是想用它作为应用服务器。脚本语言都是其中的选择之一。从两方面看NGINX:通用功能和业务功能。通用功能:将它扔进NGINX里,如果你能做模块开发,这点尤为重要。它意味着你将享受未来稳定和维护的红利。举个实际例子,我们在开发NJS里,有个querystring的功能,开发需要一定的成本,用JS语言来写会简单很多,但是我们依然选择将它放在JS引擎里。个人觉得类似Lua里的http request这种库,如果原生Lua模块里支持是再好不过了。对NJS,我们会选择放在js模块里,用户可以直接使用,而不用再引入任何库。还有不少的能用功能,比如常用工具函数md5,sha2之类的。业务功能:这个不用多说,维护好业务模块就行了。从整体看,Lua既封装了NGINX的HTTP请求,也提供了独立于请求的功能,比如timer(定时器)和cosocket(跟第三方交互的基础机制)。不管什么模块,也都是基于这两方面进行设计的。我一直推进NJS在这方面的能力,因为目前NJS只能处理请求,但即将引入内置的HTTP库,完全独立于请求的。重复一遍:请求和非请求。我自己的模块,也一直遵守这样的设计,https://github.com/hongzhidao/nginx-lua-module 给开发者的建议如果想学习NGINX,参考这篇 如何高效的学习NGINX对程序员,长期保持自己的竞争力,自研能力是很重要的技能之一。 用Lua作为开发的,不妨自己写个框架,如今API框架是非常好的练手对象。 来乌邦托的世界基于nginx-lua-module的API框架,一千行左右。find . -type f -name ‘*.lua‘ -exec wc -l {} + 109 ./balancer.lua 311 ./conf.lua 78 ./etcd.lua 286 ./http.lua 160 ./jwt.lua 214 ./router.lua 1158 total utopia架构图 即将开源,Star可以提前发源码[utopia] https://github.com/hongzhidao/utopia 感谢好友xp取寓,分布式研究小院微信公众号:gh_d5b90ac0d668
俄罗斯人Igor Sysoev于2004年创建了NGINX并将其开源。如今全球超过25%的网站选择NGINX作为web服务器。尤其对高并发网站来说,NGINX已成为首选。本文将全面探讨整个NGINX的生态。 时间线 NGINX自1995年Apache横空出世以来,几乎成为web服务器的标配,一家独大。系统管理员出身的Igor Syeoev却创建了他强烈个人风格的开源软件。核心代码行数小到5万行不到。 Lua 这里我特别把lua标出来,是因为如今Lua已经成为NGINX的标配,得益于OpenResty。Lua是巴西3个有明显数学特点的人创建的小型语言。时至今日,可以说Lua是与C语言结合最好的语言,没有之一。Lua5.1在其发展史上是一个非常重要的分水岭,但是其性能跟后面的LuaJIT和官方的Lua5.4比,有一定的差距。这也是为什么会有LuaJIT的出现。但是LuaJIT只支持Lua5.1的语言,看着也没有跟进后面版本的计划,这就变成了另一个完全独立的东西,类似tengine之于NGINX。GC一直是脚本语言最重要的特性,Lua5.2, 5.3, 5.4的GC实现一直在进化。国内的Lua骨灰级专家@云风,对Lua5.4有不错的反馈。这个版本应该也是Lua性能最好的一个版本了。 Tengine阿里在NGINX的发展上做出了非常独特的贡献。虽然到了2012年NGINX才真正有了自己的商业公司,但是在2015年之前,相对于如今应用需求这么丰富的背影下,NGINX本身的功能还是偏弱的。在2011年,Tengine fork了NGINX,明显在运维使用方面做了自己独有的改进。 OpenResty 2012年,@agentzh开源了以lua为嵌入语言的模块,极大丰富了NGINX的应用处理能力。它早期支持Lua5.1和LuaJIT,如今已经只支持LuaJIT,并且LuaJIT由OpenResty维护。OpenResty贡献了NGINX生态里最重要的模块,也是最成功的生态。 NGINX时间前进到2015年,NGINX看着开始发力了,不管是软件本身,还是其周边的。支持了stream模块,之前有@yaoweibin tcp proxy module;支持了spdy/http2,这其中google和cloudflare贡献了很大的力量;支持了TLS 1.3;支持更多AIO特性;支持线程池;等等。与此同时,这对Tengine来说,怎么跟进官方是个问题。OpenResty倒是没这方面问题,从一开始他的设计就是不入侵NGNIX源码。 NJS 也是2015年,Igor亲自实现了NJS,一个纯C的JS引擎。不得不说他的代码能力真是硬核。我也有幸成为其中的开发者。Igor实现了ECMAScript 5.1,但现在主流的JS是ECMAScript 6。初期只有他一人开发,实现了非常小的JS子集。后面由@xeioex接手,Igor帮忙Review。对开发者来说,模块化可以说是语言里必不可少的,我也是因为这个需求贡献了NJS的模块化机制实现,后面就上了这条船。目前又加入了一个很有经验的开发者。这也是NGINX的团队风格,人非常少,但是代码质量要求非常高。 Kong 这几年,API网关呼声越来越高。2018年出现的Kong应该是市面上最有份量的API网关之一。APISIX2019年,不到两年的APISIX以轻量级受很多人的欢迎,目前在Apache基金组里,未来可期。 nginx-lua-module 本文作者的模块,支持官方Lua5.4语言。NGINX在直播领域也有它的位置,nginx-rtmp模块作者@arut本身是流媒体的专家。Python是他的个人喜欢的语言。在2016年,他开源了nginx-python模块,其优秀的设计很值得学习。目前他也是以njs为主的nginx-js-module的负责人。nginx-lua-module很大程度上借鉴了nginx-python和njs。个人始终觉得官方Lua语言应该纳入NGINX的生态里,我相信进化的力量,这语言一直保持活跃和进化。当以同社区的极致简单风格维护这个模块。utopia lua实现的API网关框架,1K行左右。 关系网 [lua] https://www.lua.org[Lua 5.4 的改进及 Lua 的版本演进] https://blog.codingnow.com/2018/04/lua_54_nil_in_table.html[nginx] http://nginx.org[tengine] https://github.com/alibaba/tengine[openresty] https://github.com/openresty/lua-nginx-module[njs] https://github.com/nginx/njs[kong] https://github.com/Kong/kong[apisix] https://github.com/apache/apisix[nginx-python-module] https://github.com/arut/nginx-python-module[nginx-lua-module] https://github.com/hongzhidao/nginx-lua-module
引子我是NGINX Unit的贡献者,Unit是我非常喜欢的一个开源软件。 我将写一系列Unit的文章分享Unit的世界,相信这个优秀的软件会有非常好的前景。 NGINX Unit是什么?NGINX Unit是一个全新的,由NGINX作者亲自设计,带领NGINX核心团队开发的纯c软件。官方的定义:Unit是一个动态的web和应用服务器。因此它的三大核心为:动态,web和应用。 Unit总体架构 后续会有专门文章分析Unit构架设计,敬请关注。动态动态指两部分,动态配置和应用进程的动态管理。这里只介绍动态配置,这是它最大的亮点之一。 动态配置一直是NGINX软件的缺陷,重新设计的Unit没有这个问题。 简单说,Unit已经没有配置文件。Unit提供了http API接口,所有配置的更新都通过RESTful方式操作。 应用Unit是个多语言应用软件,它支持同时多个语言,甚至同个语言的不同版本,比如python2和python3,php5和php7。NGINX还有个问题,它不支持应用开发。是的,lua模块已经能做非常多的应用了。但是官方想支持更多主流的语言,于是有了这个设计。 webUnit已经支持了static和proxy两个功能,还比较粗糙。相信这些核心功能未来能跟nginx一样完善。其它Unit已经支持TLS,HTTP/2也在计划当中。此外不得不提的是Unit支持了类似容器的名称空间(namespace)和文件系统隔离(file system isolation)。 Unit搭建文件服务器1. 安装> git clone git@github.com:nginx/unit.git && cd unit > ./configure && make 2. 启动> ./build/unitd 3. 配置> cat config.json { "listeners": { "127.0.0.1:80": { "pass": "routes" } }, "routes": [ { "action": { "share": "/var/www/" } } ] } EOF > curl -X PUT --data-binary @config.json --unix-socket control.unit.sock http://localhost/config { "success": "Reconfiguration done." } 4. 访问> curl http://127.0.0.1:80 更多请看官方文档 下篇介绍:Unit架构设计
nginx的配置文件使用简单灵活,某些部分还具备脚本语言的特点,变量就是其中一个特色。本文将分析变量是如何设计实现的。 0. 什么是变量脚本语言都有变量这个东西,其作用就是让内容可变,用名称代替可变的内容,所以变量具有赋值和取值的特点。nginx的变量跟php一样,以$开头。两种用法:赋值:set $some_var nginx;取值:$some_var; 1.整体设计 * 创建所有变量只能在配置文件解析,也就是工作进程启动之前创建,有些是内置的变量,有些是自定义的变量。没什么区别,比如 $http_host是内置变量,set $some_var some_val。通过set指定创建的是自定义变量,当然也可以其它方式,如果你自己写模块的话。这时用到 cmcf->variables_keys, cmcf->variables 两个数组,数组元素类型为ngx_http_variable_t。 * 使用 变量使用(即拿它的值)要先获取索引(发生在配置阶段),这是为了加快访问速度,然后根据索引拿它的值(发生在运行阶段)。这时用到r->variables数组,数组元素为ngx_http_variable_value_t(ngx_variable_value_t的别名)。配置时:n = ngx_http_get_variable_index(cf, name); 运行时:v = ngx_http_get_indexed_variable(r, n); cmcf->variables_keys 创建的变量都存在这个数组cmcf->varialles 使用的变量(为了拿索引)都存储在这里。nginx会在init conf时检查cmcf->variables的所有变量必须在cmcf->variables_keys里。r->variables 将cmcf->variables导入到这里,为了更方便处理,这时只需要拿值 2、创建变量nginx有内置的变量,分布在好多个模块里,这些变量在配置解析之前构建完成,接下来解析配置,可能碰到变量(内置或自定义),最后处理所有的变量。preconfigure : 添加内置变量,比如 $request, $server_name, $args 等。parse : 添加自定义变量。init : 处理所有变量,比如自定义变量没有对应相同的内置变量,当作错误处理。如果最终处理成功,会有个hash存储这些变量的数据。我们看下内部如何实现的: * preconfigure阶段:cmcf->variables_keys:所有内置变量都会添加到这个数组里。每个成员的结构体是ngx_http_variable_ttypedef struct { ngx_str_t name; /* must be first to build the hash */ ngx_http_set_variable_pt set_handler; ngx_http_get_variable_pt get_handler; uintptr_t data; ngx_uint_t flags; # 这个比较重要,看下面解释 ngx_uint_t index;} ngx_http_variable_t; name是名称,set_handler和get_handler分别用于赋值和取值,需要配合data,这几个比较简单。index是索引的意思,可以通过根据这个值拿到对应的变量,具体后面会再讲到。 flags是变量标记,不同的标记使其用法和用途不同,有NGX_HTTP_VAR_CHANGEABLE,NGX_HTTP_VAR_NOCACHEABLE标记。有NGX_HTTP_VAR_CHANGEABLE标记意味着变量是可变的。比如 $server_name是不可变的,你不能这样操作 set $server_name "err"; $args是可变的,就可以这样操作 set $args "ok"; 内置变量在源码里都有指定它的标记,自定义变量都是可变的。 3.使用变量要获取nginx的变量的值有两种方式:索引和变量名 *索引 如前面介绍,先在配置阶段拿索引,然后在运行阶段根据索引拿值 *变量名 ngx_http_variable_value_t * ngx_http_get_variable(ngx_http_request_t *r, ngx_str_t *name, ngx_uint_t key); 在整个配置文件解析处理后,nginx会构造一个hash:cmcf->variables_hash,存储所有的变量。可想而知,根据变量名就可以快速拿到对应的变量。当然用索引的方式更快,至于选哪种视情况而定了。 4. 总结要理解变量,要先理解nginx的两个阶段,解析阶段和运行阶段。解析阶段尽量做事前工作,如创建变量、拿索引等。到了运行阶段就可以快速的拿变量的值。还可以看出nginx的一个重要设计,解析时函数的参数基本有 ngx_conf_t *cf,到了运行阶段就是 ngx_http_request_t *r了。使用到的东西也更简化了,比如变量在解析阶段,需要有get和set(可选),但到了运行阶段,只需要拿值。这种细微的设计变化,可以好好思考,并转化成自己的理解。我经常推荐看nginx源码的同学可以从ngx_log_t *log这个东西入手,因为它反映了整个nginx的生命周期。