写点什么

我们如何在 30 项关键服务任务中节省 70K 内核

  • 2022-02-09
  • 本文字数:3128 字

    阅读完需:约 10 分钟

我们如何在30项关键服务任务中节省70K内核

引言

作为 Uber 工程实现盈利的众多努力的一部分,最近我们的团队致力于通过提高效率来降低算力成本。其中最有影响力的一些工作是围绕 GOGC 优化展开的。在这篇博客,我们想分享我们在高效、低风险、大规模、半自动化 Go 垃圾回收调优机制方面的经验。

 

Uber 的技术栈由数千个微服务组成,由云原生的基于调度的基础设施支持。这些服务中的大部分都是用 Go 编写的。我们的团队——地图制作工程组,以前曾在通过调优 GC 来显著提高多个 Java 服务的效率方面发挥过重要作用。在 2021 年初,我们探讨了对基于 Go 的服务进行性能调优的可能性。我们运行了几个 CPU 配置文件来评估当前的状态,发现 GC 是大多数关键任务服务的最大 CPU 消费者。下面是一些 CPU 配置文件的代表,其中 GC(由 runtime.scanobject 方法标识)消耗了分配的计算资源的很大一部分。

 

Service #1



图 1:示例服务 #1 的 GC CPU 消耗

 

Service #2



图 2:示例服务 #2 的 GC CPU 消耗

 

由于这一发现,我们开始为相关服务进行 GC 调优。令我们高兴的是,Go 的 GC 实现和简单的调优使得我们能够自动化大部分检测和调优机制。我们将在后续部分详细介绍我们的方法及其影响。

 

GOGC 调优器

Go 运行时环境以周期性的间隔调用并发垃圾回收器,除非之前有一个触发事件。触发事件基于内存背压。因此,受 GC 影响的 Go 服务受益于更多的内存,因为这减少了 GC 必须运行的次数。另外,我们意识到我们的主机级 CPU 与内存的比率是 1:5(1 core: 5 GB 内存),而大多数 Golang 服务的配置比率是 1:1 到 1:2。因此,我们相信我们可以利用更多的内存来减少 GC CPU 的影响。这是一种与服务无关的机制,如果应用得当,会产生很大的影响。

 

深入研究 Go 的垃圾回收超出了本文的讨论范围,但以下是这项工作的相关内容:Go 中的垃圾回收是并发的,需要分析所有对象来确定哪些对象仍然是可访问的。我们将可访问的对象称为“实时数据集”。Go 只提供了一个工具——GOGC,用实时数据集的百分比表示,用来控制垃圾回收。GOGC 值充当数据集的乘数。GOGC 的默认值是 100%,这意味着 Go 运行时环境将为新的分配保留与实时数据集相同的内存量。例如:硬目标 = 实时数据集 + 实时数据集 * (GOGC / 100)。

 

然后,pacer 负责预测触发垃圾回收的最佳时间,从而避免击中硬目标(和软目标)。



图 3:使用默认配置的示例堆内存

动态而多样:没有万能的方法

我们发现,基于固定的 GOGC 值的调整不适合 Uber 的服务。其中一些挑战是:

  • 不知道分配给容器的最大内存,可能导致内存溢出问题。

  • 我们的微服务具有显著不同的内存使用量组合。例如,分片系统可以有非常不同的实时数据集。我们在其中一个服务中遇到了这种情况,其中 p99 的使用量是 1GB,而 p1 的使用量是 100MB,因此 100MB 的实例对 GC 有巨大影响。

 

自动化案例

前面提到的痛点是提出 GOGCTuner 概念的原因。GOGCTuner 库简化了服务所有者优化垃圾回收的过程,并在其上添加了一个可靠性层。

 

GOGCTuner 根据容器的内存限制(或服务所有者的上限)动态计算正确的 GOGC 值,并使用 Go 的运行时 API 进行设置。以下是 GOGCTuner 库功能的详细信息:

  • 简化配置来便于推理和确定性计算。GOGC 的 100%对于 GO 初学开发者来说并不明确,也并不确定,因为它仍然依赖于实时数据集。另一方面,70%的限制可确保服务始终使用 70%的堆空间。

  • 防止 OOM(内存溢出):这个库从 cgroup 读取内存限制,并使用默认的硬限制 70%(这是我们经验中的安全值)。

  • 值得一提的是,这种保护是有限度的。微调器只能调整缓冲区分配,因此如果您的服务的存活对象高于微调器的限制,微调器会将比较低的存活对象的使用量的 1.25 倍设置成默认的限制值。

  • 对于以下情况,允许更高的 GOGC 值:

  • 如上所述,手动 GOGC 是不确定的。我们仍然依赖实时数据集的大小。如果实时数据集是我们上一个峰值的两倍怎么办?GOGCTuner 将使用更多的 CPU 来强制执行相同的内存限制。相反,手动调整会导致内存溢出。因此,服务所有者过去常常为这些类型的场景提供大量的缓存。请参见下面的示例:

 

