jinhailang / blog

技术博客:知其然,知其所以然
https://github.com/jinhailang/blog/issues
60 stars 6 forks source link

ngx_lua 中协程的应用 #26

Open jinhailang opened 6 years ago

jinhailang commented 6 years ago

ngx_lua 中协程的使用

Nginx 在 postconfiguration 阶段执行 lua-nginx-module 模块初始化函数 ngx_http_lua_init, 该函数会调用ngx_http_lua_init_vm 来创建和初始化一个 lua 虚拟机环境,由 lua API luaL_newstate 实现,该接口函数会创建一个协程作为主协程,返回 lua_state(存放堆栈信息,包括后续的请求数据(ngx_http_request_t ),API 注册表等数据都是存放在这里,供 lua 层使用),然后调用 ngx_http_lua_init_globals ,该函数做了两件事:

另外,还会调用函数 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.sleepngx.socket I/O 之类的阻塞操作,使用协程实现异步,提高执行效率。如果放在主协程,这类操作就会阻塞主协程,导致 worker 进程无法处理其它请求。ngx.socket,ngx.sleep 等 API 都会有挂起协程的操作,只能在子协程调用,因此,这些 API 不能在 header_filter 等阶段(主协程)使用。

我们知道,协程是非抢占式的,也就是说只有正在运行的协程只有在显式调用 yield 函数后才会被挂起,因此,同一时间内,只有一个协程在处理(因为 worker 是单线程的),lua 协程还有一个特性,就是子协程优先运行,只有当子协程都被挂起或运行结束才会继续运行父协程。

ngx_lua 协程的调度可以参考下面这张图(图片来自):

06224854_qsha

lua_resume 就是恢复对应的协程运行,在请求处理时,还可能调用 API ngx.thread 来创建 light thread, 可以认为是一种特殊的 lua 协程,没有本质区别,不同的是,它是由 ngx_lua 模块进行调度的(详见下面的 ngx_http_lua_run_thread 源码)。在需要访问第三方服务时,并发执行,可以减少等待处理时间。

ngx.thread.spawn(query_mysql)      -- create thread 1
ngx.thread.spawn(query_memcached)  -- create thread 2
ngx.thread.spawn(query_http)       -- create thread 3

从上面可知,在 ngx_lua 内有三层协程 —— 全局的主协程,请求阶段的子协程,以及用户创建的 light thread,它们分别为父子关系,记住这三个协程代称,后面将会用到。 使用 ngx.exit, ngx.exec, ngx.redirect 可以直接跳出协程,而不用等待子协程处理完成。

ngx.exec("/a/b/c")          -- 内部跳转,直接从子协程结束,回到主协程
ngx.redirect("/foo", 301)   -- 重定向,终止的当前请求的处理,即不再处理后续阶段

ngx.exit 可接受多种参数:
ngx.exit(ngx.OK)        -- 完成当前阶段(退出子协程),继续下一个阶段
ngx.exit(ngx.ERROR)     -- 中断当前请求,报错
ngx.exit(HTTP_STATUS)   -- 结束 content 阶段,继续下个阶段

返回值说明

以上,就是协程在 ngx_lua 模块中的使用与调度。那么 lua 协程到底是个什么神奇的东西呢?

lua 协程

Lua 所支持的协程全称被称作协同式多线程(collaborative multithreading),由用户(lua 虚拟机)自己负责管理,在线程内运行,操作系统是感知不到的。特性就如上面所说,主要两条:

是不是很像回调?因此,lua 协程之间不存在资源竞争,也就不需要锁了。严格来说,这种协程只是为了实现异步,而不是并发。而且,lua 是没有线程概念的,lua 语言的定位就是系统嵌入式脚本,由 C 语言调度使用的,在 C 层面创建线程就行了,也使得 lua 更加简单。

主要 API

有趣的实例

使用 lua 协程实现生产者-消费者问题:

local i = 0

function receive(prod)
    i = i + 1
    local status, value = coroutine.resume(prod, i)
    return value
end

function send(x)
    return coroutine.yield(x)
end

function producer()
    return coroutine.create(function()
        while true do
            local x = io.read()
            local r = send(x)
            io.write(i,":\r\n")
        end
    end)
end

function consumer(prod)
    while true do
        local obtain = receive(prod)
        if obtain then
            io.write(obtain, "\n\n")
        else
            break
        end
    end
end

io.write(i+1, ":\r\n")
p = producer()
consumer(p)

从这里可以看到,lua 协程跟线程差别很大,更像是回调,new_thread 只是在内存新建了一个 stack 用于存放新 coroutine 的变量,也称作lua_State

lua 协程与 golang 协程区别

lua 协程与 golang [协程都是协程,但是差别还是挺大的,除了都有自己独立的堆栈空间外,唯一的共同点可能就是前面说过的都是非抢占式的(实际上[Go 1.2 开始加入了简单的抢占式调度逻辑](https://golang.org/doc/go1.2#preemption))。一个最明显的区别是,golang 父子协程是独立而平等的。
golang 调度器实现更复杂,可以将协程分配到多个线程上(GPM 模型),因此,golang 协程是可以并发(并行)的。本质上,lua 协程主要作用是单线程内实现异步非阻塞执行;golang 协程与线程更加类似,用来实现多线程并发执行。

小结

OpenResty 将 lua 嵌入到 Nginx 系统,使 Nginx 拥有了 lua 的能力,大大的扩展了 Nginx 系统的开发灵活性和开发效率。达到了以同步的方式写代码,实现异步功能的效果。不用担心异步开发中的顺序问题,又因为单线程的,也不用担心并发开发中最头痛的竞争问题。比起原生的 Nginx 第三方模块开发,开发更简单,系统也更稳定。

需要注意的是,ngx_lua 并没有提高 Nginx 的并发能力,Nginx worker 本来就是使用回调机制来异步处理多个请求的, 当前请求处理阻塞时,会注册一个事件,然后去处理新的请求,从而避免进程因为某个请求阻塞而干等着(参考知乎问答)。

jinhailang commented 6 years ago

ngx_http_lua_run_thread 源码( ngx.thread 调度部分)


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 }```