Lua 诊断控制台设计
本文定义 shield_ops 下 Lua 诊断控制台与 Lua 内存观测能力的设计边界。它是 shield_ops 的专项设计稿,覆盖 local admin socket / console / Lua inspect 观测面;各层能力的实现状态见文末「当前状态」一节(控制台本体、L1/L2 与 eval 均已落地)。
如果与总纲或运维主文档冲突,以 架构总纲 和 运维运行时语义 为准。
目标
本设计解决两个实际问题:
- 在不侵入业务 Lua API 的前提下,查看某个 service 当前 Lua VM 的运行状态。
- 在开发、测试、预发环境中,快速定位“哪个业务 service 在涨内存”“哪些 table/coroutine/timer 持有了状态”。
设计目标:
- 复用
shield_ops管理平面,不把调试入口做成普通业务 service。 - 以 每个 service 一个 Lua VM 的模型为前提,按 service 精确观测。
- 默认提供只读诊断;高风险控制能力必须显式开启。
- 不暴露 CAF、底层线程对象或完整业务 payload。
- 所有调试能力都可关闭,不影响最小 runtime 启动。
非目标
本文明确不做:
- 不把任意 Lua 执行作为默认能力。
- 不承诺远程完整 debugger(断点、单步、任意 coroutine 栈回溯、任意局部变量读写)。
- 不把插件连接池、socket buffer、CAF mailbox 内部细节伪装成 Lua 内存数据。
- 不提供业务 Lua API,如
shield.ops.*或shield.debug.*。 - 不把 Telnet 协议本身变成设计中心。目标是“文本控制台可交互”,而不是协议兼容性本身。
所属模块与入口
此能力属于 shield_ops,不属于 shield_core、shield_lua 或普通业务 service。
public surface:
local admin socket
console
optional HTTP debug endpoints规则:
- 默认入口是 local Unix domain socket(AF_UNIX,Linux / macOS / Windows 10 1803+ 通用);loopback TCP 仅作 opt-in 应急 / 转发通道,强制 bind
127.0.0.1/::1。详见下文「传输与客户端分层」。 - 是否允许
telnet/nc连接只是传输层实现细节,不改变语义 owner。 - 业务 Lua service 不直接 import、require 或调用此能力。
依赖边界
shield_ops 对 Lua 诊断控制台只拥有“读取和调度”的职责。
依赖方向:
shield_lua / shield_net / shield_plugin / shield_cluster
-> runtime snapshot / inspect providers
-> shield_ops
-> console / admin socket / HTTP规则:
shield_ops只能读取只读快照或通过 owner 线程执行受控 inspect 任务。shield_ops不能跨线程操作业务 service 的shd::state/lua_State*。例外是 console 自有的沙箱 VM:语法检查用临时luaL_newstate,eval在 console 线程创建自己的 VM 并同步执行——这些lua_State为 console 线程单线程私有,从不与任何 actor 或其他线程共享。shield_lua拥有 Lua VM inspect provider 的语义与实现。shield_ops不能通过 console 反向改写 core 语义。
传输与客户端分层
控制台的“连接方式”与“交互体验”是两层,分开实现:
- 服务器只做协议端点,不做终端。行编辑、补全、历史、着色等富体验全部由客户端实现,服务器不参与 telnet 选项协商、不维护终端状态。
- 应用协议(行文本 + 可选
--json)与传输解耦,同一套协议可跑在任意传输上。 - 监听端 in-process 于 server,不单独起 ops daemon:Lua VM 活在 server 进程,
lua.inspect/lua.eval必须在 server 进程执行,独立进程无法隔离 eval 的崩溃风险,只会多一条内部控制通道与一组故障点。这与 Erlang shell / Node inspector / Java JMX 的做法一致。 - 客户端(
shield opsTUI / Web 控制台 /curl)是独立 binary,通过 endpoint 连接。 - 可选硬化:监听端按需 attach(如
SIGUSR1触发临时拉起),平时不开 socket,把暴露面压到只在真正调试时存在。
传输
默认本地 Unix domain socket。Linux / macOS / Windows 10 1803+ 均支持 AF_UNIX,实现上复用同一套 local::stream_protocol,单一代码路径。loopback TCP 作为 opt-in,仅在需要 nc / curl 应急或跨 SSH 从笔记本连远端服务时启用。
| 传输 | 平台 | 默认 | 鉴权 |
|---|---|---|---|
| Unix domain socket(AF_UNIX) | Linux / macOS / Win10+ | 是 | 文件权限 + 对端凭据(Linux/macOS)/ NTFS ACL(Windows) |
| 命名管道 | Windows(可选) | 否 | 命名管道 ACL |
| loopback TCP | 全平台 | 否(opt-in) | 强制环回 bind + 显式 token |
选 socket 的理由同 Docker / systemd / PostgreSQL / ssh-agent:路径不抢端口、本地 gating 不用管 token。
鉴权
- Linux
SO_PEERCRED、macOSgetpeereid直接拿到连接方 uid/pid,用于审计。 - Windows AF_UNIX 无对端凭据机制,靠 NTFS ACL 卡住“谁能连”,审计层面拿不到对端 uid/pid,粒度依赖客户端自报。
- TCP(opt-in)必须额外提供 token,且 bind 强制
127.0.0.1/::1,配置层拒绝非环回地址。
客户端分层
| 客户端 | 形态 | 场景 |
|---|---|---|
shield ops TUI | 终端,linenoise / crossline 行编辑 + 补全 + 历史 | 日常主入口,SSH 场景 |
| Web 控制台 | xterm.js + WebSocket,复用 HTTP /ops/* | 最接近 Jupyter / IPython 体验 |
shield ops exec / curl | 一行 in / 一行 out,--json | 脚本化、CI |
nc / telnet | 裸字节流 | 仅应急,无补全,标为 degraded |
参考:Elixir IEx 是“对 per-node VM 做富 REPL”的最近参照;skynet 的 telnet debug console 是反面教材。
endpoint 与自动判断
传输选择统一成一条带 scheme 前缀的 endpoint 字符串,scheme 决定传输,避免“先试 socket 再试 tcp”的猜测式判断:
unix:///run/shield/ops.sock
tcp://127.0.0.1:9090- 显式:
shield ops --endpoint unix:///run/shield/ops.sock - 环境变量:
$SHIELD_OPS_ENDPOINT - 默认(自动判断):server 启动时把自身 endpoint 写进 discovery 文件(与 pid 文件同目录,如
/run/shield/ops.endpoint),客户端默认读该文件——读到unix:走 socket、读到tcp:走 TCP;文件不存在时回退到规范化 socket 路径。
富体验所需的协议原语
除命令执行外,协议需额外支持两个请求,二者均复用 L2 inspect 路径、在目标 service 的 owner 线程上执行:
completion:给定部分输入与 service,返回候选列表(枚举 module table key / 子命令)。introspection:obj?风格,返回类型 / 大小 / 最近采样摘要。
基本模型
Shield 当前推荐 Lua VM 模型是每个 service 一个 Lua VM。因此 Lua 诊断控制台的最小观测单位是:
- 一个本地 service
- 对应一个 Lua VM
- 对应一组协程、timer、pending call 和业务 table
这意味着控制台命令默认采用:
lua.inspect <service> ...
lua.eval <service> ...而不是:
lua.inspect global ...
lua.eval all ...跨 service 聚合视图由 shield_ops 组合多个 service 的快照给出,但不改变“按 service 归属”的底层模型。
能力分层
Lua 诊断能力分三层:
| 层级 | 说明 | 默认策略 |
|---|---|---|
| L1 Snapshot | 只读快照,返回稳定字段 | 开发/测试可启用;生产可局部启用 |
| L2 Inspect | 在目标 VM owner 线程上执行受限 inspect 逻辑 | 开发/测试默认可启;生产默认关闭 |
| L3 Eval | 执行用户提供的 Lua 表达式/代码片段 | 仅开发/预发显式启用;生产默认禁止 |
默认要求:
- 先落地 L1 和 L2。
- L3 必须晚于 L1/L2,且依赖明确的安全开关。
L1:只读快照
L1 不执行任意 Lua 代码,只暴露运行时可稳定采集的数据。
最小字段
每个 service 的 Lua 快照至少应包含:
{
"service": "player.42",
"vm": {
"mode": "per_service",
"memory_kb": 2048
},
"runtime": {
"pending_calls": 1,
"timers": 5,
"coroutines": 2,
"pending_tasks": 0
}
}字段解释:
memory_kb:Lua VM 当前堆占用。语义限定为 Lua allocator 视角,不等于进程 RSS。pending_calls:当前 coroutine-aware call 未完成数。timers:该 service 拥有的 timer 数。coroutines:该 service 当前挂起或活跃 coroutine 数。pending_tasks:fork/task 队列中属于该 service 的任务数。
只读快照要求
- 采集必须低开销、可界定延迟。
- 尽可能通过现有 runtime 计数器或 owner 线程下安全读取获得。
- 不遍历完整业务对象图。
- 不输出完整业务 table 内容。
L2:受限 inspect
L2 允许在目标 service 的 Lua VM 上执行受控、预定义的 inspect 逻辑,但不是任意 Lua 执行。
目标
它主要用于回答:
- 哪个 table 占内存最多?
- 哪些 cache / map / registry 在涨?
- 某个 service 当前有哪些 coroutine、timer、pending call?
- 两次采样之间,哪些对象计数变化最大?
命令面
建议最小命令集:
lua.inspect <service> summary
lua.inspect <service> memory
lua.inspect <service> refs
lua.inspect <service> coroutines
lua.inspect <service> timers
lua.inspect <service> pending_calls
lua.snapshot <service> [name] [refs]
lua.diff <service> <snapshot_a> <snapshot_b>含义:
summary:聚合显示该 service 当前 Lua 诊断摘要。memory:owner 线程一次 dispatch 内联 GC 采样与 retainers 摘要(同refs的 fork task + 2s bounded wait 模型):total_bytes/memory_kb(GCCOUNT+GCCOUNTB精确采样)、gc(collector 运行标志、模式 incremental|generational、六个 collector 参数 minormul/majorminor/minormajor/pause/stepmul/stepsize)、retainers(从 module table 出发 depth 4 / 20000 节点一次遍历的头:nodes_visited/truncated/top_tables)。owner 忙时以memory dispatch timeout (owner busy)报错,不挂起 console。refs:从 service module table 出发,按深度/数量限制遍历对象图,输出 top tables。coroutines:owner 线程一次 dispatch 枚举 live 协程注册表(同memory的 fork task + 2s bounded wait 模型):total、by_status(suspended/finished/error/other,按 owner 线程lua_status分类——协程恒由 owner 线程 resume,枚举期间无并发驱动)、entries(每协程:status、origin(首驱来源 handler/fork/timer/client_rpc)、resumes(C++ resume 计数)、last_resume(最近 C++ resume 来源 dispatch/call-response/call-timeout;Lua 自驱动 wrap 协程的 resume 无 C++ 观测点,如实停在末次 C++ 驱动)、age_ms、driving、waiting_call(服务中的 call session——callee 侧或 caller 侧,无则 null)),按最近 resume 排序保留 32 条并附truncated。owner 忙时以coroutines dispatch timeout (owner busy)报错。timers:列出 timer 数量、最近到期时间、重复/单次类型摘要。pending_calls:列出等待返回的 coroutine-aware call 摘要。snapshot:保存一份 Lua 内存/对象图摘要快照。第三参数refs使快照附带 owner 线程对象图摘要(同refs遍历投影:nodes_visited/truncated/counts/top_tables);owner 忙时 gauges 照常冻结、摘要缺口以refs_error如实记录。diff:比较两份快照,定位增长项。两端均采样过对象图时,delta 附refs(counts 逐字段 delta + top_tables 按 path 匹配的 added/removed/delta);任一端未采样则refs: null,不虚构基线。
执行模型
L2 inspect 任务必须满足:
- 由
shield_ops请求 - 投递到目标 service 所属 Lua VM 的 owner 线程执行
- 在严格时间预算内完成
- 结果被序列化为只读摘要返回
规则:
- inspect 逻辑不能阻塞业务线程过长时间。
- inspect 结果必须做大小限制。
- inspect 必须支持超时和截断。
- inspect 不应调用业务 handler,也不应发送业务消息。
输出限制
默认输出限制:
- 最大遍历节点数
- 最大递归深度
- 最大返回字节数
- 最大字符串截断长度
如果超过限制,结果必须明确标出:
{
"truncated": true,
"reason": "node_limit"
}L3:Lua eval
L3 是高风险能力,仅作为开发和预发环境的显式调试工具。
能力定位
lua.eval 的目标是快速验证状态,而不是提供远程热修平台。
推荐形态:
lua.eval <service> <expr>
lua.exec <service>
<multiline code>
.建议优先只支持表达式或受限 chunk,返回单次执行结果。
安全策略
lua.eval 默认必须满足(括号内为当前实现状态):
- 仅在 eval 显式开启时可用(当前 console
eval仅受console.enabled门控;ops.lua_console.*/ops.console.*配置键全仓不存在;token/开关门控目前只存在于 HTTP/ops/eval——http.eval_enabled+http.eval_token) - 默认仅允许
localhost或 local admin socket - 需要独立权限,不与普通只读命令共用最低权限
- 必须有执行超时(当前 console
eval没有执行超时;attach对目标 service 的单次执行等待有 5s 超时) - 必须有输出大小上限(当前 console
eval没有输出上限) - 默认禁止文件系统 / OS / 网络相关危险能力
生产环境策略
生产默认:
lua.inspect可按需启用lua.snapshot/lua.diff可按需启用lua.eval必须关闭
若确需开启,必须满足:
- 仅本地入口
- 有审计日志
- 有显式 session 级鉴权
- 有速率限制
- 有强制超时与输出上限
Lua 内存语义
Lua 诊断控制台里的“内存”只表示 Lua 运行时可观测的那部分,不应误导为整个进程内存。
能观测的内容
- Lua VM 堆占用
- service module table 及其可达 table 数量
- coroutine 数与状态摘要
- timer / pending call / pending task 数量
- 业务 cache、map、registry 等 Lua 对象的估算大小或节点数
不能直接等同的内容
- 进程 RSS
- C++ 容器占用
- socket send/recv buffer
- plugin 连接池内存
- OpenSSL / Boost / CAF 内部内存
这些内容应由其他 snapshot/provider 提供,例如:
- plugin pool stats
- listener/session stats
- process-level resource stats
Snapshot 与 Diff 语义
Lua 内存排查最实用的能力不是单次 dump,而是 snapshot + diff。
Snapshot
快照记录的是摘要,不是完整对象镜像。建议包含:
- 采样时间
- service 名
- Lua VM 总内存
- top tables / caches / registries
- coroutine / timer / pending call 摘要
- 截断标记
Diff
lua.diff 输出关注:
- 总内存变化
- table 数变化
- top growth retainers
- coroutine / timer / pending call 是否同步增长
这样可以区分:
- 真正的 Lua table 泄漏
- timer 未清理
- pending call 堆积
- 消息堆积/背压引发的“看起来像内存涨”的现象
线程与时序要求
这是该设计最关键的约束。
规则:
- 同一时间最多一个 OS thread 进入同一 Lua VM。
shield_ops不直接读取目标 VM 的内部 Lua 对象。- inspect/eval 都必须通过 owner 线程串行执行。
- owner 线程如果繁忙,控制台命令可以排队、超时或返回
busy。
建议错误码:
module_unavailableservice_not_foundservice_deadinspect_timeoutinspect_busyinspect_truncatedpermission_deniedlua_eval_disabledunsafe_operation_forbidden
输出与脱敏
默认输出应该是摘要,不是原始业务状态转储。
允许默认暴露:
memory total
table counts
top retainers summary
coroutine count
timer count
pending call count
service names
cluster node status
plugin instance state禁止默认暴露:
完整玩家状态
完整背包/订单/payload 内容
token/password/secret
完整业务请求参数
任意 upvalue/raw registry dump如果 inspect 需要展示示例值,应遵守:
- 默认只展示 key 名和类型
- 字符串值默认截断
- 敏感字段名按规则 redaction
审计与限流
所有控制台命令都应进入运维审计日志。至少记录:
- 时间
- 来源(local socket / loopback / remote)
- 操作人或认证主体
- 命令名
- 目标 service
- 是否成功
- 是否超时/截断
以下操作需要更严格审计:
lua.evallua.execlua.snapshotlua.diff- 任何未来的控制类操作
同时应支持:
- 每连接速率限制
- 每命令超时
- 并发 inspect 上限
配置草案
建议由 ops 配置段拥有该能力:
ops:
enabled: true
console:
enabled: true
endpoint: "unix:///run/shield/ops.sock" # scheme 决定传输;tcp opt-in 用 tcp://127.0.0.1:9090
discovery_file: "/run/shield/ops.endpoint" # server 启动时写入实际 endpoint,客户端默认读取
protocol: text # text(+ 可选 --json)| json_rpc
on_demand: false # true = 仅按需 attach(如 SIGUSR1 触发),平时不开 socket
auth:
mode: peer_credential # socket 默认;tcp opt-in 下强制 token
token: ${OPS_TOKEN} # 仅 tcp opt-in 需要
lua_console:
enabled: true
inspect_enabled: true
snapshot_enabled: true
eval_enabled: false
max_eval_time_ms: 100
max_output_bytes: 65536
max_inspect_nodes: 5000
max_inspect_depth: 8
max_snapshots_per_service: 8说明:
ops.console.*负责管理入口本身。ops.lua_console.*负责 Lua 诊断能力。eval_enabled必须独立于inspect_enabled。endpoint的 scheme 决定传输(unix://默认 /tcp://opt-in);客户端默认从discovery_file读取实际 endpoint。- 实现状态:本节为配置草案,以上
ops.console.*/ops.lua_console.*配置键当前均未接线(源码无解析);console 入口当前只使用console.enabled门控。
命令返回形态
建议同时支持两类输出:
- 人类可读文本
- JSON
例如:
> lua.inspect player.42 summary
service: player.42
lua_memory_kb: 2048
pending_calls: 1
timers: 5
coroutines: 2
top_tables:
M.players 640 KB
M.cache 512 KB> lua.inspect player.42 summary --json这样后续 HTTP /ops/* 可以复用同一组 provider,而不用再定义一套不同语义。
分阶段落地建议
Phase A
statusservicesservice <name>lua.inspect <service> summarylua.inspect <service> memory
目标:先打通 runtime snapshot 和 owner-thread inspect 执行链路。
Phase B
lua.inspect <service> refslua.inspect <service> coroutineslua.inspect <service> timerslua.snapshotlua.diff
目标:解决日常 Lua 泄漏与状态滞留排查。
Phase C
lua.evallua.exec
前提:
- 安全开关、审计、超时、输出限制都已实现
allow_os/allow_io等 Lua sandbox 语义已真正落地
与其他文档的关系
- 运维面总规则见 运维运行时语义
- Lua VM 单线程/每 service 模型见 Lua VM 运行时语义
- optional module 横向边界见 官方可选模块契约
- plugin pool 等非 Lua 内存观测见 Plugin Pool Stats
当前状态
按上文能力分层逐层核对(勿再笼统标注):
- 诊断控制台本体:已实现,两层架构详见 diagnostics-console.md——Root 层
root.*只读观测命令(status/services/service/plugins/config/cluster/server/global/log.level)与 Script 层attachREPL /eval沙箱(含 HTTP/ops/eval,token 门控)。 - L1 只读快照:已落地。
/ops/services/:name与/ops/metrics暴露 runtime 计数器与瞬时 gauge(requests/errors/uptime/timers/pending_calls/pending_tasks/coroutines/memory_kb,见 运维运行时语义)。coroutines经协程生命周期埋点采集(handler 工厂启动登记、终态 resume 与服务 teardown 擦除);memory_kb在 owner 线程 dispatch 退出采样lua_gc(GCCOUNT)(O(1),不跨线程触碰 lua_State)。 - L2 受限 inspect:已全量落地。
lua.inspect <service> summary|timers|pending_calls(registry 锁内只读投影,console 线程零 Lua 触碰、零 actor 往返)、lua.inspect <service> memory(owner 线程一次 dispatch:GC 采样——total_bytes/memory_kb、running、模式与六个 collector 参数——加 retainers 头,depth 4 / 20000 节点同 refs 默认,fork task + 2s bounded wait)、lua.inspect <service> coroutines(owner 线程枚举 live 协程注册表:lua_status 分类 + 每协程 origin/resumes/last_resume/waiting_call 记账,32 条上限带 truncated)、lua.inspect <service> refs [depth](owner 线程受限对象图遍历:fork task 投递 + 2s bounded wait,depth∈[1,8]、节点预算即时间预算,输出 counts/top_tables/truncated)、lua.snapshot <service> [name] [refs](L1 gauge 快照,重名覆盖、每服务环形保留最近 8 份、随 incarnation teardown 清除;refs参数附加对象图摘要,owner 忙记refs_error不阻塞采集)、lua.diff <service> <a> <b>(逐字段 delta,负向变化如实呈现;两端均有对象图摘要时附 counts/top_tables 按 path 匹配的 refs delta)。已在 console 命令面可用(L1 数据源即上条)。 - Phase C 的
lua.eval/lua.exec语义已由attachREPL 与eval承载。门控与超时现状:consoleeval仅受console.enabled门控,没有执行超时与输出上限;token 门控(http.eval_enabled+http.eval_token)与超时约束目前只存在于 HTTP/ops/eval;attach对目标 service 的单次执行等待有 5s 超时。
上文列出的 shield_ops、local admin socket(console 默认 AF_UNIX socket)、Lua inspect provider(lua.inspect/lua.snapshot/lua.diff)、Lua snapshot/diff 与 Lua eval 均已在当前源码中实现并注册(console 命令面:root.* 只读命令 + attach/eval/lua.*;HTTP 面:/ops/* + /ops/eval)。本文继续作为这些能力的边界与语义参照,后续修订时源码、测试和配置校验应向本文与 runtime-ops.md 收敛。