洪志道 洪志道的专栏
浏览 5.96 K
文章 11
订阅 54
打开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架构设计  

点赞 6
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 条评论
nginx源码分析之缓存设计 洪志道 发表于 : 2020-08-31 14:35

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的代码精致程度。如一惯风格,写作主要以白话方式分享原理,细节点到即止,细节上有疑问欢迎交流。

点赞 5
0 条评论
nginx源码分析之配置图解 洪志道 发表于 : 2020-08-31 14:31

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世界里,全局变量可不少,每个模块都是个全局变量,为什么这样设计呢?因为模块之间是有依赖性的,所以需要互相访问。 配置文件解析这块的代码极具借鉴,本文只管窥般分析了配置文件,不能剥夺读者阅读源码的享受,点到即止。

点赞 7
0 条评论
如何高效的学习NGINX 洪志道 发表于 : 2020-06-06 00:41

本文适合对象:源码开发人员和软件使用人员先说说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源码的设计。源码文章只会写一篇,只是为了更快进入源码大门。上面提到的看源码是最好的方式,不想剥夺读者思考的乐趣。

点赞 18
2 条评论
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社区的经验分享 一文提过,专业的程序员擅长整体设计和细节处理能力。本文探讨整体设计,尤其是模块化这个技能。全能天才,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<= n <= 2^31-1. If false, the shape is guaranteed not to havesmall array index properties */uint8_t has_small_array_index;uint32_t hash; /* current hash value */uint32_t prop_hash_mask;int prop_size; /* allocated properties */int prop_count;JSShape *shape_hash_next; /* in JSRuntime.shape_hash[h] list */JSObject *proto;JSShapeProperty prop[0]; /* prop_size elements */};里面指针还用到负操作, 他是数学行家玩的转。 为什么NJS不能这样呢?依赖,各细节之间相互引用。软件开发中没办法的事情。 还以打球为例,那些走位和发力非常老道的球手,打法往往是简单有效的,不要奇怪为什么有些球不先击打进去,而选择更不好打的,一切在掌握之中。 设计重于实现这是我这两年比较大的体会。以前会觉得有这设计的功夫,早把东西实现好了,而且认为重构能解决一切的设计不足。这是没错的,问题是花了更多的时间在走弯路。 write some code, think, write more, meditate, write a meaningful commit log, take a sleep, think again, and re-read, split/fold/re-write, think, become happy with the final result. 以上是Unit的负责人给的建议,个人觉得这是一种可行有效的方式。NGINX的http2实现就出自他的手笔。对了,NGINX的http3即将完成。有方法才有可行本系列文章都会有实操方法。实践对想提升代码的同学是很有效的方式,我个人觉得学习或写项目是一种方式。 utopia是我写的一个API网关框架,只有一千行代码。里面的一些设计就参考 Unit,尤其是路由部分。我了解他们的设计历程,非常优秀。这是一个非常适合学习的项目。 设计可以聊的实在太多,远不止一文可以讲完,以后会不断的夹杂在其它章节。[nginx-lua-module] https://github.com/hongzhidao/nginx-lua-module [the-craft-of-programming] https://github.com/hongzhidao/the-craft-of-programming 技术问题欢迎issue里交流 [utopia] https://github.com/hongzhidao/utopia 还未开源,关注公众号及时了解更新 
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是一个多语言动态应用服务器,同样支持代理和静态文件。 是由NGINX作者Igor Sysoev亲自设计带领团队从零实现的高性能纯C服务器。 Unit的使用对开发者和运维非常友好。关键特性:灵活用HTTP协议的RESTful JSON API更新任意粒度配置 极其强大和易用的路由管理请求 支持所有主流语言:GO, Python, JAVA, Perl, PHP, NodeJS, Ruby 支持负载均衡的代理 支持静态资源服务性能高并发:使用异步事件处理模型 低内存:使用连接和内存复用 每个应用语言的进程管理支持动态和静态分配 对Java, Perl, Python, Ruby支持原生多线程处理安全和稳定性处理请求的进程以非特权方式运行 进程间完全独立 不同的应用完全相互隔离 支持用容器化方式对应用程序进行namespace和file system隔离 支持SSL/TLS社区和文档容易上手 保持一直活跃的开发状态: https://github.com/nginx/unit/ 官方一直更新使用文档:https://unit.nginx.org/完整示例:{ "certificates": { "bundle": { "key": "RSA (4096 bits)", "chain": [ { "subject": { "common_name": "example.com", "alt_names": [ "example.com", "www.example.com" ], "country": "US", "state_or_province": "CA", "organization": "Acme, Inc." }, "issuer": { "common_name": "intermediate.ca.example.com", "country": "US", "state_or_province": "CA", "organization": "Acme Certification Authority" }, "validity": { "since": "Sep 18 19:46:19 2018 GMT", "until": "Jun 15 19:46:19 2021 GMT" } }, { "subject": { "common_name": "intermediate.ca.example.com", "country": "US", "state_or_province": "CA", "organization": "Acme Certification Authority" }, "issuer": { "common_name": "root.ca.example.com", "country": "US", "state_or_province": "CA", "organization": "Acme Root Certification Authority" }, "validity": { "since": "Feb 22 22:45:55 2016 GMT", "until": "Feb 21 22:45:55 2019 GMT" } } ] } }, "config": { "settings": { "http": { "header_read_timeout": 10, "body_read_timeout": 10, "send_timeout": 10, "idle_timeout": 120, "max_body_size": 6291456, "static": { "mime_types": { "text/plain": [ ".log", "README", "CHANGES" ] } }, "discard_unsafe_fields": false } }, "listeners": { "*:8000": { "pass": "routes", "tls": { "certificate": "bundle" } }, "127.0.0.1:8001": { "pass": "applications/drive" }, "*:8080": { "pass": "upstreams/rr-lb" } }, "routes": [ { "match": { "uri": "/admin/*", "scheme": "https", "arguments": { "mode": "strict", "access": "!raw" }, "cookies": { "user_role": "admin" } }, "action": { "pass": "applications/cms" } }, { "match": { "host": "admin.emea-*.*.example.com", "source": "*:8000-9000" }, "action": { "pass": "applications/blogs/admin" } }, { "match": { "host": ["blog.example.com", "blog.*.org"], "source": "*:8000-9000" }, "action": { "pass": "applications/blogs/core" } }, { "match": { "host": "example.com", "source": "127.0.0.0-127.0.0.255:8080-8090", "uri": "/chat/*" }, "action": { "pass": "applications/chat" } }, { "match": { "host": "example.com", "source": [ "10.0.0.0/7:1000", "10.0.0.0/32:8080-8090" ] }, "action": { "pass": "applications/store" } }, { "match": { "host": "wiki.example.com" }, "action": { "pass": "applications/wiki" } }, { "match": { "uri": "/legacy/*" }, "action": { "return": 301, "location": "https://legacy.example.com" } }, { "match": { "scheme": "http" }, "action": { "proxy": "http://127.0.0.1:8080" } }, { "action": { "share": "/www/static/", "fallback": { "proxy": "http://127.0.0.1:9000" } } } ], "applications": { "blogs": { "type": "php", "targets": { "admin": { "root": "/www/blogs/admin/", "script": "index.php" }, "core" : { "root": "/www/blogs/scripts/" } }, "limits": { "timeout": 10, "requests": 1000 }, "options": { "file": "/etc/php.ini", "admin": { "memory_limit": "256M", "variables_order": "EGPCS", "expose_php": "0" }, "user": { "display_errors": "0" } }, "processes": 4 }, "chat": { "type": "external", "executable": "bin/chat_app", "group": "www-chat", "user": "www-chat", "working_directory": "/www/chat/", "isolation": { "namespaces": { "cgroup": false, "credential": true, "mount": false, "network": false, "pid": false, "uname": false }, "uidmap": [ { "host": 1000, "container": 0, "size": 1000 } ], "gidmap": [ { "host": 1000, "container": 0, "size": 1000 } ], "automount": { "language_deps": false, "procfs": false, "tmpfs": false } } }, "cms": { "type": "ruby", "script": "/www/cms/main.ru", "working_directory": "/www/cms/" }, "drive": { "type": "perl", "script": "app.psgi", "threads": 2, "thread_stack_size": 4096, "working_directory": "/www/drive/", "processes": { "max": 10, "spare": 5, "idle_timeout": 20 } }, "store": { "type": "java", "webapp": "/www/store/store.war", "classpath": ["/www/store/lib/store-2.0.0.jar"], "options": ["-Dlog_path=/var/log/store.log"] }, "wiki": { "type": "python", "module": "asgi", "protocol": "asgi", "callable": "app", "environment": { "DJANGO_SETTINGS_MODULE": "wiki.settings.prod", "DB_ENGINE": "django.db.backends.postgresql", "DB_NAME": "wiki", "DB_HOST": "127.0.0.1", "DB_PORT": "5432" }, "path": "/www/wiki/", "processes": 10 } }, "upstreams": { "rr-lb": { "servers": { "192.168.1.100:8080": { }, "192.168.1.101:8080": { "weight": 2 } } } }, "access_log": "/var/log/access.log" }}
引子我是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架构设计