关于nginx:编程是门手艺做一名优秀的将军

2次阅读

共计 4535 个字符,预计需要花费 12 分钟才能阅读完成。

编程是门手艺,NGINX 社区的教训分享 一文提过,业余的程序员善于整体设计和细节解决能力。本文探讨整体设计,尤其是模块化这个技能。

全能蠢才,Fabrice Bellard

FFmpeg,最弱小的流媒体库
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.js

var 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…
适度设计是一种很容易踩进去的陷井。
探讨 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 + 1
                                  before 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 have
       small 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…
[the-craft-of-programming] https://github.com/hongzhidao…
技术问题欢送 issue 里交换
[utopia] https://github.com/hongzhidao…
还未开源,关注公众号及时理解更新

正文完
 0