- 新增 lua-embed-analysis.md 技术分析文档 - 新增 lua-nginx-module 文档目录 - 更新 gitignore 允许跟踪 docs/lua-nginx-module/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
19 KiB
19 KiB
Golang Lua 运行时嵌入分析
本文档分析 Go 语言嵌入 Lua 运行时的方案,为 lolly 项目实现类似 lua-nginx-module 功能提供技术参考。
一、主流 Lua 运行时方案对比
1.1 可选方案
| 方案 | 语言 | 性能 | Lua版本 | 特点 |
|---|---|---|---|---|
| gopher-lua | 纯 Go | ~Python3 | Lua 5.1 + goto | 原生 Go 实现,goroutine/channel 集成 |
| go-lua | Go + C | ~原生Lua | Lua 5.1 | CGO 调用 C Lua,性能接近原生 |
| luaJIT (CGO) | C | 极高 | LuaJIT 2.1 | 最快,FFI 强大,但 CGO 开销 |
| glua (Shopify) | Go + C | 高 | Lua 5.2/5.3 | Shopify 废弃,不推荐 |
1.2 详细对比
gopher-lua
优势:
- 纯 Go 实现: 无 CGO 依赖,交叉编译友好
- 原生并发:
LChannel类型直接操作 Go channel - Context 支持:
SetContext(ctx)实现超时取消 - 字节码复用:
FunctionProto跨 LState 共享 - 安全沙箱:
SkipOpenLibs精细控制标准库加载
劣势:
- 性能约 Python3 级别,低于原生 Lua/LuaJIT
- 不支持 Lua 5.2+ 特性(bit32, utf8 等)
- GC 压力较大(大量 LValue 对象)
适用场景:
- 需要纯 Go、交叉编译
- 性能要求中等
- 需要与 Go goroutine/channel 深度集成
go-lua (CGO binding)
优势:
- 性能接近原生 Lua(通过 CGO 直接调用 C API)
- 支持 Lua 5.1 标准库完整功能
劣势:
- CGO 依赖,交叉编译复杂
- Go-C 边界开销(每次调用 ~50ns)
- 协程与 goroutine 交互困难(C 栈问题)
适用场景:
- 性能关键场景
- 已有 C Lua 生态依赖
- 可接受 CGO 复杂度
LuaJIT via CGO
优势:
- 极致性能: JIT 编译,接近 C 速度
- FFI 强大: 直接调用 C 函数无开销
- 内存高效: 更小的内存占用
劣势:
- LuaJIT 2.1 开发停滞
- CGO 集成复杂
- JIT 在某些环境受限(容器、安全限制)
- Go-LuaJIT 协程映射困难
适用场景:
- 性能极致要求
- 已有 OpenResty/LuaJIT 生态
- 运行环境可控
1.3 推荐选择
对于 lolly 项目,推荐 gopher-lua:
- 纯 Go: 与项目技术栈一致,交叉编译无障碍
- 并发集成: 天然支持 goroutine/channel,契合 Go HTTP 服务器架构
- 性能足够: Python3 级性能对脚本处理场景已够用
- 成熟稳定: yuin/gopher-lua 维护活跃,社区成熟
二、gopher-lua 核心 API
2.1 LState 状态机
import "github.com/yuin/gopher-lua"
// 创建 VM
L := lua.NewState(lua.Options{
SkipOpenLibs: true, // 安全:禁用默认库
IncludeGoStackTrace: true, // Panic 时输出 Go 调用栈
})
defer L.Close()
// 执行脚本
L.DoString("print('hello')")
L.DoFile("script.lua")
// Context 控制(超时)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
L.SetContext(ctx)
L.DoString("while true do end") // 5秒后自动取消
2.2 栈操作
// 基本栈操作
L.Push(lua.LNumber(42)) // 压入数字
L.Push(lua.LString("hello")) // 压入字符串
v := L.Get(-1) // 获取栈顶
L.Pop(2) // 弹出 2 个
// 全局变量
L.SetGlobal("myvar", lua.LNumber(100))
val := L.GetGlobal("myvar")
// Table 操作
tbl := L.NewTable()
L.SetField(tbl, "name", lua.LString("lolly"))
L.SetField(tbl, "version", lua.LString("0.2.0"))
L.SetGlobal("config", tbl)
2.3 函数注册
// Go 函数签名: func(L *lua.LState) int (返回压栈结果数)
func Double(L *lua.LState) int {
n := L.CheckInt(1) // 获取第1个参数
L.Push(lua.LNumber(n * 2)) // 压入结果
return 1 // 返回结果数
}
// 注册到全局
L.SetGlobal("double", L.NewFunction(Double))
// 批量注册到模块
mod := L.NewTable()
L.SetFuncs(mod, map[string]lua.LGFunction{
"double": Double,
"add": Add,
"sub": Sub,
})
L.SetGlobal("mathx", mod)
// 闭包(带 upvalue)
counter := 0
L.SetGlobal("counter", L.NewClosure(func(L *lua.LState) int {
counter++
L.Push(lua.LNumber(counter))
return 1
}))
2.4 调用 Lua 函数
// 受保护调用(推荐)
err := L.CallByParam(lua.P{
Fn: L.GetGlobal("myFunc"), // 函数
NRet: 1, // 期望返回值
Protect: true, // 拦截 panic
}, lua.LNumber(10), lua.LString("arg"))
if err != nil {
// 错误处理
}
ret := L.Get(-1) // 获取返回值
L.Pop(1) // 清理栈
2.5 模块加载
// 预加载自定义模块
L.PreloadModule("lolly", func(L *lua.LState) int {
mod := L.NewTable()
L.SetFuncs(mod, map[string]lua.LGFunction{
"say": SayHello,
"log": LogMessage,
"sleep": Sleep,
})
L.Push(mod)
return 1 // 返回模块表
})
// Lua 中使用
L.DoString(`
local lolly = require("lolly")
lolly.say("hello")
`)
三、协程支持
3.1 协程 API
// 创建协程线程
co, cancel := L.NewThread() // 共享全局状态
// 获取 Lua 协程函数
fn := L.GetGlobal("coroutine_func").(*lua.LFunction)
// Resume(恢复执行)
state, err, values := L.Resume(co, fn, lua.LNumber(10))
// state: lua.ResumeOK / lua.ResumeYield / lua.ResumeError
// values: yield 返回的值列表
// 检查状态
status := L.Status(co) // "suspended" / "running" / "normal" / "dead"
3.2 Yield 模式
Lua 侧:
function async_task()
print("start")
coroutine.yield("waiting") -- 挂起,返回值
print("continue")
return "done"
end
Go 侧:
co, _ := L.NewThread()
fn := L.GetGlobal("async_task").(*lua.LFunction)
// 第一次 resume
st, err, vals := L.Resume(co, fn)
if st == lua.ResumeYield {
fmt.Println("yielded:", vals[0]) // "waiting"
}
// 第二次 resume(恢复)
st, err, vals = L.Resume(co)
if st == lua.ResumeOK {
fmt.Println("done:", vals[0]) // "done"
}
3.3 与 Go Channel 集成
gopher-lua 提供 LChannel 类型,让 Lua 操作 Go channel:
// 创建 Go channel
ch := make(chan string, 10)
// 传递给 Lua
luaCh := lua.LChannel{Channel: ch}
L.SetGlobal("mychannel", luaCh)
// Lua 中操作
L.DoString(`
-- 发送
mychannel:send("hello")
-- 接收(阻塞)
local msg = mychannel:receive()
print(msg)
`)
四、错误处理
4.1 ApiError 结构
type ApiError struct {
Type ApiErrorType // Run/Syntax/Panic/Memory/File
Object LValue // Lua 错误对象
StackTrace string // 调用栈
Cause error // 底层错误
}
4.2 Panic Handler
// 注册 panic handler(类似 lua-nginx-module)
L.SetPanic(func(L *lua.LState) {
// 捕获 panic,记录日志
log.Error("Lua panic: ", L.Get(-1))
// 可以选择重建 VM 或返回错误
})
// 使用 PCall 保护调用
err := L.PCall(0, 0, nil)
if err != nil {
apiErr := err.(*lua.ApiError)
log.Error("Lua error: ", apiErr.StackTrace)
}
五、字节码缓存与复用
5.1 编译与复用
// 编译脚本为字节码(可跨 VM 复用)
proto, err := lua.CompileString("function foo() return 42 end", "foo.lua")
// 多个 LState 共享字节码
L1 := lua.NewState()
fn1 := L1.NewFunctionFromProto(proto)
L2 := lua.NewState()
fn2 := L2.NewFunctionFromProto(proto) // 无需重新编译
5.2 缓存设计(类似 lua-nginx-module)
type CodeCache struct {
mu sync.RWMutex
protos map[string]*lua.FunctionProto // MD5(key) -> proto
}
func (c *CodeCache) GetOrCompile(src string) (*lua.FunctionProto, error) {
key := md5Key(src)
c.mu.RLock()
proto, ok := c.protos[key]
c.mu.RUnlock()
if ok {
return proto, nil
}
// 编译并缓存
proto, err := lua.CompileString(src, key)
if err != nil {
return nil, err
}
c.mu.Lock()
c.protos[key] = proto
c.mu.Unlock()
return proto, nil
}
六、与 lolly 项目集成设计
6.1 架构映射
| lua-nginx-module | lolly (Go) 实现 |
|---|---|
ngx_http_lua_main_conf_t |
LuaWorker 结构,持有 VM |
ngx_http_lua_ctx_t |
LuaRequestCtx,请求上下文 |
ngx_http_lua_co_ctx_t |
LuaCoroutine,协程状态 |
| Phase Handlers | Middleware 集成点 |
| Filter Chain | Response Filter 中间件 |
6.2 核心结构设计
// internal/lua/engine.go
package lua
import (
"context"
"sync"
glua "github.com/yuin/gopher-lua"
)
// LuaWorker - worker 级单 VM(对应 ngx_http_lua_main_conf_t)
type LuaWorker struct {
L *glua.LState // 主 VM
codeCache *CodeCache // 字节码缓存
coroPool *CoroutinePool // 协程池
modules map[string]glua.LGFunction // 已加载模块
mu sync.RWMutex
}
// LuaRequestCtx - 请求上下文(对应 ngx_http_lua_ctx_t)
type LuaRequestCtx struct {
Worker *LuaWorker
Request *fasthttp.RequestCtx // fasthttp 请求
Coroutine *LuaCoroutine // 当前协程
Variables map[string]string // ngx.var
Output []byte // ngx.say 输出缓冲
Phase Phase // 当前阶段
Ctx context.Context // Go context
}
// LuaCoroutine - 协程状态(对应 ngx_http_lua_co_ctx_t)
type LuaCoroutine struct {
Thread *glua.LState // 协程线程
Status CoroutineStatus // running/suspended/dead
Parent *LuaCoroutine // 父协程
ResumeFunc func() // 恢复回调(类似 resume_handler)
}
// Phase - 处理阶段
type Phase int
const (
PhaseInit Phase = iota
PhaseAccess
PhaseContent
PhaseLog
PhaseHeaderFilter
PhaseBodyFilter
)
6.3 Worker 级单 VM 初始化
// internal/lua/worker.go
func NewLuaWorker() *LuaWorker {
// 创建 VM(安全模式)
L := glua.NewState(glua.Options{
SkipOpenLibs: true,
})
worker := &LuaWorker{
L: L,
codeCache: NewCodeCache(),
coroPool: NewCoroutinePool(L),
modules: make(map[string]glua.LGFunction),
}
// 加载必要标准库
worker.loadSafeLibs()
// 注册 lolly.* API
worker.registerLollyAPI()
return worker
}
func (w *LuaWorker) loadSafeLibs() {
// 只加载安全的库(禁用 os, io 等危险库)
w.L.CallByParam(glua.P{
Fn: w.L.NewFunction(glua.OpenBase),
Protect: true,
})
w.L.CallByParam(glua.P{
Fn: w.L.NewFunction(glua.OpenTable),
Protect: true,
})
w.L.CallByParam(glua.P{
Fn: w.L.NewFunction(glua.OpenString),
Protect: true,
})
w.L.CallByParam(glua.P{
Fn: w.L.NewFunction(glua.OpenMath),
Protect: true,
})
}
6.4 lolly.* API 注册
// internal/lua/api.go
func (w *LuaWorker) registerLollyAPI() {
// 创建 lolly 模块表
lollyMod := w.L.NewTable()
// 注册核心 API
w.L.SetFuncs(lollyMod, map[string]glua.LGFunction{
// 输出
"say": w.apiSay,
"print": w.apiPrint,
// 请求
"req": w.apiRequest,
"get_uri": w.apiGetURI,
"get_arg": w.apiGetArg,
"get_header": w.apiGetHeader,
// 响应
"resp": w.apiResponse,
"set_header": w.apiSetHeader,
"set_status": w.apiSetStatus,
// 变量
"var": w.apiVar,
"set_var": w.apiSetVar,
// 控制流
"exit": w.apiExit,
"sleep": w.apiSleep, // 异步 sleep
"throw": w.apiThrow,
// 日志
"log": w.apiLog,
"err": w.apiLogErr,
"warn": w.apiLogWarn,
"info": w.apiLogInfo,
})
w.L.SetGlobal("lolly", lollyMod)
// 兼容 nginx 命名(可选)
w.L.SetGlobal("ngx", lollyMod)
}
// apiSay - 输出内容
func (w *LuaWorker) apiSay(L *glua.LState) int {
ctx := getRequestCtx(L) // 从 LState 获取请求上下文
str := L.CheckString(1)
ctx.Output = append(ctx.Output, str...)
return 0
}
// apiGetURI - 获取请求 URI
func (w *LuaWorker) apiGetURI(L *glua.LState) int {
ctx := getRequestCtx(L)
uri := string(ctx.Request.URI().Path())
L.Push(glua.LString(uri))
return 1
}
// apiSleep - 异步睡眠(yield 实现)
func (w *LuaWorker) apiSleep(L *glua.LState) int {
ctx := getRequestCtx(L)
ms := L.CheckInt(1)
// 创建定时器,yield 当前协程
ctx.Coroutine.ResumeFunc = func() {
// 定时器到期后 resume
ctx.Worker.ResumeCoroutine(ctx.Coroutine)
}
// 注册定时器
go func() {
time.Sleep(time.Duration(ms) * time.Millisecond)
ctx.Coroutine.ResumeFunc()
}()
// Yield
L.Yield(glua.LNumber(ms))
return 0
}
6.5 请求上下文绑定
// internal/lua/context.go
// 请求开始时绑定上下文
func (w *LuaWorker) NewRequestCtx(req *fasthttp.RequestCtx) *LuaRequestCtx {
ctx := &LuaRequestCtx{
Worker: w,
Request: req,
Variables: make(map[string]string),
Phase: PhaseAccess,
Ctx: req,
}
// 创建请求协程
ctx.Coroutine = w.coroPool.Acquire()
ctx.Coroutine.Parent = nil
// 绑定到 LState(使用 exdata 模式)
// gopher-lua 不支持 exdata,使用全局变量
ctx.Coroutine.Thread.SetGlobal("__lolly_req", glua.LUserData{
Value: ctx,
Metatable: w.L.NewTable(), // 可设置 __index 方法
})
return ctx
}
// 从 LState 获取请求上下文
func getRequestCtx(L *glua.LState) *LuaRequestCtx {
ud := L.GetGlobal("__lolly_req")
if ud == glua.LNil {
return nil
}
return ud.(*glua.LUserData).Value.(*LuaRequestCtx)
}
6.6 Yield/Resume 与 Go 异步集成
关键设计: Lua yield → Go channel → 恢复执行
// internal/lua/coroutine.go
type CoroutinePool struct {
L *glua.LState
free chan *LuaCoroutine
resume chan *LuaCoroutine // 恢复队列
}
func (p *CoroutinePool) Acquire() *LuaCoroutine {
select {
case co := <-p.free:
return co
default:
// 创建新协程
thread, _ := p.L.NewThread()
return &LuaCoroutine{
Thread: thread,
Status: StatusSuspended,
}
}
}
// 执行脚本(支持 yield)
func (ctx *LuaRequestCtx) RunScript(script string) error {
// 获取或编译字节码
proto, err := ctx.Worker.codeCache.GetOrCompile(script)
if err != nil {
return err
}
fn := ctx.Coroutine.Thread.NewFunctionFromProto(proto)
// 开始执行
state, err, _ := ctx.Worker.L.Resume(ctx.Coroutine.Thread, fn)
for state == glua.ResumeYield {
// 协程 yield,等待恢复信号
ctx.Coroutine.Status = StatusSuspended
// 等待 ResumeFunc 触发
select {
case <-ctx.Worker.coroPool.resume:
// 恢复执行
state, err, _ = ctx.Worker.L.Resume(ctx.Coroutine.Thread)
case <-ctx.Ctx.Done():
// 请求超时/取消
return ctx.Ctx.Err()
}
}
if state == glua.ResumeError {
return err
}
return nil
}
6.7 中间件集成
// internal/middleware/lua_middleware.go
package middleware
import (
"rua.plus/lolly/internal/lua"
"github.com/valyala/fasthttp"
)
type LuaMiddleware struct {
worker *lua.LuaWorker
config LuaConfig
}
func (m *LuaMiddleware) Process(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
// 创建 Lua 请求上下文
luaCtx := m.worker.NewRequestCtx(ctx)
// 执行 access_by_lua
if m.config.AccessScript != "" {
if err := luaCtx.RunScript(m.config.AccessScript); err != nil {
ctx.Error("Lua error: "+err.Error(), 500)
return
}
}
// 执行 content_by_lua(如果有)
if m.config.ContentScript != "" {
luaCtx.Phase = lua.PhaseContent
if err := luaCtx.RunScript(m.config.ContentScript); err != nil {
ctx.Error("Lua error: "+err.Error(), 500)
return
}
// 输出 Lua 内容
ctx.Write(luaCtx.Output)
return
}
// 继续下一个 handler
next(ctx)
// 执行 log_by_lua
if m.config.LogScript != "" {
luaCtx.Phase = lua.PhaseLog
luaCtx.RunScript(m.config.LogScript)
}
}
}
七、实现路线图
7.1 阶段一:基础嵌入(Week 1-2)
| 任务 | 文件 | 说明 |
|---|---|---|
| 添加 gopher-lua 依赖 | go.mod |
github.com/yuin/gopher-lua |
| 创建 LuaWorker | internal/lua/worker.go |
单 VM 管理 |
| 代码缓存 | internal/lua/cache.go |
字节码缓存 |
| 基础 API 注入 | internal/lua/api.go |
say/print/get_uri 等 |
7.2 阶段二:请求集成(Week 3-4)
| 任务 | 文件 | 说明 |
|---|---|---|
| LuaRequestCtx | internal/lua/context.go |
请求上下文绑定 |
| LuaMiddleware | internal/middleware/lua_middleware.go |
中间件集成 |
| 配置支持 | internal/config/lua.go |
Lua 指令解析 |
7.3 阶段三:异步支持(Week 5-6)
| 任务 | 文件 | 说明 |
|---|---|---|
| CoroutinePool | internal/lua/coroutine.go |
协程池 |
| Yield/Resume | internal/lua/coroutine.go |
异步 sleep 等 |
| LChannel 集成 | internal/lua/channel.go |
Go channel 操作 |
7.4 阶段四:高级功能(Week 7-10)
| 任务 | 说明 |
|---|---|
| 共享内存 (shdict) | Go sync.Map + Lua Table API |
| Cosocket | 非阻塞 TCP socket |
| 子请求 | 内部 location capture |
| Timer | ngx.timer.at 实现 |
| Balancer | 动态负载均衡 |
八、性能考量
8.1 性能优化点
| 优化 | 方法 |
|---|---|
| 字节码缓存 | 预编译脚本,跨请求复用 |
| 协程池 | 预创建协程,避免频繁创建销毁 |
| 减少 GC | 使用 LTable 而非大量小对象 |
| 并行执行 | 多 worker 独立 VM,无锁竞争 |
8.2 性能基准
// 建议基准测试
func BenchmarkLuaSimple(b *testing.B) {
L := lua.NewState()
defer L.Close()
L.DoString("function test() return 1 + 1 end")
b.ResetTimer()
for i := 0; i < b.N; i++ {
L.CallByParam(lua.P{Fn: L.GetGlobal("test")})
}
}
func BenchmarkLuaWithCtx(b *testing.B) {
// 带请求上下文的基准
}
九、安全考虑
9.1 安全沙箱
// 禁用危险库
L := lua.NewState(lua.Options{SkipOpenLibs: true})
// 只加载安全库
safeLibs := []glua.LGFunction{
glua.OpenBase, // 基础
glua.OpenTable, // 表操作
glua.OpenString, // 字符串
glua.OpenMath, // 数学
// 禁用: OpenOS, OpenIO, OpenPackage (部分)
}
9.2 资源限制
// Context 超时
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
L.SetContext(ctx)
// 内存限制(通过 Options)
L := lua.NewState(lua.Options{
RegistryMaxSize: 1024 * 1024, // 限制注册表大小
})
// CPU 限制(通过 goroutine 监控)
go func() {
time.Sleep(5 * time.Second)
cancel() // 强制取消
}()
十、参考资源
- gopher-lua GitHub - 主仓库
- gopher-lua API 文档 - GoDoc
- Lua 5.1 手册 - 语言参考
- lua-nginx-module 文档 - 架构参考(已生成)