Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ngx_lua 中协程的应用 #26

Open
jinhailang opened this issue Jul 20, 2018 · 1 comment
Open

ngx_lua 中协程的应用 #26

jinhailang opened this issue Jul 20, 2018 · 1 comment

Comments

@jinhailang
Copy link
Owner

jinhailang commented Jul 20, 2018

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 ,该函数做了两件事:

  • 创建 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.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_DONE 处理告一段落(子协程完成),还有协程(light thread)被挂起
  • NGX_AGIN 子协程未结束,继续等待下次唤醒重入
  • NGX_OK 当前阶段处理完成(子协程和运行在内的所有 light thread 都结束)

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

lua 协程

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

  • 非抢占
  • 同一时间内只有一个协程在运行

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

主要 API

  • coroutine.create() 创建 coroutine,返回 coroutine,协程创建后是挂起状态
  • coroutine.resume(co, a1, a2 ... an) 重新恢复运行 coroutine,第一个返回值(boolean)表示协程是否正常运行:
    • 第一个返回值为 true:后面的返回值就是 yield 的参数值(b1 ... bn)
    • 第一个返回值为 false:后面的返回值就是错误原因字符串
  • coroutine.yield(b1, b2 ... bn) 挂起当前协程,唤醒后返回对应 resume 的参数(a1 ... an)

有趣的实例

使用 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
Copy link
Owner Author

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

@jinhailang jinhailang changed the title ngx_lua 中协程的使用 ngx_lua 中协程的应用 Aug 16, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant