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