正常流量(实时数据集是 150M)



图 4:正常操作。左边是默认配置,右边是手动调整。

 

流量翻倍(实时数据集是 300M)



图 5:负载翻倍。左边是默认配置,右边是手动调整。

流量翻倍且 GOGCTuner 设置为 70%(实时数据集是 300M)



图 6:流量翻倍,但使用微调器。左边是默认配置,右边是 GOGCTuner 调整。

 

  • 使用MADV_FREE内存策略的服务会导致错误的内存度量。例如,我们的可观测性指标显示了 50%的内存使用量(实际上它已经释放了这 50%中的 20%)。然后,服务所有者只使用这个“不准确的”指标来调整 GOGC。

 

可观测性

我们发现,我们缺乏一些可以让我们对每个服务的垃圾回收有更多了解的关键指标。

  • 垃圾回收之间的间隔:这可以使我们了解是否还可以调整。如果你的服务仍然有很高的 GC 影响,但你已经看到了这个图 120s,这意味着你不能再使用 GOGC 进行调整。在这种情况下,您需要优化分配。

 


图 7:GC 之间的间隔图。

 

  • GC CPU 影响:让我们知道哪些服务受 GC 影响最大。



图 8:p99 GC CPU 消耗图。

 

  • 实时数据集大小:帮助我们识别内存泄漏。服务所有者注意到的问题是,他们看到了内存使用量的提高。为了向他们表明没有内存泄漏,我们添加了“实时使用量”指标,展示了稳定的内存使用量。



图 9:p99 实时数据集预估图。

 

  • GOGC 值:对于了解调整的效果非常有用。



图 10:微调器给应用程序分配 min、p50、p99 GOGC 值的图。

 

实现

我们最初的方法是,让一个计时器每秒运行一次来监控堆指标,然后相应地调整 GOGC 值。这种方法的缺点是,开销开始变得相当大,因为为了读取堆指标,Go 需要执行一次 STW(ReadMemStats),这还不怎么准确,因为我们每秒可能会多次进行垃圾回收。

 

幸运的是,我们找到了一种替代方案。Go 有 finalizers(SetFinalizer),它们是在垃圾回收对象时运行的函数。它们主要用于清理 C 代码或其它资源中的内存。我们可以使用一个自引用的 finalizer,在每次 GC 调用时重置自己。这能够使我们减少任何 CPU 开销。例如:



图 11:GC 触发事件的示例代码。

 

调用运行时。在 finalizerHandler 中的 SetFinalizer(f, finalizerHandler)允许应用程序在每个 GC 上运行;它基本上不会让引用消亡,因为它不是一个代价高昂的资源(它只是一个指针)。

 

影响

在我们的几十个服务中部署了 GOGCTuner 之后,我们深入研究了其中一些在 CPU 使用量上有显著的两位数提升的服务。仅这些服务就累积节省了约 70K 内核。下面是 2 个这样的例子:



图 12:在数千个计算内核上运行,实时数据集的标准差很高(最大值是最小值的 10 倍)的可观测性服务,显示 p99 CPU 的使用降低了约 65%。

 


图 13:运行在数千个计算核心上的关键任务 Uber eats 服务,显示 p99 CPU 的使用降低了约 30%。

 

由此导致的 CPU 使用的减少在战术上优化了 p99 的延迟(以及相关的 SLA、用户体验),并在战略上优化了性能成本(因为服务是根据他们的使用量进行扩展的)。

 

结语

垃圾回收是影响应用程序性能的最难以捉摸且被低估的因素之一。Go 强大的 GC 机制和简化的调优,我们多样化的大规模的 Go 服务足迹,以及强大的内部平台(Go、计算、可观测性),共同让我们能够产生如此大规模的影响。由于技术和我们能力的变化,问题本身正在演变,我们希望继续改进 GC 调优的方式。

 

