配置运行时语义
本文档是 Shield 最小 runtime 配置入口 的权威来源。官方可选模块可以定义自己的配置段,但不能把配置项反向塞进最小 runtime schema。
当前状态:本文冻结 Phase 1 配置契约;源码已实现启动期最小验证,旧字段仍需要按本文逐步清理。
配置原则
- YAML 只做声明式绑定,不承载业务逻辑。
- 最小 runtime schema 只覆盖单节点 runtime 必需能力。
- 配置驱动 Lua service、网络监听、插件实例/binding 和日志;每个配置段必须有明确 owner。(
bootstrap.timeout等启动期超时键未实现,见下文 schema 说明。) - 不在 core 中提供服务发现、Prometheus、健康检查、DI、注解或条件装配配置;插件系统使用独立
plugins子树。 - optional module 的配置由对应模块自己验证;未启用模块时,出现对应 optional 配置段必须启动失败,不能由 core 静默忽略。
Phase 1 Schema
配置段 owner:
| 配置段 | owner |
|---|---|
app | shield_bootstrap |
log | shield_log |
lua | shield_lua |
actors | shield_bootstrap + shield_core + shield_lua |
actors[].network | shield_net |
net | shield_net |
plugins | shield_plugin |
bootstrap / shutdown | shield_bootstrap |
{
"app": {
"name": "my_game",
"version": "1.0.0"
},
"log": {
"level": "info",
"console": true,
"file": {
"enabled": false,
"path": "logs/shield.log",
"max_size_mb": 100,
"max_files": 10
}
},
"lua": {
"vm": {
"mode": "per_service"
},
"sandbox": {
"allow_os": false,
"allow_io": false
},
"script_path": "scripts",
"module_path": "scripts/?.lua;scripts/?/init.lua",
"cache": {
"enabled": true,
"max_size": 100,
"ttl_seconds": 0
}
},
"actors": [
{
"name": "gateway",
"script": "scripts/auth.lua",
"instances": 1,
"required": true,
"network": {
"tcp": "0.0.0.0:8001",
"max_connections": 10000,
"max_frame_size": 65536
},
"options": {
"route_table": "login_routes"
},
"restart": {
"policy": "on-failure"
}
},
{
"name": "player",
"script": "scripts/player.lua",
"instances": 0
}
],
"plugins": {
"directory": "./plugins",
"instances": [
{
"id": "db.main",
"package": "database.mysql",
"required": true,
"config": {
"host": "127.0.0.1",
"port": 3306,
"database": "game",
"user": "root",
"password": "${DB_PASSWORD:}",
"pool_size": 8,
"connect_timeout_ms": 5000,
"query_timeout_ms": 30000
}
},
{
"id": "cache.session",
"package": "cache.redis",
"required": true,
"config": {
"host": "127.0.0.1",
"port": 6379,
"db": 0,
"password": "${REDIS_PASSWORD:}",
"pool_size": 8,
"connect_timeout_ms": 5000,
"command_timeout_ms": 5000
}
}
],
"bindings": {
"database.default": "db.main",
"cache.session": "cache.session"
}
},
"bootstrap": {
"timeout": {
"config_load": 5000,
"log_init": 5000,
"core_init": 10000,
"plugin_init": 30000,
"net_init": 10000,
"script_init": 10000,
"service_spawn": 60000
},
"retry": {
"plugins": {
"max_retries": 3,
"delay": 5000
}
}
},
"shutdown": {
"timeout": {
"service_drain": 30000,
"service_stop": 10000,
"plugin_shutdown": 10000,
"total": 60000
}
}
}上面 bootstrap 段(timeout.* / retry.*)未实现:bootstrap 没有阶段超时和插件重试机制,这些键没有任何消费者,配置中出现不会生效(见 启动流程)。
以下 schema 键未实现(已从示例中移除;无消费者,配置中出现不会生效,不应使用):
log.format——logger 输出为单一固定文本格式(<timestamp_ms> [LEVEL] <logger>: <msg>,见src/log/logger.cpp的format_record),无可配置的格式项;log.file.rotation——轮转只按大小触发,由log.file.max_size_mb(写满即轮转,log.file.max_files控制保留份数)驱动;lua.vm.max_vms/lua.vm.max_memory_mb——lua.vm下当前只有mode被读取校验;actors[].transport、actors[].limits.*——完全未被读取;actors[].restart.max_retries/initial_delay/max_delay/multiplier——重启配置中当前只有policy被读取校验。
shutdown.timeout 语义:
| 字段 | 说明 |
|---|---|
service_drain | runtime shutdown drain 阶段预算(整段共享);按 spawn 逆序调用 service on_shutdown(ctx),预算耗尽后剩余 hook 跳过并记 WARN;<= 0 关闭该阶段 |
service_stop | final stop 阶段预算;用于调用 service on_exit(reason) 并释放 VM |
plugin_shutdown | 插件 instance shutdown 预算;插件关闭在 service drain/stop 之后 |
total | 整体 shutdown 最大预算,必须覆盖所有分段 |
Actor 配置
| 字段 | 必填 | 说明 |
|---|---|---|
name | 是 | actor 类型和默认 service name 前缀 |
script | 是 | Lua module 文件 |
instances | 否 | 启动实例数;0 表示只允许动态 spawn |
required | 否 | 启动失败是否导致 runtime 启动失败,默认 true |
network | 否 | gateway 类 service 的 listener 配置 |
options | 否 | 传给 on_init(args).config 的业务配置 |
restart | 否 | 服务异常退出后的重启策略;当前只有 policy 被读取校验(always/on-failure/never),重试次数与退避字段未实现 |
limits | 否 | 未实现——当前无任何消费者,配置不生效 |
actors[].network 声明的是 Gateway 边界 owner,不是暴露客户端业务回调的 Lua service。它持有 listener、live session 和 session 的单一 target 绑定;业务 Lua 不接收通用消息回调(入站只经 spawn 期编译的 RPC 绑定 handler(ctx, client, request) 到达 target),也不获得裸连接句柄。第一版 TCP listener 仍要求 instances: 1,确保每个 live session 只有一个 Gateway owner。
actors[].network.protocol 绑定 session 固定使用的 ProtocolProfile,只描述 wire 形态(envelope、body codec、限制)。客户端 RPC 路由的唯一静态来源是 actors[].rpc.routes:每个 actor 声明自己的 descriptor 条目(id/name/binding/direction/owner_service/requires_auth/action/lazy_decode 与 schema 元数据,字段契约见 protocol-routing-design.md)。config 校验单 actor 的字段与唯一性;bootstrap 把所有 actor 的条目合并为全局 descriptor 表(跨 actor 的 id/name 冲突导致启动失败)并注入 listener pipeline;每个 Lua service 在 spawn 时只编译 owner_service == 自身 的条目,c2s/bidi 的 binding 解析不到模块函数即 spawn 失败(handler_missing)。network.protocol.routes 内联路由已删除,配置中出现即报错(pre-1.0 不做兼容读)。
actors[].network.max_frame_size 是 listener 级默认单帧上限。未显式设置 protocol.envelope.max_frame_size 时,profile 继承该值;显式设置时以 envelope 值为准。两级均未设置时不是无限:取传输层默认上限 16 MiB(kDefaultMaxFrameSize,见 include/shield/transport/protocol.hpp)——异常/恶意的长度前缀不应能驱动服务端按其分配缓冲;业务确实需要更大单帧时显式配置。
协议配置只包含固定 wire/profile 部件:
envelope:frame 边界与 headerroute_id读取/写入规则;body:session 固定的 codec 与 provider;routing:遗留的 route 来源开关(header/body),仅服务过渡期入口,后续里程碑随 body route 分发一并移除。
Gateway 只从 header 读取 route_id,在 descriptor 校验表中确认其存在与方向,然后把消息投递给 session 的当前单一 target。普通 RPC body 的 decode 必须在目标 Service actor 中进行。
actors:
- name: gateway
script: scripts/gateway_control.lua
instances: 1
network:
tcp: "0.0.0.0:8001"
max_frame_size: 65536
protocol:
name: game.v1
envelope:
type: idlen
route_id_bytes: 4
length_bytes: 4
endian: big
max_frame_size: 65536
body:
codec: protobuf
provider: protocol.protobuf
rpc:
routes:
- id: 1001
name: login
binding: do_login
direction: c2s
requires_auth: false
- id: 2001
name: login_result
binding: push_result
direction: s2c预登录 RPC 在 descriptor 中声明 requires_auth: false;认证成功后由认证服务通过 shield.client.bind 把 session 的 target 原子切换到玩家服务。room、scene、map 等动态路由由玩家服务私有状态管理,不能写成静态 actor 配置。
actors[].script 可以是绝对路径,也可以是相对路径。相对路径解析规则:
- 如果是绝对路径,直接使用
- 如果路径相对于当前工作目录存在文件,直接使用
- 如果 actor 配置指定了
source_dir,尝试从该目录解析 - 如果全局配置指定了
lua.script_path,尝试从该路径解析 - 如果以上都不存在,使用原始路径(可能导致启动失败)
Lua 模块加载路径
lua.module_path 配置项用于设置 Lua 的 package.path,控制 require() 函数的模块搜索路径:
- 默认值:
"scripts/?.lua;scripts/?/init.lua" - 支持多个路径,用分号分隔
?会被替换为模块名
示例:
{
"lua": {
"script_path": "scripts",
"module_path": "scripts/?.lua;scripts/?/init.lua;libs/?.lua"
}
}这样 require("utils.helper") 会依次尝试:
scripts/utils/helper.luascripts/utils/helper/init.lualibs/utils/helper.lua
Lua 脚本缓存
lua.cache 配置项控制 Lua 脚本的缓存行为,可以显著提升重复加载相同脚本的性能:
| 字段 | 默认值 | 说明 |
|---|---|---|
enabled | true | 是否启用缓存 |
max_size | 100 | 最大缓存文件数 |
ttl_seconds | 0 | 缓存过期时间(秒),0 表示永不过期 |
缓存机制:
- 缓存基于文件路径和修改时间
- 文件修改后缓存自动失效
- 使用 LRU 策略淘汰旧缓存
- 缓存存储源代码,每次使用时重新编译
环境差异:
- 开发环境:建议
enabled: false以支持热重载 - 生产环境:建议
enabled: true以提升性能
Plugins 配置
后端能力(SQL、文档、缓存、队列、排行榜、监控等)统一走插件系统 v1。认证/JWT 属于业务层策略,不作为 Shield 官方插件发布。最小 runtime 配置层只识别 plugins 子树并把它交给 PluginHost;各实例的 config 由对应插件 manifest.yaml 的 config_schema 校验。
| 字段 | 必填 | 说明 |
|---|---|---|
plugins.directory | 否 | 插件包目录,默认由 bootstrap 决定 |
plugins.instances | 否 | 显式声明要启动的 plugin instance |
plugins.instances[].id | 是 | instance id,本配置内唯一 |
plugins.instances[].package | 是 | package id,必须能在 catalog 中找到 |
plugins.instances[].required | 否 | 默认 true;required 失败会中断启动 |
plugins.instances[].config | 否 | 传给插件的实例配置,由插件 manifest schema 拥有 |
plugins.bindings | 否 | logical_name: instance_id 显式 binding 表 |
规则:
- core 不解释数据库、Redis 或其他后端的字段;例如
pool_size、host、database都属于具体插件实例配置。 - 数据访问 Lua API 使用插件 namespace 和 binding 逻辑名,例如
shield.database.mysql("database.default")。 - 缺少对应 binding、目标实例未启动或插件未启用时,插件 Lua API 返回
nil, { code = "module_unavailable" }或等价错误对象。 - 连接池归插件 instance 自治,host 不创建全局 DB/Redis 连接池。
- 旧顶层
database/redis配置段已废弃,不属于 Phase 1 schema。
验证规则
| 字段 | 规则 |
|---|---|
app.name | 必填,1-64 字符 |
log.level | debug、info、warn、error |
lua.vm.mode | 当前只允许 per_service |
lua.script_path | 可选,默认 "scripts" |
lua.module_path | 可选,默认 "scripts/?.lua;scripts/?/init.lua" |
lua.cache.enabled | 可选,默认 true |
lua.cache.max_size | 可选,默认 100,范围 1-10000 |
lua.cache.ttl_seconds | 可选,默认 0,范围 0-86400 |
lua.sandbox.allow_os / lua.sandbox.allow_io | 可选布尔,缺省 true(fail-open:未配置即开放 os/io 标准库);生产环境必须显式声明为 false(仓库 config/app.yaml 即如此) |
actors | 至少一个 actor |
actors[].name | 必填,本配置内唯一 |
actors[].script | 必填,文件必须存在 |
actors[].instances | >= 0 |
actors[].restart.policy | always、on-failure、never |
actors[].network.* | 监听地址必须是 host:port |
actors[].network.tcp | Phase 1 要求 instances == 1;必须同时声明 network.protocol,否则拒绝启动 |
actors[].network.max_connections / max_connections_per_ip | 可选,范围 1-10000000 |
actors[].network.max_frame_size | 可选,范围 1-16777216(16 MiB) |
actors[].network.max_session_send_queue | 可选,范围 0-1000000 |
actors[].network.read_idle_timeout | 可选,范围 0-86400000(毫秒;0 关闭读空闲检测) |
actors[].network.rate_limit.* | messages_per_second、burst 均为 0-1000000;必须为 map |
actors[].network.blocklist.deny | 必须为数组;每个条目启动期按地址/CIDR 解析校验,非法条目拒绝启动 |
actors[].network.tls | 必须为 map;enabled 可选布尔,缺省 false;enabled: true 时 cert_file / key_file 必填非空,handshake_timeout_ms 范围 1-600000,缺省 10000 |
actors[].network.udp/kcp/websocket | Phase 1 拒绝启动;这些 transport 属于 deferred extension |
actors[].rpc.routes | 存在时必须是数组,每项必须是 map:id 必填整数且 >= 1、单 actor 内唯一;binding 必填且非空;direction 限定 c2s/client_to_server/s2c/server_to_client/bidi/bidirectional;action 限定 decode_local/forward_raw/drop;requires_auth / lazy_decode 出现必须是布尔;name 非空时单 actor 内唯一;出现 schema_id / response_schema 直接拒绝(已删除键) |
net.threads | 可选,默认 0(单 I/O 线程),范围 0-64 |
plugins.directory | 可选;路径必须是字符串 |
plugins.instances[].id | 必填,本配置内唯一 |
plugins.instances[].package | 必填,必须存在于插件 catalog |
plugins.instances[].config | JSON-compatible map;按插件 manifest schema 校验 |
plugins.bindings | binding 名唯一,目标 instance 必须存在 |
shutdown.timeout.total | 必须大于各分段 timeout |
验证失败时拒绝启动,输出字段路径和原因。
net.threads 默认值评估与调优
net.threads 未配置或 0 时,全部客户端面 I/O(游戏 TCP 监听、HTTP ops、 console)跑在单个 net 线程的一个 io_context 上(legacy 模式, bootstrap.cpp:空列表才退化单线程;N > 0 时开 N 线程跑同一 io_context)。
评估结论(2026-09):维持默认 0,不改默认值。 理由:
- 产品目标是小规模开箱即用:中小规模(数百连接、无 TLS 卸载)单线程绰绰 有余,且是并发行为最可预测的形态(所有 socket 事件串行)。
- 静默改默认值会改变所有存量部署的并发语义,收益不成比例;调优是上量后的 显式动作。
- 业务 Lua 永远不在 net 线程执行(bridge 只做 framing + CAF 转发,handler 跑在 CAF actor 线程),所以调大 N 不改变任何业务并发语义,只增加 socket 层并行度——默认保守没有隐藏成本。
何时调大(满足其一):
- 在线连接数持续上千,或单帧吞吐大(接近
max_frame_size的流量); - 服务端做 TLS 终结(加解密占用 net 线程);
- 抓取
/ops/metrics、console 往返在客户端高峰期明显变慢(三者共用一个 io_context,N ≥ 2即可把 ops/console 与客户端流量隔离到不同线程)。
取值建议:2 起步;按核数 2–4;超过物理核数无收益。会话读写在 per-session strand 上串行(session.hpp),多线程 io_context::run 安全; 写队列与 idle timer 同在 strand,无跨线程额外锁。
验证:调整后压测观察 /ops/metrics(抓取延迟)与客户端 RTT;口径见 runtime-ops.md。
环境变量展开(未实现)
通用
${VAR}/${VAR:default}展开仍为目标契约,配置加载器不会处理这些占位符,插件会拿到字面量字符串。唯一已实现的限定用途是cluster.auth.token:由集群消费者严格解析完整${VAR},缺失/空值拒绝启动,不支持 default,见 服务间认证。其他键请勿使用。
规划语义:
{
"plugins": {
"instances": [{
"id": "db.main",
"package": "database.mysql",
"config": {
"host": "${DB_HOST:localhost}",
"password": "${DB_PASSWORD:}"
}
}]
}
}规则:
- 未设置且无 default 时,值为空字符串。
- 环境变量展开发生在 schema 验证前。
- 敏感字段不应在日志中输出明文。
多配置文件合并
启动可以指定多个配置文件:
shield --config config/app.yaml --config config/production.yaml合并规则:
- 后面的文件覆盖前面的 scalar 和 map 字段。
actors按name合并;同名 actor 后者覆盖字段。- 不允许两个文件定义同名 actor 且 script 不同,除非后者显式设置
override: true。
当前实现状态:源码已实现 map/scalar 的后者覆盖前者以及启动期验证;actors 按 name 智能合并仍是目标规则,当前同名 actor 会在合并后的配置验证中被拒绝。
热更新
配置文件热重载当前未实现(reload_config() 是直接返回 true 的空实现,没有配置文件热重载入口);下表描述各配置项的现状:
| 配置项 | 热更新 | 行为 |
|---|---|---|
log.level | 否 | 配置文件热重载未实现(reload_config() 是返回 true 的空实现);运行期改日志级别走 console 命令 root.log.level,改完立即生效 |
log.console / log.file | 否 | Phase 1 需要重启 |
plugins.* | 否 | Phase 1 需要重启;不支持热加载/热卸载插件 |
actors[].instances | 否 | Phase 1 需要重启 |
actors[].script | 否 | 需要 service 重启或未来 hot reload 机制 |
actors[].network | 否 | 需要 listener 重建 |
lua.vm.* | 否 | 需要重启 |
lua.script_path | 否 | 需要重启 |
lua.module_path | 否 | 需要重启 |
lua.cache.enabled | 否 | 需要重启 |
lua.cache.max_size | 否 | 需要重启 |
lua.cache.ttl_seconds | 否 | 需要重启 |
bootstrap.* / shutdown.* | 否 | 需要重启 |
全局配置推送、运维热更新、cluster-wide 配置同步属于 shield_global 或 shield_ops 后续设计,不进入 core schema。
Optional Module Schema
以下配置段不属于 core schema:
| 配置段 | 所属模块 | 文档 |
|---|---|---|
cluster | shield_cluster | 集群语义 |
global | shield_global | 全局数据 |
ops | shield_ops | 运维语义 |
player / player_manager | shield_player | 玩家生命周期 |
server_manager | shield_server | 服务器状态 |
optional module 启用后,由该模块读取并验证自己的配置段。core bootstrap 只负责把未消费的 optional 配置快照传给已启用模块。
如果配置文件包含 optional module 配置段,但对应模块未构建或未启用,启动必须失败并输出明确错误;不能静默忽略。
删除的旧配置
以下旧配置直接删除,不保留兼容:
plugins.enabled- 旧插件 v0 配置
middlewarediscoverymetricshealthconditionsannotationsdiiocevent_bus