GMTC北京站9折购票最后一周,2022年大前端方向又有哪些技术热点? 了解详情
写点什么

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

  • 2022 年 2 月 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 年 2 月 09 日 19:152475

评论 1 条评论

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

日记 2021年2月14日(周日)

Changing Lin

2月春节不断更

还傻傻分不清楚equals和==的区别吗?看完就明白了

codevald

Java 源码分析 string Object

《我们脑中挥之不去的问题》 - 卓克科普(2)

石云升

读书笔记 科普 2月春节不断更

第十二周学习总结

Binary

Spring框架源码:BeanFactory与Bean的生命周期

程序员架构进阶

Java spring 源码阅读 七日更 2月春节不断更

第十二周课后作业

Binary

熬夜总结了 “HTML5画布” 的知识点(共10条)

我是哪吒

学习 读书笔记 程序员 随笔杂谈 2月春节不断更

理解「分布式系统」曾经发生的事情

守护石

分布式事务 分布式系统 分布式数据储存 TiDB EJB

Chrome浏览器多进程架构3个必会知识点

梁龙先森

面试 大前端 浏览器

Tomcat速查手册

jiangling500

Java tomcat

聊聊大公司创新的机制:饱和攻击

boshi

创新 七日更

「架构师训练营 4 期」 第七周 - 001&2

凯迪

架构师训练营 4 期

架构师训练营 4 期 第7周

引花眠

架构师训练营 4 期

Elasticsearch Mapping Overview

escray

elastic 七日更 死磕Elasticsearch 60天通过Elastic认证考试 2月春节不断更

Java SE最佳实践

jiangling500

Java 最佳实践 Java SE

我在极客时间录课的故事(三):课程录完了,服务才刚刚开始

《微信小程序全栈开发实战》专栏讲师

我在极客时间录课的故事

9. Python 学习过程的第一个山坡,99%的人都倒在了山坡下

梦想橡皮擦

Python 2月春节不断更 python入门 python学习

深入理解gradle中的task

程序那些事

Java maven Gradle 程序那些事 构建工具

JDBC速查手册

jiangling500

Java JDBC

SpringMVC专栏 第1篇 - 快速入门

小马哥

Java spring Spring MVC 七日更 二月春节不断更

ElasticSearch.01-简介

insight

elasticsearch 2月春节不断更

Tomcat异常: Unable to process Jar entry [module-info.class] from Jar

小马哥

Java maven 七日更 二月春节不断更

熬夜7天,我总结了JavaScript与ES的25个重要知识点!

我是哪吒

学习 程序员 面试 大前端 2月春节不断更

深入浅出函数式编程:Stream流水线的实现原理

码农架构

Java 架构 微服务

杨明越:Kubernetes的下一仗可能是提升标准化程度

杨明越

【STM32】串口通信---用代码与芯片对话

AXYZdong

硬件 stm32 2月春节不断更

【LeetCode】情侣牵手Java题解

HQ数字卡

算法 LeetCode 2月春节不断更

盘点关于程序员的那些经典案例

孙叫兽

程序员 程序人生 话题讨论 薪水 计算机原理

第十二周 数据应用一 作业 「架构师训练营 3 期」

胡云飞

Scrum Patterns:梳理产品待办列表(译)

Bruce Talk

敏捷开发 译文 Agile Scrum Patterns

10. 比找女朋友还难的技术点,Python 面向对象

梦想橡皮擦

Python 2月春节不断更 python入门

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