重申我们在引言中提到的:没有万能的解决方案。我们认为,由于公共云和运行在其中的容器化负载的性能高度可变,在云原生设置中 GC 性能也是变化的。再加上我们使用的绝大多数 CNCF 落地项目(Kubernetes、Prometheus、Jaeger 等等)都是用 Golang 编写的,这意味着任何外部的大规模部署也可以受益于这些工作。


 作者介绍:

Cristian Velazquez 是 Uber 的地图制作工程团队的高级二级工程师。他负责多个效率倡议,这些倡议跨多个组织,其中最相关的是 Java 和 Go 的垃圾回收调优。


原文链接:

How We Saved 70K Cores Across 30 Mission-Critical Services (Large-Scale, Semi-Automated Go GC Tuning @Uber)

2022-02-09 19:153295

评论 1 条评论

发布
用户头像
把core翻译成内核实在不应该,这里指cpu的核数,不是内核。
2022-04-08 19:08
回复
没有更多了
发现更多内容

区块链数字钱包开发,数字钱包app

13530558032

大数据任务调度 - 有向无环图(DAG)之拓扑排序

代立冬

大数据 数据结构 DAG 拓扑排序 Apache DolphinScheduler

大厂面试爱问的「调度算法」,20 张图一举拿下

小林coding

算法 操作系统 内存 进程

熟悉又陌生的 k8s 字段:finalizers

郭旭东

Kubernetes

LiteOS间歇计算技术:IOT终端真正感受“电量自由”

华为云开发者联盟

物联网 LiteOS

如何利用Chrome DevTools优化网页性能

熊斌

学习

GaussDB(DWS)应用实战:对被视图引用的表进行DDL操作

华为云开发者联盟

数据库 dll postgre

年薪150万的阿里大佬工资全部上交!家务全包!却被老婆嘲讽嫌弃!网友:你老婆外面有人!

程序员生活志

互联网 职场 大厂 薪资

USDT支付通道搭建,USDT承兑商跑分系统

合约跟单模式系统开发,交易所合约跟单源码

13530558032

探路人与解题者:腾讯数字生态大会上AI语音助手+X的无限可能

脑极体

编程的修养

紫枫

读书笔记

这13道面试题,哪怕背你也要背过来。别说我没提醒你

小Q

Java 源码 架构 面试 多线程

我四面字节跳动,拿下1-2级offer,太感谢这份“神仙级面试真经pdf”

编程 面试 计算机网络 架构师

oeasy教您玩转linux010210管理应用aptitude

o

Pulsar Flink Connector 2.5.0 正式发布

Apache Pulsar

flink 开源 flink 消费 kafak Apache Pulsar

多线程与高并发之锁

彭阿三

多线程 多线程与高并发

字节跳动半夜给员工发钱,全员沸腾了

程序员生活志

字节跳动 职场 薪资

滴滴AR实景导航背后的技术

滴滴技术

人工智能 滴滴技术 实景导航 地图与公交事业群分享月

话题讨论 | 程序员们来说一说,你们从编程开始到现在共使用过多少种语言?

InfoQ写作社区官方

写作平台 话题讨论 语言

科普:Java 后端开发常用的 10 种第三方服务

沉默王二

Java 后端 第三方服务

为了面个好公司!拼了!3.5W字的Java面试题整理(答案+学习路线)上!

Java架构师迁哥

云小课 |选定合适的证书,做“有证”的合规域名

华为云开发者联盟

证书 课程练习 ssl

[翻译]Go Code Review Comments

卓丁

从零开始搭建完整的电影全栈系统(四)——restfulApi用户的认证授权及用户注册

刘强西

RESTful API yii

区块链支付通道系统搭建,USDT跑分承兑商系统

大项目写代码写到晕头转向?敏捷多项目框架解君愁

Philips

敏捷开发 程序设计 软件架构

USDT承兑支付系统,区块链跨境支付源码

13530558032

超酷! Atlas给黑白视频“上色”

华为云开发者联盟

视频 Atlas

数字货币交易所开发方案,交易所源码

13530558032

拥抱K8S系列-06-K8S如何解决docker部署的问题

张无忌

Docker Kubernetes 运维 service

我们如何在30项关键服务任务中节省70K内核_开源_Cristian Velazquez_InfoQ精选文章