【AICon】探索RAG 技术在实际应用中遇到的挑战及应对策略!AICon精华内容已上线73%>>> 了解详情
写点什么

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

评论 1 条评论

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

云图说 | 华为云医疗智能体智联大健康:AI医学影像

华为云开发者联盟

AI 医学影像 医疗智能体 华为云医疗智能体 大健康

并发王者课-铂金6:青出于蓝-Condition如何把等待与通知玩出新花样

MetaThoughts

Java 多线程 并发

合肥智慧社区平台建设解决方案,平安小区建设

iOS不行?还是个人能力有限?

ios 程序员 IT 编程之路

数字化转型提升太平洋保险风险治理能力

数据湖洞见

大数据

Nacos配置中心交互模型是 push 还是 pull ?你应该这么回答

程序员小富

Java 编程 程序员 分布式 nacos

今天,「浪潮云说」直播间开讲啦!

浪潮云

云计算

涨薪50%,从小厂逆袭,坐上美团L8技术专家(面经+心得)

Java 编程 程序员 面试

Flink 的底层API

五分钟学大数据

flink 7月日更

系统故障防不胜防?不存在的,让大佬来给你上一课!

TakinTalks稳定性社区

高可用 测试 全链路压测 测试工具 生产环境全链路压测

并发王者课-铂金7:整齐划一-CountDownLatch如何协调多线程的开始和结束

MetaThoughts

Java 多线程 并发

并发王者课-铂金8:峡谷幽会-看CyclicBarrier如何跨越重峦叠嶂

MetaThoughts

Java 并发 多线

论文解读丨文档结构分析

华为云开发者联盟

模型 文档 文档结构分析 分割 文档结构

为什么nginx主机的io使用率会100%?

BUG侦探

nginx proxy_buffering docker镜像

DDD笔记

topsion

《持之以恒的从事运动》二

Changing Lin

CloudQuery 使用教程 No.4 数据查询(下)

BinTools图尔兹

dba 数据库管理工具 国产数据库 运维开发

不愧是阿里内部“SpringCloudAlibaba学习笔记”竟然在GitHub霸榜月余

Java 编程 架构 微服务

Java开发从二面被拒到收割阿里架构offer,我花了一年时间,复盘成功经历!

Java架构追梦

Java 阿里巴巴 架构 offer 成长笔记

阿里P8耗时半年总结的Java核心面试知识,助我轻松拿下蚂蚁offer

Java 程序员 面试 java编程 java技术宅

iOS开发 · iOS音视频开发 - ARKit 教学:如何搭配SceneKit来建立一个简单的ARKit Demo

iOSer

ios ios开发 ARKit iOSAR.

CDH安装搭建(一)

大数据技术指南

CDH 7月日更

免费分享学习Java框架Netty的优秀图书

Java入门到架构

Java 书籍推荐

聊聊数据仓库中维度表设计的二三事

云祁

数据仓库 维度建模 7月日更

Rust从0到1-Cargo-安装来自Crates.io的程序

rust cargo install

阿里技术分享:闲鱼IM基于Flutter的移动端跨端改造实践

JackJiang

flutter 即时通讯 IM

一文讲懂Hive高可用、HiveServer2高可用及Metastore高可用

白程序员的自习室

数据仓库 7月日更 HiveServer2高可用 Metastore高可用 Hive高可用

从结构体、内存池初始化到申请释放,详细解读鸿蒙轻内核的动态内存管理

华为云开发者联盟

鸿蒙

从零实现一个 k-v 存储引擎

roseduan

存储 Go 语言 KV存储引擎 存储系统

国家网信办:“滴滴出行” 下架整改!

学神来啦

Serverless 崛起背后的五大挑战

Serverless Devs

Serverless

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