OpenResty万级并发依赖Nginx事件循环与Lua协程协作调度:每个请求在单worker内以同步风格编写,实则通过ngx.yield挂起、C回调唤醒,全程无系统线程切换,协程生命周期由Nginx严格控制。

OpenResty 的万级并发能力,不靠多线程,也不靠进程池,而是靠 Nginx 事件循环 + Lua 协程的协作式调度。它让每个请求在单个 worker 进程内“看起来像同步写,实际是非阻塞跑”。
协程不是线程,是用户态的执行上下文切换
Lua 协程(coroutine.create、coroutine.resume、coroutine.yield)本身不涉及操作系统调度,只是保存/恢复 Lua 栈和寄存器状态。OpenResty 没有直接用原生 coroutine.yield,而是封装了 ngx.yield(内部调用 lua_yield),并将其与 Nginx 的事件循环深度绑定。
关键点:
- 每个 HTTP 请求进入时,
lua-nginx-module为其创建一个主协程(main coroutine),对应一个独立的 Lua 栈 - 这个协程在 worker 进程的单一线程中运行,不抢占 CPU,也不需要锁
- 一旦调用
ngx.location.capture、resty.http:request或ngx.sleep,协程立刻yield,交出控制权 - Nginx 事件循环继续处理其他就绪连接,不等待 I/O 完成
挂起后怎么恢复?靠 C 回调触发 resume
协程挂起后不会自己醒来。真正唤醒它的,是 Nginx 底层注册的 C 回调函数——比如子请求返回时触发 ngx_http_lua_handle_subrequest,Redis 响应到达时走 ngx_http_lua_redis_handler。这些回调最终都会调用 ngx_http_lua_run_thread(),再用 lua_resume 恢复对应协程。
这意味着:
- 协程生命周期完全由 Nginx 控制,开发者只需写同步风格代码
- 没有手动 resume / yield 配对错误的风险(不像裸 Lua 协程)
- 如果异步操作失败(如超时、连接拒绝),回调仍会触发,但协程恢复后会收到错误值(如
nil, "timeout") -
pcall包裹的协程代码能捕获这类错误,但无法拦截底层 C 层崩溃(如内存越界)
为什么不用原生 coroutine.yield?
因为裸 coroutine.yield 只是暂停,没有告诉 Nginx “我等什么事件”。OpenResty 必须知道协程因何挂起(co_op 字段记录:是等子请求?等 cosocket?还是 sleep?),才能在正确时机、用正确参数恢复它。
典型区别:
- 你写
ngx.sleep(0.1)→ 实际注册一个定时器事件,并设置ctx->co_op = LUA_CO_SLEEP - 你写
ngx.location.capture("/api")→ 发起子请求,设ctx->co_op = LUA_CO_SUBREQ - 你误写
coroutine.yield()→ Nginx 不认识这个 yield,后续无法恢复,请求卡死或直接 500
所有 OpenResty 提供的异步 API,本质都是“挂起 + 注册事件 + 等待回调”,不是简单让出时间片。
协程调度的隐含成本在哪?
协程切换开销极低,但真实瓶颈常出现在别处:
- 每个协程独占一份 Lua 栈(默认 128KB),大量并发请求会快速吃光 worker 进程的栈空间
- 频繁调用
ngx.location.capture会创建大量子请求上下文,比直连 upstream 更重 - 滥用
resty.core的 JIT 优化可能反而导致热代码缓存污染,尤其在动态生成函数时 -
lua_shared_dict是唯一跨协程共享的数据结构,但读写需注意原子性(incr安全,set不保证)
真正容易被忽略的,是协程与 Nginx phase 的耦合关系:比如在 access_by_lua* 中发起耗时 cosocket 调用,会导致整个请求卡在 access 阶段,后续 rewrite / content 都无法并行准备——这不是协程的问题,而是阶段设计失当。

















