refactor(docs): 重构文档目录结构,nginx 文档移至子目录
将 docs/ 根目录下的 nginx 相关文档统一移动到 docs/nginx/ 子目录, 提高文档组织性和可维护性。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0a332e8cef
commit
972eab4267
@ -1,233 +0,0 @@
|
||||
# 配置参考文档
|
||||
|
||||
## 目录
|
||||
|
||||
- [变量系统](#变量系统)
|
||||
- [DNS 解析器](#dns-解析器)
|
||||
- [访问日志格式](#访问日志格式)
|
||||
|
||||
---
|
||||
|
||||
## 变量系统
|
||||
|
||||
Lolly 支持 nginx 风格的变量系统,可用于访问日志格式、代理请求头和 URL 重写规则。
|
||||
|
||||
### 内置变量
|
||||
|
||||
| 变量名 | 说明 | 示例值 |
|
||||
|--------|------|--------|
|
||||
| `$host` | 请求的主机名(Host 头) | `example.com` |
|
||||
| `$remote_addr` | 客户端 IP 地址 | `192.168.1.1` |
|
||||
| `$remote_port` | 客户端端口 | `54321` |
|
||||
| `$request_uri` | 原始请求 URI(包含查询参数) | `/api/users?page=1` |
|
||||
| `$uri` | 解码后的 URI 路径 | `/api/users` |
|
||||
| `$args` | 查询参数字符串 | `page=1&limit=10` |
|
||||
| `$request_method` | HTTP 请求方法 | `GET`, `POST` |
|
||||
| `$scheme` | 协议 | `http`, `https` |
|
||||
| `$server_name` | 服务器名称 | `localhost` |
|
||||
| `$server_port` | 服务器端口 | `8080` |
|
||||
| `$status` | HTTP 响应状态码 | `200`, `404` |
|
||||
| `$body_bytes_sent` | 发送的响应体字节数 | `1024` |
|
||||
| `$request_time` | 请求处理时间(秒) | `0.050` |
|
||||
| `$time_local` | 本地时间 | `08/Apr/2026:11:04:58 +0800` |
|
||||
| `$time_iso8601` | ISO8601 格式时间 | `2026-04-08T11:04:58+08:00` |
|
||||
| `$request_id` | 唯一请求标识符 | `uuid` |
|
||||
|
||||
### 动态 HTTP 头变量
|
||||
|
||||
以 `$http_` 开头的变量用于获取 HTTP 请求头值:
|
||||
|
||||
- `$http_user_agent` - User-Agent 头
|
||||
- `$http_referer` - Referer 头
|
||||
- `$http_x_forwarded_for` - X-Forwarded-For 头
|
||||
- 其他任意请求头:`$http_header_name`
|
||||
|
||||
### 变量格式
|
||||
|
||||
支持两种格式:
|
||||
|
||||
1. **简单格式**: `$var`
|
||||
```
|
||||
$host $uri
|
||||
```
|
||||
|
||||
2. **花括号格式**: `${var}`
|
||||
```
|
||||
${host}:8080
|
||||
${scheme}://${host}${uri}
|
||||
```
|
||||
|
||||
### 在代理请求头中使用变量
|
||||
|
||||
```yaml
|
||||
proxy:
|
||||
- path: /api
|
||||
targets:
|
||||
- url: http://backend:8080
|
||||
headers:
|
||||
set_request:
|
||||
X-Real-IP: "$remote_addr"
|
||||
X-Forwarded-Host: "$host"
|
||||
X-Request-ID: "$request_id"
|
||||
```
|
||||
|
||||
### 在访问日志中使用变量
|
||||
|
||||
```yaml
|
||||
logging:
|
||||
access:
|
||||
format: '$remote_addr - $remote_user [$time_local] "$request_method $uri $scheme" $status $body_bytes_sent'
|
||||
```
|
||||
|
||||
### 自定义变量
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
set:
|
||||
app_name: "lolly"
|
||||
version: "1.0.0"
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- `$request_id` 为内置变量,自动为每个请求生成唯一 UUID,无需配置
|
||||
- 自定义变量名不能与内置变量冲突
|
||||
- 变量名只允许字母、数字、下划线
|
||||
|
||||
---
|
||||
|
||||
## DNS 解析器
|
||||
|
||||
Lolly 内置 DNS 解析器,支持动态解析后端服务域名。
|
||||
|
||||
### 配置选项
|
||||
|
||||
```yaml
|
||||
resolver:
|
||||
enabled: true # 是否启用
|
||||
addresses: # DNS 服务器地址列表
|
||||
- "8.8.8.8:53"
|
||||
- "8.8.4.4:53"
|
||||
valid: 30s # 缓存有效期(TTL)
|
||||
timeout: 5s # DNS 查询超时
|
||||
ipv4: true # 查询 IPv4 地址
|
||||
ipv6: false # 查询 IPv6 地址
|
||||
cache_size: 1024 # 缓存最大条目数
|
||||
```
|
||||
|
||||
### 功能特性
|
||||
|
||||
- **DNS 缓存**: 按 TTL 缓存解析结果,减少 DNS 查询延迟
|
||||
- **后台刷新**: 自动在 TTL/2 时刷新缓存,避免过期
|
||||
- **故障转移**: 解析失败时使用缓存 IP 继续服务
|
||||
- **健康检查**: 首次解析失败标记目标不健康
|
||||
|
||||
### 使用场景
|
||||
|
||||
当后端目标使用域名时,DNS 解析器自动生效:
|
||||
|
||||
```yaml
|
||||
proxy:
|
||||
- path: /api
|
||||
targets:
|
||||
- url: http://backend.example.com:8080 # 使用域名
|
||||
weight: 1
|
||||
```
|
||||
|
||||
### 监控指标
|
||||
|
||||
通过状态端点获取 DNS 解析统计:
|
||||
|
||||
- `CacheHits` - 缓存命中次数
|
||||
- `CacheMisses` - 缓存未命中次数
|
||||
- `CacheEntries` - 当前缓存条目数
|
||||
- `ResolveErrors` - 解析错误次数
|
||||
- `AverageLatency` - 平均解析延迟
|
||||
|
||||
---
|
||||
|
||||
## 访问日志格式
|
||||
|
||||
### nginx 兼容格式
|
||||
|
||||
Lolly 默认提供 nginx 兼容的访问日志格式:
|
||||
|
||||
```yaml
|
||||
logging:
|
||||
access:
|
||||
format: '$remote_addr - $remote_user [$time] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"'
|
||||
```
|
||||
|
||||
示例输出:
|
||||
```
|
||||
192.168.1.1 - - [08/Apr/2026:11:04:58 +0800] "GET /api/users HTTP/1.1" 200 1024 "-" "Mozilla/5.0"
|
||||
```
|
||||
|
||||
### JSON 格式
|
||||
|
||||
设置格式为 `json` 输出结构化日志:
|
||||
|
||||
```yaml
|
||||
logging:
|
||||
access:
|
||||
format: 'json'
|
||||
```
|
||||
|
||||
示例输出:
|
||||
```json
|
||||
{
|
||||
"remote_addr": "192.168.1.1",
|
||||
"request": "GET /api/users HTTP/1.1",
|
||||
"status": 200,
|
||||
"body_bytes_sent": 1024,
|
||||
"http_user_agent": "Mozilla/5.0"
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义格式
|
||||
|
||||
使用变量创建自定义格式:
|
||||
|
||||
```yaml
|
||||
logging:
|
||||
access:
|
||||
format: '$remote_addr $request_method $uri $status $request_time'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整配置示例
|
||||
|
||||
```yaml
|
||||
server:
|
||||
listen: ":8080"
|
||||
name: "localhost"
|
||||
|
||||
proxy:
|
||||
- path: /api
|
||||
targets:
|
||||
- url: http://backend.example.com:8080
|
||||
headers:
|
||||
set_request:
|
||||
X-Real-IP: "$remote_addr"
|
||||
X-Forwarded-Host: "$host"
|
||||
X-Request-ID: "$request_id"
|
||||
|
||||
resolver:
|
||||
enabled: true
|
||||
addresses:
|
||||
- "8.8.8.8:53"
|
||||
valid: 30s
|
||||
timeout: 5s
|
||||
|
||||
variables:
|
||||
set:
|
||||
app_name: "lolly"
|
||||
|
||||
logging:
|
||||
access:
|
||||
format: '$remote_addr - $remote_user [$time_local] "$request_method $uri $scheme" $status $body_bytes_sent'
|
||||
path: "/var/log/lolly/access.log"
|
||||
error:
|
||||
level: "info"
|
||||
path: "/var/log/lolly/error.log"
|
||||
```
|
||||
@ -1,835 +0,0 @@
|
||||
# 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**:
|
||||
|
||||
1. **纯 Go**: 与项目技术栈一致,交叉编译无障碍
|
||||
2. **并发集成**: 天然支持 goroutine/channel,契合 Go HTTP 服务器架构
|
||||
3. **性能足够**: Python3 级性能对脚本处理场景已够用
|
||||
4. **成熟稳定**: yuin/gopher-lua 维护活跃,社区成熟
|
||||
|
||||
---
|
||||
|
||||
## 二、gopher-lua 核心 API
|
||||
|
||||
### 2.1 LState 状态机
|
||||
|
||||
```go
|
||||
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 栈操作
|
||||
|
||||
```go
|
||||
// 基本栈操作
|
||||
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
|
||||
// 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 函数
|
||||
|
||||
```go
|
||||
// 受保护调用(推荐)
|
||||
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 模块加载
|
||||
|
||||
```go
|
||||
// 预加载自定义模块
|
||||
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
|
||||
|
||||
```go
|
||||
// 创建协程线程
|
||||
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 侧:
|
||||
```lua
|
||||
function async_task()
|
||||
print("start")
|
||||
coroutine.yield("waiting") -- 挂起,返回值
|
||||
print("continue")
|
||||
return "done"
|
||||
end
|
||||
```
|
||||
|
||||
Go 侧:
|
||||
```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
|
||||
// 创建 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 结构
|
||||
|
||||
```go
|
||||
type ApiError struct {
|
||||
Type ApiErrorType // Run/Syntax/Panic/Memory/File
|
||||
Object LValue // Lua 错误对象
|
||||
StackTrace string // 调用栈
|
||||
Cause error // 底层错误
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Panic Handler
|
||||
|
||||
```go
|
||||
// 注册 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 编译与复用
|
||||
|
||||
```go
|
||||
// 编译脚本为字节码(可跨 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)
|
||||
|
||||
```go
|
||||
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 核心结构设计
|
||||
|
||||
```go
|
||||
// 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 初始化
|
||||
|
||||
```go
|
||||
// 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 注册
|
||||
|
||||
```go
|
||||
// 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 请求上下文绑定
|
||||
|
||||
```go
|
||||
// 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 → 恢复执行
|
||||
|
||||
```go
|
||||
// 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 中间件集成
|
||||
|
||||
```go
|
||||
// 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 性能基准
|
||||
|
||||
```go
|
||||
// 建议基准测试
|
||||
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 安全沙箱
|
||||
|
||||
```go
|
||||
// 禁用危险库
|
||||
L := lua.NewState(lua.Options{SkipOpenLibs: true})
|
||||
|
||||
// 只加载安全库
|
||||
safeLibs := []glua.LGFunction{
|
||||
glua.OpenBase, // 基础
|
||||
glua.OpenTable, // 表操作
|
||||
glua.OpenString, // 字符串
|
||||
glua.OpenMath, // 数学
|
||||
// 禁用: OpenOS, OpenIO, OpenPackage (部分)
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 资源限制
|
||||
|
||||
```go
|
||||
// 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](https://github.com/yuin/gopher-lua) - 主仓库
|
||||
- [gopher-lua API 文档](https://pkg.go.dev/github.com/yuin/gopher-lua) - GoDoc
|
||||
- [Lua 5.1 手册](https://www.lua.org/manual/5.1/) - 语言参考
|
||||
- [lua-nginx-module 文档](../lua-nginx-module/) - 架构参考(已生成)
|
||||
Loading…
x
Reference in New Issue
Block a user