写点什么

我们如何在 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:153320

评论 1 条评论

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

python列表转字符串

ベ布小禅

4月日更

卧槽,误删数据库了,会被开除吗?

AI乔治

Java 数据库 sql 架构 SQL语法

聪明人的训练(十四)

Changing Lin

4月日更

特权访问管理(PAM)即服务

龙归科技

架构实战营 - 模块 2- 作业

carl

架构实战营

全国沿海港口首个区块链木材业务服务平台上线试运行,“区块链+港口”撬动数千万元“福利”

CECBC

港口

自学Java走进阿里,仅用了六个月,他是怎么做到的?

Java架构师迁哥

portal认证-上线流程

箭上有毒

硬核!阿里内部这份《Java面试核心知识手册》在Github上已获赞高达89.7K!

Java架构之路

Java 程序员 架构 面试 编程语言

工业互联网的脖子被卡死了?

工业互联网

阿里总结出Java九大核心专题,1159页内容,吃透后我上个月砍下5个大厂Offer!

Java架构追梦

Java 阿里巴巴 架构 面试 九大核心专题

面试官:聊一聊SpringBoot服务监控机制

AI乔治

Java spring 架构 微服务 springboot

每天学一个 Linux 命令(2):shutdown

民工哥

Linux 程序员 运维

理论 + 标准 + 工程 —— 阿里云视频云编码优化的思考与发现

阿里云CloudImagine

阿里云 视频编码 视频算法 视频处理

从中国企业进入IEC最高决策机构,看科技领先的产业价值与用户价值

脑极体

一篇文章了解CI/CD管道全流程

禅道项目管理

DevOps 持续集成 持续交付

剖析6个MySQL死锁案例的原因以及死锁预防策略

北游学Java

Java MySQL 数据库 死锁

canvas小球绕斜椭圆轨迹运动

空城机

JavaScript 大前端 canvas 4月日更

如果以这样的方式,你愿参与到碳普惠行动中吗?

CECBC

区块链

当造车成为风潮,谁帮助“造车党”连接未来?

脑极体

spring中让你眼前一亮的代码技巧

AI乔治

Java spring 架构 微服务

入职字节跳动那一天,我哭了(蘑菇街被裁,奋战7个月拿下offer)

Java架构追梦

Java 架构 字节跳动 面试

GitHub持续霸榜!2021年Java核心知识:面试突击版

Java架构之路

Java 程序员 架构 面试 编程语言

代码回现 | 如何实现交易反欺诈?

VoltDB

数据分析 金融科技 VoltDB

云上接单不空跑 京东云助力“佬司机”为货运物流业降本增效

CECBC

京东云

2021金三银四:狂刷398道Java最新MySQL笔记;成功收获9个Offer

比伯

Java MySQL 编程 架构 计算机

一个CURD三年的Java程序员刷完这份《阿里面试指南(恒山版)》,居然斩获了十七个offer

Java架构之路

Java 程序员 架构 面试 编程语言

一份完美的阿里开源Java面试宝典,Github上star数已30K+

Java架构师迁哥

阿里巴巴架构师王小瑞“墙裂”推荐:RocketMQ核心实战原理

Java架构师迁哥

每天一个 Linux 命令(1):cd

民工哥

Linux 运维

小程序支持MQTT协议

风翱

小程序 websocket mqtt 4月日更

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