引擎配置优化
结论
不存在适用于所有项目的“最优 XML”。最优配置是:在目标 Entity 数、在线人数、AOI 密度、网络质量和数据库负载下,满足延迟与吞吐目标,并为突发流量留下余量的配置。
配置优化应遵循以下原则:
| 原则 | 做法 |
|---|---|
| 先测量再修改 | 先用 性能分析工具 和 压力测试 建立基线,记录 Tick、CPU、内存、网络和数据库指标。 |
| 一次只改一组参数 | 例如先调 Tick,再调 Witness 预算;否则无法判断收益来自哪个参数。 |
| 只覆盖项目差异 | 在项目资源目录的 res/server/kbengine.xml 中覆盖需要修改的节点,其余节点使用引擎默认值。 |
| 保留安全余量 | 以 p95/p99 Tick 延迟、网络积压和数据库队列作为上线门槛,不以平均值作为唯一依据。 |
| 先限制工作量,再增加并发 | 优先使用完成事件、AOI 和日志预算平滑单 Tick 工作,确认后端有余量再增加线程或队列。 |
本文中的数值均为压测起始值,不是最终生产值。当前默认值以 kbengine.xml 配置 为准。
调优流程
建立基线
固定以下条件后再压测:
| 项目 | 要求 |
|---|---|
| 构建 | 使用 Release 构建;不要用 Debug 的 CPU 结果推导生产参数。 |
| 负载 | 固定客户端数量、Entity 数、登录速率、技能频率和 AOI 分布。 |
| 测试阶段 | 预热、稳定运行、突发流量、超载、恢复,至少分别记录一段数据。 |
| 记录指标 | Tick p50/p95/p99、各组件 CPU、RSS、网络收发、Witness 待处理量、KCP 重传、数据库查询延迟和错误率。 |
| 通过标准 | 先定义业务目标,例如 p99 Tick 小于 50 ms、数据库无持续积压、网络队列可恢复。 |
判断瓶颈
| 现象 | 优先检查 | 不要先做的事 |
|---|---|---|
| Tick p99 升高,CPU 接近满载 | 脚本 profile、AOI 数量、每 Tick 完成事件和日志量 | 直接提高线程池或网络队列 |
| CPU 不高但 Tick 有长尾 | 数据库、文件 IO、同步日志、单次 completion 时间预算 | 只看平均 Tick |
| 网络发送队列持续增长 | Witness 预算、客户端消费速度、KCP 重传和带宽上限 | 无限增大队列 |
| CellApp 间消息多、Ghost 积压 | Ghost 距离、更新频率、Space 划分和边界 Entity 数 | 只增加 CellApp 数量 |
| 数据库连接繁忙或查询变慢 | 查询耗时、连接池大小、数据库锁和磁盘 IO | 同时扩大连接池和线程池 |
每次修改后至少完成一次稳定阶段和一次突发阶段测试;若 p99、内存或积压恶化,应立即回滚该组参数。
Tick 与异步任务
| 配置 | 当前默认值 | 适合调大的情况 | 适合调小或保持默认的情况 | 主要代价 |
|---|---|---|---|---|
gameUpdateHertz | 10 | 业务需要更高的位置、技能或逻辑刷新频率,且 Tick 仍有余量。 | Entity 数多、脚本执行重,或主要瓶颈是 CPU。 | Tick 次数、脚本调用、移动、AOI 和网络调度全部增加。 |
asyncioRepeatOffset | 0 | 确实有引擎托管的 asyncio 回调,需要降低协程恢复延迟时,可从 0.01 或 0.02 开始。 | 没有异步回调,或协程本身执行时间较长。 | 非阻塞等待不会占用主线程,但协程恢复后的 Python 代码仍在主线程执行;CPU 密集计算和同步 IO 会延迟 Tick。 |
performanceProbes.enabled | false | 只在定位网络、Witness、Space 等热点时临时开启。 | 生产环境和常规压测。 | 计时、计数和百分位窗口会增加 CPU 与内存开销。 |
示例:
<gameUpdateHertz>10</gameUpdateHertz>
<asyncioRepeatOffset>0</asyncioRepeatOffset>
<performanceProbes>
<enabled>false</enabled>
</performanceProbes>gameUpdateHertz 提高后,必须重新评估脚本和网络预算;单独提高 Tick 不能修复脚本中的阻塞 IO 或长循环。
完成事件与窗口限制
完成事件预算用于限制单 Tick 内处理 IO 完成回调的工作量,窗口阈值用于发现发送、接收或单 Tick 输出异常。它们是稳定性保护,不是吞吐量开关。
| 配置 | 当前默认值 | 调整方向 | 风险 |
|---|---|---|---|
channelCommon.completionBudget.maxCompletionsPerTick | 1024 | IO 完成积压且 Tick 有余量时逐步增加;Tick 长尾时降低。 | 太大时单 Tick 被 IO 回调占满,脚本和 AOI 延后。 |
channelCommon.completionBudget.maxProcessingTimeMS | 8 | 高 IO 负载下希望减少跨 Tick 积压时增加;p99 Tick 变差时降低。 | 增加主循环尾延迟。设为 0 只关闭时间让步,不会取消数量上限。 |
windowOverflow.send.tickSentBytes | 0 | 需要限制单 Tick 网络突发时设置合理上限。 | 过小会把正常同步推迟到后续 Tick。 |
windowOverflow.receive.* | 见默认 XML | 外部客户端异常或突发流量需要更早告警、丢弃或断开时降低。 | 过小会误伤高延迟或突发合法请求;过大则增加内存和攻击面。 |
bitsPerSecondToClient | 20000 | 客户端确实需要更高的持续发送带宽,且出口带宽有余量。 | 慢客户端多、实时消息被资源下载或大 AOI 挤压时降低。 |
调优窗口时,要同时观察网络队列、Tick p99 和客户端重试;不要通过无限增大 critical 阈值掩盖服务端处理能力不足。
Reliable UDP/KCP
KCP 参数必须和 RTT、丢包率、MTU 以及带宽一起评估。队列单位通常是 segment,队列越大并不代表延迟越低。
| 配置 | 当前默认值 | 调大时机 | 调小时机 | 主要代价 |
|---|---|---|---|---|
readPacketsQueueSize.external | 128 | 外部入口有短时突发且接收处理跟得上。 | 外部流量异常、内存受限或需要更早背压。 | 每连接接收缓存增加。 |
writePacketsQueueSize.external | 128 | 合法突发发送且带宽足够。 | 慢客户端多、发送队列长期积压。 | 队列大时延迟和内存都会上升。 |
writeQueueMaxBytes.external | 65536 | 高 RTT、合法大包突发,且允许更高单连接缓存。 | 需要严格限制慢客户端占用。 | 增大后单连接内存和排队延迟增加。 |
flushSegmentsBudget.external | 4 | 发送队列稳定、Tick 有余量,希望加快排空。 | 网络突发导致 Tick 长尾。 | 每 Tick 加密、封包和发送工作增加。 |
volatileBackpressure.highSegments / lowSegments | 128 / 32 | 短时位置更新突发且可接受缓存。 | 积压持续增长或客户端已明显落后。 | 高水位过大增加单连接内存;高低水位差过小会频繁开关背压。 |
volatileBackpressure.highBytes / lowBytes | 32768 / 8192 | 高 RTT、短时位置更新突发且出口带宽有余量。 | 慢客户端多或发送队列长期不降。 | 增大只会延后背压,不能增加链路带宽。 |
tickInterval | 10 ms | 通常不建议修改,除非明确理解 KCP 时钟与组件 Tick 的关系。 | 高 CPU 或连接数大时保持默认。 | 更短间隔增加计时和协议处理开销。 |
minRTO | 50 ms | 低 RTT 且丢包稳定,可通过压测验证降低。 | 公网、高抖动或误重传明显。 | 过小会增加无效重传和带宽。 |
nodelay | true | 低延迟实时交互通常保持开启。 | 带宽极紧张且业务允许更高延迟时测试关闭。 | 开启会增加报文和 CPU 压力。 |
congestionControl | false | 多连接共享公网带宽或需要更强拥塞抑制时测试开启。 | 专用内网且希望保持当前吞吐特性。 | 拥塞窗口变化可能降低短消息吞吐。 |
KCP 示例只覆盖项目确实需要的覆盖项:
<root>
<channelCommon>
<reliableUDP>
<writeQueueMaxBytes>
<external>65536</external>
</writeQueueMaxBytes>
<flushSegmentsBudget>
<external>4</external>
</flushSegmentsBudget>
</reliableUDP>
</channelCommon>
</root>CellApp、AOI 与 Witness
AOI 参数同时影响 CPU、跨 Cell 消息、客户端带宽和内存。最有效的优化通常是减少不必要的观察关系,而不是单纯增加预算。
| 配置 | 当前默认值 | 调大时机 | 调小时机 | 主要代价 |
|---|---|---|---|---|
cellapp.defaultViewRadius.radius | 80 | 玩法确实需要更远可见范围,且 AOI 消息量可控。 | 同屏 Entity 多、客户端带宽紧张。 | 观察关系近似按面积增长,消息和内存可能快速增加。 |
cellapp.defaultViewRadius.hysteresisArea | 5 | 边界附近频繁 Enter/Leave 时增加。 | 需要更快离开视野、空间密度低。 | 增大后关系保持时间更长。 |
aliasEntityID | true | 通常保持开启;View 内 Entity 少于 255 时可减少 Entity ID 带宽。 | 只在客户端协议明确不支持别名时关闭。 | 关闭会增加每条相关消息的网络字节数。 |
entitydefAliasID | true | 通常保持开启;客户端属性或方法 UID 不超过 255 时降低带宽。 | 只在客户端协议明确不支持别名时关闭。 | 关闭会增加属性和方法广播的网络字节数。 |
ghostDistance | 500 | Cell 边界跨越频繁且需要更远 Ghost 预取。 | Cell 间消息和 Ghost 内存过高。 | 跨 Cell 复制和同步成本增加。 |
ghostingMaxPerCheck | 64 | Ghost 检查积压但单 Tick 有余量。 | CellApp Tick 长尾。 | 批次过大造成单 Tick CPU 峰值。 |
ghostUpdateHertz | 30 | 边界移动同步需要更快收敛。 | Cell 间消息量或 CPU 偏高。 | Ghost 更新消息增加。 |
coordinate_system.rangemgr_y | false | 玩法确实需要按高度过滤 View、Trap 等范围关系。 | 大多数地面游戏或 Entity 密集场景。 | 启用后空间索引和范围查询增加 Y 轴计算。 |
entity_posdir_additional_updates | 2 | 停止移动后的末端位置在弱网下不易收敛时增加。 | 停止频繁、冗余位置包较多时降低,但不要在未验证客户端收敛前设为极端值。 | 设置为 0 表示持续发送,会产生长期冗余流量。 |
entity_posdir_updates.type | 2 | 通常保持智能模式;高同屏且能接受少量精度损失时使用 1。 | 需要完整浮点精度或 View 可能超过 500 时使用 0。 | 紧凑和智能模式的 View 范围限制在 500 以内。 |
witness_volatile_bytes_per_tick | 512 | 位置更新积压且网络、序列化有余量。 | 客户端慢、发送队列积压。 | 单 Witness 带宽和序列化工作增加。 |
witness_total_bytes_per_tick | 2048 | 需要提高同屏状态收敛速度。 | Tick 长尾或外部带宽紧张。 | 每连接同步量和突发发送增加。 |
witness_global_bytes_per_tick | 1048576 | CellApp 有足够 CPU 和出口带宽。 | 多 Witness 共享带宽或 AOI 序列化成为热点。 | 全局提高会放大单 Tick 峰值。 |
witness_global_updates_per_tick | 1024 | 活跃 Witness 多且待处理量持续增长。 | CellApp CPU 长尾。 | 更多 Witness 在同一 Tick 被序列化。 |
witness_volatile_lod.* | true / 32 / 20 / 50 / 2 / 4 | 同屏密集、位置变化远多于属性变化时保持或加强。 | 低 Entity 数且要求每 Tick 位置刷新时可关闭。 | 只影响位置/方向刷新频率,不影响 Enter、Leave、普通属性和方法。 |
witness_volatile_lod 是底层位置/方向刷新优化,不是 EntityDef 的 DetailLevel。前者按距离和同屏数量错峰位置更新,后者控制属性详情级别;两者不要混为一谈。
Space 分配与跨 Cell 压力
| 配置 | 当前默认值 | 调整建议 | 观察指标 |
|---|---|---|---|
cellappmgr.spaceAllocationMaxSkew | 2 | 多 Space 自动分配时保持较小值,避免 Space 数量严重倾斜;只有确认 Space 负载差异可接受时才放宽。 | 各 CellApp CPU、Entity 数、Space 数、迁移和 Witness pending 数。 |
cellappmgr.witnessPendingPressureWeight | 1.0 | Witness pending/active 比例成为瓶颈时逐步增加;CPU 负载已经不均衡时不要盲目增加。 | pending Witness 比例、Space 创建耗时、CellApp Tick p99。 |
Space 分配优化的目标是减少最忙实例的尾延迟,不是追求每个进程的 Entity 数完全相等。
BaseApp、持久化与资源下载
| 配置 | 当前默认值 | 调大时机 | 调小时机 | 主要代价 |
|---|---|---|---|---|
baseapp.archivePeriod | 300 秒 | 数据安全目标允许更长归档周期,且数据库写入是瓶颈。 | 需要更短的持久化窗口。 | 缩短会增加脚本回调、数据库写入和 IO。 |
baseapp.backupPeriod | 300 秒 | Base/Cell 消息和复制压力较低。 | 灾备要求更严格。 | 缩短会增加跨组件消息和内存复制。 |
baseapp.entityRestoreSize | 32 | 恢复阶段数据库和 Tick 有余量。 | 启动恢复造成 CPU、内存或 DB 峰值。 | 批量大时恢复更快,但单 Tick 峰值更高。 |
baseapp.downloadStreaming.bitsPerSecondTotal | 1000000 | 资源下载链路有独立带宽。 | 下载影响游戏消息或出口带宽。 | 过大可能挤占实时业务带宽。 |
baseapp.downloadStreaming.bitsPerSecondPerClient | 100000 | 客户端资源下载需要更快完成。 | 慢客户端多或出口受限。 | 单连接占用带宽增加。 |
baseapp.respool.checktick | 60 秒 | 资源缓存释放不及时且扫描成本低。 | 资源很多、扫描本身造成抖动。 | 扫描频率过高增加 CPU 和锁竞争。 |
归档和备份参数属于可靠性与性能的折中,不能仅以“减少数据库写入”为目标修改。
线程池与数据库连接池
线程数必须同时受 CPU 核数、任务类型、数据库连接上限和单任务阻塞时间约束。
| 配置 | 当前默认值 | 调整建议 | 风险 |
|---|---|---|---|
thread_pool.timeout | 300 秒 | 通常保持默认;只有任务确实可能长时间等待时才调整。 | 过长会掩盖任务卡死,过短会误报超时。 |
thread_pool.init_create | 1 | 启动阶段任务多且需要减少首次创建抖动时增加。 | 启动内存和线程创建成本增加。 |
thread_pool.pre_create | 2 | 已知稳定并发量、希望提前消除创建延迟时增加。 | 空闲线程占用栈内存。 |
thread_pool.max_create | 8 | IO 等待明显、CPU 有余量时逐步增加。 | 上下文切换、栈内存、锁竞争和后端压力增加。 |
DB numConnections | 5 | 查询排队且数据库 CPU、连接上限和磁盘 IO 有余量时增加。 | 连接数过多会放大锁竞争和数据库压力。 |
不要同时把 thread_pool.max_create 和数据库 numConnections 调到很大。先通过查询延迟和连接等待确认瓶颈属于客户端连接池还是数据库本身。
日志与诊断开销
| 配置 | 当前默认值 | 生产建议 | 诊断建议 |
|---|---|---|---|
tick_max_buffered_logs | 131070 | 保持默认并控制高频日志源。 | 只在短时诊断需要保留更多日志时增加,并监控 RSS。 |
tick_sync_logs | 0 | 高频日志场景可设置上限平滑 Logger 网络和磁盘 IO。 | 需要完整日志时临时保持 0,但要确认磁盘可承受。 |
performanceProbes.enabled | false | 关闭。 | 控制变量压测或定位热点时开启,测试结束后恢复。 |
提高日志缓存只能延后问题暴露,不能修复日志生产过量;应优先降低高频 INFO/DEBUG 日志或调整日志级别。
场景化起始模板
下面模板只覆盖需要关注的节点,适合作为第一次压测的起点。正式上线前必须根据基线数据二次调整。
小型房间服
适用于 Entity 数较少、低延迟技能交互、Space 生命周期短的服务:
<root>
<gameUpdateHertz>10</gameUpdateHertz>
<channelCommon>
<completionBudget>
<maxCompletionsPerTick>1024</maxCompletionsPerTick>
<maxProcessingTimeMS>8</maxProcessingTimeMS>
</completionBudget>
</channelCommon>
<cellapp>
<defaultViewRadius>
<radius>60</radius>
<hysteresisArea>5</hysteresisArea>
</defaultViewRadius>
<coordinate_system>
<rangemgr_y>false</rangemgr_y>
<entity_posdir_updates>
<type>2</type>
<smartThreshold>10</smartThreshold>
</entity_posdir_updates>
</coordinate_system>
</cellapp>
</root>密集 AOI 服
适用于同屏 Entity 多、位置更新远多于属性变化的服务。优先使用 Witness 预算和 Volatile LOD 平滑工作量:
<root>
<cellapp>
<coordinate_system>
<witness_total_bytes_per_tick>2048</witness_total_bytes_per_tick>
<witness_global_bytes_per_tick>1048576</witness_global_bytes_per_tick>
<witness_global_updates_per_tick>1024</witness_global_updates_per_tick>
<witness_volatile_lod>
<enable>true</enable>
<minimumViewEntities>32</minimumViewEntities>
<nearDistance>20</nearDistance>
<mediumDistance>50</mediumDistance>
<mediumIntervalTicks>2</mediumIntervalTicks>
<farIntervalTicks>4</farIntervalTicks>
</witness_volatile_lod>
</coordinate_system>
</cellapp>
</root>高 RTT 或轻微丢包
先观察重传和发送积压,再小步调整队列;不要仅因为 RTT 高就无限增加缓存:
<root>
<channelCommon>
<reliableUDP>
<minRTO>50</minRTO>
<writeQueueMaxBytes>
<external>131072</external>
</writeQueueMaxBytes>
</reliableUDP>
</channelCommon>
</root>如果队列持续增长,根因通常是出口带宽不足、客户端处理慢或 AOI 发送量过大,应回到 Witness 和带宽限制检查,而不是继续增大 KCP 队列。
验证、回滚与上线门槛
| 阶段 | 必须检查 | 通过条件 |
|---|---|---|
| 功能回归 | 登录、Entity 创建/销毁、Space 迁移、AOI Enter/Leave、断线重连、归档恢复。 | 无协议错误、无事件丢失、无异常积压。 |
| 稳态压测 | Tick p95/p99、CPU、RSS、网络队列、DB 查询延迟。 | 指标低于业务门槛,并有突发余量。 |
| 超载测试 | 提升登录速率、同屏数或网络丢包,观察恢复时间。 | 过载时有界,负载下降后队列能恢复。 |
| 回滚演练 | 保存 XML 变更、记录版本和测试结果。 | 能在一次发布内恢复上一组配置。 |
推荐把每次配置调整记录为:参数路径 -> 旧值 -> 新值 -> 测试负载 -> 指标变化 -> 是否保留。这样可以避免把某个场景的偶然收益误当成通用优化。
