Skip to content

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:

text
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 诊断控制台只拥有“读取和调度”的职责。

依赖方向:

text
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 ops TUI / 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、macOS getpeereid 直接拿到连接方 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”的猜测式判断:

text
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

这意味着控制台命令默认采用:

text
lua.inspect <service> ...
lua.eval <service> ...

而不是:

text
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 快照至少应包含:

json
{
  "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?
  • 两次采样之间,哪些对象计数变化最大?

命令面 ​

建议最小命令集:

text
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,也不应发送业务消息。

输出限制 ​

默认输出限制:

  • 最大遍历节点数
  • 最大递归深度
  • 最大返回字节数
  • 最大字符串截断长度

如果超过限制,结果必须明确标出:

json
{
  "truncated": true,
  "reason": "node_limit"
}

L3:Lua eval ​

L3 是高风险能力,仅作为开发和预发环境的显式调试工具。

能力定位 ​

lua.eval 的目标是快速验证状态,而不是提供远程热修平台。

推荐形态:

text
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_unavailable
  • service_not_found
  • service_dead
  • inspect_timeout
  • inspect_busy
  • inspect_truncated
  • permission_denied
  • lua_eval_disabled
  • unsafe_operation_forbidden

输出与脱敏 ​

默认输出应该是摘要,不是原始业务状态转储。

允许默认暴露:

text
memory total
table counts
top retainers summary
coroutine count
timer count
pending call count
service names
cluster node status
plugin instance state

禁止默认暴露:

text
完整玩家状态
完整背包/订单/payload 内容
token/password/secret
完整业务请求参数
任意 upvalue/raw registry dump

如果 inspect 需要展示示例值,应遵守:

  • 默认只展示 key 名和类型
  • 字符串值默认截断
  • 敏感字段名按规则 redaction

审计与限流 ​

所有控制台命令都应进入运维审计日志。至少记录:

  • 时间
  • 来源(local socket / loopback / remote)
  • 操作人或认证主体
  • 命令名
  • 目标 service
  • 是否成功
  • 是否超时/截断

以下操作需要更严格审计:

  • lua.eval
  • lua.exec
  • lua.snapshot
  • lua.diff
  • 任何未来的控制类操作

同时应支持:

  • 每连接速率限制
  • 每命令超时
  • 并发 inspect 上限

配置草案 ​

建议由 ops 配置段拥有该能力:

yaml
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

例如:

text
> 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
text
> lua.inspect player.42 summary --json

这样后续 HTTP /ops/* 可以复用同一组 provider,而不用再定义一套不同语义。

分阶段落地建议 ​

Phase A ​

  • status
  • services
  • service <name>
  • lua.inspect <service> summary
  • lua.inspect <service> memory

目标:先打通 runtime snapshot 和 owner-thread inspect 执行链路。

Phase B ​

  • lua.inspect <service> refs
  • lua.inspect <service> coroutines
  • lua.inspect <service> timers
  • lua.snapshot
  • lua.diff

目标:解决日常 Lua 泄漏与状态滞留排查。

Phase C ​

  • lua.eval
  • lua.exec

前提:

  • 安全开关、审计、超时、输出限制都已实现
  • allow_os/allow_io 等 Lua sandbox 语义已真正落地

与其他文档的关系 ​

当前状态 ​

按上文能力分层逐层核对(勿再笼统标注):

  • 诊断控制台本体:已实现,两层架构详见 diagnostics-console.md——Root 层 root.* 只读观测命令(status/services/service/plugins/config/cluster/server/global/log.level)与 Script 层 attach REPL / 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 语义已由 attach REPL 与 eval 承载。门控与超时现状:console eval 仅受 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 收敛。

Apache License 2.0