Open jinhailang opened 6 years ago
ngx_http_lua_run_thread(lua_State *L, ngx_http_request_t *r,
952 ngx_http_lua_ctx_t *ctx, volatile int nrets)
953 {
954 ...
973 NGX_LUA_EXCEPTION_TRY {
974 ...
982 for ( ;; ) {
983 ...
997 orig_coctx = ctx->cur_co_ctx;
998 ...
1015 rv = lua_resume(orig_coctx->co, nrets);//通过lua_resume执行协程中的函数
1016 ...
1032 switch (rv) {//处理lua_resume的返回值
1033 case LUA_YIELD:
1034 ..
1047 if (r->uri_changed) {
1048 return ngx_http_lua_handle_rewrite_jump(L, r, ctx);
1049 }
1050 if (ctx->exited) {
1051 return ngx_http_lua_handle_exit(L, r, ctx);
1052 }
1053 if (ctx->exec_uri.len) {
1054 return ngx_http_lua_handle_exec(L, r, ctx);
1055 }
1056 switch(ctx->co_op) {
1057 ...
1167 }
1168 continue;
1169 case 0:
1170 ...
1295 continue;
1296 ...
1313 default:
1314 err = "unknown error";
1315 break;
1316 }
1317 ...
1444 }
1445 } NGX_LUA_EXCEPTION_CATCH {
1446 dd("nginx execution restored");
1447 }
1448 return NGX_ERROR;
1449
1450 no_parent:
1451 ...
1465 return (r->header_sent || ctx->header_sent) ?
1466 NGX_ERROR : NGX_HTTP_INTERNAL_SERVER_ERROR;
1467
1468 done:
1469 ...
1481 return NGX_OK;
1482 }```
ngx_lua 中协程的使用
Nginx 在 postconfiguration 阶段执行
lua-nginx-module
模块初始化函数ngx_http_lua_init
, 该函数会调用ngx_http_lua_init_vm
来创建和初始化一个 lua 虚拟机环境,由 lua APIluaL_newstate
实现,该接口函数会创建一个协程作为主协程,返回 lua_state(存放堆栈信息,包括后续的请求数据(ngx_http_request_t
),API 注册表等数据都是存放在这里,供 lua 层使用),然后调用ngx_http_lua_init_globals
,该函数做了两件事:global_state
数据结构,这个结构体保存全局相关的一些信息,主要是所有需要垃圾回收的对象。ngx_http_lua_inject_ngx_api
,注册各种 Nginx 层面的 API 函数,设置字符串ngx
为表名,lua 代码中就可以使用ngx.*
来调用这些 API 了。另外,还会调用函数
ngx_http_lua_init_registry
,ngx_http_lua_ctx_tables
就是在这里注册到 Nginx 内存的(lua 中没有引用的变量会被 GC 掉),用来存放单个请求的 ctx 数据(table),即ngx.ctx
。所以,与ngx.var
不一样,ngx.ctx
其实是 lua table,只是在 Nginx 内存中添加了引用。也就不难理解,ngx.ctx
生命周期是在单个 location,因为内部跳转时,会清除对应的 ctx table。要想在父子请求间共享 ngx.ctx,可以参考这篇文章,过程大概是,将对应的 ctx 再次插入 ngx_http_lua_ctx_tables,创建新的索引,索引保存在 ngx.var 中,在子请求时取出重新赋值给 ngx.ctx。master fork worker 进程时,Lua 虚拟机自然也被复制(COW)到了 worker 进程。
请求是在 worker 进程内处理的,处理共分为 11 个阶段,其中在 balancer_by_lua, header_filter, body_filter, log 阶段中,直接在主协程中执行代码,而在 rewrite_by_lua, access_by_lua 和 content_by_lua 阶段中,会创建一个新的协程(boilerplate "light thread" are also called "entry threads")去执行此阶段的 lua 代码。这些新的子协程相互独立,数据隔离,但是共享
global_state
。为什么 content 等几个阶段的处理要在子协程里面处理呢?原因可能是 content 等阶段,需要调用
ngx.sleep
,ngx.socket
I/O 之类的阻塞操作,使用协程实现异步,提高执行效率。如果放在主协程,这类操作就会阻塞主协程,导致 worker 进程无法处理其它请求。ngx.socket
,ngx.sleep
等 API 都会有挂起协程的操作,只能在子协程调用,因此,这些 API 不能在 header_filter 等阶段(主协程)使用。我们知道,协程是非抢占式的,也就是说只有正在运行的协程只有在显式调用 yield 函数后才会被挂起,因此,同一时间内,只有一个协程在处理(因为 worker 是单线程的),lua 协程还有一个特性,就是子协程优先运行,只有当子协程都被挂起或运行结束才会继续运行父协程。
ngx_lua 协程的调度可以参考下面这张图(图片来自):
lua_resume
就是恢复对应的协程运行,在请求处理时,还可能调用 APIngx.thread
来创建light thread
, 可以认为是一种特殊的 lua 协程,没有本质区别,不同的是,它是由ngx_lua
模块进行调度的(详见下面的ngx_http_lua_run_thread
源码)。在需要访问第三方服务时,并发执行,可以减少等待处理时间。从上面可知,在 ngx_lua 内有三层协程 —— 全局的主协程,请求阶段的子协程,以及用户创建的
light thread
,它们分别为父子关系,记住这三个协程代称,后面将会用到。 使用ngx.exit
,ngx.exec
,ngx.redirect
可以直接跳出协程,而不用等待子协程处理完成。light thread
)被挂起light thread
都结束)以上,就是协程在
ngx_lua
模块中的使用与调度。那么 lua 协程到底是个什么神奇的东西呢?lua 协程
Lua 所支持的协程全称被称作协同式多线程(collaborative multithreading),由用户(lua 虚拟机)自己负责管理,在线程内运行,操作系统是感知不到的。特性就如上面所说,主要两条:
是不是很像回调?因此,lua 协程之间不存在资源竞争,也就不需要锁了。严格来说,这种协程只是为了实现异步,而不是并发。而且,lua 是没有线程概念的,lua 语言的定位就是系统嵌入式脚本,由 C 语言调度使用的,在 C 层面创建线程就行了,也使得 lua 更加简单。
yield
的参数值(b1 ... bn)resume
的参数(a1 ... an)使用 lua 协程实现生产者-消费者问题:
从这里可以看到,lua 协程跟线程差别很大,更像是回调,new_thread 只是在内存新建了一个 stack 用于存放新 coroutine 的变量,也称作
lua_State
。小结
OpenResty 将 lua 嵌入到 Nginx 系统,使 Nginx 拥有了 lua 的能力,大大的扩展了 Nginx 系统的开发灵活性和开发效率。达到了以同步的方式写代码,实现异步功能的效果。不用担心异步开发中的顺序问题,又因为单线程的,也不用担心并发开发中最头痛的竞争问题。比起原生的 Nginx 第三方模块开发,开发更简单,系统也更稳定。
需要注意的是,ngx_lua 并没有提高 Nginx 的并发能力,Nginx worker 本来就是使用回调机制来异步处理多个请求的, 当前请求处理阻塞时,会注册一个事件,然后去处理新的请求,从而避免进程因为某个请求阻塞而干等着(参考知乎问答)。