Go、容器与 Linux 调度器

容器化的 Go 服务常常误判自己实际拥有多少 CPU,导致 Go 运行时分配过多线程,并在 Linux 的 CFS 调度器下遭遇严重限流。评论者比较了 CPU quota 与 shares,争论是否应依赖 Kubernetes/Docker 的限制还是应用层调优,并指出像 `automaxprocs` 以及具备 cgroup 感知的运行时(如 Java 和 .NET)是实用的修复方式。更广泛地说,这场讨论突出了严格 CPU 限制、为了更高利用率而超额分配,以及在大型多租户集群中保持可预测延迟之间的权衡。

Go、CFS 与容器 CPU 限制

  • 主要问题:Go 的运行时会根据可见核心数来设置 GOMAXPROCS,而忽略 cgroup 的 CPU 配额,因此在容器里它往往会创建远超实际可用 CPU 时间的 OS 线程。
  • 这会导致严重的 CFS 限流、吞吐量和延迟下降,以及多年来反复出现的“浪费核心”现象。
  • 有人认为核心 bug 在 Go 的运行时里(用了错误的度量指标),而不在 Linux 调度器。

Docker/Kubernetes:quota、shares 与相关参数

  • Docker 中的 --cpus 映射到 CFS quota/period,而不是 cpuset,即使在大多空闲的主机上也可能造成硬性限流。
  • cpu.shares 是按比例/相对的调度方式,只在竞争时生效;quota(cpu.cfs_quota_us)则是硬性的时间限制。
  • 在 Kubernetes 中:CPU request → shares;CPU limit → quota。有人主张“不要设置 limits,只设 requests”;也有人认为 quota 对强隔离至关重要。
  • 对于 CFS quota 何时真正会限流存在分歧:有人说“只在竞争时”;也有人引用文档和实验指出它是无论如何都会生效的硬上限。

变通办法:GOMAXPROCS 与 cgroup 探测

  • 常见做法:根据 cgroup 数据设置 GOMAXPROCS(例如 cpu.cfs_quota_us / cpu.cfs_period_us),或使用 automaxprocs 之类的库。
  • 有多份报告称,这样做后延迟和吞吐量都有显著提升,优于“无限制”以及“有限制但未调优”两种情况。
  • 也有人提醒,把 GOMAXPROCS 限制到 quota 可能会损害突发处理能力并增加尾延迟;“最大并行度不应等于长期 CPU 预算。”
  • 还有建议包括:用 mutating webhook 注入 GOMAXPROCS、使用 nproc/sched_getaffinity,或借助 lxcfs 伪造 /proc 以提供容器感知视图。

争论:到底该不该使用 CPU limits?

  • 一派观点:“停止使用 CPU limits;只用 reservations/requests。” 理由是:limits 会浪费 CPU、在主机空闲时也会触发限流,并让容量行为变得不可预测。
  • 反方认为必须使用 limits,原因包括:
    • 防止 noisy neighbors 影响关键服务。
    • 避免依赖“免费的突发 CPU”,因为节点填满后这部分资源可能消失。
    • 在进行真实容量规划时模拟最坏情况。
  • 还有人指出,超额分配以及把低延迟和批处理工作负载混在一起本来就很复杂;K8s 抽象可能会误导经验不足的运维团队。

其他运行时与容器感知

  • Java、.NET 和 Rust 都加入了容器感知逻辑,使用 cgroup 限制来调整线程池、GC 线程等大小。
  • Java 的行为已经从使用 shares 发展到使用 quota;也有标志位可控制容器 quota 是否影响 CPU 数量。
  • 有人建议 Go 也可以采用类似机制,或通过可 preload 的 shim 来实现。

容器 vs VM/unikernel 之于 Go

  • 一个反复出现的问题是:既然 Go 产出静态二进制,容器到底带来什么价值?
  • 支持容器的观点包括:标准化打包、网络/文件系统/进程隔离、资源控制、编排兼容性(Kubernetes)以及更容易的 CI/CD 和多服务部署。
  • 怀疑者认为,对于单一 Go 服务,容器可能只是多余的“臃肿”;unikernel 加 Go 二进制也许更合适,不过工具链和部署仍然比较粗糙。
  • 共识是:生态系统和工作流标准化,是 Go 应用仍然进入容器的主要原因。

其他 / 不够明确的点

  • 有一些关于 Kubernetes CPU manager 策略(static vs none)及其如何改变可见 CPU mask 的讨论;不同设置下行为不同。
  • 提到了新兴的 Linux 调度器(EEVDF),但线程中没有给出具体的生产经验。
  • 总体来看,参与者一致认为语言运行时、CFS、cgroups 与编排器之间的交互依然微妙,很容易配置错误。