洪志道 洪志道的专栏
浏览 2.71 K
文章 9
订阅 40
编程是门手艺,做一名优秀的将军 洪志道 发表于 : 2020-10-31 12:53

编程是门手艺,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

点赞 4
1 条评论
编程是门手艺,NGINX社区的经验分享 洪志道 发表于 : 2020-10-28 20:42

前言关于我: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 

点赞 3
0 条评论
一文看透NGINX开发史 洪志道 发表于 : 2020-10-25 17:42

我的职业生涯大部分时间都在跟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 

点赞 3
0 条评论
一文看透NGINX生态城 洪志道 发表于 : 2020-10-24 18:34

俄罗斯人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   

点赞 3
0 条评论
打开NGINX Unit世界 洪志道 发表于 : 2020-08-31 22:08

引子我是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架构设计  

点赞 5
1 条评论
nginx源码分析之变量设计 洪志道 发表于 : 2020-08-31 14:36

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的生命周期。

点赞 5
0 条评论
TA的热门
本文适合对象:源码开发人员和软件使用人员先说说nginx能吸收的营养知识* http协议和服务器是如何实现的* 网络编程的知识* 简直就是c教科书* 代码阅读能力* 良好的开发习惯* 锻炼逻辑能力* 完全掌握nginx提升自信心深入NGINX源码的学习路径建议尽量只看源码,不看任何其它的资料,但是可以少量参考。如果你是新手,选最初发布版本 nginx0.1.0。(2004年发布)从代码量比较 nginx0.1.0:  4w+  vs  nginx1.19.0: 19w+幸运的是架构几乎没变化。因为模块化是nginx架构的核心,这个在0.1就支持了。不幸运的是这也成了一个问题,举个例子,为什么nginx改配置需要reload呢?有了模块化之后,涌现了很多的第三方模块,有些质量非常不错,推动了社区的发展。有些质量比较低。但不管怎么样,这些大量的第三方模块,让nginx的API不能轻易改动,更不用说架构了。20年前互联网没这么复杂,reload完全不是问题。但是放在现在,长连接业务已经变的很常见,这就成为一个明显的问题了。这里不得不感叹很多厉害人物,在20岁出头就已经有神作了。选好版本后,带着问题从main开始阅读。我按顺便列了些问题:0. 动手编译hello world模块1. 解释`void ****conf_ctx`2. 怎么响应3023. 为什么能输出html4. 为什么reload配置能刷新5, 为什么upgrade能平滑升级6. 解释超时怎么处理的7. 解释proxy怎么工作的为方便大家搭建环境,弄了个仓库,有问题可以建立issue,在时间允许上会回复。https://github.com/hongzhidao/nginx-code-reading记得star,让我知道对多少人有帮助。提示:碰到不会再学习* 如果不熟悉gdb,没关系。配置文件里配置 daemon off; 会printf即可。* 如果不熟悉c语言,没关系。先搞懂指针,剩下看源码时碰到不会再学习。* 如果不熟悉网络编程,也没关系。碰到看不懂再学习。* 其它类似epoll等一样适用。* hello world模块用1.19编译。* nginx 0.1.0有些bug,作了修复。如果你觉得跨过新手级别了,看最新版的源码,同样带着问题,但是要写模块。1. 看官方开发指南https://github.com/baishancloud/nginx-development-guide这是跟另一朋友一起翻译的,openssl社区的成员,杨洋。2. 多写模块,做到写出来的风格跟源码里的看起来一样。模块自己想,能在工作中解决问题最好了。3. 提高代码能力。a. 抄源码是个方式。 b. 找独立的函数,自己实现,然后跟源码里对比。 (问题待整理)如果哪天你觉得对360无死角了,像社区的一样,看nginx应该看的都是不足,试试解决这些不足。如果你是一名使用者,包括运维同学。也想深入nginx怎么办?首先,你已经具备nginx的环境,因为你碰到了问题,想深入nginx能在工作中带来帮助。这已经成功一半了。 进入源码吧,比你想象的简单,这跟学习英语一样,用久就习惯了。举个例子,你想知道map的详细用法。1. 查看官方文档:http://nginx.org/en/docs/ 找到 map 模块,详细阅读。2. 对比源码,看它的逻辑: src/http/modules/ngx_http_map_module.c为什么看源码有效呢?首先,完全可以将c代码当作普通的英语描述(俗称伪代码)其次,你会惊讶于nginx的代码质量,你需要的功能都有专门的一个地方体现。最后,我一直说的,nginx源码非常锻炼人的逻辑能力。这几年业余时间跟NGINX社区的一起写代码,对nginx/njs/unit都算熟悉,仅供参考。下篇待续,整体上欣赏NGINX源码的设计。源码文章只会写一篇,只是为了更快进入源码大门。上面提到的看源码是最好的方式,不想剥夺读者思考的乐趣。
nginx配置结构清晰,层次分明,这得益于整个架构的模块化设计,文本将揭示配置文件如何被处理和应用。整个配置文件解析后的结果如图这样存储。  一、解析的核心机制nginx源码里,ngx_conf_t是解析的关键结构体ngx_conf_handler函数里:/* set up the directive‘s configuration context */conf = NULL;/* direct指令,一般是core类型模块指令,比如 daemon, work_processes */if (cmd->type & NGX_DIRECT_CONF) {    conf = ((void **) cf->ctx)[ngx_modules[i]->index]; /* 直接存储,比如 ngx_core_conf_t *//* main指令,比如 events, http,此时指向它的地址,这样才能分配数组指针,存储属于它的结构体们。 */} else if (cmd->type & NGX_MAIN_CONF) {    conf = &(((void **) cf->ctx)[ngx_modules[i]->index]); /* 参考图片 */} else if (cf->ctx) {    confp = *(void **) ((char *) cf->ctx + cmd->conf);    /* 有移位的,因此http有三个部分,main, srv, conf,这个就为此而设计的,继续下面的sendfile指令 */     if (confp) {        conf = confp[ngx_modules[i]->ctx_index];    }}rv = cmd->set(cf, cmd, conf);比如sendfile指令:{ ngx_string("sendfile"),  NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF                    |NGX_CONF_FLAG,  ngx_conf_set_flag_slot,  NGX_HTTP_LOC_CONF_OFFSET,  /* 这个对应上面理解,正因有这个offset,它才找到loc部分的配置 */  offsetof(ngx_http_core_loc_conf_t, sendfile),  NULL } 二、配置的应用1、最简单形式,direct方式/* 定义获取配置文件的宏 */#define ngx_get_conf(conf_ctx, module)  conf_ctx[module.index]ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);if (ccf->master && ngx_process == NGX_PROCESS_SINGLE) {    ngx_process = NGX_PROCESS_MASTER;} 2、稍微复杂点形式,main方式/* 定义获取配置文件的宏,注意指针 */#define ngx_event_get_conf(conf_ctx, module)                                  \             (*(ngx_get_conf(conf_ctx, ngx_events_module))) [module.ctx_index];ngx_epoll_conf_t  *epcf;epcf = ngx_event_get_conf(cycle->conf_ctx, ngx_epoll_module);nevents = epcf->events; 3、不简单的http配置 /* 宏定义,r是什么,稍后解释 */#define ngx_http_get_module_loc_conf(r, module)  (r)->loc_conf[module.ctx_index]ngx_http_log_loc_conf_t  *lcf;lcf = ngx_http_get_module_loc_conf(r, ngx_http_log_module);.../* r是请求,它是这么来的,在ngx_http_init_request函数里(ngx_http_request.c文件)*/r = ...;cscf = addr_conf->default_server;r->main_conf = cscf->ctx->main_conf;r->srv_conf = cscf->ctx->srv_conf;r->loc_conf = cscf->ctx->loc_conf; 还有个要提的,http配置分main, src, loc,下级的配置可以覆盖上级,很明显,上级只是默认设置值而已。 三、重提模块化设计学着用模块化的角度去看nginx的整体设计,一切以模块为核心,配置依赖于模块,即模块本身就携带着它的配置。正因为这样的设计的,配置文件的解析,使用非常简单。避免过多使用全局变量好像成为一种共识,但是在nginx世界里,全局变量可不少,每个模块都是个全局变量,为什么这样设计呢?因为模块之间是有依赖性的,所以需要互相访问。 配置文件解析这块的代码极具借鉴,本文只管窥般分析了配置文件,不能剥夺读者阅读源码的享受,点到即止。
引子我是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的生命周期。
nginx一向以高性能著称,缓存是其中一大利器,本文将分享nginx中各个级别缓存的原理和内部是如何实现的。 0.什么是缓存缓存存在计算机中的各个领域,其目的都是为了提升处理速度,可以说是以空间换取了时间。但事实并不是这么简单,因为引入了缓存,就要处理一致性、过期处理、缓存策略和命中率等。例如CPU有几级缓存,memcached作为内存缓存服务器,redis也是,还有文件缓存服务器,CDN等。总而言之,访问目标后‘备份’到某中间层,下次再访问时,直接从中间层获取,加快了访问速度。所以目标越往后,处理速度越慢,程序要做的就是尽量命中在前面,不要让访问穿透到后面目标。 1.nginx缓存nginx属于web应用服务器,是典型的request/response模式。整体而言,nginx的缓存可以总结为3个:304响应、静态级别缓存、动态级别缓存。下面一一介绍:  *304响应这个其实不算真正意义上的缓存,之所以提出来,是因为它的重要性,也因为涉及客户端的缓存。当客户端访问某URL时,如果服务器响应304状态码,其意义是告诉客户端您访问的东西没有变化过,内容从自己本地获取。浏览器就是典型的应用。当你第一次访问某静态资源时,响应200,当按F5时,会响应304。实现原理是nginx根据头部信息if_modified_since与静态资源的时间戳比较,如果一样,则返回304。做web开发应该清楚这个状态码。  *静态级别缓存location /test {    open_file_cache  max=100;}这个级别的缓存是缓存静态资源文件的句柄。nginx处理一个请求时,会打开一个文件句柄,然后在请求处理结束后关闭它。加入open_file_cache后,就会将文件句柄保存在工作进程中(不是共享内存),只要工作进程存在,当后面的请求访问同样的文件时,就不用重新打开文件。这样的好处是节省了文件打开关闭的开销。  *动态级别缓存http {    ...    fastcgi_cache_path  /tmp/abc keys_zone=c1:10M;    server {        location ~ \.php$ {            ...            fastcgi_cache  c1;            fastcgi_cache_valid  200 30s;        }    }} 这个级别的缓存是缓存动态请求的内容。当客户端访问某动态文件时(http://example.com/test.php),nginx将请求重新封装成fastcgi协议,然后转发给php-fpm处理,当php-fpm正常响应后,nginx将响应内容保存到某目录的文件里。下次同样的请求过来,就直接从缓存目录的文件中读取,比第一次快且省事很多。  聪明的你可能已经想到动态级别缓存可以再加上静态级别缓存,没错的。这3种响应是独立的,可以灵活配合使用。现在您已经明白缓存的原理,接下来看下如何实现的。  2.304响应ngx_http_not_modified_filter_module处理了这个功能,属于http request header filter模块。源码非常简单。static ngx_int_t ngx_http_not_modified_header_filter(ngx_http_request_t *r){    ...    if (r->headers_in.if_modified_since) {         if (r->headers_in.if_modified_since            && ngx_http_test_if_modified(r))        {            return ngx_http_next_header_filter(r);        }         /* not modified */        r->headers_out.status = NGX_HTTP_NOT_MODIFIED;        return ngx_http_next_header_filter(r);    }     return ngx_http_next_header_filter(r);} static ngx_uint_t ngx_http_test_if_modified(ngx_http_request_t *r){    ...     if (r->headers_out.last_modified_time == (time_t) -1) {        return 1;    }     clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);     if (clcf->if_modified_since == NGX_HTTP_IMS_OFF) {    ims = ngx_parse_http_time(r->headers_in.if_modified_since->value.data,                              r->headers_in.if_modified_since->value.len);     if (ims == r->headers_out.last_modified_time) {        return 0;    }    if (clcf->if_modified_since == NGX_HTTP_IMS_EXACT        || ims < r->headers_out.last_modified_time)    {        return 1;    }    return 0;} 3.静态级别缓存typedef struct {    ngx_rbtree_t             rbtree;    ngx_rbtree_node_t        sentinel;    ngx_queue_t              expire_queue;    ngx_uint_t               current;    ngx_uint_t               max;    time_t                   inactive; } ngx_open_file_cache_t; 记得这个只在工作进程中处理,不涉及共享内存。看的出来ngx_open_file_cache_t是一个红黑树,也是一个队列。每个文件的文件(根据完整文件名)对应一个节点。这样就完整的维护了所有的可能要缓存的文件。又因为缓存需要处理过期,所以最近访问的文件会保持在expire_queue队列的前面,每次访问将从队列中删除,然后移到最前面。整个逻辑封装在一个函数里:ngx_int_t ngx_open_cached_file(ngx_open_file_cache_t *cache, ngx_str_t *name, ngx_open_file_info_t *of, ngx_pool_t *pool) { ... } 4.动态级别缓存ngx_http_file_cache_t {    ngx_http_file_cache_sh_t {        tree        queue        ...    }}ngx_http_cache_t {    keys    key    ngx_http_file_cache_node_t}如果简化点处理过程就是:根据url算出md5,然后将动态请求响应内容存储到文件名为某md5的文件里。http://example.com/a.php -> d41d8cd98f00b204e9800998ecf8427e -> /tmp/abc/d41d8cd98f00b204e9800998ecf8427ehttp://example.com/b.php -> d41d8cd98f00b204e9800998ecf8432d -> /tmp/abc/d41d8cd98f00b204e9800998ecf8432dhttp://example.com/c.php -> d41d8cd98f00b204e9800998ecf84a32 -> /tmp/abc/d41d8cd98f00b204e9800998ecf84a32因为文件存储在硬盘里,所以这里用到共享内存,而不是工作进程的内存中,来保存这种对应关系。我们可以将每个请求(r)当作各个缓存节点,对应ngx_http_cache_t。ngx_http_file_cache_t就是缓存的集合。明白这个关系后,我们解释下整个流程是如何从开始到结束的。  a)根据fastcgi_cache_path的keys_zone,nginx创建了多个 ngx_http_file_cache_t。根据fastcgi_cache指定了某个location将用到哪个ngx_http_file_cache_t。b)为每个请求创建一个ngx_http_cache_t(在ngx_http_file_cache_new函数里),然后继续产生一个key,作为本节点的索引。你可以将请求、请求响应内容、内容存储文件都当作同一个东西,最终都是节点,对应ngx_http_cache_t。所以需要有       key这个东西。c)处理缓存目录和文件名d)nginx处理动态请求是在ngx_event_pipe里的,当需要缓存里,pipe会有个temp_path将响应内容存储到这个文件里e)最后将pipe的temp_path重命名成前面产生的文件名,结束了。 5.小结动态请求的缓存处理代码还是有点复杂的,有兴趣的同学研究下其如何实现很有好处,可以体会下nginx的代码精致程度。如一惯风格,写作主要以白话方式分享原理,细节点到即止,细节上有疑问欢迎交流。