한국어English日本語简体中文繁體中文DeutschไทยTiếng ViệtРусскийPortuguês (Brasil)EspañolBahasa Indonesia

游戏卡顿白皮书 › 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)集群的争用问题

出处

  1. Amdahl's Law in the Multicore Era IEEE
    IEEE Computer 2008 论文(作者公开版)。不能并行的比例为 1−f 时,核心再多,加速比也不会超过 1/(1−f)(阿姆达尔定律)
  2. Request scheduling Microsoft
    Orleans 的 grain(Actor)采用逐个把请求处理完的单线程执行模型,不会同时修改状态;互相等待对方的响应时可能死锁
  3. pidstat(1) — Linux manual page sysstat
    -w 的 cswch/s 是因等待资源而停住的自愿上下文切换次数,-t 按线程显示
  4. Demonstrations of offcputime, the Linux eBPF/bcc version IO Visor
    按调用栈汇总线程停住、离开 CPU 的时间(off-CPU),-p 指定进程
  5. Well-known EventCounters in .NET Microsoft
    Monitor Lock Contention Count(monitor-lock-contention-count):获取 monitor 锁时发生竞争的次数
  6. .NET runtime metrics .NET
    .NET 9 起的 dotnet.monitor.lock_contentions:进程启动以来获取 monitor 锁时发生竞争的次数

相关原因

同一层:L9 服务器游戏进程

其他层中同样导致“操作延迟”的原因

查看含图示和实验的原卡片