游戏卡顿白皮书 › L9 服务器游戏进程
锁竞争 Lock contention
原因 ID sp-lock · 主责 研发团队·服务器开发
在含图示和实验的完整版中打开此卡片 →
多个线程为了写同一份数据而等同一把锁时,线程再多,一次也只有一个在执行。
起因 拍卖行、公会仓库这类公共数据被多个线程同时使用 → 结果 拿到锁的线程结束之前,其余线程都在等 → 画面表现 只有特定功能慢,严重时整个 tick 延迟
- 症状
- 操作延迟, 卡住
- 因素
- 停顿
- 谁会遇到
- 仅特定功能, 全服
- 何时出现
- 人多的时候
- 负责方
- 主责 研发团队·服务器开发
- 研发团队要做的事
- 把锁拆细;减少锁内的工作;采用基于消息的架构(每份数据指定一个负责线程,其他线程只发消息请求)。
- 数值参考
- 锁内工作占全部工作的 20% 时,线程再多,吞吐量最多也只有单线程的 5 倍;占 40% 时停在 2.5 倍。
- 监控图上
- 随人数/负载上升 · 请求处理时间、各线程 CPU 和上下文切换
- 查看位置
- 用 pidstat -w -t 1 看各线程的自愿上下文切换(cswch/s,等待资源而停住的次数),用 bcc offcputime -p 看线程离开 CPU 后在哪里等待(按调用栈统计等待时间)。.NET 看 dotnet-counters 的锁竞争次数(.NET 9 及以后为 dotnet.monitor.lock_contentions,8 及以前为 Monitor Lock Contention Count)
- 确认依据
- 负载增加时 CPU 使用率依然很低,处理时间却变长,大部分等待时间集中在获取锁的调用栈上,锁竞争次数也一起上升
- 排除依据
- CPU 已经跑满,是计算量问题(“Tick 超出预算”“单线程区域过载(热点)”)。等待的地方是 DB、文件调用,看“游戏线程中的同步调用”
- 确认手段
- 运维工具即可确认(无需游戏代码)
- 深入了解
- 多个线程一起修改游戏数据的架构才会出现这个问题。每个区域、功能由一个线程负责、只通过消息交互的架构几乎没有锁,但要当心工作集中到一个线程的问题(单线程区域过载)。游戏线程如果在等慢速存盘操作持有的锁,那个 tick 整个都会停住。
- 真实案例
- Roblox 2021: Roblox 73 小时故障:服务发现(Consul)集群的争用问题
出处
- Amdahl's Law in the Multicore Era IEEE
IEEE Computer 2008 论文(作者公开版)。不能并行的比例为 1−f 时,核心再多,加速比也不会超过 1/(1−f)(阿姆达尔定律) - Request scheduling Microsoft
Orleans 的 grain(Actor)采用逐个把请求处理完的单线程执行模型,不会同时修改状态;互相等待对方的响应时可能死锁 - pidstat(1) — Linux manual page sysstat
-w 的 cswch/s 是因等待资源而停住的自愿上下文切换次数,-t 按线程显示 - Demonstrations of offcputime, the Linux eBPF/bcc version IO Visor
按调用栈汇总线程停住、离开 CPU 的时间(off-CPU),-p 指定进程 - Well-known EventCounters in .NET Microsoft
Monitor Lock Contention Count(monitor-lock-contention-count):获取 monitor 锁时发生竞争的次数 - .NET runtime metrics .NET
.NET 9 起的 dotnet.monitor.lock_contentions:进程启动以来获取 monitor 锁时发生竞争的次数
相关原因
同一层:L9 服务器游戏进程
其他层中同样导致“操作延迟”的原因
查看含图示和实验的原卡片