GrpConf-2025-笔记-全-

GrpConf 2025 笔记(全)

001:欢迎与开幕致辞 🎤

在本节课中,我们将学习gRPC Conf 2025开幕致辞的核心内容,了解gRPC项目的现状、社区动态以及未来的发展方向。

大家好,欢迎来到gRPC Conf。我是Kevin Nelson,今晚的主持人。我是gRPC指导委员会的新成员,同时也是本次会议的程序主席,负责筛选我们所有的精彩演讲。

说到演讲,我们邀请了来自许多优秀公司的演讲者,包括Apple、Cloudflare、Mastercard、Netflix、WSO2、LinkedIn、Reddit等众多公司。能再次来到这里,真的非常令人兴奋。

社区互动与回顾

上一节我们介绍了本次会议的基本情况,本节中我们来看看社区的参与情况。

首先,我想了解一下大家的参与情况。去年对gRPC而言是非常重要的一年,我们庆祝了其十周年纪念。令人兴奋的是,gRPC依然保持着强大的生命力,我们今天将为大家介绍许多超级激动人心的新内容。现在,我们即将迎来它的第十一个年头。

我想感谢所有到场的各位。我知道大家都和我一样,日程繁忙,有很多事情要处理,能来到这里实属不易。希望我们精心准备的内容能让大家今天有所收获,享受学习过程,并结识gRPC团队的优秀成员。

以下是关于参会者背景的几个问题:

  • 有多少人来自本地湾区?
  • 有多少人来自加州以外的美国其他地区?
  • 有多少人是第一次参加gRPC Conf?
  • 有多少人参加了之前在底特律的会议?

接下来,我想了解大家使用的编程语言情况:

  • 有多少人在使用gRPC Java或Kotlin?
  • 有多少人在使用Python?
  • 有多少人在使用.NET/C#?
  • 有多少人已经在使用Rust?这是我们今天超级兴奋的新内容,我们正在进行早期预览,并准备了相关的代码实验室。这是几年前在gRPC Conf和KubeCon上从社区听到的需求,我们为此组建了团队并努力开发。
  • 有多少人在使用超过一种语言?超过两种?超过三种?有没有使用超过四种的多语言开发者?

致谢与项目进展

在进入正题之前,我要感谢一些人。

首先,感谢Google Cloud作为钻石赞助商,提供了场地、食物、T恤、背包等所有支持。我和gRPC团队都非常感激Google的帮助,让今天的活动得以举办。

其次,感谢CNCF(云原生计算基金会)。gRPC团队要感谢CNCF,我们在项目发展和本次会议筹备中得到了CNCF许多人的大量支持。我们正在申请CNCF毕业阶段,这是CNCF项目生命周期的最高阶段,标志着项目达到了最高水平的成熟度、采用率和社区支持。在此过程中,我们与CNCF紧密合作,并在其帮助下,彻底重写了我们的治理结构,创建了新的gRPC指导委员会来帮助推动gRPC的未来方向。我们将在下周内向CNCF TOC提交毕业申请。今天下午3点15分,Richard Bellville将上台详细介绍我们的治理变革,如果你对此感兴趣或有任何反馈,欢迎前往聆听。

接下来,感谢程序委员会的Antoine、Eric和Trinniy,他们帮助我决定和优先安排了今天的内容。

我还要感谢组织委员会的Kathy、Cellia、Cheryl和Stay,他们在过去九个月里筹备会议,安排场地、食物等所有细节。特别感谢Kathy在各方面的大力支持。

gRPC项目成立了新的指导委员会来帮助指引方向。如果你看到这些成员,请随时向他们提出你的想法。他们今天都在现场,这是一群长期参与和贡献于gRPC的人,正在帮助我们确定发展方向,满足用户需求。祝贺并欢迎台上的各位:Antoine、April、Craig、Gina、我自己、Mark和Pur。

此外,今天有超过30名gRPC团队成员到场,他们来自Google、Apple、LinkedIn、Datadog、Microsoft等公司。

最后,我想表彰两位来自社区的杰出贡献者。Lucofranco在我们Rust实现的早期提供了巨大帮助。我们与他合作,共同开发了即将作为gRPC官方支持语言发布的Rust版本。另一位是来自Datadog的Antoine,他为Go代码库做出了大量重大贡献,大约一年前我们晋升他为维护者。

参与渠道与资源更新

上一节我们感谢了各方支持,本节中我们来看看如何与gRPC社区互动并获取最新资源。

我想提醒大家几个关键的链接和渠道,以便了解更多信息并与gRPC团队互动。

以下是主要的参与渠道:

  • 文档:访问 grpc.io
  • Google群组groups.google.com/forum/#!forum/grpc-io。这是与其他gRPC用户交流的好地方,可以分享想法、提问或讨论有趣的用法。
  • YouTube频道:我们收到了反馈,开发者越来越多地从YouTube获取文档。因此我们投入了大量精力,创建了许多视频内容。
  • Meetup:我们在Sunnyvale(谷歌园区)和班加罗尔有线下聚会,也有虚拟聚会。欢迎加入。
  • 社交媒体:我们的公告(如gRPC Conf消息)会发布在X(原Twitter)上。

正如之前提到的,我们根据大家的反馈,对开发者指南进行了大量改进。

以下是资源更新的具体内容:

  • 新增了6个之前没有的新用户指南。
  • 增加了许多新示例。
  • 本周发布了新的代码实验室,采用了更易使用的新格式,包括Rust的新实验。今天全天都会有代码实验室活动,如果你是gRPC新手或想尝试新语言,这是一个很好的起点。
  • 最后,如前所述,我们制作了大量新的YouTube视频。今天的活动视频将发布在CNCF YouTube频道以及gRPC团队的YouTube频道上。今年晚些时候在班加罗尔举行的活动内容也会发布。

项目增长与数据

我想展示一个我总是在这种活动或KubeCon演讲前查看的图表,它反映了我们的发展趋势。

我们的主代码库(图中红色部分)清晰地显示,gRPC持续增长,保持其重要性,用户数量随时间不断增加。这对于一个拥有11年历史、经历了11年错误修复和性能改进的项目来说,是一个很好的迹象。感谢大家帮助gRPC项目取得今天的成就。

以下是一些令人印象深刻的下载统计数据:

  • Maven Central(Java):每周700万次下载。
  • Python:每周4300万次下载。
  • NPM:每周1500万次下载。

这些数字非常惊人,我们很高兴看到它们持续增长,并且每年都在不断攀升。

本节课中我们一起学习了gRPC Conf 2025的开幕致辞,了解了gRPC社区的活力、项目的最新进展(包括向CNCF毕业的申请、新治理结构、Rust支持等),以及丰富的学习资源和持续增长的项目数据。

我的部分到此结束,稍后我会回来。现在,有请Google高级总监Rajeiv,他将谈谈我们今天看到的gRPC发展现状与激动人心的前景。

谢谢。

002:主题演讲

在本节课中,我们将学习gRPC的发展历程、成功的关键因素、在云原生和人工智能领域的最新进展,以及未来的发展方向。


🚀 gRPC的广泛采用

我是Rajeiv。我使用gRPC已经超过10年,大概就在它开源之后不久。这段时间里,我在初创公司和大公司都有使用经验。因此,我非常高兴能作为gRPC和gRPC conf的赞助商来到这里。

感谢大家参加gRPC conf。能与各位相聚,我感到非常兴奋。在接下来的几分钟里,我将讨论gRPC令人难以置信的采用率、我们当前的状况以及未来的发展方向。

自我们发布gRPC以来,已经过去了10年。在这段时间里,gRPC在各个行业经历了快速的采用。其高性能和对多种语言的支持,使其成为构建现代分布式系统高效、可扩展通信的优选方案。

思科可能是第一个开始集成gRPC的公司,大概在gRPC开源一年后。随后是Netflix,它既是贡献者,也将其用于后端通信,接着是Spotify、Reddit、LinkedIn等公司。事实上,我记得在去年的Qcon大会上,LinkedIn分享了他们如何将5万个端点从内部框架迁移到gRPC。看到我们的解决方案被广泛接受,并在各个行业和应用中产生积极影响,这非常令人鼓舞。


🔑 gRPC成功的关键因素

上一节我们看到了gRPC的广泛采用,本节中我们来看看是什么让它如此成功。

gRPC非常适合云原生环境。它旨在通过使用协议缓冲区来减少消息大小,以及利用HTTP/2来提高网络效率和实现实时通信,从而实现高效通信。这使其成为需要交换大量数据的微服务的理想选择。

gRPC与语言无关的特性允许您使用偏好的语言构建和部署服务。这种可移植性对于云原生环境至关重要,因为在云原生环境中,使用不同语言构建微服务是很常见的。

最后,gRPC支持微服务的架构模式。这允许团队将大问题分解为更小、封装良好的服务,最终提升开发速度,并让小型团队能够并行工作,从而加速整体开发过程。此外,gRPC还提供了负载均衡和重试等功能,以提高这些分布式系统的可靠性。

我们在Google内部大量使用gRPC,例如GKE、Cloud Run、Kubernetes以及许多Google项目都将其用于内部通信。因此,gRPC已成为云原生环境中构建现代、可扩展、高效应用程序的热门选择。


☁️ gRPC的云原生演进

我们持续致力于让gRPC更加云原生友好。无代理的gRPC服务网格简化了部署流程,并消除了运行和维护边车代理的操作开销。这种方法不仅降低了复杂性,还提高了资源效率,使其对大规模云原生环境具有吸引力。

此外,gRPC自带许多功能,允许您将这些服务网格能力直接集成到应用程序中。如果您想了解更多相关信息,Eric Anderson将在下午1:50讨论服务网格和gRPC。

在去年的gRPC conf上,我们宣布了与Tokio的合作,并且自那时起一直与维护者紧密合作。过去一年我们取得了重大进展,并将很快推出预览版。请参加Doug在下午2:20的精彩演讲,深入了解gRPC Rust并了解我们的进展。如果您想亲自动手并获得一些gRPC Rust的实践经验,我们在下午4:10还有一个代码实验室。


🤖 gRPC在人工智能与机器学习中的角色

过去几年,人工智能与机器学习领域蓬勃发展。gRPC正在彻底改变AI模型通信和操作的方式。其固有的速度、效率和对数据流的支持,使其成为这些AI/ML管道中的关键构建块,有助于训练大型模型并快速处理推理请求。

gRPC促进的高效数据摄取和处理,显著加速了我们的模型开发生命周期。训练大规模机器学习模型需要使用海量数据,gRPC由协议缓冲区驱动的数据序列化能力,结合其处理大负载的能力,使其成为一个可靠且稳健的解决方案,能够将这些数据直接导入训练管道。

得益于其低延迟和高吞吐量,gRPC也能高效处理推理请求。像TensorFlow这样著名的机器学习框架就利用gRPC来管理和处理大量的推理请求。NVIDIA的Triton推理服务器框架,帮助您构建和部署模型,也使用gRPC作为其传输层。因此,gRPC确保了实时应用能够以最小的延迟接收预测。

我们在Google的AI/ML领域做了大量工作。今天我们可以宣布一些关于MCP和A2A的令人兴奋的事情。MCP(模型上下文协议)允许AI代理连接并使用外部工具、数据库和服务。A2A是一种协议,允许不同的AI代理相互协作、相互委派任务等。gRPC将伴随您的AI/ML之旅,我们今天晚些时候还有几场会议会讨论这个话题。


📝 总结

本节课中我们一起学习了gRPC过去十年的发展历程和广泛采用。我们探讨了其成功的关键因素,包括对云原生环境的卓越适应性、语言无关性以及对微服务架构的支持。我们还了解了gRPC在云原生演进方面的最新进展,例如无代理服务网格,以及它与Rust生态的集成。最后,我们看到了gRPC在人工智能和机器学习领域扮演的关键角色,它正成为高效数据管道和实时推理的基石。

再次感谢大家来到gRPC conf。我们真诚感谢您抽出时间来到这里。我们衷心希望您能度过充满激动人心话题和宝贵见解的美好一天。关于gRPC如何被使用以及它如何可能帮助您解决问题,还有很多值得学习的地方。祝您会议期间一切愉快。

003:gRPC核心概念与生命周期详解 🚀

在本节课中,我们将要学习gRPC的核心概念及其完整的生命周期。如果你是gRPC的新手,你将掌握其基本概念。对于已经熟悉gRPC的开发者,这可以作为一个快速的复习,为后续深入学习高级主题和用例做好准备。

什么是gRPC? 💡

gRPC是一个开源的高性能远程过程调用(RPC)框架。它易于使用、高效,并已成为行业标准。简单来说,你可以将gRPC视为一个高速的数据传输服务,它能在互联网上可靠且快速地在你的服务与应用之间传递信息。

gRPC的流行源于其灵活性和高性能。除了支持广泛的编程语言和平台外,其性能在业界处于领先地位。该框架采用可插拔架构,能够灵活高效地与各种开发栈集成。此外,gRPC还提供了丰富的功能集,用于流量管理、安全和服务网格集成。

核心设计决策 🔧

gRPC的一个关键设计选择是使用Protocol Buffers作为其接口定义语言来构建服务消息。Protobuf采用二进制编码格式,这带来了更小的消息体积和高效的解析效率。与其他RPC框架相比,这直接促成了gRPC的高性能和高灵活性。

gRPC的另一个基础设计决策是构建在HTTP/2之上。这确保了与许多现有负载均衡器的兼容性。HTTP/2的头部压缩、二进制编码以及在单个TCP连接上的多路复用特性,使得gRPC成为一个能降低网络延迟、更高效利用资源的高性能框架。

gRPC核心概念 🧩

上一节我们介绍了gRPC的基础,本节中我们来看看它的核心组成部分。

通道

一个gRPC通道是一个对象,代表到一个gRPC服务的虚拟连接,由目标URI标识。gRPC通道负责管理与目标服务的TCP连接,根据应用需求,可以建立0个、1个或多个连接。通道还负责处理关键功能,如名称解析和负载均衡,开发者无需自行处理。

存根与调用

建立gRPC通道后,你可以从中创建一个客户端存根。这个从你的proto定义生成的存根,是你用来进行远程调用的对象。当你在存根上调用一个方法时,它会在gRPC运行时内发起一个逻辑调用。这个调用随后在传输层被映射到一个HTTP/2流,由该流处理实际的数据传输。本质上,RPC、调用和流指的是同一个基本概念,只是在不同抽象层使用的不同术语。

名称解析

在gRPC通道连接到服务器之前,它必须首先执行名称解析。这是因为应用程序在创建gRPC通道时使用目标URI来指定要连接的服务,但底层的传输层需要具体的IP地址来建立连接。在gRPC中,名称解析是根据给定的URI找出服务器IP地址的过程。可以将其视为gRPC的电话簿。当客户端想要调用服务器时,它会使用服务器URI来查找其地址,然后再建立连接。虽然这听起来很像DNS的工作,但gRPC的名称解析更加灵活,它采用可插拔架构,允许使用不同的策略来查找服务器地址。

名称解析器随后将服务配置返回给下一个组件:负载均衡器。

负载均衡

负载均衡器管理到后端服务器的开放连接,并根据服务配置在多个后端服务器之间分发请求。gRPC内置了负载均衡功能,提供了几种常见的负载均衡策略,例如轮询、加权轮询,默认策略是“pick first”。负载均衡是gRPC中最关键的组件之一。

一旦连接建立,gRPC会序列化请求数据,并按照HTTP/2协议以帧的形式发送。

服务器端处理流程 🔄

服务器端与客户端镜像对应。服务器传输层接收请求,然后通知应用层。应用服务器逻辑随后处理请求,并返回响应发送回客户端。

通信模式 📡

gRPC支持四种通信模式,你可以选择最适合你通信需求的一种。

以下是四种主要的通信模式:

  1. 一元RPC:经典的请求-响应模型。客户端发送单个请求,并收到单个响应。
  2. 服务器端流式RPC:客户端发送单个请求,并收到多个响应。
  3. 客户端流式RPC:客户端发送多个请求,并收到单个响应。
  4. 双向流式RPC:客户端和服务器各自向对方发送独立的、并行的消息流。

其他重要特性 ⚙️

现在我们已经介绍了gRPC的生命周期,接下来看看其他几个重要特性。

拦截器

gRPC拦截器是gRPC框架中一个强大的中间件组件,允许你在请求到达预定目的地之前或之后拦截并修改它们。如图所示,gRPC会在特定点调用你的拦截器任务。拦截器提供了一种简洁的方式来为你的gRPC服务添加横切关注点,如认证、授权、错误处理等,而无需污染主应用逻辑。

截止时间

截止时间是gRPC提供的另一个特性,它是一种客户端机制,用于防止RPC无限期运行。客户端指定愿意等待响应的时间,这个超时可以设置为一个固定的时间点或一个持续时间。如果超过时间,调用将被取消,并返回“截止时间已过”的状态。这可能由多种原因导致,例如连接服务器失败,或者服务器认为客户端设置的截止时间太短而无法完成。

至关重要的是,gRPC支持截止时间传播。当服务器收到请求时,它也会收到关联的截止时间。如果该服务器随后向其他上游服务发起自己的RPC调用,它会自动转发剩余时间,确保原始客户端的时间限制在整个分布式系统中得到执行。这是应对网络延迟或服务器问题的重要保障措施。我们强烈建议你在应用中始终设置截止时间或超时。

取消

除了截止时间,gRPC客户端还可以在不再需要时手动取消一个RPC。取消信号通过HTTP/2传输层传播到服务器,允许服务器停止处理并清理资源。为了使此功能有效,特别是对于长时间运行的RPC,服务器处理程序应定期检查其正在服务的调用是否已被取消。一旦检测到取消,服务器应立即停止处理,清理资源,并理想情况下将取消传播到它可能调用的任何下游服务。

重试

gRPC重试是一个允许客户端在RPC调用失败时自动重试的特性。这是一个构建弹性应用的强大机制,可以优雅地处理短暂的服务器端问题或临时网络问题,而无需在应用代码中添加复杂逻辑。

当你启用gRPC重试时,客户端通道会配置一个重试策略。该策略定义了重试失败调用的规则,例如最大尝试次数、退避延迟和可重试状态码。当满足条件时,gRPC会在指数退避延迟后创建一个新的重试流。一旦收到响应,将不再尝试重试,gRPC会将调用移交给应用。如果你启用了可观测性,你将能看到重试指标,包括每次调用的重试尝试次数和退避延迟等详细信息。

状态码与终止

每个RPC最终都会终止,要么成功,要么返回错误码。调用的终止通过状态码进行通信,状态码告知客户端RPC的结果。

在关闭应用程序时,正确终止gRPC通道非常重要。你可以使用shutdown方法启动优雅终止,该方法会拒绝新的调用,但允许任何正在进行的请求完成。由于关闭是异步的,你必须等待该过程完成。你可以使用awaitTermination方法阻塞直到通道完全终止,或达到某个特定时间,以确保干净的关闭。对于更即时的停止,shutdownNow方法将强制取消所有正在进行的和新的调用。

总结 📝

本节课中我们一起学习了gRPC的核心概念与生命周期。

我们首先从基础开始:gRPC是一个现代、开源、高性能的远程过程调用框架,构建在Protocol Buffers和HTTP/2之上。

接着,我们走查了一个gRPC调用的完整生命周期,从通道创建、名称解析到负载均衡。

最后,我们涵盖了几个关键的最佳实践和用例,这些对于构建健壮的应用程序至关重要:

  • 使用拦截器作为强大的中间件,在gRPC调用到达预定目的地之前或之后修改它们。
  • 设置截止时间或超时以防止RPC无限期运行,并且截止时间会传播到任何上游服务。
  • 使用取消功能,如果你不再关心RPC的结果。服务器处理程序应在处理请求前定期检查其调用是否已被取消,并强烈建议将取消传播到任何下游服务。
  • 配置重试策略以实现容错和弹性。
  • 在应用程序关闭期间优雅地终止gRPC通道。

希望本教程能帮助你更好地理解和使用gRPC。祝你在gRPC应用开发中取得成功!

004:主题演讲

在本节课中,我们将探讨gRPC在人工智能时代的基础性作用。我们将回顾市场趋势,分析AI对网络基础设施提出的新要求,并介绍Google Cloud为支持AI训练与推理、保障安全以及赋能智能体(Agent)通信所推出的关键技术与创新。

市场趋势与AI时代的网络需求 🚀

首先,我们简要回顾过去25年的市场趋势。众所周知,我们经历了互联网时代,构建了大量基础设施、工作负载和电子商务,许多新公司应运而生。随后,我们进入了以YouTube和Netflix等为代表的流媒体时代,云计算也随之兴起。

如今,我们正处于第四个,也是数十年来最大的转折点。这意味着我们现有的网络和gRPC能力必须演进,不能与过去二十年完全相同。我们需要重新构想网络栈的未来面貌。

在AI训练和推理的背景下,我们需要一个高性能、低延迟的云基础设施,能够支持所有类型的模型,连接任何类型的智能体,并安全地接入任何模型或工具。

行业现状与挑战 ⚙️

接下来,我们具体看看行业从AI视角面临的现状与挑战。

以下是企业普遍观察到的情况:

  • 模型多样性:约93%的财富500强公司已在同时使用多个模型。平均而言,这些企业针对不同工作负载会使用多达三个模型。这带来了复杂性,并可能导致GPU/TPU基础设施利用率不足和成本效率低下。
  • 数据量与工作负载转移:我们看到因训练而产生的数据传输量达到PB级别。同时,几乎所有企业都在大力推动将其应用演进为生成式AI应用,这使得推理工作负载的重要性将远超训练。
  • 安全至关重要:安全必须内建于我们所做的一切之中,不能是事后补救。在AI领域,我们已经看到关于大语言模型被轻易攻破、护栏被移除、模型被投毒、数据被窃取或恶意流量注入导致模型产生幻觉的新闻。例如,有报告显示,90%的提示词泄露数据都针对某个在年初发布的特定模型。

因此,企业必须确保数据受到保护,LLM模型、智能体、工具以及它们之间的通信都尽可能安全。

Google Cloud的AI基础设施创新 🛠️

基于上述挑战,我们首先介绍过去几个月推出的关键能力。在与客户探讨AI时,他们通常围绕三大策略进行构建:

  1. AI与云战略:确定GPU部署位置,选择GPU供应商,标准化模型,并确定优先迁移到云端以利用并演进为生成式AI应用的工作负载。
  2. 数据管理战略:如何以安全、高性能、低延迟的方式将PB级数据传输到GPU/TPU基础设施附近。
  3. 网络演进战略:网络是确保企业能够迁移并利用云能力、AI能力以及模型基础设施和智能体的基础。

我们正是从这些方面着手。首先从数据摄取开始,让客户能更轻松简单地将数据移入云端。我们很高兴地宣布推出400Gb Google Cloud Interconnect,客户现在可以以4倍于以往的速度进行连接。这提供了更优的价格点,并能以高性能、低延迟的方式更快地移动更多数据。它是完全可靠、完全由SLA管理的。

第二大能力是支持GKE。众所周知,Kubernetes和GKE几乎是任何推理或生成式AI应用的事实标准。我们现在支持每个GPU集群高达65000个节点。不仅如此,我们还开发了RDMA VPC,能以低延迟、高性能、非阻塞的方式为GPU到GPU或TPU到TPU通信提供每秒3.2TB的带宽。这对我们至关重要。

最后,我们意识到生成式AI和AI/ML应用(包括训练和推理)的流量模式与传统的基于Web的应用截然不同。必须考虑KV缓存等指标以实现智能流量管理。为此,我们推出了推理网关

深入推理网关与成本优化 💡

接下来,我们详细探讨推理网关,这对我们而言非常令人兴奋。我们是首家在云端提供与GKE架构原生集成的推理网关的厂商。它能将推理延迟降低60%,吞吐量提升40%。我们能做到这一点,是因为我们拥有来自GPU模型KV缓存的智能,能够优化流量管理,从而获得最佳体验。它支持任何类型的模型,如vLLM、NVIDIA TensorRT-LLM、JetStream等。所有这些都需要能够通过gRPC基础设施提供,这一点极其重要。

进入第二大支柱:成本优化。众所周知,GPU/TPU成本极高。网络能否通过提供支持来降低总体拥有成本?我们团队应对的挑战之一是,能够基于基础设施上单个或多个模型的GPU利用率来提供智能流量管理。我们相信这可以将您的总体拥有成本降低约30%。这不仅指网络成本,而是包括占主导地位的GPU/TPU或AI基础设施投资在内的整体物料成本。

安全集成与智能体通信演进 🔒

上一节我们讨论了成本,现在谈谈安全,这对我们至关重要。我们见过诸如提示词注入攻击、训练数据投毒、模型窃取、敏感数据泄露等攻击。通过推理网关,我们现在集成了业界领先的安全设备,可以是您选择的ISV供应商(如Palo Alto Networks的AI防火墙),也可以是我们原生的Model Armor。这为您提供了灵活性,确保您的安全控制、SecOps团队对将工作负载迁移到云端充满信心,并确保它们符合安全团队的监管要求。

但我们不会止步于此,因为挑战仍在继续。模型只是我们AI旅程的一部分。客户端或智能体访问模型,也只是智能体AI拼图的一环。传统上,我们会为每个API构建自定义连接器。但这种方法效率低下且无法扩展。想象一下模型和工具的数量,如果每个都构建专属API,您将永远无法进入生成式AI时代。

这就是Anthropic宣布的统一协议——模型上下文协议的重要性所在。它允许每个API生产者暴露一个MCP服务器。这使得任何AI智能体都能统一访问工具或数据,简化部署,保持一致性,并实现快速采用。您无需担心创建专属API,可以依赖MCP这一单一基础设施。

最后,未来将会有大量智能体。它们可能由企业内部开发,也可能由外部提供,甚至可能由LLM模型按需生成和删除代理代码。我们希望构建一个能够一致、全面地支持所有这三种情况的基础设施,并以标准化的方式提供支持。这正是Google开创A2A(智能体到智能体通信)的原因,我们已经看到许多企业开始采纳和验证它。

未来愿景与核心挑战 🎯

综上所述,我们的愿景是让模型、智能体和工具无处不在,无论它们是在本地、Google Cloud还是其他云中,都能安全地连接到您的开发人员和公共基础设施,并在此过程中提供治理和安全保障。

然而,实现这一战略也带来了诸多挑战。以下是我们认为的核心挑战:

  • 挑战一:协议支持:生成式AI希望利用gRPC工具和数据源来加速开发速度。但MCP协议目前不支持gRPC。
  • 挑战二:运行时支持:无服务器Kubernetes(如GKE或原生Kubernetes)将成为智能体开发者的首选平台。但目前,我们在无服务器运行时以及智能体SDK上缺乏gRPC支持。
  • 挑战三:新型威胁与安全集成:生成式AI将引入新的威胁向量。智能体需要访问其他智能体、工具或LLM模型,并且您希望能在任何地方、通过您首选的安全供应商来实现。传统的安全设备(在链路中或云中插入代理以重定向流量)非常复杂,且可能在成本、性能甚至安全方面并非最优。

宣布的创新解决方案 🚀

针对这些挑战,我们宣布了三项创新:

  1. 启用gRPC作为A2A和MCP的原生传输协议:您现有的、连接到传统应用的所有gRPC数据源和工具,在演进到生成式AI基础设施或应用时,可以使用相同的传输协议。
  2. 以安全为核心,扩展服务扩展能力:我们投资了一项名为“服务扩展”的技术,提供数据平面可编程性。现在,我们对其进行扩展,您既可以使用Google原生的Model Armor来保护您的LLM和智能体,也可以引入第三方解决方案(如Palo Alto Networks的AI防火墙、NVIDIA的解决方案等)。
  3. 在Google所有计算运行时上支持gRPC:包括Cloud Run。这样,您开发的所有智能体SDK都能自动扩展,并可用于这个新的智能体世界。

我们对这些创新感到非常兴奋。当我们将这些创新推向预览版时,非常希望获得大家的反馈。今天实现的许多功能,都得益于在座各位的反馈。我们投入了大量精力,以确保gRPC能够延续您过去的使用体验,并适用于下一代应用。

演示:gRPC作为MCP的传输协议 💻

接下来,我们通过一个演示来具体展示。演示将展示gRPC作为MCP传输选项的进展(目前尚未发布)。我们以一个典型云环境中的用例为例:一个云运维智能体需要安全地访问监控工具。传统上,这需要在智能体和工具之间部署代理来执行各种功能。

但gRPC改变了这一点。我们通过将gRPC集成为MCP的传输协议,并部署gRPC智能体网格进行集中运营控制,将这些由代理提供的功能直接移入智能体二进制文件中。

演示从服务器端开始。我们只需将我们的函数装饰为一个MCP工具,MCP SDK会为工具所有者处理所有底层的MCP协议复杂性,为他们提供一个生产就绪的接口。

现在,看一个用户查询的实际例子。我们给智能体一个自然语言查询:“查找利用率不足的VM”。智能体和LLM理解请求并调用正确的工具。作为MCP SDK一部分的gRPC核心,随后负责提供安全的TLS连接到工具,将所有复杂性从智能体所有者处卸载。智能体二进制文件现在拥有了调用工具所需的所有功能,无需中间盒,没有运营复杂性。

这就是智能体代理基础设施的承诺:简单、安全、合规、自给自足,全部构建在gRPC之上。

总结 📝

本节课中,我们一起学习了gRPC在AI时代的基础角色演变。我们探讨了市场向AI的转型对网络基础设施提出的高性能、低延迟和安全需求。重点介绍了Google Cloud推出的400Gb互联、RDMA VPC、推理网关等创新,以优化AI训练与推理。我们深入分析了智能体通信的演进,以及MCP和A2A协议的重要性。最后,我们了解了为应对协议支持、运行时集成和安全挑战而宣布的三项关键创新:将gRPC作为A2A/MCP原生传输、通过服务扩展增强安全、以及在全平台运行时支持gRPC。这些努力旨在确保gRPC继续作为连接传统应用与下一代生成式AI应用的坚实桥梁。

005:使用 Gemini Cloud Assist 解决 VM 外部 IP 地址变更问题 🐕🔧

在本节课中,我们将学习如何利用 Google Cloud 平台上的 Gemini Cloud Assist 工具,快速诊断并解决一个虚拟机(VM)实例重启后外部 IP 地址意外变更的故障。我们将跟随一位开发者的真实经历,了解从发现问题到使用 AI 辅助工具定位并修复问题的完整流程。

几天前,我帮助我的朋友 Cassie,一位才华横溢的时装设计师,将她的品牌“Pancake”(一只金毛寻回犬,也是 GRP 的吉祥物)的业务拓展到线上。我们使用 Gemini 编写了网站代码,并将服务部署在 Google Cloud 的虚拟机实例上。在重要的劳动节促销活动开始前,我们及时完成了所有设置。

然而,Cassie 打来电话告知网站无法访问。劳动节促销即将开始,情况紧急。我立即着手检查网站,发现了两个关键现象:第一,VM 网络服务器在昨晚为了应用更新而重启;第二,重启后,VM 的外部 IP 地址发生了改变。我们的任务很明确:修复网站,保留 IP 地址,并确保未来的更新不会再次引发此问题。

启动调查

对于大多数云用户来说,面对此类突发事件,有时就像在解决一个拥有百万碎片的拼图,不知从何入手。但如今,我们有了 Gemini Cloud Assist。打开 Cloud Console 中的 Cloud Assist 面板,就像有一位云专家坐在你身边。

我通过发起一项新的调查来开始工作。在调查描述中,我粘贴了之前的观察:“虚拟机的外部 IP 地址变更,需要修复。” 同时,我将时间范围设置为大约 50 分钟前,并将我们的 Web 服务器 VM 实例附加到此调查中。

AI 分析与诊断

创建调查后,Gemini 开始在后台为我们进行全面的分析。它首先为问题描述创建一个高维空间中的嵌入向量,这代表了对问题和用户意图的理解。基于此,它开始获取与当前情境最相关的信息,包括事件前后 VM 配置的差异、过去 15 分钟内的审计日志,并交叉参考公共文档和 Google 工程师的领域知识。

很快,Gemini 给出了清晰的诊断结果:我们使用的是临时性的临时外部 IP 地址。这意味着每次 VM 实例停止后启动,该地址都会改变。解决方案是创建一个静态外部 IP 地址并将其分配给我们的 VM 实例。

执行修复

更棒的是,Gemini 甚至直接提供了我们需要运行的精确 gcloud 命令,并且自动填充了我们创建的 VM 实例名称。我只需复制并粘贴该命令到 Cloud Shell 中执行即可。

命令类似于:

gcloud compute addresses create [STATIC_IP_NAME] --region=[REGION]
gcloud compute instances delete-access-config [INSTANCE_NAME] --access-config-name="external-nat"
gcloud compute instances add-access-config [INSTANCE_NAME] --access-config-name="external-nat" --address=[STATIC_IP_ADDRESS]

执行完成后,网站立即恢复了访问。

总结与展望

本节课中,我们一起学习了如何使用 Gemini Cloud Assist 将可能耗时数小时的故障排除过程缩短至几分钟。通过一个真实的 VM 外部 IP 地址变更案例,我们体验了从描述问题、附加资源到获取 AI 驱动的诊断和具体修复方案的完整流程。

Gemini Cloud Assist 目前处于公开预览阶段,它能极大地提升您在 Google Cloud 平台上的开发和运维效率。正如演示所示,借助 AI 的力量,解决问题的时间甚至可以比制作一个煎饼还要短。我们鼓励每位开发者尝试并利用这一强大工具来优化您的工作流。

006:从车库到云端,构建 Jshu 电商平台

在本节课中,我们将通过一个虚构的创业故事,学习如何使用 Google Cloud Platform (GCP) 构建一个可扩展的电商应用。我们将跟随两位前谷歌工程师 Pay 和 Michael,看他们如何将初创公司 Jshu 从家庭车库迁移到云端,并利用 GCP 的各项服务处理数据与人工智能。

概述:从车库创业开始

故事始于 2026 年,Pay 和 Michael 决定从谷歌退休,创立一家名为 Jshu 的在线鞋店。作为软件工程师,他们的第一反应是从技术栈开始,但创业的第一步其实是找到一个办公地点。他们决定效仿谷歌的创始人,从自家车库开始。

初始架构:车库里的服务器

以下是他们构建的最初技术架构:

  • 计算资源:他们购买主板、内存和 CPU,组装成几台大型服务器。一台用作 Web 服务器,另一台用作处理认证、库存和支付等任务的应用服务器
  • 数据库:他们准备了两种数据库。关系型数据库用于存储用户资料、交易记录和会员订阅等事务性数据。非关系型数据库用于存储文章和产品目录等。
  • 前端与域名:借助代码助手,他们轻松构建了前端界面。并成功注册了域名 Jshu.com,通过 DNS 服务器将域名解析到车库中 Web 服务器的 IP 地址。

几周后,应用成功上线。然而,随着业务增长,流量激增,用户开始遇到错误页面,系统遇到了扩展性瓶颈。

第一次扩展:应对流量增长

为了解决流量问题,他们考虑了两种扩展方式:

  1. 纵向扩展:为现有服务器购买更多 CPU 和内存,放入更大的机箱。
  2. 横向扩展:购买额外的 Web 服务器和应用服务器。

纵向扩展存在物理极限,因此对于大流量场景,横向扩展通常是首选方案。

新的架构引入了几个核心组件:

  • 负载均衡器:由于现在有多台 Web 服务器,需要一个负载均衡器位于其前方。它拥有一个单一的 IP 地址,接收所有用户流量,并根据预定义的算法(如轮询或最少连接)将流量分发到后端的多台 Web 服务器。公式可以简化为:
    用户请求 -> 负载均衡器(单一IP) -> 算法分发 -> [Web服务器1, Web服务器2, ...]
  • 内部负载均衡:应用服务器层同样需要负载均衡,但这属于内部网络流量,无需暴露在公网。
  • 缓存层:为了减轻关系型数据库的压力并降低查询延迟,他们增加了内存缓存层,用于缓存频繁访问的查询结果。

尽管新架构性能大幅提升,但新的问题出现了:房东朋友开始抱怨电费飙升和机器噪音。这促使他们迈出关键一步——将整个系统迁移到云端。

迁移到 Google Cloud Platform

作为前云工程师,他们自然选择了 Google Cloud Platform。在 GCP 中构建应用,第一步是创建一个虚拟私有云,它为所有云资源提供托管的网络功能。

计算服务选择

对于 Web 和应用服务器(计算基础设施),GCP 提供了多种选项,选择取决于团队规模和对控制的偏好。

以下是主要的计算选项:

  • Cloud Run / App Engine无服务器选项。适合小型团队,无需管理基础设施,可自动扩展。Cloud Run 支持运行无服务器容器,处理 Web 流量、WebSocket 和 gRPC。
  • Google Kubernetes Engine:如果需要更多配置灵活性来运行容器化应用,GKE 是理想选择。它帮助用户基于 Kubernetes 部署应用,同时允许用户控制底层节点的配置。
  • Compute Engine:提供最大控制权的选项,即纯粹的虚拟机。用户可以自定义 CPU、内存等配置,但也需自行负责扩展、管理和维护。

网络与负载均衡

在云端,他们使用 Cloud Load Balancing。这是一个完全分布式、软件定义的负载均衡系统,每秒可处理超过百万次查询。它基于 Anycast IP 技术,意味着可以为全球用户提供一个单一的 IP 地址,并将请求路由到离用户最近的服务器,从而实现低延迟和高可用性。同样,GCP 也提供内部负载均衡用于内部服务。

数据库服务选择

GCP 为不同类型的数据提供了全托管的数据库服务。

以下是数据库选项:

  • 关系型数据库
    • Cloud SQL:适用于通用 SQL 需求(如 MySQL, PostgreSQL),完全托管。
    • Cloud Spanner:适用于需要大规模水平扩展且保持强一致性的关系型数据库,完美支持高并发事务。
  • 非关系型数据库
    • Firestore无服务器文档数据库,易于设置,实时响应复杂查询,支持离线数据同步,非常适合移动、Web 和游戏应用。
    • Cloud Bigtable:一个高性能的 宽列 NoSQL 数据库,支持海量读写和极低延迟,非常适合 IoT 设备数据、时间序列数据和个性化推荐。
  • 缓存服务:他们使用 Cloud Memorystore,这是一个全托管的 Redis 和 Memcached 服务。它省去了配置、复制和修补的复杂工作,提供极低的延迟和高性能,适用于会话存储、实时排行榜等场景。

域名解析

最后,他们使用 Cloud DNS 作为 DNS 解析服务。Cloud DNS 基于谷歌的全球网络提供高容量、权威的 DNS 解析服务,提供 100% 的服务可用性,通过遍布全球的冗余位置确保高可用性和低延迟。

至此,Jshu 的应用完全运行在 Google Cloud 上,具备了服务全球数亿用户的能力。但故事并未结束,海量用户意味着海量数据,下一步是利用这些数据进行分析和人工智能预测。

数据处理与分析

随着 Jshu 用户量的增长,产生了大量数据。Michael 接下来介绍了如何利用 GCP 进行数据处理和分析,从原始数据一直推导出 AI 驱动的预测。

数据摄取

第一步是将数据导入云端。GCP 处理两种主要类型的数据:

  • 批量数据:对于来自日志文件或其他对象存储的离线数据,使用 Cloud Storage 作为可扩展、持久的数据着陆区。
  • 实时数据:对于来自 Web 或应用服务器的流式数据,使用 Pub/Sub。这是一个无服务器消息服务,每秒可轻松摄取数百万事件,非常适合流式数据。

数据处理

数据摄取后,需要进行处理(如清洗、转换、丰富),以便分析。GCP 提供了多种处理选项:

  • Dataflow:基于 Apache Beam 框架,可用于统一处理批数据和流数据。用户只需编写一次处理逻辑,即可部署到多种运行引擎。
  • Dataproc:如果团队已在使用 Hadoop 或 Spark 生态系统,Dataproc 是最佳选择。它是一个托管的 Hadoop/Spark 服务,让用户专注于任务而非集群管理。
  • Dataprep:对于偏好无代码方式的团队,Dataprep 提供了基于 UI 的数据转换工具。

数据存储与分析

处理后的数据存储在哪里?他们使用 BigQuery。BigQuery 是一个完全无服务器的企业数据仓库,可处理 PB 级数据,运行标准 SQL 查询。它同时充当长期数据湖和分析引擎。

有了 BigQuery 中的数据仓库,就可以开始获取洞察:

  • 可以连接 LookerData Studio 等工具,直接基于 BigQuery 构建强大的交互式仪表板。
  • BigQuery 本身通过 BigQuery ML 提供了内置的机器学习能力,允许用户直接使用 SQL 语句在数据仓库内创建和运行机器学习模型进行预测。
  • BigQuery 团队还在尝试利用自然语言 API,让用户能够用自然语言生成 SQL 语句,进一步简化使用。

高级机器学习与 Vertex AI

最令人兴奋的部分是高级机器学习,其核心是 Vertex AI。Vertex AI 是谷歌统一的人工智能平台,可以把它想象成一个超级充电的工作坊,能够利用 BigQuery 中的数据构建真正令人惊叹的应用。

Vertex AI 提供了一系列选项:

  • 预训练模型:对于许多常见任务,可以直接使用谷歌强大的预训练模型,例如支持文本、图像、音频、视频等多种输入的多模态模型 Gemini。此外,还有专注于特定领域的模型,如用于徽标检测、人脸识别的 Vision AI,以及 Speech-to-TextText-to-Speech
  • AutoML:就像为你雇佣了一位博士。你只需上传自己的数据集,AutoML 会自动为你寻找最佳的模型架构,无需深厚的机器学习专业知识。
  • 自定义模型训练:对于拥有数据科学家或机器学习工程师的专业团队,Vertex AI 提供了完整的环境,可以使用 PyTorch、TensorFlow 等框架构建和训练自己的模型,并轻松部署到生产环境。

Vertex AI 还有其他值得注意的功能:

  • Model Garden:就像 AI 模型的“应用商店”,汇集了来自谷歌和其他领先公司的超过 200 个模型,可以浏览、测试并一键部署。
  • Agent Builder:一个创新的工具,允许用户以低代码/无代码方式创建复杂的智能体和聊天机器人,这些智能体可以自动化任务、回答复杂问题。
  • MLOps 工具:包括用于自动化机器学习工作流的 Vertex AI Pipelines,用于集中管理模型版本的 Model Registry,以及确保生产环境模型性能的 Model Monitoring

当然,还必须提到最先进的硬件 TPU,它能够以极高的成本效益和可扩展性部署和运行模型。

借助 Vertex AI,我们不仅仅是在进行预测,更是在构建能够以几年前科幻小说中的方式看、听、说和理解世界的智能应用。

总结

本节课中,我们一起学习了如何利用 Google Cloud Platform 构建和扩展一个完整的应用。我们从车库创业的简单架构开始,经历了应对流量增长的横向扩展,最终将整个系统迁移到云端。我们探讨了 GCP 的核心服务,包括计算选项(如 Cloud Run, GKE)、网络服务(如 Cloud Load Balancing)、数据库(如 Cloud SQL, Firestore, BigQuery)以及缓存服务(Cloud Memorystore)。随后,我们深入了解了如何利用 GCP 进行数据处理、分析,并最终通过强大的 Vertex AI 平台集成高级机器学习能力,从数据中获取洞察并构建智能应用。这个故事展示了 GCP 如何为从初创公司到大型企业的各种场景提供全面、可扩展且强大的云解决方案。

007:Protobuf 版本与 Editions 🚀

在本节课中,我们将学习 Protobuf 语言和 API 的演进历史,并重点介绍全新的 Editions 框架。这个框架旨在为 Protobuf 提供可控、可扩展的演进路径,解决过去版本升级带来的诸多痛点。


背景与挑战

Protobuf 已驱动无数系统超过二十年。虽然它从一开始就通过未知字段和灵活模式等特性强调了模式的演进,但其他方面的演进则困难得多。例如,自 Proto 1 以来,其线格式本身几乎没有变化,并且安全地推出这些变更极其困难。

本次讨论将完全聚焦于语言和 API 的演进,这催生了 Editions 的设计。


过去的演进方式

上一节我们介绍了 Protobuf 演进的整体背景,本节中我们来看看过去是如何处理语言和 API 变化的。

运行时 API 的演进

虽然我们能够通过破坏性变更来演进运行时 API,但这确实具有破坏性,并且对于为不同配置文件生成的 API 来说,这种方式的扩展性不佳。

语言本身的演进

在 Proto 语言的历史上,有两次主要的演进:

  1. Proto 1 到 Proto 2:最初 Google 内部使用 Proto 1,后来我们创建了 Proto 2 并于 2008 年开源。这次迁移是一项极其痛苦、耗时多年的工作,至今仍未 100% 完成。
  2. Proto 3:我们于 2016 年创建并发布了 Proto 3。这次演进的目标是不取代 Proto 2,主要是为了避免刚刚完成的痛苦迁移。但这最终导致了生态系统的分裂。

在创建 Proto 3 时,我们以前向不兼容的方式添加和删除了一大批语言特性。例如:

  • 字段存在性变为隐式。
  • 枚举改为遵循开放语义。
  • 重复字段默认打包。
  • 字符串更可靠地进行 UTF-8 验证。
  • 我们添加了映射、JSON 支持,并用 Any 类型替换了扩展。

其中许多特性后来被反向移植到 Proto 2。但我们在 Proto 3 中也犯了不少错误,有些花了数年才纠正,很多至今仍未解决。

Proto 3 Optional 的教训

2020 年,我们决定通过放回 Proto 2 中明确的字段存在性语义(使用 optional 关键字)来从根本上改变 Proto 3。这之所以引人注目,是因为它证明了我们可以进行前向不兼容的更改,但实际落地需要大量工作。

每个 Protobuf 插件现在都需要永久声明是否支持此新功能。使用 proto3 optional 的新文件将无法与未声明支持此功能的旧版本编译器或插件一起工作。

我们还有很多其他想要进行的更改,但也希望能够更渐进地改进语言。引入像 Proto 4 这样重量级且期望它永远不变的东西,只会重蹈覆辙。


Editions 框架介绍

上一节我们回顾了过去的演进挑战,本节将介绍我们的解决方案:Protobuf Editions。它受 Rust Editions 启发,是一个为 Protobuf 语言和生成代码提供可控、可扩展演进的新框架。

与像 Proto 4 这样的新单体语法版本不同,Editions 更像是一个框架,用于在更细粒度的级别上配置单个语言特性。它的设计考虑了向后兼容性,因此从旧 Edition 到新 Edition 将始终存在迁移路径。

以下是 Editions 的核心优势:

  • 精细控制:你可以将 .proto 文件升级到最新的 Edition 以使用新功能,也可以在需要时保留旧行为,即使它们不再是默认行为。
  • 增量升级:新项目可以使用具有最新功能和默认值的最新 Edition,同时与仍在使用旧 Edition 的文件保持完全兼容。旧文件也可以按自己的计划,通过根据需要覆盖特性来逐步升级。
  • 稳定性承诺:一旦一个 Edition 正式发布,其所有定义的行为(除小错误修复外)都将被冻结。这意味着你的生成代码和运行时行为将保持一致。

Editions 实战示例

了解了 Editions 框架的理念后,我们通过具体例子来看看它是如何工作的。

示例 1:从 Proto 3 迁移到 Edition 2023

以下示例展示了如何将 Proto 3 文件升级到 Edition 2023,同时不改变任何行为。

我们从一个简单的 Proto 3 消息开始,它有两个字符串字段。其中一个标记为 optional,使其具有像 Proto 2 那样的显式存在性跟踪行为;另一个则具有 Proto 3 默认的隐式存在性行为。这两者之间的主要区别基本上在于是否为字段生成 has_ 方法。

在右侧的 Editions 中,我们不再指定 syntax,而是显式声明其 Edition(这里我们使用了第一个 Edition:2023)。这个声明锁定了该文件中所有内容的每个特性的默认值集合。

在 2023 Edition 中,字段存在性行为的默认值是 显式的(类似 proto3 optional)。因此,为了保持字段 A 的隐式存在性,你需要像我们在这里做的那样指定一个特性覆盖。

// Proto 3 语法
syntax = "proto3";
message MyMessage {
  string a = 1;
  optional string b = 2;
}

// Edition 2023 语法
edition = "2023";
message MyMessage {
  string a = 1 [features.field_presence = IMPLICIT];
  string b = 2;
}

特性像普通选项一样指定。主要区别在于,当它们未设置时,其默认值可能因 Edition 而异。

在另一种场景中,如果你的 .proto 文件有很多隐式字段,你可能希望在文件级别覆盖字段存在性,指定一个新的文件级默认值。在这种情况下,你需要在字段 B 上指定特性覆盖,将其设置回显式存在性。

这凸显了特性的词法作用域属性:任何特性覆盖都会更改包含作用域内所有内容的默认值。因此,在文件级别设置的特性将覆盖文件内所有内容的默认值,而在消息上设置的特性将覆盖该消息内定义的所有内容的默认值。

这个例子也展示了我们在 Edition 2023 中做出的一个语言更改:我们再次从语言中移除了 optional 关键字,并在意识到隐式存在性的一系列问题后,恢复了默认跟踪字段存在性的 Proto 2 语义。

示例 2:用于 API 演进的 C++ 特性

我们使用 Editions 的另一种方式是用于 API 演进。这是通过语言特性完成的,任何 Protobuf 插件都可以管理自己的一组特性,这些特性在原型中指定,并遵循与全局特性相同的规则集。

在左侧,你可以看到我们导入了 cpp_features.proto,它包含了我们的 C++ 语言特性定义。在我们的第一个 Edition 中,我们发布了 C++ 字符串类型语言特性,作为持续优化工作的一部分。

以前,为字符串字段生成的访问器会返回字符串引用,如右侧字段 B 所示。这个 API 实际上有点问题,因为它迫使我们将该字段在内存中存储为实际的 std::string 对象,这会泄露实现细节,并阻止我们在底层进行各种优化。

通过选择加入新的 view 特性,这些访问器将开始返回更不透明的字符串视图,如字段 A 所示。这将使我们能够自由地优化未来的内部内存表示,而不会破坏任何使用此特性的人。

在我们的下一个 Edition(稍后讨论)中,在内部迁移之后,我们将此特性的默认值翻转为 view。因此,升级后,你只需要切换特性覆盖的位置:字段 A 不再需要覆盖,因为默认值现在是 view;字段 B 需要被覆盖回旧的字符串行为。结果是你会得到完全相同的生成代码。

import "google/protobuf/cpp_features.proto";

option (pb.cpp).features.string_type = VIEW;

message MyMessage {
  string a = 1; // 将生成返回 string_view 的访问器
  string b = 2 [(pb.cpp).features.string_type = REF]; // 覆盖为返回 string&
}

Editions 的核心原则与长期支持

上一节我们通过例子看到了 Editions 的灵活性,本节我们来总结其核心原则和对未来的承诺。

Editions 框架的力量来自于能够在选定的 Edition 内显式启用或禁用单个特性,从而为你提供对模式的精细控制。虽然我们选择的默认值代表了我们的建议和未来方向,但任何 Proto 2 或 Proto 3 行为都可以通过适当的特性覆盖集在 Editions 中表示。

随着 Protobuf Editions 的推出,我们也做出了新的稳定性承诺。一旦一个 Edition 正式发布,其所有定义的行为(除小错误修复外)都将被冻结。这意味着,如果你有一个 Edition 2023 文件,即使我们发布了新版本的 Protobuf,其生成代码和运行时行为也将保持一致。这种一致性在该 Edition 的整个支持生命周期内都得到保证。

任何新功能或默认行为的更改都只会在新的 Editions 中引入。这让你,即用户,牢牢掌握控制权。你可以通过显式更新 .proto 文件中的 Edition 声明来决定何时采用新功能。我们的目标是消除对意外破坏的恐惧以及 Protobuf 发布的复杂性。你可以放心开发,因为知道在更新到最新版本的 Protobuf 时,你的协议将继续按预期工作,不会发生变化。


Proto 2/3 与 Editions 的关系

一个可能不明显的问题是 Proto 2 和 Proto 3 如何融入这个新框架。但就所有目的而言,它们现在都被视为特殊的历史 Editions。事实上,我们的 Edition 枚举为 Proto 2 和 Proto 3 设置了特殊值,现在我们几乎所有的内部代码都将它们视为任何其他 Edition。它们甚至拥有自己为每个特性指定的一组默认值。

就像明确编号的 Editions 一样,它们的行为现在将被完全冻结,并将继续获得长期支持。这意味着错误修复、性能改进和编译器更新仍将适用于你所有的 Proto 2 和 Proto 3 文件。但这项能力的代价是,如果不升级到最新的 Edition,你将无法使用我们发布的任何新功能。

不过,不会有任何强制迁移。由于我们的长期支持保证,在可预见的未来,你绝对可以安全地继续使用现有的 Proto 2 和 Proto 3 文件。但需要明确的是,长期支持并不等同于永久支持。像任何软件一样,非常旧的 Editions 最终会达到生命周期终止阶段。然而,这个过程将始终是透明的。我们承诺在任何 Edition 达到生命周期终止之前,提前数年提供明确的弃用政策和充足的通知。

我们尚未为弃用旧 Editions 或特性设定任何时间表,但在我们这样做之前,我们将提供充足的工具和文档来帮助你完成过渡。


内部实践与工具

了解了 Editions 的长期支持策略后,我们来看看它在 Google 内部的实践情况以及相关工具。

自首次公开发布以来,Editions 已在 Google 内部进行了大规模推广。这不是一次小规模测试,而是跨越我们庞大代码库的全面迁移。我们已成功将数十万个 Google 内部 .proto 文件迁移到 Edition 2023,这项工作在现实世界的高性能场景中测试并验证了该框架。

我们的内部使用确保了我们的编译器和各种语言运行时在 Editions 下是健壮且功能齐全的,并且我们在此过程中解决了许多细节和边缘情况。这也加强了我们对开源社区的稳定性保证。

这次内部迁移是使用 Prototiller 工具完成的,这是我们开发的一个用于以编程方式转换协议的工具。虽然 Prototiller 目前仍仅限于内部使用,但我们正在积极努力使其为公开发布做好准备,目标是使未来的迁移在外部也能尽可能平滑和自动化。


开源生态中的注意事项

与所有组件运行在同一版本的 Google3 单体仓库相比,Editions 框架在开源生态系统中存在一些复杂性。主要区别来自于需要与你的 .proto 文件交互的各种组件之间的版本偏差。

这里有四种基本类型的组件需要考虑:

  1. Protoc 编译器本身:要使用任何特定的 Edition,你需要一个实际理解该 Edition 的 Protoc 版本。
  2. 语言生成器:这些生成器都需要向 Protoc 显式声明其支持的 Edition。如果你尝试使用不支持其 Edition 的插件编译 .proto 文件,Protoc 将给出清晰的错误消息。
  3. 运行时实现:你不能指望任意旧版本的运行时理解未来生成的代码,但具体的保证可能因语言而异。对于我们发布的所有语言,我们已规定绝不支持使用比运行时版本更新的生成器生成的代码。
  4. 第三方工具:任何利用我们插件框架的工具都必须像语言插件一样声明其支持的 Edition 范围。更复杂的情况是那些直接解析 .proto 文件而不使用我们任何库的工具。

Edition 2024 发布

最后,我们很高兴地宣布,我们的第二个 Edition——2024,已于两周前正式发布并可用。

虽然我们的第一个 Edition 主要专注于统一 Proto 2 和 Proto 3,但 Edition 2024 实际上做出了渐进式改进。我们保持更改相当有限,针对两个基本类别的特定用户痛点:

  1. 特定语言更改:例如,我们添加了一个 Java 特性,允许你生成更大的枚举,以绕过该语言中的一些大小限制。
  2. Protoc 前端更改:这些更改完全在 Protoc 二进制文件中实现,不需要对语言插件或运行时进行任何更改。例如,我们添加了一个特性,在使用 Protoc 构建时强制执行 .proto 文件中的命名风格指南。

由于这种作用域限定,对于已经添加了 Editions 支持的第三方工具来说,实现 Edition 2024 应该会容易得多。有关我们添加的所有新功能的更多信息,请参阅我们在 Protobuf.dev 上的文档和新闻公告。


总结

在本节课中,我们一起学习了 Protobuf Editions 框架。我们回顾了 Protobuf 语言和 API 演进的挑战,深入探讨了 Editions 如何通过提供精细的特性控制、向后兼容的迁移路径以及明确的稳定性承诺来解决这些问题。我们还通过示例了解了如何使用 Editions,并探讨了其在开源生态中的实践和最新动态(Edition 2024)。Editions 标志着 Protobuf 进入了一个更可控、更灵活的演进新时代。

008:是否应该使用gRPC? 🚀

概述

在本节课中,我们将探讨在构建AI工具栈时,是否应该采用gRPC作为通信框架。我们将了解gRPC的核心概念、它如何解决AI应用中的性能痛点、其带来的权衡,以及它最适合的应用场景。


什么是gRPC? 🤔

上一节我们介绍了课程主题,本节中我们来看看gRPC究竟是什么。

gRPC本质上是一个让服务之间相互通信的框架。但其实现方式使其速度非常快。它不使用JSON,而是使用协议缓冲区,后者更小、更高效。因为它基于HTTP/2构建,所以你能获得双向流和多路复用等特性。

一个简单的比喻是:REST就像通过邮件寄信,一次请求,一次回复。而gRPC就像在进行实时通话,双方可以同时说和听。

核心概念公式/代码描述:

  • 协议缓冲区 (Protocol Buffers): 一种比JSON更高效的序列化数据格式。
  • HTTP/2: 支持多路复用和双向流的网络协议。

gRPC对AI为何重要? ⚡

随着AI从研究走向产品,用户的期望发生了变化。人们不希望等待,他们希望答案能即时地以流式方式生成。这正是gRPC的用武之地:它速度快,流式处理能力强,并且能跨多个核心扩展。

因此,gRPC已成为大规模服务AI模型的热门工具。下图展示了gRPC在AI模型和并发调用数量增加时,仍能保持恒定、低延迟的性能,这使其非常适合实时AI工作负载。


AI工具栈中的常见痛点 🐌

当我们剖析任何一个AI工具时,通常会经历以下几个阶段:

  1. 用户发送输入(文本、语音、图像)。
  2. 模型处理输入并生成响应。
  3. 有时需要调用外部API或数据库以获取额外信息。
  4. 同时,以流式方式将资源返回给用户,让他们看到实时进度。
  5. 最后,用户可能给出反馈甚至中途取消。

以下是每个阶段可能遇到的痛点:

  • 冷启动延迟: 第一个请求耗时极长,因为模型需要启动。
  • 令牌生成延迟: 模型生成文本的速度很慢,用户缺乏耐心。
  • 中间调用响应迟缓: 中断AI去调用API会导致响应迟钝,破坏对话流畅性。
  • 流式传输不可靠: 用户可能面对一个冻结的屏幕。
  • 取消操作无效: 用户取消后,系统仍在消耗计算资源,浪费金钱。

如果你曾感觉某个AI工具反应迟钝,很可能就是上述问题之一导致的。


gRPC提供的解决方案 🛠️

针对上述痛点,gRPC提供了一系列可能的解决方案。

以下是具体的解决方案列表:

  • 预热启动: 预先启动模型,避免第一个请求像等待水烧开一样漫长。
  • 高效令牌流: 每个令牌一旦准备就绪就立即发送,使响应感觉更快。
  • 双向工具调用: 无需中断AI来发起API调用,可以在同一通道上处理,避免往返延迟。
  • 即时取消: 用户停止的瞬间,系统实际停止工作。

这些不是抽象概念,它们决定了AI演示是笨拙难用,还是成为人们乐于使用的产品。


gRPC的关键优势场景 🎯

在AI交互中,有两个关键时刻性能至关重要。

第一个是AI需要在响应过程中调用外部工具。 即使是很小的延迟也会让对话感觉中断。使用gRPC,你可以获得单一的双向流,因此响应可以即时返回,无需等待建立全新的连接。

第二个是取消操作。 当用户决定停止时,你希望一切立即关闭。否则,你就在浪费计算时间和资金。gRPC内置了快速可靠的取消支持。

仅这两点就能完全改变用户体验,尤其是在大规模场景下。


实践示例:AI代码审查 ✨

这是一个由gRPC驱动的简单AI代码审查工具示例。

请注意,反馈并非在最后一次性全部出现,而是建议在生成过程中就以流式方式传入。这意味着当模型仍在“思考”时,用户已经能看到关于其代码的有用提示。这种实时反馈使体验感觉是交互式和生动的,而非“停止-等待”模式。

这正是gRPC旨在交付的那种流畅体验。


gRPC的权衡与不足 ⚖️

但是,我们必须现实一点。gRPC并非完美无缺。

设置gRPC需要更多工作。你需要定义协议缓冲区、生成调用代码以及额外的工具链,这对于小团队或快速原型来说可能负担较重。浏览器并不原生支持gRPC,因此如果你的产品重度依赖浏览器,你需要一个gRPC Web代理,这增加了系统的复杂性。

与普通的HTTP相比,调试也更困难。消息不是人类可读的,因此你需要外部工具来查看。坦白说,如果你的工具栈只偶尔发出JSON请求,gRPC可能就大材小用了。


何时使用gRPC? 📈

那么,何时使用gRPC才有意义呢?

当你的应用需要实时流式传输响应、进行频繁的中间响应调用,以及要求取消操作立即生效时,gRPC绝对会大放异彩。这是它的最佳应用场景。

另一方面,如果你正在构建更简单的东西,比如一个原型、一个浏览器优先的应用,或者一个只偶尔进行轻量级调用的系统,REST通常是更好的选择。

这不一定非此即彼。你可以混合使用两者。在简单的地方使用REST,在性能真正重要的地方引入gRPC。关键在于为正确的工作选择正确的工具。


总结

本节课中,我们一起学习了gRPC在AI工具栈中的应用。我们了解到,gRPC通过其高效的协议缓冲区和HTTP/2基础,为AI应用提供了低延迟、双向流和可靠的取消机制,特别适合需要实时交互和高性能的场景。

然而,它也存在设置复杂、浏览器支持需要代理等权衡。最终,决策不应基于gRPC是否是“默认答案”,而应基于它是否能在你的特定场景中带来切实的回报——尤其是在处理实时流、频繁中间调用和即时取消需求时。希望本教程能为你提供一个清晰的评估框架。

009:在Apple容器化中使用gRPC Swift实现容器代理

在本节课中,我们将学习Apple容器化框架如何利用gRPC Swift来实现容器代理(VMinitd),以协调和管理容器启动时的运行时配置。我们将从容器化的高层概念入手,逐步深入到gRPC通信的具体实现。


概述:容器化与轻量级虚拟机

我的名字是Aga,是Apple Swift服务器团队的一名软件工程师。过去几年,我参与了容器化项目和一个Swift NTP库的开发工作,它们都可以在GitHub.com/apple找到。今天,我们将探讨如何在容器化框架中使用gRPC Swift。

每次使用容器化框架启动一个容器时,例如运行 container run hello-world,我们都会启动一个gRPC服务器。这个服务器帮助我们协调启动容器所需的所有运行时配置,这正是我们今天要深入探讨的内容。

我们将讨论容器化框架帮助我们实现了什么,以及我们如何使用一个在容器化容器中运行的、带有gRPC服务器的代理来协调运行时配置。这是一个成功的案例。最后,我们将总结在Swift和gRPC中使用这一技术的体验。


容器化架构概览

为了理解我们的gRPC服务器如何作为容器代理工作,让我们从高层开始,逐步深入探讨容器化。

我们在今年的WWDC上发布了容器化项目,我鼓励你观看我的同事Michael关于此的演讲。

我们也在GitHub上将其开源,如果你有兴趣,我们非常欢迎贡献。

那么,这个项目是做什么的呢?容器化使应用程序能够使用Linux容器。我们实现方式的一个独特之处在于,每个容器都表现为一个轻量级虚拟机,这提供了有助于安全和隐私的隔离优势。


理解容器与轻量级虚拟机

现在,让我们进一步解析什么是容器,并在此过程中更深入地理解“作为轻量级虚拟机的容器”这一概念。这将帮助我们构建起用于将虚拟机配置为实际容器的gRPC服务器。

退一步看,什么是容器?让我们以一个服务器端应用程序为例。容器是运行一组打包应用程序的单一单元,这可能是一个Linux服务器端应用程序及其依赖项。通常,这些被打包成一个单一的镜像,你可以用正确的工具指定运行它。

例如,如果服务器端应用程序叫做“Hello World”,它被构建为一个二进制文件,并且需要安装一些操作系统依赖项,那么该应用程序及其依赖项就可以被打包到那个镜像中。

一旦我有了Hello World的镜像,我就可以确保在我的MacBook上安装了开源的容器CLI,并运行命令 container run hello-world 来启动一个容器。这将运行我刚打包的Hello World镜像,并为其分配一个IP地址,使其在我的本地机器上可通过网络寻址。

假设我打包的Hello World应用程序是一个运行在80端口的简单HTTP服务器。一旦该容器与服务器一起运行,我将能够通过容器的IP地址在80端口进行curl操作,并获得一个200 OK响应。

因此,从用户的角度来看,你只需要执行 container run 就可以开始了。


实现方式:利用虚拟化

现在,如果我们思考一下我们是如何到达这一步的,也就是容器运行时的实际实现,有很多方法可以实现。在容器化中,我们特别采用了利用虚拟化概念的方法。具体来说,我们利用虚拟化框架在每个轻量级虚拟机内部执行Linux容器。

要了解更多关于虚拟化框架的信息,developer.apple.com上有很棒的文档。

但是,要运行我之前在上一张幻灯片中执行的 container run hello-world,我们需要使用这个框架来启动轻量级虚拟机,然后由虚拟机启动服务器。

进一步剖析,虚拟机如何知道实际启动服务器进程呢?这就是我们的容器代理(VMinitd)的作用,它负责管理其他运行时配置。这个代理是我们称为VMinitd或虚拟机初始化守护进程的init进程。我们稍后会深入探讨,但正是这个进程承载了我们用于通信的gRPC服务器。


架构全景与通信

让我们整体看一下容器化架构,以了解这实际上是在哪里以及如何发生的。

我们的容器作为轻量级虚拟机运行,显示在示意图的一侧。在容器化框架中,我们需要一个客户端来实际启动我们一直在讨论的这个容器。这个客户端表示在另一侧。例如,它可能是我们刚刚提到的开源容器CLI,或者是你自己利用容器化框架编写的程序。

无论哪种方式,为了利用虚拟化框架依赖,这个客户端都将在macOS上运行以创建和运行我们的容器。

现在,客户端必须将启动需求传达给VMinitd。例如,它应该运行什么进程?我们想运行Hello World吗?我们想运行Postgres吗?如果想,我们实际上如何运行Postgres?为了理解我们的gRPC服务器将要利用的这个连接,让我们看一下项目中创建Linux容器的一些代码,我们甚至会涉及一些虚拟化框架的内容,以便理解我们是如何通过gRPC跨越虚拟化边界的。


代码示例:创建Linux容器

我们在这里为容器运行时使用Swift,这在某些方面看起来很熟悉,但无论如何我会尝试添加一些注释。从容器化代码的概要开始,我们有一个 LinuxContainer,它遵循一个 Container 协议。对于Swift新手来说,协议与其他语言中的接口非常相似。

为了能够在我们的代码中定义一个Linux容器,我们需要一些配置,以便容器知道它是什么样子。然后我们最终可以创建底层容器的虚拟机。你会注意到函数签名中的 async throws。简单来说,这是Swift中处理异步编程和错误处理的一种方式,我们稍后会详细讨论。

现在,让我们看看一些实际帮助创建容器的虚拟化代码。这非常重要,因为它定义了gRPC服务器如何跨越虚拟化主机边界进行通信。

首先,我们将导入虚拟化框架来定义虚拟机的初始配置,这个虚拟机最终将成为我们的容器。你会注意到从虚拟化框架导入的任何内容前面都有 VZ 前缀。例如,这里为了定义配置,我们有 VZVirtualMachineConfiguration

然后我们可以为其提供一些资源。虚拟机需要特定的大小,我以CPU数量为4、内存大小为1024兆字节为例。这些可以根据你的项目和硬件配置轻松调整。实际上,当你在自己的机器上启动容器时,无论是通过CLI执行 container run 还是直接在容器化中操作,你都可以自己调整这些值并进行自定义。

现在,这一行是今天演讲中最重要的。我们需要思考如何跨越虚拟化主机边界进行通信,以便主机能够向客户机发送RPC调用。虚拟化框架允许我们暴露一个vsock设备,这是一个管理主机和客户机之间基于端口的连接的设备。

由于我们通过VMinitd在客户机上运行着一个gRPC服务器,并且我们希望利用它来设置启动时的动态运行时配置,我们需要这个通信通道来通过gRPC发送我们所需的请求。

现在,为了本次演讲的目的,我们已经完成了一组最小的配置,我们可以用我们定义的配置实例化一个虚拟机。我说最小是因为这个代码片段为我们今天的演讲提供了一个很好的思路。当然,虚拟化框架在此上下文之外还有很多更强大的功能可以利用。


深入容器代理:VMinitd

现在,让我们回到架构图,以完全理解我们跨平台使用gRPC和vsock来建立所需通信通道的方式。

让我们深入了解实际利用gRPC的容器代理VMinitd的更多细节。那么,VMinitd在高层是做什么的呢?它帮助设置所有运行时配置,我们需要在虚拟机上建立一个执行环境,使其表现为一个容器。

需要注意的是,这些是非常底层的操作,通过多个RPC调用来协调。通常,我看到gRPC更多地用于分布式系统设置,例如调用另一个与负载均衡器后面的数据库交互的服务,或者在其他容器运行时中用于客户端和守护进程之间的交互。我们今天在主题演讲之前也看到了一些例子。但在这种情况下,跨越虚拟化主机边界进行通信有点独特。

因此,让我们具体看看VMinitd帮助了什么,并回顾一些功能。

我最喜欢的例子之一是挂载和卸载调用,可以通过几种不同的方式利用。你可以利用挂载将文件系统挂载到容器上。一个非常酷的用法是当你想在Apple Silicon上的Linux容器内运行x86工作负载时。挂载调用用于在你运行的Linux容器上配置Rosetta模拟。这非常强大,能够让你运行并非为多架构构建的镜像,在你的Apple Silicon机器上仍然运行良好。

还有一个网络层,例如,对于IP地址管理,我们有一些逻辑来启动和关闭接口,并配置我们想要的特定路由。这使用了我们用Swift编写的底层netlink代码。

进程监督将启动你指定或运行的进程。例如,如果你想运行我们讨论过的 container run postgres,它将确保Postgres实际正确启动。事实上,我们稍后将在演讲中查看一些这方面的代码。但当你执行 container stop 时,它也会确保回收子进程。所以,这是一个很酷的init守护进程,在底层做了许多不同的事情。

而所有这些都是通过gRPC API调用来协调的。


gRPC Swift的优势

gRPC Swift是一个用于在Swift中利用gRPC的很棒的开源库。你可能之前听说过它。我的同事George去年就在这个会议上做过演讲。

我们已经使用它一段时间了,并且非常满意。说实话,它是我们今天谈论的成功案例的核心。

因为我们跨越主机边界,所以需要它超级高效。例如,你希望你的容器启动非常快,如果你希望你的Hello World应用程序或Postgres容器能够尽快提供服务。其次,我们很多人过去都使用过gRPC,所以很自然地,我们会在Swift生态系统中寻找同样稳定、出色的工具。有些人可能认为这很“无聊”,但像这样的“无聊”技术非常棒。我们可以在不担心底层细节的情况下进行构建。在这方面,gRPC绝对做到了。

它在数据中心也运行得很好,我们稍微讨论过这一点,但我学到的一点是,它在本地机器上跨越虚拟化边界也运行得很好。

最后,该工具还帮助我们从一开始就考虑版本控制和迭代。

当我们谈论版本控制时,你可以看到项目中的代码摘录,我们现在是第3版。这意味着我们已经迭代了两次。但即使在我们不得不完全破坏版本控制之前,Protobuf也非常适合随时间演进。我们能够通过添加客户端不会立即使用的字段,在某些情况下将我们的类型从V1演进到V2,甚至从V2演进到V3。此外,可选字段等功能有助于我们偶尔需要一些额外配置的用例。

在屏幕上,你可以看到我们以 CreateProcessRequest 消息为例。这提供了我们需要通过VMinitd提供的所有内容,以便在容器上为我们配置一个进程。我们可以立即看到它如何与gRPC服务器一起使用,包括请求及其匹配的响应。

gRPC的一个很棒的地方是它为服务器和客户端生成的代码,这使得两者都可以立即生成并在我们的代码中使用,这非常方便。

这是我们可以查看的相应客户端代码,你可以看到上面proto中的字段反映在这个外部函数中,在内部我们使用 CreateProcessRequest 调用生成的客户端作为 try await client.createProcess

gRPC Swift另一个很棒的地方是该库如何很好地采用了Swift运行时并发概念。因此,在处理客户端和服务器时,异步编程是常规活动。同样重要的是要认识到,没有工具或语言特性编写异步代码可能会冗长、复杂,在某些情况下甚至不正确。Swift有 async/await 关键字,这是几年前引入的,可以真正帮助解决这个问题。


异步编程示例

让我们看一个带有一些代码的例子。具体来说,让我们看看我们在容器化中使用的一个简单的gRPC请求,从请求和响应消息开始。当容器启动时,我们希望确保正确的进程随之启动,正如我们讨论过的。

在过去的几张幻灯片中,你已经看到了这个过程是如何创建的。现在讨论的是我们如何启动已创建的进程。例如,如果你正在执行 container run postgres,我们希望确保Postgres实际上以你希望的方式、在你希望的端口上启动,等等。

正如你所看到的,StartProcessRequest 接收我们创建的进程的ID和我们想要在其上启动进程的容器ID。响应在进程启动后返回给我们进程的PID。

RPC调用简单地接收 StartProcessRequest 并返回我们刚刚讨论过的 StartProcessResponse。为了看看这一切是如何实际运作的,让我们看看其他一些代码。

当容器代理VMinitd实际被调用来启动进程时,我们使用 try await 来调用。这里的 try 非常适合错误处理,而 await 确保虽然代码以线性格式编写,但它仍然是我们能够理解的异步代码。有趣的是,这也向上传播到函数调用栈。这显示了包含 async throws 在其签名中的封闭 start 函数,当然,这与 tryawait 的顺序相反。


成功经验总结

如果我们把最后几节的内容放在一起,我们可以构建一个简化的视图,了解在轻量级虚拟机中启动Linux进程需要什么,在容器化项目中,利用gRPC作为我们的通信通道。

我之前稍微提到过这一点,但对我来说,这是一个学习过程:将gRPC与Swift一起用于系统编程很容易。在我们开始一些实验之前,我并没有真正这样想过gRPC甚至Swift。

事实上,在早期的原型设计阶段,VMinitd是用Go编写的,拥有gRPC接口允许我们在决定简化技术栈以使用一种语言之前,演进项目的其余部分。统一使用一种语言来简化技术栈,在保持良好用户体验的同时,极大地改善了开发人员体验。所以,从开发人员的角度来看,我很高兴我们做出了这个改变。

回顾一些在gRPC Swift系统端帮助我们的代码,当我们想从主机连接到客户机时,我们实例化一个VMinitd的客户端。gRPC在如何创建与远程对等体的连接方面非常灵活。它不仅仅需要主机和端口。因此,在我们的案例中,当我们想通过vsock连接虚拟机时,我们只需传递从虚拟化框架获得的文件句柄,然后我们就差不多可以开始了。

在运行在Linux容器内部的服务器端,gRPC服务器也会有一种方式使用上下文ID和端口原生地使用vsock连接。总的来说,在连接方面是双赢的。


Swift作为容器运行时语言

Swift被证明是一种用于容器运行时的非常棒的语言,当我们在项目开始时,这对我来说也有点意外。Swift是一种现代的通用语言,具有表现力、安全性和快速性。使用它也是一种乐趣。过去几年,我一直在多个系统和服务器用例中使用它,并对它的良好表现感到惊喜。

还有一个非常棒的C互操作性,这对我从事的这类项目很有效。对于我们的用例,我们还希望利用一个具有原生Swift绑定的框架。因此,使用相同的编程语言确实有帮助。但它的跨平台特性也确实有帮助,因为我们确实有那些macOS和Linux的部分。事实上,我们实际上是在Mac上交叉编译我们的Linux部分,比如VMinitd,这从开发人员体验的角度来看又是一个很大的便利。


关键要点与下一步

那么,从我们今天的成功故事中,我们得到了哪些启示呢?

  1. 架构成功:带有容器代理和gRPC的容器化架构,使我们能够执行运行时配置,使执行环境启动并运行。
  2. 通信高效:通过vsock连接的gRPC通信通道非常适合我们的用例,用于跨越虚拟化主机边界发送那些RPC调用。
  3. 语言与框架契合:gRPC与Swift配合得很好,Swift是一种我逐渐喜欢的编程语言。
  4. 工具链成熟:尽管在今天的演示中,我们只构建了VMinitd进程管理能力的简化视图(包括创建进程和启动进程通信),但gRPC Swift在版本控制、代码生成和Swift的异步特性方面都表现得非常好。

下一步你可以做什么?

  • 尝试容器:观看演讲,下载并安装CLI来运行你自己的容器,甚至使用该框架编写你自己的客户端。
  • 探索gRPC Swift:给仓库点星、复刻,完成超级快速的教程,并观看George去年的演讲。
  • 参与社区:George和我都将在10月参加伦敦的Server-Side Swift会议,他将在那里谈论gRPC Swift。我将主持一个关于如何使用Swift作为服务器端编程语言,让HTTP服务器与数据库协同工作的研讨会。所以,请在10月1日加入我们在伦敦的活动。

感谢大家今天的到来。希望你们有很好的体验。

010:在 Cloudflare 规模下将 HTTP/3 引入 gRPC 🚀

概述

在本教程中,我们将跟随 Cloudflare 协议工程师 Lucas 的分享,了解如何将 HTTP/3 引入 gRPC,并使其在 Cloudflare 的全球规模下运行。我们将从背景知识开始,回顾 HTTP/2 的挑战,探讨 HTTP/3 和 QUIC 协议的优势,并最终了解 Cloudflare 的实现历程与性能收益。


背景:QUIC 与 HTTP/3 📚

我是 Lucas,一名常驻伦敦的 Cloudflare 协议工程师。今天我想和大家分享在 Cloudflare 规模下将 HTTP/3 引入 gRPC 的历程。

首先,我们需要理解两个核心协议:QUIC 和 HTTP/3。

QUIC 协议

QUIC 不是 TCP。它是一个不同的传输层协议,由 IETF 标准化为 RFC 9000。QUIC 也不是 TLS,但它集成了基于 TLS 模型的安全性。这意味着握手过程可以更高效,有时能节省一个或更多的往返时间(RTT)。

QUIC 的关键设计点在于它运行在 UDP 之上,利用 UDP 的不可靠性来构建可靠、多路复用的安全传输。你可以将一个 QUIC 流(stream)近似理解为一条 TCP 连接,但在一个 QUIC 连接内可以并行多个流。

核心公式/概念:

  • QUIC 连接:一个安全的、多路复用的传输层连接。
  • QUIC 流:连接内的独立、有序的字节流。流之间互不阻塞。

HTTP/3 协议

HTTP/3 不是 HTTP/2,但它提供了 HTTP/2 的所有核心能力,例如流、帧、头部压缩等。它与 HTTP/2 功能兼容,因此也完全支持标准的 HTTP 语义。

HTTP/3 的关键改进在于,它使用 QUIC 作为传输层,从而避免了 队头阻塞(Head-of-Line Blocking)。在 HTTP/2 中,如果 TCP 包丢失,所有多路复用的流都会被阻塞。而在 HTTP/3 中,由于 QUIC 流是独立的,一个流的丢包不会影响其他流。

核心概念:

  • 避免队头阻塞HTTP/3 over QUIC 使得单个流的丢包不会阻塞同一连接内的其他流。

上一节我们介绍了 QUIC 和 HTTP/3 的基本概念,接下来我们看看它们在协议栈中的位置。


协议栈对比 🧱

为了更直观地理解,我们可以对比传统的 HTTP/2 栈和新的 HTTP/3 栈:

传统 HTTP/2 栈:

  1. 应用层: HTTP
  2. 安全层: TLS
  3. 传输层: TCP
  4. 网络层: IP

HTTP/3 栈:

  1. 应用层: HTTP
  2. 传输/安全层: QUIC (集成了 TLS 功能)
  3. 网络层: IP (基于 UDP)

在 HTTP/2 中,帧(如 HEADERS 帧、DATA 帧)和流的概念都属于 HTTP/2 层。而在 HTTP/3 中,流的概念下放到了 QUIC 层,HTTP/3 的帧(同样是 HEADERS 和 DATA)则映射到这些 QUIC 流上。这种分离是架构上的一个重要区别。


Cloudflare 的实现时间线 ⏳

了解了协议基础后,我们来看看 Cloudflare 是如何实现和部署这些技术的。

Cloudflare 是一个全球性的“连接云”,我们在世界各地拥有大量数据中心,旨在为客户提供安全、高性能的网络连接。我们的系统由一系列代理服务器链组成,处理包括 HTTP、gRPC、WebSocket 在内的多种流量。

以下是我们在 QUIC 和 HTTP/3 上的关键里程碑:

  • 2018年9月: 开始支持草案版本的 QUIC,参与标准化过程的运行代码测试。
  • 2019年: 开源了用 Rust 编写的 QUIC 和 HTTP/3 库 —— quiche。这个库后来成为我们边缘基础设施的核心组件。
  • 2019年9月: 向早期用户开放 HTTP/3 支持。
  • 2024年12月: 开源了协议测试和调试工具 h3i,以应对复杂的协议层测试挑战。

我们的代理链大致如下:客户端连接到 Cloudflare 边缘(Ingress,负责 TLS 终止),然后流量经过一系列内部代理(应用缓存、路由规则等),最后从 Egress 代理发往用户源站。


回顾:gRPC over HTTP/2 的挑战 🔙

在探讨 HTTP/3 之前,有必要回顾一下我们最初引入 gRPC over HTTP/2 时遇到的挑战。这能帮助我们理解升级的必要性。

2020年,应客户需求,Cloudflare 推出了 gRPC over HTTP/2 支持。目标是让 gRPC 流量也能享受到 Cloudflare 为常规 HTTP 流量提供的保护。

当时的架构挑战在于,我们内部的代理链之间仍在使用 HTTP/1.1。为了支持端到端的 HTTP/2,我们设计了一个方案:在入口(Ingress)和出口(Egress)代理都处理 HTTP/2,但在中间使用 gRPC-Web 协议进行转换。

这个方案虽然可行,但也带来了复杂性和一些实际问题。HTTP/2 协议层暴露的攻击面让我们遇到了不少挑战:

以下是几个具体的例子:

  • Ping Flood 攻击 (2019): HTTP/2 的一种拒绝服务攻击方式。我们实施的缓解措施意外影响了新建的 gRPC 连接,导致它们被默认的 Ping 帧淹没而中断。这体现了底层协议行为可能带来的意外后果。
  • Rapid Reset 攻击 (2023年8月): 攻击者利用 HTTP/2 协议完全合法但非预期的方式,在我们的代理链中制造了大量的工作积压。
  • Continue Reset 攻击 (近期): 与 Rapid Reset 类似但方向相反的另一种攻击。

这些案例表明,HTTP/2 在协议设计上存在一些固有的弱点,修复起来很困难。而 QUIC 协议从设计上就对许多此类攻击具有免疫力,部分原因在于其加密和帧结构的设计。


机遇:gRPC over HTTP/3 🆙

鉴于 HTTP/2 的挑战,将 gRPC 迁移到 HTTP/3 上显得尤为有吸引力。这不仅是为了安全性,也为了性能提升。

gRPC 社区对 HTTP/3 的支持工作早已开始:

  • 2019年: gRPC 项目开启了 HTTP/3 支持的相关议题。
  • 2021年: James K. 和 Eric A. 等人提出并合并了初步设计提案。

从 gRPC 应用层的角度看,迁移到 HTTP/3 的变化并不大,因为差异主要被底层的 RFC 标准所封装。开发者可以使用 curl 等工具通过 --http3 标志进行测试。

对于 Cloudflare 而言,让我们能够提供 gRPC over HTTP/3 服务的最后一个技术障碍是完善对 Trailers(尾部头部)的支持。一旦解决,前端支持 HTTP/3 的 gRPC 流量就可以无缝通过 Cloudflare 网络,而用户的后端源站无需任何更改。


性能表现 📊

理论上的优势需要数据验证。我们进行了一些实验室测试和早期客户数据验证。

在实验室环境中,模拟从家庭网络通过 WiFi 发起简单 gRPC 请求:

  • 平均情况: 节省了约 1个 RTT(往返时间)。
  • P90情况 (90%分位): 节省了多达 5个 RTT。这表明在网络状况不佳时,HTTP/3 的优势更加明显。

更重要的是来自早期采用者 Uber 的生产环境数据(经授权分享):

  • 延迟: 在 P75 和 P95 分位数上,HTTP/3 的延迟均低于 HTTP/2
  • 成功率: 使用 HTTP/3 的请求成功率也更高。

这些数据令人鼓舞,我们正在收集更多信息以全面评估性能收益。


未来展望与问答要点 💡

在分享的最后,Lucas 回答了听众的一些问题,并提到了未来的发展方向:

  1. 与 MASQUE 协议的关系: MASQUE 是一种基于 HTTP(包括 HTTP/3)的代理和隧道协议,用于转发 UDP 等流量。gRPC over HTTP/3 可以通过 MASQUE 隧道运行,这为隐私增强和复杂网络环境下的代理提供了可能性。
  2. 连接失败检测: QUIC 的错误检测机制与 TCP 类似(如重传超时),但提供了更丰富的错误码和可配置的空闲超时,使得连接管理更精细。由于其加密特性,未来也有可能设计更快速的故障检测扩展。
  3. 高丢包环境下的性能: QUIC 流独立的特性意味着在高并发请求场景下,个别流的延迟或丢包对其他流的影响更小,这可能带来比 HTTP/2 更好的整体体验。但具体性能高度依赖于实现和算法调优。
  4. 延迟收益分析: 实验室数据显示,主要收益来自 QUIC 握手节省的 1个 RTT(约数毫秒)。在生产中,对于长连接和连接池,HTTP/3 在弱网环境下(高延迟、高丢包)的流独立性优势会带来更显著的尾部延迟改善。
  5. 未来工作 - 传输层抽象: 目前 QUIC 必须运行在 UDP 上,这可能会带来一些系统调用开销。IETF 正在开展一项新工作,旨在将 QUIC 流抽象 与底层传输层解耦,使其可以运行在任意双向传输协议上(如 Unix Domain Socket, RDMA)。这为数据中心内部等高性能场景的 gRPC over HTTP/3 应用打开了新的大门。

总结 🎯

本节课我们一起学习了将 HTTP/3 引入 gRPC 的完整历程:

  1. 回顾了基础:了解了 QUIC 和 HTTP/3 协议如何通过解决队头阻塞等问题,为 gRPC 带来潜在的性能和安全优势。
  2. 追溯了历史:看到了 Cloudflare 从支持 gRPC over HTTP/2 时遇到的协议层挑战,这些挑战推动了向 HTTP/3 的演进。
  3. 看到了实践:介绍了 gRPC 社区对 HTTP/3 的支持时间线,以及 Cloudflare 通过解决 Trailers 支持等最后障碍来实现该功能。
  4. 验证了价值:通过实验室和早期生产数据,证实了 HTTP/3 在延迟和成功率上带来的切实改善。
  5. 展望了未来:探讨了与 MASQUE 等协议的协同,以及 QUIC 流抽象未来可能脱离 UDP,应用于更高性能的内部网络场景。

对于开发者而言,开始尝试 gRPC over HTTP/3 的壁垒正在迅速降低。随着像 Cloudflare 这样的边缘提供商以及客户端库的广泛支持,这项技术有望为分布式应用带来更快速、更稳健的网络通信体验。

011:什么是Kubernetes?

在本节课中,我们将从零开始学习Kubernetes和GKE。我们将通过三个不同的视角来理解Kubernetes:作为开发者的日常工具、作为团队协作的平台,以及作为计算机集群的操作系统。

概述

Kubernetes是一个用于自动化部署、扩展和管理容器化应用程序的开源系统。在本教程中,我们将从三个层面逐步深入,帮助你构建对Kubernetes的全面理解。

开发者的视角:软件部署

上一节我们介绍了课程概述,本节中我们来看看开发者如何使用Kubernetes。

谈论Kubernetes时,我们无法避免频繁提及容器。那么,什么是容器?

容器常被描述为“轻量级虚拟机”。虽然其底层技术与虚拟机不同,这使得它们平均速度更快,但容器为用户提供的界面与虚拟机非常相似。它们看起来就像是你计算机内部的一个独立计算机,拥有自己的文件系统、进程树、权限管理等一切。

容器也是一种软件打包和分发机制。就像Java的Maven或Python的Pip一样,你可以从Docker Hub以容器镜像的形式拉取现成的、可工作的软件。不同之处在于,容器镜像中打包的软件可以用任何语言编写。

过去,如果我想运行网上找到的某个软件,我必须经历一系列复杂的步骤,才能让它在我特定版本的Linux上,配合所有正确版本的依赖项运行起来。而有了容器,运行任何软件真的只需要几秒钟。

这就是为什么容器是解决“在我电脑上能运行”问题的方案。过去,你写完代码,让它运行起来,提交后,经常有人会说:“嘿,这东西在我这儿跑不起来。”经过一番头疼的调试,你最终发现问题出在某个你甚至没意识到的晦涩依赖项的版本不匹配,或者某个你从未听说过的目录中存在冲突的配置文件。容器直接打包了整个Linux发行版的轻量级文件系统,因此这个问题不复存在。

当然,你可能在容器语境下听说过Docker。Docker是目前最流行的用于构建、运行和分发容器的软件。事实上,我们现在拥有的容器开放标准,很大程度上直接源自Docker。但如今实际上有许多其他容器实现,包括Podman等。你可以使用几乎任何你喜欢的容器实现与Kubernetes配合。

好的,我们知道了什么是容器。那么这如何应用于为Kubernetes开发呢?

答案是:用你喜欢的任何方式为Linux构建软件,将其放入容器,然后在几分钟内将其运行在Kubernetes上。最后一步通常只需要几分钟。

历史上,托管软件部署对如何构建软件的要求非常苛刻。例如Apache Heroku、Google App Engine等。它们要求你用特定的语言编写代码,严格按照某种结构布局文件,并使用非常特定的运行时版本。Kubernetes使用容器来声明:如果你的软件能在某个版本的Linux上运行,你就可以在Kubernetes上运行它。用Go、Ruby、Java 4写代码?谁在乎?它都能在Kubernetes上运行。所以,如果你不熟悉这类系统,这是Kubernetes真正解放生产力的第一点。

构建完软件后,下一步是将其放入容器中。这个过程通常比想象的要简单得多。有大量公开可用的基础镜像,可以为你提供所需的运行时环境。如果你使用特定JVM版本的Java,你可以从Docker Hub拉取该镜像,并非常轻松地将你的软件层叠在上面。Python也是如此。如果你能构建静态二进制文件(比如用Go),你的容器可以非常精简,甚至只包含那一个二进制文件。

最后,你编写一些YAML来定义你的工作负载。在该YAML中,你引用新创建的容器镜像,并使用kubectl CLI工具将其部署到你的集群。通常只需要一两分钟编写,几秒钟即可启动并运行。

以上就是定义工作负载的方式。Kubernetes真正闪耀的地方在于运维,即操作你的软件部署。这方面的深度远超本次分享所能涵盖,因此我将聚焦于两个最重要的部分:扩缩容和滚动更新。

传统软件部署中最具挑战性的事情之一就是扩容。根据你已有的自动化水平,将用于运行服务的计算资源翻倍可能需要数天或数周。但在Kubernetes中,这就像在你的部署YAML文件中增加一个数字一样简单。当在像GCP这样的云提供商上运行时,如果增加副本数意味着机器不足,GKE会直接配置更多虚拟机来运行你的软件。

事情甚至可以比这更自动化。Kubernetes允许你将各种指标(如CPU利用率或每秒请求数)挂钩,以自动触发扩容。这被称为水平Pod自动扩缩,简称HPA。

Kubernetes擅长的另一项运维任务是:在不中断用户流量的情况下推出服务的新版本。Kubernetes通过服务的滚动更新来实现这一点。假设你开始时有三个Pod以版本A处理请求,你想将它们全部更新到版本B。使用Kubernetes,你只需要将部署的版本从A更新到B。Kubernetes将关闭一个版本A的Pod,启动一个新的版本B的Pod,并重复此过程,直到所有Pod都运行版本B。这个滚动更新过程的许多细节都是可配置的,因此你可以调整它以完全按照你想要的方式工作。

平台团队的视角:团队协作

上一节我们从开发者的角度体验了Kubernetes,本节中我们来看看它如何帮助平台团队和整个组织协作。

让我们回溯一下时间。在行业里待过一段时间的人可能熟悉基于虚拟机的软件部署和运维模型。无论你是在GCP、AWS、Azure上运行,甚至在很大程度上在本地运行,这都是基本的操作模式。你使用云提供商的CLI工具启动一些虚拟机,使用相同的CLI工具配置一些负载均衡器,然后通过某种方式(最基本的是SSH)将你的软件部署到虚拟机上。

在这种模式下,组织管理软件部署的方式大致有两种。

人们有时称第一种模式为“DevOps”,因为每个开发者也负责运维或操作他们的软件。这意味着每个团队都需要管理自己的虚拟机池和所有相关的基础设施。CTO或多或少地给团队云预算,然后说:“搞定它。”他们需要弄清楚如何将软件部署到虚拟机:是打包成tarball还是Debian包?他们需要弄清楚已部署软件的生命周期管理:如何处理崩溃和内存不足错误?也许使用systemd或supervisord。他们需要弄清楚调试访问方法:是否允许到处使用SSH?这甚至还没涉及到最困难的部分:扩缩容。我刚才提出的许多问题的答案在尝试扩展系统时效果不佳。

在这个图表中,你可以看到两个功能团队:开发Foo应用的Foo团队和开发Bar应用的Bar团队。他们的部署是完全独立的。除了计费账户或云项目外,没有任何共享。开发人员通常只想把代码发布出去,所以他们不会花时间研究管理部署的最新、最棒的工具。因此,他们大多使用Stack Overflow、gcloud命令和通过SSH执行的chmod命令的混合体。换句话说,在这种模式下,每个开发人员还需要成为一名系统管理员,照料他们部署软件的个人小花园。

这大致是我上一家公司的模式。虽然这对培养系统管理技能很有好处,但这也导致了长达17小时的部署会议。对任何人来说都不太有趣。

这也意味着整个组织在软件部署方面缺乏统一性,这使得强制执行横向标准变得非常困难。并且,这使得组织其他部门的人员很难在不先做大量研究的情况下,介入并了解另一个团队系统中发生了什么。

显然,这很难管理。不是每个大学毕业生都会带着专家级的系统管理技能和出色的编码技能入职。因此,一些组织采取的替代方案是将组织划分为开发团队和运维团队。

运维团队负责管理计算资源池和其他云基础设施。运维团队将大部分时间用于研究和决定云基础设施的最佳实践。他们确保软件部署生命周期管理在整个组织内是统一的,这使得强制执行横向标准变得容易得多。他们确保负载均衡和扩缩容对每个服务都运行良好。这解决了我们在上一张幻灯片中看到的许多问题。但现在,问题变成了开发团队和运维团队之间的交接。很多时候,这种交接是粗糙的。

那么,在这种模式下,开发团队如何发布新版本的软件呢?他们向运维团队创建一个工单。当部署失败时他们做什么?添加一条评论。换句话说,根据设计,开发团队不接触基础设施,运维团队不接触代码。这里的官僚主义变得非常痛苦。当然,这两个极端之间存在一个谱系,但在对开发团队的专业知识要求和对这些开发团队的繁文缛节之间的权衡总是存在的。

回到GKE和Kubernetes,Kubernetes使运维团队能够就基础设施做出跨领域的决策,同时也为开发团队提供了可编程的自助服务API,以便在运维团队设定的边界内部署和管理他们的软件。

这是一个简化的图景,但Kubernetes为以下问题提供了答案:工作负载将被调度到哪些虚拟机上?应用程序将使用什么交付格式?日志如何收集?以及许多其他问题。对于那些Kubernetes本身不发表意见的问题,它通常提供一种机制,让运维团队为该特定Kubernetes集群的所有用户提供他们自己的横向标准。

这意味着开发团队能够按照自己的时间表重新部署,并且他们可以使用一套更加精简、在所有云提供商中都相同的API来完成。如果出现问题,他们会立即知道,并且能够访问调试所需的所有工具。因此,在平衡运维需求和开发需求方面,Kubernetes让你鱼与熊掌兼得。

抽象视角:集群操作系统

上一节我们探讨了Kubernetes如何促进团队协作,本节我们将从更抽象的视角,将其视为一个操作系统。

我们的最后一个视角将更加抽象。我们将拉回到一万英尺的高空。我要提出一个主张:Kubernetes是计算机集群的操作系统,不是单台计算机,而是多台计算机的操作系统,可能用于整个数据中心,甚至多个数据中心。我曾与GKE项目的产品经理争论过,认为这个主张过于宽泛。但我认为有充分的理由支持它,并且论证这一点将真正帮助你感受Kubernetes。

那么,如果Kubernetes是一个操作系统,那么什么是操作系统?让我们以Linux为例。

首先,最重要的是,操作系统是一组抽象。如果完全没有操作系统更容易开发,那么你就不会使用那个操作系统。提供的具体抽象因平台而异。例如,Android提供的抽象就与Linux不同。好的操作系统用共同的惯用法构建这些抽象。也就是说,提供的抽象应该彼此足够相似,以至于整个系统不会崩溃成一堆没有开发者能理解的复杂性。真正优秀的操作系统提供一个工具链,为99%的用例铺平道路。

现在,让我们通过观察Linux来具体化这一点。Linux提供了相当多的抽象。最明显的是进程的概念,它使你不必担心将应用程序的地址空间调度到内存中。你有很多用于与CPU外部事物交互的抽象:用于驱动旋转盘片和SSD的文件系统API、用于闪存驱动器和键盘的设备驱动程序,以及用于连接网络硬件的通用API。Linux通过统一“一切皆文件”的概念,使这些抽象更容易理解。你获得一个文件描述符,然后从中读取字节并向其写入字节。

最后,使你能够利用这一切的工具链以C语言为基础,并用Lib C包装原始的、中断驱动的系统调用。你可以直接针对Linux的汇编API编程,就像Go所做的那样。但依赖Lib C要容易得多,就像我们大多数人甚至没有意识到的那样。

Kubernetes的情况惊人地相似。现在,面对多台机器,我们希望抽象掉的是我们的应用程序最终运行在哪台机器上。Kubernetes负责查看它可以访问的所有机器,并决定调度所有工作负载的最佳方式。

它提供了一个API来持久化数据。由于工作负载可能最终运行在任何机器上,这变得更为困难,而Kubernetes允许你控制这一点。Kubernetes为你提供API来配置负载均衡器以与外部世界通信。现在,面对多台机器,你需要担心整个网络的配置,而不仅仅是本地机器到该网络的连接。

Kubernetes为大多数这些API提供了一个共同的惯用法:一切皆资源,即存储在Kubernetes内部的YAML对象。我们之前看到过一些。CLI工具kubectl利用所有资源共有的结构来简化工作流。

最后是工具链。虽然你可以在Kubernetes上运行任何语言编写的代码,但Kubernetes提供的client-go库,如果你想与Kubernetes紧密集成(例如自己读写那些资源),它是最容易的选择。这类似于Lib C和C语言对Linux的作用。

现在,如果我们将上游开源Kubernetes与Linux进行比较,它最像的是没有附加组件的内核。如果你曾经从源代码构建Linux内核并尝试用它运行一个系统,你就会知道它基本上没用。没有你需要的打印机或WiFi设备驱动程序,没有GUI,没有文字处理器,没有文本编辑器,甚至没有像ls这样的基本命令。Kubernetes本身(开源版)是类似的。实际上,唯一内置的功能是调度容器的能力。其他一切,包括网络模型、数据持久化和服务发现,都是由各种插件实现的,通常是供应商特定的插件。也就是说,有些能在GCP上工作,有些能在AWS上工作,只有少数能在任何地方工作。所有这些功能都是由各种插件提供的。

第一种插件称为控制器操作符。这些是可以在任何地方作为工作负载运行的应用程序:在集群内、直接在虚拟机上,甚至在你的桌面上。它们使用标准的Kubernetes惯用法:通过HTTP传输YAML。它们监视Kubernetes跟踪的资源的变化,并根据这些Kubernetes资源的内容在外部世界做出更改。在像GKE这样的云托管Kubernetes服务上,有一堆控制器监视Kubernetes资源,并将它们直接转换为等效的GCP资源。这样,你可以直接在Kubernetes中定义所有GCP基础设施。

换句话说,控制器是控制循环。它们查看Kubernetes资源所传达的意图,并将该意图转化为GCP层面的现实。

Kubernetes中的另一种插件实际上就叫做插件。这些是必须在节点上运行才能工作的东西。通常,这些插件会修改在节点上运行的容器。因此,它们必须直接在主机上运行,或者作为特权容器运行。这些包括像CNI(容器网络接口)插件这样的东西。这些是负责为Pod分配独立IP、将这些IP分配给底层容器并确保网络连接到网络的东西。这些插件,令人惊讶的是,不使用HTTP和YAML,而是使用gRPC和Protocol Buffers。这对于这类插件是更好的选择,因为API通常是命令式的而非声明式的,并且它们不使用构成Kubernetes API其余部分的资源。

什么是GKE?

现在你理解了Kubernetes如何扩展,终于可以回答这个问题了:什么是GKE?它是一个产品,将上游Kubernetes与一系列GCP特定的插件捆绑在一起,使其变得非常有用。换句话说,GKE是一个Kubernetes发行版。你可能熟悉像Debian、Ubuntu或Red Hat这样的Linux发行版。它们对Linux内核做的正是同样的事情。那么GKE具体做什么呢?

实际上,它做了很多事情。它完全自动化了在GCP上配置虚拟机、配置其网络、确保它们具有正确的Linux版本以及将所有这些软件安装到这些虚拟机上以将它们绑定到Kubernetes集群的任务。如果你使用像Kelsey Hightower的“Kubernetes the Hard Way”这样的教程手动完成这个过程,你会了解到这绝非易事。相对于纯粹的上游Kubernetes,这里有巨大的附加价值。

除此之外,还有大量的运行时集成,例如Pod原生网络,每个Pod都获得自己可单独寻址的IP,而无需借助NAT将数据包发送到单个Pod。这个列表只是GKE实际添加到上游Kubernetes功能的一小部分。最终,GKE是试图使GCP成为运行Kubernetes的最佳场所。

总结与建议

本节课中我们一起学习了Kubernetes的三个核心视角:作为简化软件部署和运维的开发工具,作为促进开发与运维团队高效协作的平台,以及作为抽象计算机集群资源的操作系统。我们还了解了GKE作为Kubernetes的一个特定发行版,如何通过集成GCP插件提供开箱即用的强大功能。

如果你对Kubernetes感兴趣,我强烈建议你尽快开始与一个集群进行实际交互,而不是试图阅读大量书籍。即使只是在GKE上设置一个简单的Web服务器应用程序,也能极大地帮助你理解这一切是如何运作的。

012:在规模下演进关键系统——什么有效、什么有害、下一步是什么

在本节课中,我们将学习万事达卡(Mastercard)的工程师团队如何利用gRPC双向流技术,在庞大的支付网络系统中实现高性能、高可扩展性和高安全性的服务演进。我们将探讨实践中的成功经验、遇到的挑战以及未来的技术方向。


P12.1:什么有效——gRPC如何成为支付网络的基石

上一节我们介绍了课程概述,本节中我们来看看gRPC技术栈在万事达卡支付系统中的成功应用。

万事达卡是一家技术公司,管理着一个全球网络,用于处理跨越地理区域的金融交易、服务和数据。我们以极高的吞吐量、低延迟和安全性处理这些支付交易。我们构建的技术栈经过大量定制,以在全球范围内传输低延迟数据。

我们通过使用双向协议进行了定制,但下一步是如何在全球范围内构建可扩展的支付网络。大约五年前,我们开始接触gRPC。自那时起,我们围绕gRPC协议演进并构建了万事达卡的技术栈和事件驱动架构,因为它提供了安全性、开发者体验和更快的交付速度。如今,我们每天在全球范围内以闪电般的速度处理数亿笔交易。这就是我们演进万事达卡网络的方式。

以下是gRPC带来核心优势的几个方面:

  • 安全性与性能:gRPC提供了内置的安全性(如TLS)和基于HTTP/2的高性能通信。
  • 开发者体验:强类型API、清晰的合约(Protocol Buffers)以及多语言支持,使其易于在我们的多语言生态系统中大规模采用。
  • 边缘集成:与传统的REST over HTTP/1.1系统不同,gRPC在可信边界上暴露API时提供了清晰的语义,并支持细粒度的访问控制。

在金融科技领域,性能不是奢侈品,而是必需品。每一个字节、每一毫秒都决定了交易的成本和效率。gRPC无缝地契合了安全、性能、可扩展性和最小开销这些核心原则。


P12.2:可扩展性与可观测性——持久连接带来的新范式

上一节我们介绍了gRPC的核心优势,本节中我们来看看在引入gRPC双向流后,可扩展性和可观测性方面面临的挑战和解决方案。

gRPC协议是一个多层网络协议。底层是TCP/IP(第3/4层),之上是HTTP/2流,最上层是应用层消息。当消息在全球范围内发送时,如何维持持久的双向连接,并在出现问题时进行报告和修复,是网络层面持续给我们带来的挑战。

我们进行了巨大的投资,在每一个层面(TCP、HTTP/2、流、通知)都构建了遥测系统,并嵌入了详细的追踪。在双向通信中,客户端或服务器任何一端出现问题都会导致交易失败,因此必须从两端审视流量,并建立告警机制。

关于可扩展性,当流量跨越多个区域时,如何实现横向扩展?传统的基于实例扩展的思维在gRPC世界中并不完全适用,因为gRPC是基于持久连接的。一旦与后端服务器实例建立连接,该连接就会保持,后续的所有RPC调用都指向同一个后端实例。这可能导致某些实例过载,而其他实例闲置。

因此,我们不得不采用不同的策略来实现水平扩展,并转向了客户端负载均衡。客户端需要具备智能,了解所有可用的后端服务器实例,并在启动时建立到多个实例的连接和流,从而将流量均匀地分布到所有可用的流上。


P12.3:安全性考量——从无状态REST到有状态gRPC流的转变

上一节我们讨论了扩展性挑战,本节中我们深入探讨引入gRPC双向流协议时,必须解决的关键安全问题。

万事达卡是一家受到严格监管的支付行业公司。引入像gRPC这样的新协议时,必须确保其符合现有的安全标准、法规和客户要求。这些要求最终会转化为技术控制措施。

最大的一个安全范式转变是从基于HTTP/1.1的无状态REST API,转向在客户端和服务器之间保持基于连接状态的协议。

  • REST API:每个请求都是独立的,服务器每次都需要对客户端进行身份验证和授权,然后连接关闭。
  • gRPC双向流:客户端和服务器建立连接后,可能会长时间通信。在大多数情况下,客户端可能只进行一次身份验证(通过通道凭证或调用凭证),之后便基于这个已建立的连接进行通信。

这里的关键在于信任基础。gRPC运行在TCP之上,而TCP本身不是安全的连接协议。唯一能保证连接安全的是其上的TLS(传输层安全)。TLS在密码学上将客户端和服务器绑定在一起。

为了验证gRPC连接的安全性,我们进行了深入测试。我们在沙箱环境中,在gRPC客户端和服务器之间放置了一个TCP代理(如Nginx),并编程方式修改TLS记录中的单个字节。测试证实,一旦TLS层抛出致命警报,客户端和服务器都会收到通知,TCP连接会被关闭。当连接重新建立时,身份验证会被强制再次执行。这符合我们的安全要求:我们不希望gRPC在子通道(承载实际TCP连接的逻辑实体)出现问题时,在不重新进行身份验证的情况下尝试自动修复它。


P12.4:什么有害——实践中遇到的痛点与挑战

上一节我们确认了gRPC在安全模型上是可靠的,本节中我们来看看在实际大规模应用gRPC双向流时,遇到的具体困难和“痛点”。

负载均衡的思维转变

传统的负载均衡器(如F5)是服务器端负载均衡器,在TCP层进行路由决策。这对于HTTP/1.1很有效,因为路由决策在连接建立时做出。

但在gRPC世界中,连接是持久的。一旦与某个后端实例建立连接,所有后续RPC都通过这个通道指向同一个实例。随着时间的推移,这会导致某些实例过载,而其他实例闲置。因此,传统的服务器端负载均衡器在这种情况下效果不佳

解决方案是转向客户端负载均衡。客户端需要内置智能,知晓所有可用的后端服务实例,并在初始化时建立到多个实例的连接和流。这样,客户端可以基于每个消息(per-message)或更细的粒度,将流量智能地分发到所有健康的流上,实现真正的均衡。

服务发现与API暴露

我们内部已经大规模安全地实现了数十个gRPC API。在向外部客户暴露这些API时,我们遇到了挑战。

我们采用了gRPC反射机制来帮助内部集成,但反射器会暴露一些内部方法,因此需要确保外部API得到适当的安全保护。

此外,服务发现、端点发现等挑战仍然存在。为此,我们正在采用xDS(通用数据平面API)协议来实现服务发现、端点发现、负载均衡技术和流量整形技术。我们计划在熟悉xDS后,基于此协议向外部客户暴露API。

协议转换与开发者体验

在前端技术栈(如接收来自用户或前端应用的数据)和后端gRPC服务之间,存在协议转换的摩擦点。对于成千上万的开发者而言,如何处理这种转换是一个挑战。

我们的方法是首先确立标准:使用强类型API和清晰的合约(Protocol Buffers)。我们有一个中央仓库来管理所有的原型定义。清晰的合约有助于在开发周期早期发现问题,减少歧义。对于必要的协议转换层,我们通过服务等级协议(SLA)进行治理,并精细调整这些底层的TCP参数。


P12.5:下一步是什么——未来的技术探索方向

上一节我们剖析了当前的挑战,本节中我们展望万事达卡在gRPC和通信协议方面的未来规划。

更智能的客户端与xDS

除了采用xDS协议来实现地理服务发现、故障转移和动态配置外,我们还将结合一项自适应的请求处理机制。该机制会监控通道健康度、流健康度等网络层信息,从而动态、自适应地路由流量,其能力将远超现有gRPC框架。

探索更轻量、更快速的协议

我们始终在寻找更轻量、更快、更具弹性的协议。在这方面,我们正在探索HTTP/3 QUIC协议。目前已有试点项目在进行中。

QUIC基于UDP构建,其流彼此独立,避免了TCP队头阻塞等缺点。此外,其安全连接可以做到0-RTT或1-RTT,因此速度更快,对连接失败的恢复能力也更强。

统一的客户端库与外部集成

我们目前使用的具备客户端负载均衡等智能的客户端库,既可用于内部也可用于外部。未来的方向是将这种能力通过统一的接口暴露给所有客户。无论是支付处理器还是希望构建自身逻辑的现代化客户,都能利用这些库与我们的网络集成。这需要谨慎设计,确保支付网络细节的保密性。


P12.6:总结与问答精要

本节课中,我们一起学习了万事达卡利用gRPC双向流技术演进其关键支付系统的旅程。

核心总结如下:

  • 什么有效:gRPC凭借其高性能、强类型合约、多语言支持和良好的安全基础,成为构建现代、可扩展支付服务的基石。
  • 什么有害:持久连接改变了负载均衡的范式(需转向客户端负载均衡),服务发现与安全暴露存在挑战,协议转换层需要精心设计。
  • 下一步是什么:拥抱xDS实现更智能的服务治理,探索HTTP/3 QUIC以获得更佳性能,并致力于提供统一的客户端库以简化外部集成。

以下是问答环节中提炼的关键点:

  • 关于负载均衡:我们采用基于消息的客户端负载均衡。通过建立足够数量的冗余连接和流,系统保持无状态。每个消息都有唯一ID,可以在任何可用的流上处理,通过元数据来区分消息和流。
  • 关于客户端位置:智能客户端库可部署在内部或外部。它封装了负载均衡逻辑,未来目标是将其作为统一接口提供给合作伙伴和客户。
  • 关于服务器亲和性:在我们的设计中,没有服务器亲和性。依靠冗余的连接和流,任何消息都可以由任何可用的流处理,从而保证弹性和无状态性。

(注:由于原始内容为研讨会转录,存在口语化、重复和逻辑跳跃。本教程已对内容进行梳理、重组和精简,在严格保留每一句原意的基础上,使其更符合教程的连贯性和可读性要求。)

013:项目治理变革与未来展望

在本节课中,我们将学习gRPC项目为达成CNCF毕业目标所进行的治理结构变革。我们将了解CNCF的基本情况、项目成熟度等级,并详细解析gRPC新引入的贡献者阶梯制度和指导委员会。

认识CNCF

我是Richard Bellville,gRPC项目的维护者之一。本次演讲将详细解释gRPC项目为在CNCF内达到毕业状态所做的准备,以及近期为确保达成目标而实施的一些变革。

在深入细节之前,我们先来了解一下CNCF。许多人可能对它并不熟悉。

CNCF代表云原生计算基金会。如果你听说过它,很可能是在与Kubernetes相关的语境中。这是因为CNCF持有Kubernetes的商标,并投入大量时间推广Kubernetes。

深入细节来看,CNCF是Linux基金会的一个非营利性子公司,由包括AT&T、思科、Docker、谷歌、推特等多家公司在2015年共同创立,旨在推动云原生技术的协同发展。

当时,这主要意味着涉及容器(如Docker、Kubernetes等)的软件。事实上,Kubernetes是第一个捐赠给CNCF的项目。

随着时间的推移,CNCF已扩展到涵盖种类繁多的软件。

CNCF的职责

CNCF充当捐赠给它的项目的管理者。公司将其软件项目开源并捐赠给CNCF,希望提高该软件在更广泛领域的采用率。

CNCF在其旗舰活动KubeCon等会议上推广这些项目。KubeCon每年在全球各地举办多次,每次吸引数千名访客。事实上,您正在参加的gRPCConf也是一个CNCF活动。

下图是云原生全景图,展示了CNCF旗下所有项目的分布。项目数量众多,无法一一辨认。

显然,公司将项目捐赠给CNCF能获得巨大价值。捐赠能提高项目采用率的原因是,全球公司的软件开发人员关注CNCF,因为它以质量著称。

当CNCF推广一项技术时,通常意味着它是高质量、安全、经过实战检验的,并且是您可以放心用于生产系统的技术。

gRPC与CNCF

基于同样的原因,谷歌在2017年将gRPC捐赠给了CNCF。

如今在2025年,CNCF在众多不同领域托管着超过200个项目,涵盖存储、可观测性、CI/CD、网络等,数量之多难以在此列举。

但并非所有项目都处于相同的状态,每个项目都有其指定的成熟度等级。

项目成熟度等级

当一个项目首次捐赠给CNCF时,它从沙箱项目开始。CNCF将沙箱项目称为实验、早期工作和前沿技术。换句话说,它们是未来值得关注的事物,但不一定是当前需要下重注的事物。

下一个成熟度等级是孵化中。项目通过CNCF技术监督委员会的审查流程,从沙箱项目晋升为孵化项目。

技术监督委员会认证新的孵化项目拥有健康的贡献者群体,并且至少被几家公司成功用于生产环境。

最终等级是已毕业。孵化项目被认为是稳定和成熟的,而已毕业项目则被广泛采用,并已成为云原生计算领域的基石。

从孵化状态晋升到毕业状态的过程,涉及与CNCF技术监督委员会进行更深入的认证流程。

gRPC的现状与目标

信不信由你,gRPC目前还不是一个已毕业项目,而只是一个处于中间状态的孵化项目。

当我告诉别人这一点时,他们通常非常惊讶。gRPC难道不是云原生计算领域的基石吗?无论您在哪家公司,在世界上哪个国家,它都是RPC系统的事实标准。

这正是我们认为现在应该认真追求CNCF毕业的原因。

迈向毕业的治理变革

不久前,我们与CNCF的一些人员会面,就我们需要做什么才能走上毕业之路获得了一些指导。

他们的反馈是,我们能做的最重要的事情是对治理结构进行一些调整,以提高成功几率。

我们的治理章程最初写于七年前的2018年,此后从未更改。CNCF的联系人指出了其中的不足,例如维护者退出流程定义不够明确,可能被人利用。

我们的结构是完全扁平的。你要么是维护者,要么完全不是项目的一部分。对于某人何时应该成为维护者,也没有真正的标准。那么,个人或公司想要参与gRPC项目该如何做呢?

我们采纳了所有反馈,查看了CNCF推荐的治理模板,制定了一份提案,并在几周前召集了所有67位现任维护者,投票通过了该提案。

以下是该提案的高层概述。

新的贡献者阶梯

首先,我们将之前完全扁平的结构分解为具有多个层级的贡献者阶梯。每个层级代表更高的承诺度和专业水平。

在这个新阶梯中,维护者是顶峰。我们的维护者现在是最资深、最专业的贡献者。

我们制定此治理章程的最终目标是展示新公司如何逐步晋升为维护者。稍后将详细介绍阶梯结构。

新增指导委员会

我们还新增了一个选举产生的指导委员会。指导委员会由七人组成,负责与CNCF沟通、代表项目发表声明,并确定宣传和营销的整体方向。

我们刚刚完成了第一届指导委员会的选举,他们的任期已于8月15日开始。

贡献者阶梯详解

以下是新的贡献者阶梯的具体细节。

在新的贡献者阶梯中,人们将以组织成员的身份加入项目。根据经验法则,组织成员每年至少贡献四个PR、错误修复或代码审查,算是初步接触项目。

达到此级别只需要两位现有组织成员或更高级别成员的推荐。

一旦成为组织成员,他们可以在特定代码仓库中被分配问题和审查任务,并可以推荐他人成为组织成员。

随着时间的推移,当他们在某个领域获得专业知识时,他们会被晋升为核心贡献者以表彰其专长。

此晋升只需要两位现有核心贡献者或更高级别成员的推荐。

在此阶段,您的专业领域会被正式记录在贡献者阶梯中。它可能是一个代码目录、整个代码仓库,或一个更跨领域的问题,例如可观测性或安全性。

无论领域如何,核心贡献者负责审查该领域的大部分PR,同时与维护者和各自仓库的负责人合作,确保工作朝着整个gRPC项目的战略方向发展。

最后,当该人员展示了跨多个领域的广泛专业知识,并参与了指导新贡献者时,他们会被投票选为维护者以表彰这些成就。

成为维护者不仅意味着负责项目的关键部分,例如整个语言实现,还意味着定义项目的整体技术战略。

例如,维护者经常花费很长时间共同研究如何在不同实现之间存在巨大差异的情况下,使跨语言设计工作。

治理变革的实施

综合来看,这为新贡献者加入gRPC并逐步晋升到最高层提供了一条非常清晰的路径。

当我们投票通过这项新治理章程时,我们在旧章程下有67位维护者,他们的经验和专长水平差异巨大。其中一些维护者已经在gRPC上工作了十年,而另一些只工作了几个星期。

我们使用本幻灯片上的经验法则,根据每位贡献者在项目中的经验水平,将他们回溯性地安置在阶梯的相应层级。

指导委员会的职责

gRPC项目一直有一些人较少参与日常琐碎细节,而更多地参与项目的高层方向。指导委员会在开源项目中正式确立了这一点,并让他们负责与CNCF沟通、代表项目发表声明,并处理宣传和营销事务。

因此,指导委员会每年选举一次。成员不必是维护者,尽管今年名单中有几位维护者。您可能会从今天的会议上注意到这个名单上的一两个名字。

指导委员会将定期举行会议,这些会议默认在线且公开。请关注gRPC邮件列表以获取相关设置详情。

毕业之路的后续步骤

通过这些治理变革,我们认为我们已经为在CNCF成功毕业做好了准备,但仍需采取一些更具体的步骤。

首先,我们需要技术咨询小组撰写一份尽职调查报告。他们将按照清单进行检查,确保我们确实遵循了治理章程、保持供应商中立、遵循安全最佳实践等。

之后,我们将向技术监督委员会提交正式申请。他们会指派人员审查我们的材料和尽职调查报告。希望一切顺利,我们就能在KubeCon上看到像迪士尼世界的米老鼠一样走动的充气煎饼吉祥物。

如何参与gRPC项目

我们已经了解了CNCF和gRPC项目结构的具体细节。显然,这些治理变革的一个重要部分是让新成员更容易加入项目。

如果您对此感兴趣,最好的开始方式是加入邮件列表。如果您已经知道自己感兴趣的领域,例如特定语言或特定代码仓库,我们有很多标有“需要帮助”或“适合新手”的问题。

如果您完成了一些此类贡献,您就是成为组织成员的绝佳候选人。如果您有兴趣追求这一点,请随时通过邮件列表联系我们。

总结

本节课中,我们一起学习了gRPC项目为达成CNCF毕业目标所进行的治理结构变革。我们了解了CNCF的概况及其项目成熟度体系,详细解析了gRPC新建立的贡献者阶梯制度和指导委员会,并明确了项目后续的毕业申请步骤。这些变革旨在使项目治理更加清晰、开放,并鼓励更广泛的社区参与。

014:使用服务网格与gRPC 🚀

在本节课中,我们将要学习服务网格(Service Mesh)的基本概念、它与gRPC的交互方式,以及不同的服务网格架构如何影响gRPC应用的性能、安全性和部署。我们将探讨何时选择代理模式(Proxy)或无代理模式(Proxyless),并理解其中的权衡。

概述

服务网格是一个用于增强服务间通信的基础设施层。它处理诸如加密、负载均衡、可观测性和弹性等功能,并将这些功能从应用代码中解耦。这使得开发者可以专注于业务逻辑,而服务网格则确保服务交互的可靠与安全。

什么是服务网格? 🧩

服务网格是应用的基础设施层,用于增强服务到服务之间的通信。它处理诸如加密、负载均衡、可观测性和弹性功能。这些功能与应用代码解耦,因此可以独立管理。这也使得开发者能够专注于业务逻辑,而服务网格则确保服务交互的可靠与安全。

gRPC与服务网格的兼容性

一个常见的问题是:服务网格能与gRPC一起工作吗?答案是肯定的。gRPC需要HTTP/2,而HTTP/2通常被广泛支持。另一个相关的问题是:哪种服务网格与gRPC配合得最好?这两个问题的答案都取决于你使用服务网格要解决的具体问题。因此,请记住,选择是围绕你的具体目标展开的。

问题在于,“你”的需求可能有些模糊。你的团队可能有特定需求,但公司里的其他团队也在优化他们自己的需求。例如,基础设施团队可能希望部署简单,而兄弟团队可能想要更多功能,另一个团队可能追求性能。因此,建议你记住,这是一个看似简单但实则复杂的问题。

服务网格架构类型 🏗️

考虑到这些不同的目标,让我们看看gRPC如何与服务网格交互。为此,我们需要了解一些服务网格架构。以下是几种粗略的架构分类,不同的人可能使用不同的名称,但足以用于本次讲解:网关(Gateway)、守护进程集(DaemonSet)、边车(Sidecar)、无代理(Proxyless)。让我们逐一看看它们有何不同。

无网格架构

首先从“无网格”开始。我们只有应用程序,服务直接相互通信。这种方式可以工作,但可能存在负载均衡问题。总的来说,它缺乏安全性,并且对策略变更的响应可能很慢。

“对策略变更响应慢”意味着,如果你想更改一个设置,必须推出应用程序的不同版本。如果需要对客户端进行更新,情况可能特别棘手。例如,如果你是图中的App4,可能很难让App1重新部署。

网关架构

你可以在客户端和服务器之间放置一个代理,现在你获得了良好的负载均衡,并且可以快速更新服务的策略,而无需重启或重新部署客户端或服务器。

我使用“网关”来称呼这种模型,这可能看起来有些奇怪,因为我们最常在网格间通信时使用“网关”,但其架构本质上是相同的。与典型的服务网格相比,这种架构主要缺少的是安全性。请注意,代理可以在多个服务之间共享,也可以每个服务拥有自己的代理。

守护进程集架构

现在我们来到第一种服务网格的架构。我们在每个节点上放置一个代理,并设置一些内核规则,使得该代理能够控制应用程序的网络通信。

现在,代理可以强制执行并使用双向TLS来加密网络流量,并限制对服务的访问。代理使用Kubernetes的守护进程集在每个节点上运行,然后在节点上的Pod之间共享。

边车架构

接下来是第二种人们常与服务网格关联的架构。不是每个节点一个代理,而是每个Pod一个代理。这种架构与守护进程集的主要区别不在于功能,而在于部署和资源。如果一个应用产生大量流量,你可以给该Pod的代理分配更多资源。但另一方面,它将共享Pod的资源,这可能会增加部署的复杂性。

回到起点:为什么需要服务网格? 🤔

既然这是一个关于gRPC的演讲,让我们回到第一张架构幻灯片。服务网格的初衷是,人们有一些应用程序自身无法实现各种功能,而为各种语言的不同客户端和服务器库添加这些功能工作量太大。因此,他们决定使用代理。但是,如果客户端和服务本身直接具备这些功能,那么你就不需要代理也能实现。

让我们再次看看我们的架构列表。这些架构有一定的灵活性。例如,Linkerd可以切换到边车模式,以便按Pod启用,这得益于更高效的代理成为可能。Istio的Ambient Mesh则切换到一个更小的守护进程集代理,以简化资源管理,因为代理不再与应用竞争资源。

但为什么Istio会同时使用守护进程集和网关呢?我们可能不需要完全理解这一点。

代理类型:L4与L7 🔌

当我们谈论代理时,有两种主要类型对架构很重要。一种是OSI参考模型中的第3层或第4层,即网络层或传输层。这类代理工作在IP或TCP层面。

我们将它们统称为L4代理,因为尽管从技术上讲它们可能工作在不同的层,但它们产生的结果相似。

另一种是第7层,即OSI模型中的应用层。对我们来说,这类代理通常处理HTTP,尽管它也可以处理其他协议。

我知道有些人可能只认为L7代理才是代理。一些L3代理,我们只称之为路由器,人们可能不认为它们是代理,但它们确实是。它们在做很多相同的事情。这应该提醒我们,实现方式很重要。路由器速度非常快,我们甚至不考虑它们的延迟成本。它们就在那里,这很好。部署和维护也被抽象化了。我们通常不考虑使用什么规格的机器或CPU使用率,只是让它运行。

概括来说,L4代理往往更高效,而L7代理往往功能更丰富。考虑到这一点,再次思考:为什么Istio会同时使用守护进程集和网关?答案是,一个是L4代理,一个是L7代理。

gRPC对服务网格的特殊之处 🎯

好了,我们已经有几张幻灯片没谈gRPC了。那么,是什么让gRPC对网格来说很特别呢?答案很简单:HTTP/2。HTTP/2可以在单个连接上处理多个请求。这与HTTP/1.1不同。

回顾我们之前提到的代理类型,L4代理只能对连接进行负载均衡。HTTP/2需要更少的连接,因此获得负载均衡的机会更少。你们中的许多人可能对Kubernetes中的这种情况很熟悉。如果每个客户端只占服务器总负载的一小部分,Kubernetes原生的L4负载均衡可能就足够了。但对于负载较重的客户端,你需要进行L7负载均衡,而gRPC原生支持这一点。这就是为什么人们随后会使用无头服务(Headless Services)。HTTP/2在部署这些网格时也会导致类似的行为。

架构示例分析 📊

例如,这对gRPC效果如何?我们使用较新的Istio架构,但选择不使用Waypoint代理。这些代理只能进行L4级别的负载均衡。

因此,这将表现得类似于Kubernetes的负载均衡。如果你有小型客户端,这可能没问题。否则,你可能需要为每个RPC添加一个Waypoint代理,以实现更好的后端分发。

再举一个Linkerd的例子。这看起来非常相似。但现在代理是L7的。所以实际上这样就足够了,你不需要再添加其他东西。

性能与安全考量 ⚖️

我时间掌握得不错,所以再补充一点。你可能想知道HTTP/2是怎么回事。Linkerd实际上可以从HTTP/1.1升级到HTTP/2。当它使用某些东西时,Istio的Ztunnel也可以使用HTTP/2进行自身通信。然后我们让gRPC使用HTTP/2。为什么我们要为所有这些使用HTTP/2?答案实际上是安全。进行安全握手可能非常昂贵。如果你使用明文,你不会注意到,因为没有TLS握手。但一旦你开始尝试做安全,建立连接就变得昂贵得多。这就是为什么这些不同的工具系统试图重用连接并避免不必要的握手。

如果你的服务对性能敏感,那么无代理模式最终可能对你选择哪种架构更有意义。代理会增加延迟。因此,当你追求极致性能时,你会希望移除任何可能的延迟源。如果你几乎只使用gRPC,那么无代理模式可能是一个不错的选择,因为我见过一些开发者选择它,因为他们不需要担心部署代理。他们可以迭代式地只对特定工作负载推出,并一次性地开启网格部署。

但这假设你不需要代理来处理非gRPC流量。如果你的应用程序混合使用gRPC和其他东西(如REST),无论如何你都需要部署和管理代理。所以这没问题。你会使用一个基于常规L7代理的网格,它与gRPC配合得很好。

根据你的需求,你仍然可以尝试将其与无代理模式混合使用,但你的网格需要支持这一点。

可选的无代理模式示例:Google Cloud Service Mesh ☁️

为了演示我所说的“可选地使用无代理模式”是什么意思,让我们看看Google的Cloud Service Mesh。顺便提一下,如果你熟悉那个名字,它以前叫做Traffic Director。

Cloud Service Mesh支持无代理模式,但你可以为每个Pod选择是否使用代理。然而,它仍然形成一个单一的网格。因此,图中的App1将使用无代理模式,但App4使用边车,但它们仍然可以直接相互通信。

我知道Istio也对无代理模式有实验性支持。gRPC确实在那里引入了一个不兼容性,但我希望这能在C++和Java中很快得到解决。

问答环节精选 💬

问: 关于版本控制,特别是当你进行版本变更时,哪种服务架构能真正扩展到数千个gRPC部署?

答: 对于模式更新,你需要服务器与旧客户端兼容,所有架构都工作得差不多,这更多是在应用逻辑层面。一个区别是,如果你想进行A/B测试或红绿部署等变体,网格有工具可以帮助。但就管理而言,哪种架构真正能扩展?一般来说,扩展性主要与节点或Pod的数量有关,客户端和服务器的版本数量可能根本不是一个重要因素。

问: 请详细说明为什么建议对gRPC重度使用无代理模式,以及在这种情况下服务网格的功能将如何实现?

答: 如果有很多重度使用gRPC的工作负载,如果你能只启动一个作业并开始为这一个服务使用无代理模式,那会非常好。你可以尝试一下,看看是否喜欢。它不需要是整个Kubernetes集群的选择。我知道其他一些网格也允许你按Pod启动,但你得到了类似的好处,可以轻松尝试,对代码做一个小改动,就可以选择并比较你是否喜欢它。gRPC将实现各种功能。是的,有可能你想要的一些功能gRPC没有实现。在那些情况下,你最终会想要使用代理。很多人想要的功能都在gRPC中。所以,如果是一些常规功能,检查一下,它可能正在做你想要的事情。如果不是,我们可能有兴趣弄清楚那个功能是什么。但是,是的,如果明天发布了一个新版本的网格,gRPC可能不支持那个新功能,而L7代理可能会支持。这是需要考虑的。所以,是的,无代理模式可能功能较少。但对很多gRPC用户来说,这不是问题。它要么对你是个问题,要么不是。有些人可能谈论使用WASM之类的东西,功能使用越复杂,gRPC不支持的可能性就越大。

问: 关于安全性,如果gRPC是无代理的,谁来管理边车?管理边车是一种开销,是你在网络或其他方面必须考虑的其他问题。那么,如果我们都回到无代理模式,为什么我还要选择边车模型呢?

答: 最初你会。在我的演讲中,有第三点:你混合了各种东西。网格出现时,你有一些遗留应用程序,或者你只是以五种不同的方式做事。很难在所有地方都获得所有功能。代理在这种情况下工作得非常好。你是在做权衡。你要么在资源成本、延迟等方面付出代价,而你可能甚至不在乎,因为你获得的价值与这些微小成本相比是如此巨大。但是,是的,如果你主要是一家gRPC商店,有很多通信,进行大量RPC,那么这就是一个不同的计算了。

问: 关于gRPC的安全性,我完全同意建立连接会很昂贵。所以这就是为什么一旦你建立了连接,你可能只做一次。无代理模式是如何工作的?你只在建立连接时进行身份验证和授权吗?

答: gRPC的行为方式与代理在那里时的行为方式类似,你尝试重用连接。关于身份验证的具体细节,你如何进行身份验证。但正常的网格代理身份验证会以相同的方式发生。正常的模式是,你在每个事务发生时都进行身份验证,因为它更像是HTTP/1.1级别。所以那是每个事务。我希望gRPC模型能让我们摆脱那种方式,对于网格来说,很多他们谈论的身份验证,你可以谈论两种身份验证:一种是服务到服务的身份验证,另一种是像用户身份验证。服务到服务的身份验证可以每个连接做一次,然后他们尝试重用这些连接。

总结 📝

本节课中,我们一起学习了服务网格的核心概念及其与gRPC的交互。我们探讨了从无网格到网关、守护进程集、边车和无代理等多种架构,理解了它们各自的优缺点和适用场景。关键点在于,gRPC基于HTTP/2的特性(如多路复用)对服务网格的负载均衡行为有显著影响。L4代理高效但功能有限,L7代理功能丰富但可能引入延迟。选择无代理模式可以避免代理开销,特别适合gRPC重度使用且对性能敏感的场景,但前提是你的应用栈相对统一且所需功能gRPC原生支持。最终,架构选择应基于你的具体需求,在功能、性能、安全性和部署复杂度之间做出权衡。

015:使用Kubernetes原生API管理平台

概述

在本节课中,我们将学习如何使用Kubernetes原生API管理平台来管理gRPC API。我们将探讨为什么需要API管理,介绍一个具体的解决方案及其架构,并通过几个实际用例演示如何部署gRPC API、应用安全策略、流量治理和请求拦截。

为什么需要API管理

在开发后端应用、B2B应用或连接消费者应用时,我们经常面临点对点连接带来的问题。API管理解决方案旨在解决这些问题,它允许你保护并暴露数字应用,提供工具来控制应用的可管理性和可观测性。无论你使用的是gRPC应用还是其他类型的应用,都需要解决这些核心问题。

公司及解决方案介绍

我们是一家拥有近20年历史的开源公司,提供API管理、集成、低代码/无代码和管理解决方案。在API管理领域,我们已有约18年的经验,是最早向市场引入API管理解决方案的公司之一。

我们的API管理解决方案提供开源版本,可供用户下载并在本地数据中心或云端定制运行。同时,我们也提供SaaS平台选项。

我们的解决方案架构区分了控制平面和数据平面。

  • 在控制平面,我们提供多种网关选项,包括我将重点讨论的支持gRPC协议的Kubernetes原生网关。
  • 我们也提供基于特定需求的传统网关。
  • 此外,我们还支持API联邦,可以管理第三方网关(如AWS、Google的网关),并正在集成更多网关类型。
  • 另一个重要部分是AI能力,我们支持通过AI进行API设计、测试、搜索和治理。

解决方案组件结构

上一节我们介绍了解决方案的概览,本节中我们来看看其核心组件结构。

我们的平台提供API生命周期治理、管理入口和出口API、API开发设计以及API货币化能力。它可以与市场上的各种工具集成,包括分析工具和可观测性工具。

在网关组件方面,我们支持多种网关类型,gRPC是我们正在集成到网关组件中的协议之一。该平台是内部B2B集成、API联邦、集成SaaS系统以及为添加API提供开发扩展的中心点。

为什么需要gRPC API管理

gRPC是一种用于内部应用间通信的高性能协议。然而,在部署这些应用时,如果你需要实施某些策略(如安全策略),可能会面临挑战。最佳设计实践要求将这些策略与业务逻辑解耦,这正是gRPC与API管理结合需要解决的问题。

以下是gRPC API管理需要解决的具体问题:

  • 安全与访问控制:需要应用不同类型的安全协议,可能涉及集成第三方身份管理工具。
  • 流量治理:需要限制流量速率,控制访问权限,并根据不同用户上下文实施策略。
  • 操作策略:需要基于负载结构对请求和响应进行头部转换、中介等操作。
  • 可观测性:需要集成可观测性工具。
  • API与版本管理:当拥有大量API时,需要妥善管理所有API及其版本。
  • B2B会话管理:需要管理B2B会话。

这些是驱动API管理解决方案需求的关键问题。

解决方案:Kubernetes原生API网关

针对上述问题,我们构建并集成了一个名为“Kubernetes网关”的API网关组件。该网关基于Kubernetes网关API规范构建。

我们利用Kubernetes的HTTPRoute等规范,并构建了适配器将这些规范转换并部署到网关上。这样做的好处是,网关可以无缝融入Kubernetes生态,实现自动扩缩容和故障负载均衡,使其成为一个真正的云原生组件。在我们的内部云解决方案中,该网关已应用于生产环境,证明了其可扩展性和高吞吐能力。

快速开始

由于本次课程时间有限,我将快速展示几个场景和gRPC用例。

首先,你可以通过我们的文档快速开始。基本要求是拥有一个Kubernetes环境。只需按照指南中的几个步骤操作,我们已通过Helm Chart打包了所有组件,安装后多个API管理组件即可启动并运行。

用例演示

接下来,我们将通过几个具体用例来演示如何使用该平台管理gRPC API。

用例1:部署gRPC API配置

在详细演示之前,需要了解部署gRPC API到Kubernetes网关的两种模式。开发者可以使用控制器,我们支持REST API和自定义资源(CRD)两种类型的控制器。

第一种方法:使用REST API部署
首先,我们部署一个gRPC后端服务,然后通过Kubernetes网关控制对该API的访问。

  1. 使用REST API方法部署API。
  2. 通过Kubernetes命令检查后端服务是否已部署。
  3. 使用grpcurl命令和gRPC配置定义。我们利用API配置生成一个名为APK配置的规范。该规范源自你的.proto文件,包含了类型、操作等信息,你还可以在其中添加后端服务等操作。
  4. 添加后端服务到APK配置,你还可以在此处添加熔断器逻辑。
  5. 使用基础路径重新定义API,并提供ID以唯一标识该API。
  6. 将API部署到Kubernetes网关。网关内置了密钥管理器,可以生成JWT令牌。
  7. 检查API部署状态,确认其已启用安全机制。
  8. 使用生成的grpcurl命令调用API。此时,API已部署并启用了内部安全(JWT令牌验证)。

第二种方法:使用自定义资源定义(CRD)部署
如果开发者更习惯使用资源定义文件,也可以采用此方法。

  1. 指向.proto文件,生成自定义资源定义。
  2. 围绕这些CRD文件应用所需的所有策略。
  3. 与第一种方法类似,复制APK配置命令,输出CRD文件。这会创建包含API、操作、路由和定义信息的文件夹。
  4. 如需应用额外策略,只需修改并部署这些文件。
  5. 检查API定义,确认其已部署。
  6. 应用JWT安全令牌,然后调用API。

以上我们介绍了两种部署方法:基于REST API的部署和基于自定义资源定义(YAML文件)的部署。

用例2:更改认证方式

当暴露一个API时,你可能希望应用不同类型的认证,甚至是自定义认证。

  1. 由于我们使用自定义资源定义,你可以为特定网关启用CRD。
  2. 定义认证方式,指定需要将安全策略应用到哪个API。
  3. 部署该认证配置。
  4. 检查安全配置,确认认证已应用。
  5. 生成令牌,调用getUser操作。
  6. 如果你想为某些内部通信禁用认证,也可以为该API启用此设置。我们演示了禁用认证并尝试调用API,结果调用失败。

用例3:配置速率限制

现在,你想在调用API时实施策略,例如限制每秒调用次数。

  1. 创建一个API规范,定义速率限制策略文件。
  2. 修改定义,指定速率策略应应用于哪个特定API。策略可以应用于资源级别、API级别或操作级别。这里我们指定应用于API类型。
  3. 将此策略应用到目标API。
  4. 等待策略部署(约5秒)。
  5. 检查API定义,确认速率限制已正确应用。
  6. 使用curl命令多次调用API以测试。当请求超过限制(例如每秒2次)时,网关会拒绝请求,并在响应头中提示强制执行的策略,客户端将暂时无法访问API,直到限制重置。

这是一个如何为gRPC API设置速率限制的快速演示。

用例4:启用头部修改

在某些场景下,你可能需要修改请求或响应的头部。

  1. 获取API定义。
  2. 在定义中指定所需的测试头部。
  3. 再次执行部署流程。
  4. 调用API时,原本需要Authorization头部,但根据新策略,现在需要改为test头部。如果仍发送Authorization头部,调用会失败。改为发送test头部后,调用成功。
  5. 这演示了如何通过规范快速启用头部修改。

用例5:实现拦截器

另一个常见需求是能够拦截gRPC调用,在请求或响应间执行自定义策略。

  1. 我们编写了一个简单的Java拦截器。
  2. 通过Kubernetes资源操作指定拦截器服务名称、镜像等信息。
  3. 在拦截器中,我们强制添加了一些规则,例如添加一个自定义头部以展示策略生效。
  4. 重新部署包含拦截器的镜像。
  5. 检查Pod状态,确认拦截器已部署。
  6. 尝试调用API。
  7. 在响应中,你会发现添加了额外的头部。这演示了如何在请求或响应间启用拦截器。

以上快速演示了利用gRPC和Kubernetes网关所能实现的部分强大功能。

总结

本节课中,我们一起学习了使用Kubernetes原生API管理平台进行gRPC API管理。我们从API管理的必要性讲起,介绍了相关的解决方案和架构,并重点演示了如何部署gRPC API、应用认证、速率限制、头部修改和拦截器等关键用例。这些功能帮助你将策略与业务逻辑解耦,更好地管理、保护和观测你的gRPC服务。

016:通过自动化优先负载削减处理流量高峰 🚀

在本节课中,我们将学习Netflix如何通过自动化、基于优先级的负载削减策略,来优雅地处理突发的流量高峰,从而在不增加服务器容量的情况下保护核心用户体验。

概述

我是Benjamin Fudorka,来自Netflix Java平台团队。我的主要工作是帮助工程师们轻松地集成他们的后端系统。我们通过提供一系列RPC框架(包括Netflix JRPC Java、Netflix Web Client、Spring WebMVC等)以及相关工具链,为JVM应用中的点对点通信提供一致的开发者体验。

Netflix JRPC Java是我们功能最丰富、最复杂的RPC框架,支撑着超过600个应用、1300项服务和1500多个客户端应用。确保这一切易于使用是我们的核心目标。今年,我们将深入探讨我们如何处理流量高峰,同时保护客户体验的能力。

为什么不能简单地自动扩容?🤔

一个自然的想法是:当流量增加时,自动增加服务器数量。这确实是我们通常的做法,我们允许集群自动扩容,并在重要内容发布前进行预扩容。

然而,单纯依赖扩容存在几个问题:

  • 成本高昂:增加服务器意味着直接增加资源成本。
  • 存在容量上限:我们可能会遇到无法添加更多节点的限制,例如控制平面达到极限或数据库连接数饱和。
  • 速度不够快:自动扩容需要时间启动新实例,而新实例在启动初期性能较差(“冷启动”问题),这使其在面对瞬时突发流量时反应不够迅速。

因此,虽然我们改进了预扩容和自动扩容技术,但并未完全依赖它们来实现我们的目标。

理解系统故障:拥堵性失败 🚧

如果不做任何处理,系统在流量高峰下最常见的故障模式是“拥堵性失败”。

想象一个系统接收的流量持续增加。起初,所有请求(绿色)都能成功处理。随着流量和系统利用率攀升,系统会到达一个临界点(利用率达到100%),此时几乎所有请求(红色)都会失败。这是最坏的情况,系统会迅速从正常工作状态过渡到完全失败,并且通常无法自行恢复,即使重启节点也需要付出代价。

核心概念:成功缓冲区与失败缓冲区 🛡️

我们可以将系统想象成一条高速公路。大多数时候,道路有充足的容量容纳所有车辆,我们预留的这部分额外容量称为成功缓冲区。它允许系统成功处理额外的请求而不产生任何失败。

当流量过大时,会发生“交通堵塞”,即拥堵性失败。一个简单的改进是:在入口匝道设置一个红绿灯,只在道路有容量时才放行车辆。我们预留的这部分用于优雅拒绝请求的容量,称为失败缓冲区。被红绿灯拦下的请求可以被“削减”,即永远不会被处理。但关键的是,仍然有一些请求能够通过,并且我们知道哪些请求被削减了,因此可以做出优雅的响应。

从测量到行动:关键指标与自动化决策 📊

要操作这个“红绿灯”,我们需要知道系统能支持多少请求。但这取决于许多因素:单节点能力、节点数量、请求成本差异、依赖的下游服务能力等。手动为每个集群确定一个静态数值既耗时又不准确。

因此,我们决定测量少数几个具体的、可客观度量的信号:

  • 节点的CPU消耗
  • 资源是否出现排队
  • 支撑服务的利用率
  • 最重要的是:每个请求的耗时(延迟)。在高速公路的类比中,这就是“你到家需要多长时间”。

挑战在于,我们需要了解每个请求的正常耗时,而它们各不相同。我们需要自动区分系统执行的不同类型的工作,并判断其延迟是否符合预期分布。过去我们在服务级别跟踪延迟分布,但现在我们为每个独立的RPC进行跟踪,这帮助我们更好地适应一天中不断变化的请求组合。

集群中的每个服务器持续分析自身性能,将原始数据标准化为一个代表系统利用率的数值。当系统利用率过高时,我们就开始拒绝请求。

演进:从平等削减到优先感知削减 ⚖️

上述技术并非全新,我们多年来一直在优化。然而,仅凭这些仍无法提供我们所需的全部容量。

我们之前的假设是:所有请求应有同等的概率被拒绝。但并非所有请求都具有相同的价值。我们可以利用这一点。

例如,当您访问Netflix网站时,网站可以预测您可能点击的内容并预取信息。这些预取请求如果出错是可以接受的。但一旦您点击了某个内容,获取该数据的请求就变得至关重要

关键在于:我们并不需要为所有请求都增加成功缓冲区,只需要为我们最关键的请求增加即可。因此,我们需要对流量进行优先级分类,实现优先感知的负载削减。

  • 无拥堵时:服务所有请求(包括预取和非关键请求),以提供更好体验。
  • 负载增长快于扩容速度时:根据优先级逐步阻止请求。
  • 拥堵非常严重时:甚至可能开始削减重要(但非最关键)的请求,以确保最关键的业务能通过。

以前我们只在边缘应用此逻辑,现在则能在调用栈的所有层级应用,使系统对意外的负载模式和调用图深处的问题反应更灵敏。

示例:四级优先级的削减策略

我们将请求优先级简化为四个等级进行说明:

  1. 利用率极低时:接受所有类型的请求。
  2. 利用率开始上升时:逐步削减较低优先级的请求。
  3. 利用率变得很高时:开始削减重要(但非最关键)的请求。我们宁愿服务部分关键流量,也不愿让系统崩溃重启。

我们为每个集群调整这些阈值,以根据其预期的请求分布定制分级响应。

进阶策略:通过去分片化创造缓冲区 🧩

几年前,我们会根据关键性对流量进行分片(即运行独立的集群)。成功缓冲区和失败缓冲区是通过保有未使用的容量来实现的,这很昂贵。

我们可以通过允许非关键请求拥有更小的缓冲区来节省成本。我们还可以针对吞吐量而非延迟来优化非关键流量分片,从而降低其成本。如果我们将这些流量去分片化(合并回主集群),那么所有这些容量现在都可用于关键流量。

这显著增加了关键调用的成功缓冲区。我们甚至可能使用更少的总体容量,因为我们现在能更高效地利用资源来服务非关键流量。此外,运营更少的集群也减轻了工程师的认知负担。

我们不需要对所有服务都进行去分片化,但这项技术是为你最关键流量增加成功缓冲区的绝佳方式。它让我们在需要时能立即获得额外的计算资源,同时在正常负载下仍能高效使用它们。

总结:组合策略的效果 ✅

本节课中,我们一起学习了Netflix处理流量高峰的组合策略:

  1. 优化现有负载削减能力:通过测量延迟和利用率,维持特定的成功与失败缓冲区。
  2. 实施优先感知削减:认识到我们不需要为所有请求增加容量,只需为最关键请求保障容量。通过削减低优先级请求,将容量让给高优先级请求。
  3. 去分片化集群:为最关键流量创造额外的成功缓冲区。

通过自动化这些决策,系统的响应速度可以快于人工操作。通过在每集群基础上应用此能力,我们允许特定功能优雅降级,而其他部分仍以其最大容量运行。

与遭受拥堵性失败的系统相比,具备优先负载削减能力的系统不会突然崩溃。它能在整个流量高峰期间持续服务基线流量。在高峰初期,系统会切换为仅服务关键流量,确保我们为已服务的请求提供最大价值。当然,如果负载超过了我们设定的失败缓冲区,服务器仍会拒绝请求,但这是一种受控的、优雅的降级,而非全面的系统崩溃。

技术实现与自动化配置 ⚙️

上一节我们介绍了策略思想,本节中我们来看看如何将其大规模落地。系统需要大量调优:每个RPC的预期延迟分布、每个集群的请求优先级分布、每个集群的CPU和数据源预期利用率水平等。手动维护这些信息是不可行的。

因此,我们设计并构建了一个系统,用于按集群管理和验证负载削减配置。该系统的工作流程如下:

以下是自动化配置管理的关键步骤:

  1. 自动分析:在正常运营期间,自动分析每个集群的历史调用延迟、调用量、请求优先级、CPU使用率等信号,结合少量专家建议,生成一个韧性配置。
  2. 实验验证:通过实验自动化平台验证每个韧性配置。实验将候选配置应用于集群,并验证其能否正确服务基线负载。然后,向实验集群施加流量高峰,确认其具备预期的成功与失败缓冲区。
  3. 自动应用:一旦验证通过,自动化系统会分析结果并将配置推广到生产集群。

我们的优先负载削减工作也改进了自动扩缩容逻辑。韧性配置中包含快速检测流量高峰和预测所需资源的信息,以便在有容量时自动扩容,满足流量需求。

架构与开源共享 🤝

从技术角度看,负载削减能力主要通过相对简单的JRPC拦截器和Envoy过滤器实现。

以下是核心组件交互:

  • 客户端:JRPC客户端拦截器为发出的请求标注其优先级。该优先级通过调用栈传播到所有下游请求。
  • 代理层:负载削减决策在边车代理Envoy中做出,依据的是客户端设置的请求优先级。
  • 服务端:对于被允许的请求,gRPC服务端拦截器监控其延迟分布,并与该特定服务或RPC的预期分布进行比较,用于计算系统利用率。实际延迟分布也会保存到指标数据库,供未来调优使用。
  • 反馈:应用程序通过边信道,使用开放标准(如ORCA)将完整的利用率数据集传回Envoy。

我们站在巨人的肩膀上,也通过博客和演讲持续与社区分享我们的进展。

本节课中,我们一起学习了Netflix如何通过自动化、基于优先级的负载削减策略,在不增加服务器容量的情况下,优雅应对流量高峰,保障核心用户体验。这套组合拳包括优化传统负载削减、实施请求优先级识别与差异化处理,以及通过去分片化策略高效利用资源,并通过全自动化的配置管理管道实现大规模落地。

017:介绍 gRPC Rust

在本节课中,我们将学习 gRPC Rust 的当前进展、面临的挑战以及 Rust 语言的基础知识。课程分为两部分:首先介绍 Rust 语言的核心特性,然后深入探讨 gRPC Rust 的实现细节和未来规划。

Rust 基础:为何它如此特别? 🦀

上一节我们概述了课程内容,本节中我们来看看 Rust 语言的基础知识。Rust 是一门近年来人气迅速增长的语言。在 2023 年和 2024 年初的 gRPC 大会上,我们收到了许多关于 gRPC Rust 支持的询问。截至 2024 年,Rust 开发者数量已达到 400 万,自 2021 年以来增长了三倍,赶上了许多流行语言。

此外,自 2016 年起,Rust 每年都被 Stack Overflow 评为最受喜爱的语言。Rust 于 2015 年首次发布,这确实表明人们对它充满热情。

从 TIOBE 指数可以看出,Rust 自首次发布以来人气确实大幅增长。

许多大公司都在使用 Rust。例如,微软正在用 Rust 重写部分 Windows 组件。亚马逊用 Rust 构建了微虚拟机 Firecracker。Meta 也在用 Rust 重写一些内部源代码控制工具。

Rust 变得如此流行主要归因于三点:安全性、性能和便利性。

Rust 是一门非常安全的语言,它在编译时强制执行内存安全。如果你使用过 C 或 C++ 等其他低级语言,可能遇到过一些难以追踪的 bug,甚至让 bug 进入了生产环境。对于 Rust,编译器会告诉你代码是否存在这些问题。它会指出代码是否存在段错误、内存泄漏或数据竞争。

Rust 通过所有权生命周期这两个概念来强制执行内存安全。

所有权是一套安全管理内存的规则。编译器检查这些规则以确保代码内存安全。基本规则是:Rust 中每个值在任意时刻有且只有一个所有者。然而,你可以通过引用来借用所有权。但使用引用也可能出现问题,因此 Rust 有一个借用检查器。它强制执行关于数据如何被访问的规则,确保你只能有一个可变引用,或者任意数量的不可变引用。

此外,Rust 中的变量具有生命周期。生命周期本质上是一个引用有效的范围。它确保引用在值被释放后不会指向垃圾数据或垃圾内存。

Rust 的总体目标是防止段错误、数据竞争和内存泄漏。

Rust 也是一门性能很高的语言。如果你使用过 Java 或其他带有垃圾回收的编程语言,你可能知道垃圾回收会导致相对较长的暂停,并且可能有些不可预测。Rust 没有这个问题。此外,使用 Rust 可以直接管理内存,这意味着程序员可以进一步优化性能。Rust 还具有零成本抽象的特性,这意味着高级代码(如函数或结构体)不会比机器级代码产生更多开销。

如前所述,内存安全检查都在编译时进行。因此在运行时,你不会产生这种开销。

Rust 也是一门非常方便的语言。我个人非常喜欢用 Rust 编码,觉得它容易得多。这得益于 Cargo 包管理器,它使得管理依赖和创建项目变得非常容易。我们今天没有时间举例,但它确实让编码变得更容易。此外,Rust 处理错误非常方便,稍后 Doug 会举例说明。Rust 还有诸如 rustfmtrust-analyzer 等工具,它们可以格式化代码,并在编译前检查代码是否存在问题。

此外,如果你遇到编译器错误,Rust 有非常详细的编译器错误信息。它会几乎准确地告诉你如何修复以及修复方法,这让我的工作轻松了许多。

为了给你一个直观对比,Rust 和 C++ 都具有零成本抽象,性能都很高。然而,Rust 能防止内存和并发错误,而 C++ 不能。Rust 内置了生命周期和所有权机制,C++ 则没有。

为何要构建 gRPC Rust? 🤔

上一节我们了解了 Rust 的优势,本节我们探讨构建 gRPC Rust 的原因。从幻灯片可以看出,实际上很多人对此抱有期望。这只是我们邮件列表中的一部分,但很多人对添加 gRPC Rust 充满热情,非常关注进展并希望了解更新。

此外,Rust 在 gRPC 用户群之外也越来越受欢迎,我们希望满足所有人的需求。

Rust 和 gRPC 的理念一致,因为它们都关心安全性、可靠性和性能,因此这确实是一个完美的组合。

gRPC Rust 的实现与挑战 ⚙️

上一节我们讨论了构建 gRPC Rust 的动机,现在 Doug 将接手,介绍 gRPC Rust 的实现背景、团队面临的挑战以及项目进展。

如果你不了解,Rust 已经有一个 gRPC 实现。它是用纯 Rust 编写的,叫做 Tonic。它可以说是事实上的标准,使用非常广泛。但它确实有一些局限性。

它基本上只实现了有线协议,而没有提供我们在大多数其他语言中提供的所有额外功能,例如负载均衡以及进行客户端健康检查的能力(该功能允许客户端检测服务器是否遇到问题并避免使用它们)。Tonic 期望你在其周围构建这些功能,它只提供 gRPC 的基本功能。

这就是 gRPC Rust 团队的切入点。我们正在 Tonic 的基础上进行构建。我们将把上一张幻灯片中提到的所有 gRPC 功能添加到我们为 gRPC Rust 提供的产品中。

我们将继承 Tonic 的一些设计目标,例如保持纯 Rust 实现,以便 Rust 用户获得最佳体验。当这个版本发布时,你可以在 crates.io 上找到它。我们已经获得了 grpc 这个 crate 名称。

实际上,我们至少在过去一年里一直与 Tonic 的维护者 Lucio Franco 合作。他去年参加了 gRPC 大会并与我进行了交谈。我们正在与 Tonic 合作,而不是创建另一个让大家纠结该用哪个的新东西。我们与他合作,本质上是为了提供一个新的东西,它将取代 Tonic。

当我说“取代”时,并不意味着你必须迁移,否则东西就无法工作。我们预计会继续维护 Tonic 并为其发布安全补丁。但它最终将停止增加新功能。我们将把整个项目上游到 CNCF 下的 gRPC 项目中。

但如果你已经在使用 Tonic,并且选择迁移到 gRPC Rust,那么你应该不会注意到任何变化,并且你将获得所有新功能。

不幸的是,它不会完全向后兼容。我稍后会谈谈原因。但我们会提供迁移指南,以便使用 Tonic 的用户可以按照基本步骤进行迁移。

我想特别感谢 Lucio。他今天不能到场。但在过去的一年里,我基本上每周都和他开会讨论这个项目。非常感谢 Lucio。

Tonic 的现状与改进方向 🔍

上一节我们介绍了项目背景,现在我想谈谈我们在推进这个项目过程中遇到的一些问题。我们正在研究 Tonic 的 API,试图弄清楚:我们能用它做什么?这真的是我们用户想要的体验吗?等等。我们确实发现了一些问题,其中一些是我们最近才注意到的。

例如,出现了一些安全问题,一些我们认为可以做得更好的微小性能问题,以及一些我们可以修复的 bug,还有一些我们想借此机会改进的可用性问题。

以下是几个例子。这是一段当前编写的 Tonic 代码,这里有一个非常微妙的安全问题。

这是一个服务实现,用于处理客户端请求。为了做到这一点,它需要调用一个内部服务作为其工作的一部分。它查看响应,检查它是否是秘密内容。如果是,则返回错误;否则,直接返回。bug 出现在这个看似无害的问号附近。

这个问号的作用是:获取调用内部服务时产生的任何错误,并将其转发给调用你的客户端。问题不仅在于错误代码可能不正确(你可能想转换它,但这允许你绕过这一点,这不是我们想要的),也不仅仅是错误消息本身可能有问题。实际上,Tonic 中的状态包含元数据,而元数据可能包含你的 API 令牌等,你肯定不希望这些泄露给客户端。因为它让这件事变得如此容易,我们认为确实需要改变并防止这种情况发生。

我们发现的另一个可以改进的问题是,Tonic 非常喜欢对请求和响应中的 protobuf 消息使用拥有所有权的消息。

作为 Tonic 用户,这非常直观且易于使用,但它确实带来了性能损失。因为你发出的每个请求都需要分配一个请求消息。对于每个响应,gRPC 也需要为响应分配一个消息。双方在完成后都会将其丢弃。因此,尽管这不是一门垃圾回收语言,但分配内存仍然有成本。如果我们能重用这些消息会更好。所以我们也在研究改进的方法。

最后,这只是我们发现 Tonic 问题的一长串清单中的一部分。最近出现的一个问题是截止时间传播。传入的请求有一个截止时间,确保你将其传播到传出的请求。正是这类问题让我们需要重新审视 Tonic 的现状。

gRPC 架构与 Rust 适配的挑战 🏗️

上一节我们讨论了 Tonic 的改进点,现在我想稍微深入幕后,看看我们 gRPC Rust 团队在将适用于当今所有语言的 gRPC 设计迁移到 Rust 时遇到了什么,以便我们能够支持之前讨论的所有功能。

在所有语言中,从高层次看,这些都是 gRPC 的基本设计挑战。我们的通道是非常异步的。它们同时处理很多事情。gRPC 通道不断创建连接,并可能每秒通过它们路由数百万个 RPC。

我在这里稍微介绍一下背景,让大家快速了解 gRPC 架构的工作原理。这将是非常高层次的,会很快。如果你跟不上,没关系。如果你对此感兴趣,我的同事 Ewar 在 4:10 有一个演讲,他会更详细地介绍这些工作原理。如果你有兴趣,可以去看看。如果你在 YouTube 上观看,也可以在我们的 YouTube 频道上找到那个视频。

那么,我们开始吧。gRPC 通道基本上是你连接到服务的典型客户端连接,是虚拟连接。那么,其中运行着哪些组件呢?有一个名称解析器组件。它负责将通道目标(例如 example.com)转换为 IP 地址列表。它不断地进行此操作,并随着 DNS 随时间变化而产生新的 IP 地址。

它还输出配置,用于配置负载均衡策略。负载均衡器,我们在 gRPC 中提供不同类型的负载均衡器。我们有 pick_firstround_robin,还有加权轮询,然后是 XDS。如果你一直在听关于代理的内容,所有的 XDS 功能主要通过负载均衡策略实现。因此,名称解析器告诉通道如何配置它。然后负载均衡器启动。它的工作是创建连接,或要求通道创建连接(我们称之为子通道)。然后它还产生一个叫做选择器的东西。选择器用于路由 RPC。

这些是独立的组件,它们都在并发运行。但当 RPC 发生时,它们会询问选择器使用哪个通道或哪个子通道,然后 RPC 通过该子通道进行。

为了让事情更复杂,增加更多复杂性,你可以链式组合负载均衡策略。当你查看 XDS 时,我们广泛地这样做。因此,不同的功能可以位于负载均衡器树中的不同位置。

所有这些事情同时发生。如果你没跟上也没关系。但所有这些事情同时发生时,我们如何保持一切有序?如何让编写这些负载均衡器变得容易?因为我们有超过 20 个负载均衡器。

我们的做法是,将所有这些插件之间的交互序列化,以便一次只发生一件事。这些操作不在 RPC 路径上,它们的性能要求稍低。事实证明,Rust 非常擅长表达这一点。

例如,如果我们看一个负载均衡策略 trait,这是一个 trait 的例子。对于非 Rust 专家,trait 类似于 Go 或 Java 中的接口。本质上,这是负载均衡策略为了接入 gRPC 而实现的东西。我们可以通过在函数方法中放入一个可变接收器来表示串行访问的概念。

这意味着,当这个函数被调用时,负载均衡策略可以可变地访问它自己的状态。因此它可以做任何想做的事情。它能够使用我们传递给它的可变通道控制器来执行操作。所以一切都很完美,对吧?Rust 很完美,我们可以很好地做到这一点。

但事实上有一个问题。当然。当负载均衡策略创建子通道时,这些子通道也是持久存在的,它们会进入连接状态,变为已连接,断开连接等等。因此,这些生命周期事件需要传递给负载均衡策略,以便它知道如何使用该子通道。

在我们所有其他语言中,这些都是通过回调传递的。在那些语言中,这些回调获得与进入负载均衡策略的其他调用相同的保证。因此一次只发生一件事。所以在其他语言中,当你通过回调被调用时,你只需假设你可以直接访问并随意改变你的状态。

但 Rust 对此并不满意。在安全的 Rust 中你不能这样做。在 Rust 中,如果你没有数据的可变引用,你就不能改变它。就是这样。

处理这个问题的唯一方法是,将你需要从回调或内联调用中访问的数据包装在一个互斥锁中。然后在你的回调中放入一个对它的引用,在你的负载均衡策略中也放入一个对它的引用。现在你就在获取锁,即使你知道你不需要,因为你是被同步调用的。我们确实找到了一个可以使用不安全逃生舱口的变通方法。但这有点丑陋。

那么我们决定怎么做呢?我们决定基本上将这些在其他语言中通过回调发生的事情,在 Rust 中变成直接的 trait 调用。这就是我们决定解决这个问题的方法。

但不幸的是,这导致了另一个问题。正如我提到的,我们有我们的架构图。你最终可能会有很多负载均衡策略,实际上这里可能有一个非常深的树。现在每一层都需要跟踪将调用转发给哪个子级,因为我们现在调用父策略,由它转发给正确的子级。所以这里有一个额外的责任。

为了解决这个问题,我们构建了一个辅助类,任何有子策略的负载均衡策略都会使用它。它将监视所有子级的操作,并为我们处理路由。

如果你没跟上,完全没关系。这些不是你作为 gRPC 用户需要担心的事情。这些只是幕后工作,让你了解我们一直在做的事情,以及如果你第一次使用 Rust 编写异步应用程序,可能会遇到的类似情况。

项目状态与未来计划 🚀

上一节我们探讨了技术挑战,现在时间不多了,但我想快速更新一下我们项目的状态。正如我所说,我们至少在过去一年里一直在做这个项目。我们在 Tonic 代码库中工作,现在进展相当迅速。我们在那里的 grpc 子目录下工作,因为那将是发布时的 crate 名称。

我们已经让基于 Google protobuf 的代码生成器工作了。目前它调用到 Tonic,但我们也完成了上一张幻灯片中展示的基本通道架构。它是完全功能性的。我们有示例来展示它,但那些示例还没有使用 protobuf 代码生成器。所以,我们接下来的任务就是将其连接起来并使其工作。

展望未来,我们目前希望达到的日期是:今年晚些时候发布测试版。如果你正在考虑在 Rust 中启动一个项目,最好等到那时再开始,因为我们认为那时会有一个相当稳定的 API 供你使用。然后我们预计在明年年初发布 1.0 版本。

很快提一下,我们有一个代码实验室。如果你想动手尝试一下 Rust 和在 Rust 中使用 gRPC,我们有一个代码实验室供你查看。今天 4:10 在 Big Maple Leaf 有活动。如果你在线观看,或者你不想或不能今天来,那么这里有一些链接,你可以用它们找到代码实验室,并在自己的时间运行它们。

看起来时间完全用完了。如果有人有任何问题,我非常兴奋能与大家交谈并了解他们的问题。如果我能在大厅里或欢乐时光见到你们,无论如何,我今天一整天都会在。请把我拉到一边。我想听听每个人的意见。

谢谢。

总结 📝

本节课中我们一起学习了 gRPC Rust 的全面介绍。我们从 Rust 语言的基础开始,了解了其因安全性、性能和便利性而流行的原因,以及所有权和生命周期等核心概念。接着,我们探讨了构建 gRPC Rust 的必要性,即满足社区需求并与 gRPC 的安全可靠理念契合。然后,我们深入了解了当前基于 Tonic 的实现、其存在的局限性以及改进方向,包括安全、性能和功能完整性。最后,我们分析了将 gRPC 异步架构适配到 Rust 时遇到的技术挑战(如状态管理的同步访问),并了解了项目的当前进展和未来发布计划。通过本课程,你对 gRPC Rust 的生态、技术细节和发展路线有了清晰的认识。

018:在gRPC生态系统中管理上下文元数据

在本节课中,我们将学习Netflix如何在其大规模、多语言、多框架的微服务架构中,管理和传播上下文元数据。我们将探讨上下文数据的定义、设计原则、传播机制以及如何确保系统的可观测性。

概述:什么是上下文数据?

上下文数据是指与具体业务请求本身无关、但用于实现跨领域功能的通用元数据。它通常是结构化的数据模型,用于增强服务能力。

以下是上下文数据的一些典型用例:

  • 故障注入测试:用于在服务中注入错误,以验证系统的故障恢复能力。
  • 自定义路由:支持基于上下文(如A/B测试、金丝雀发布、设备标识)的路由决策。
  • 增强的弹性功能:例如,请求优先级负载卸载。这允许边缘服务设置优先级,下游过载服务根据该优先级决定卸载哪些请求,确保关键请求成功。

上下文数据是双向的,它不仅向下游传播,也可以携带更新流回上游,用于实现背压缓解重试预算控制等功能。

架构演进的经验教训

在介绍当前方案前,我们先回顾从先前架构中学到的一些关键经验,这些经验塑造了我们的设计决策。

  • 明确数据所有权:了解数据的发布者、消费者和所有者对于简化支持模型和问题排查至关重要。
  • 控制负载大小:随着功能增加,上下文数据负载可能增长并超过请求头的最大限制,导致失败。必须跟踪大小并定义明确的限制。
  • 支持多语言生态:Netflix使用多种编程语言(Java, Node.js, Python, Go等)。这要求传输和序列化协议是语言无关的,并且需要提前规划标准化。
  • 聚焦“铺平的道路”:我们维护一组高度定制化的框架(称为“铺平的道路”),只支持有限的gRPC客户端和服务器实现。这降低了维护成本,但要求服务遵循这条路径,否则可能导致上下文传播中断。
  • 管理传播复杂性:确保上下文数据在服务内(包括跨线程)和服务间自由、一致地流动是复杂但必需的。

核心设计:如何管理数据?

数据管理是维护上下文系统的核心。我们确保数据所有者是架构中的一等公民。

我们使用 Protocol Buffers 来定义语言无关的数据模型。这很好地契合了gRPC生态系统,并简化了跨语言的代码生成。Protobuf还提供了前向和后向兼容性,便于数据演进。

所有数据模型都集中在一个公共注册中心,以简化一致性管理、代码生成和权限管理。

让我们看一个定义示例:

// 使用 Protobuf 定义数据模型
syntax = "proto3";

package netflix.context;

import "netflix/context/context_extensions.proto";

message LoadSheddingInfo {
  option (netflix.context.owner) = "Platform Resilience Team";
  option (netflix.context.key) = "load-shedding";
  option (netflix.context.bidirectional) = false;

  int32 request_priority = 1; // 请求优先级字段
}

在这个模型中:

  1. LoadSheddingInfo 是结构化的上下文数据。
  2. 我们使用自定义扩展(如 owner, key, bidirectional)来提供框架所需的元数据,包括所有权和上下文标识信息。

上下文传播:数据如何在服务中流动?

上下文传播是指在单个服务内,管理特定请求生命周期(如一元调用、流式调用)中的上下文数据。它确保数据可以跨线程和服务边界访问。

这类似于Go语言中的 context.Context,但我们将其抽象出来以支持REST、GraphQL等其他协议。

传播的核心依赖于我们“铺平的道路”框架中的客户端和服务器端拦截器。这些拦截器负责序列化和传输上下文。

序列化使用Protobuf将数据模型转换为二进制格式。传输则通过请求头(向下游)和响应 trailers(向上游)来完成。

以下是处理一个一元请求的简化工作流程:

  1. 客户端发布:框架(如边缘路由器)将数据(如 request_priority)设置到当前活动的上下文中。
  2. 客户端拦截:当服务发起gRPC调用时,客户端拦截器将当前上下文序列化为二进制格式,并作为元数据头附加到请求中。
  3. 服务器端拦截:下游服务的服务器端拦截器从请求头中读取二进制数据,反序列化为Protobuf模型,并为其创建一个新的上下文生命周期,关联到当前请求。
  4. 业务逻辑与更新:服务业务逻辑或框架可以消费上下文(如读取优先级以决定是否卸载负载),也可以更新上下文(如将优先级调整为更关键)。
  5. 响应返回:服务器处理完毕,响应返回。服务器拦截器会检测上下文自请求到达后的变化,但只将发生变更的字段(通过字段掩码标识)序列化,作为响应trailer发回。
  6. 客户端合并:客户端拦截器收到响应,从trailer中读取字段掩码更新,并将其合并回原始的上下文数据中,供后续调用或框架使用。

这个过程在所有服务间重复,使得数据能在整个生态系统中自由、一致地流动。

可观测性:如何确保成功?

当所有数据都在流动时,我们通过追踪和指标来观察系统并确保其成功。

  • 指标:告诉我们哪些数据、被哪些服务传播。这有助于管理数据迁移,并监控序列化后的大小,设置警报以避免超出头部限制。
  • 追踪:帮助精确定位上下文在何处丢失。我们的gRPC框架会为请求添加跨度属性,追踪在客户端和服务器级别传播了哪些键,从而可视化传播路径。

我们围绕这些数据生成仪表盘、警报和工具,以便持续监控系统的健康状况。

总结与问答回顾

本节课我们一起学习了Netflix管理gRPC上下文元数据的完整方案。

  1. 数据定义:数据所有者使用Protobuf创建和发布数据模型,并拥有其数据。
  2. 框架集成:数据所有者创建跨领域框架来发布和消费上下文。遵循“铺平的道路”确保服务能自动、一致地采用这些功能。
  3. 传播机制:gRPC拦截器通过将Protobuf模型序列化为二进制格式,并作为请求头和响应trailer传输,使上下文数据在整个生态系统中流动。
  4. 价值实现:这最终为整个请求链路提供了丰富的业务价值,包括增强的弹性、自定义路由和故障注入测试等功能。

在问答环节,我们进一步探讨了几个关键点:

  • 上下文丢失:主要源于不正确的线程处理或不遵循“铺平的道路”。我们通过追踪工具来定位丢失环节。
  • 流式调用:当前方案主要针对一元调用,对于流式调用,考虑使用包装消息体来承载上下文。
  • 冲突解决:上游数据合并存在挑战,目前策略是“最后写入获胜”,并正在探索更精细的控制(如数据应向上传播多少层)。
  • Header大小限制:通过为数据模型设置严格的大小限制和监控警报来预防。
  • 双向数据:只有被显式标记为 bidirectional=true 的上下文字段,其更新才会通过响应trailer传回上游。
  • 与协议语义的关系:上下文元数据是对协议(如状态码)的补充,而非替代。例如,重试预算信息通过上下文传播,而是否重试的决策仍由业务逻辑根据协议响应做出。

019:将 io_uring 集成到 gRPC-C++ 的经验

概述

在本节课程中,我们将学习如何将 Linux 的现代异步 I/O 接口 io_uring 集成到 gRPC-C++ 高性能网络栈中。我们将探讨其动机、实现方法、性能测试结果以及未来的优化方向。本教程旨在让初学者理解核心概念,并了解如何通过减少系统调用和提升 CPU 利用率来优化网络服务性能。


背景与动机

大家好,我是 Vi Nish,是 Google gRPC 团队的一名高级软件工程师,专注于提升 gRPC 的性能。本次分享将介绍我们将前沿的 Linux I/O API io_uring 集成到广泛用于后端服务器间通信的高性能 gRPC-C++ 栈中的经验。

gRPC-C++ 依赖一个名为 事件引擎 的内部抽象层来管理客户端与服务器之间的字节传输。这是一个用于底层网络操作的可插拔公共接口。gRPC 为事件引擎提供了不同平台特定的实现,其中 Posix 实现 是目前使用最广泛的。

在高层级上,每个事件引擎端点(对应一个 TCP 连接)会在两个阶段间交替工作:

  1. 排空阶段:反复调用 send_messagereceive_message 等操作,耗尽套接字上的可用资源。
  2. 轮询阶段:当内核报告没有更多工作可做时,如果读写字节数未达标,则进入此阶段等待更多数据可用,然后重复整个过程。

我们的目标是提升这些 TCP 端点的效率,主要关注两个问题:

  1. 能否减少系统调用次数? 现代系统调用开销显著,涉及特权级转换、内核参数验证和上下文恢复等。
  2. 如何提高 CPU 利用率? 随着工作负载消耗的内存带宽增加,在同步内存拷贝时阻塞应用会导致 CPU 资源浪费。

我们主要研究 io_uring 是否能成为这些问题的答案。


什么是 io_uring?

io_uring 是一个开源的、前沿的、Linux 特有的 API,支持异步系统调用

其核心依赖于两个内存映射的环形缓冲区:

  • 提交队列:应用程序将希望执行的系统调用描述放入此队列。
  • 完成队列:内核将已完成的系统调用结果放入此队列,应用程序从中读取以获知完成状态。

典型的工作流程如下:

  1. 用户线程获取一个 SQE,准备系统调用参数。
  2. 用户线程调用 io_uring_submit 将一批 SQE 提交给内核。
  3. 内核异步处理这些请求。
  4. 处理完成后,内核将结果作为 CQE 放入完成队列。
  5. 用户线程从完成队列中取出 CQE 并检查系统调用结果。

我们的目标是探索 gRPC 如何利用此机制,批量、异步地提交系统调用,并在它们完成时获得通知。


原型设计与实现

在我们的原型中,我们开发了一个基于 io_uring 的事件引擎,并将其性能与默认的 gRPC-C++ 栈进行了比较。

核心实现细节:

  • 我们实现了能够处理异步 send_messagereceive_message 系统调用的端点。
  • 实现基于 liburing 库,这是 io_uring 的高级 C 接口。
  • 我们为每个 CPU 核心实例化一个 io_uring 实例,因此 io_uring 的总数与系统 CPU 数量绑定。
  • 端点根据其套接字关联的 CPU 被映射到特定的 io_uring 实例上。

接下来,我们看看一个典型的系统调用提交工作流。

系统调用提交工作流

以下是使用 io_uring 端点时,提交系统调用的典型步骤:

  1. 获取 SQE:用户线程从提交队列中获取一个空闲的 SQE。注意:此步骤不涉及用户态到内核态的切换。
  2. 准备 SQE:用户设置该系统调用的参数(例如,读写缓冲区地址、大小)。
  3. 提交 SQE:用户调用 io_uring_submit 将准备好的 SQE 提交给内核。此步骤涉及一次用户态到内核态的切换。

liburing 的 API 本身不是线程安全的。因此,我们在实现中需要考虑并发控制。io_uring_submit 调用可以批量提交所有已准备的 SQE,这是减少系统调用次数的关键。

在我们的原型中,我们尝试利用原子操作来实现机会主义的系统调用批处理

  • 多个线程或端点可以并行地获取和准备 SQE。
  • 当它们都尝试提交时,我们通过原子操作选举出一个“领导者”线程。
  • 这个领导者线程执行一次 io_uring_submit 调用,提交所有线程准备好的 SQE。
  • 这样,多个系统调用被批量合并为一次提交,减少了总的系统调用次数。

系统调用完成处理

上一节我们介绍了如何提交调用,本节我们来看看如何获知调用完成。

为了获知系统调用完成,通常需要以下步骤:

  1. 注册事件通知:我们向 io_uring 注册一个 eventfd 文件描述符,并等待其变为可读状态。
  2. 读取完成项:当 eventfd 可读时,我们从内核的完成队列中读取 CQE。
  3. 处理结果:处理 CQE 以获取系统调用的结果。
  4. 归还 CQE:处理完毕后,将 CQE 归还给 io_uring,以便后续系统调用复用。

性能测试与结果分析

我们进行了一些简单的基准测试。测试环境包括同一机架内的两台机器,使用 100 Gb 网络连接。一台运行 gRPC 客户端,另一台运行服务器。

我们测试了两类工作负载:

  • 最大 QPS 基准测试:尝试生成尽可能多的数据。
  • 固定 QPS 基准测试:负载生成是固定的。

我们改变了 TCP 连接数、请求/响应负载大小等参数,并将客户端配置为使用我们的 io_uring 事件引擎。

测试结果如下:

  1. 固定低 QPS 测试:我们观察到延迟指标有轻微的性能回退。在低负载下,内存带宽限制不是主要瓶颈,因此转向异步系统调用模型带来的好处不明显。
  2. 最大 QPS 测试:我们看到了显著的性能提升。QPS 和中位数延迟都有显著改善,同时内核态 CPU 时间也大幅下降。
  3. 增加系统压力:我们进一步将每个 io_uring 实例的通道数增加到约 4000 个,以创建更多并行读写数据的端点,这增加了机会主义批处理的可能性。我们看到 QPS 和延迟趋势相似,但提交的系统调用总数减少了约 4%

结论与未来展望

本节课我们一起学习了将 io_uring 集成到 gRPC 的经验。这是首批针对像 gRPC 这样的通用网络栈进行 io_uring 实验的研究之一。

主要结论:

  • io_uring 并非现有基于 epoll 的端点的直接替代品。两者具有根本不同的编程模型:epoll 基于套接字就绪模型,而 io_uring 基于系统调用完成队列模型。
  • 当系统处于高压力状态时,io_uring 开始显现性能优势,包括更好的 CPU 利用率、更低的延迟以及系统调用总数的减少

未来,我们计划探索 io_uring 的一些高级特性:

  1. 缓冲区池:通过注册动态可调整大小的缓冲区池,内核可以从池中自动获取缓冲区来存放从套接字读取的数据,这能减少每次 TCP 读操作的内存分配总量。相关 API 如 io_uring_register_buffers
  2. 提交队列轮询模式:此模式通过让内核忙等待轮询提交队列中的新条目,可以完全消除 io_uring_submit 系统调用。但这是以增加 CPU 利用率为代价的,并非适用于所有工作负载。
  3. 系统调用链:需要按特定顺序执行的系统调用可以批量提交并链接成一个链。
  4. 多镜头系统调用:单个系统调用提交后,io_uring 可以为其发布多个完成通知。这些方法在经历高负载的高度竞争系统中能带来显著收益。

问答环节摘要

在演讲后的问答环节,讨论了一些关键点:

  • 通知机制选择:我们选择 eventfd + epoll 作为通知机制,主要是为了实现便利,便于将新的事件引擎集成到现有的、用于监控套接字的 epoll 通知机制中。我们也在积极探索 io_uring 提供的其他套接字监控方法。
  • 提交时机逻辑:当前逻辑基于最大批处理大小。多个线程并行准备 SQE,最后到达的线程负责提交所有已批处理的 SQE。没有定时器强制提交,如果流量很低,没有竞争,则会立即提交。
  • 成为默认层的可能性:虽然目前 io_uring 还不是 epoll 的完全替代品,但其提供的高级功能(如多镜头调用、缓冲区池、系统调用链)如果运用得当,无疑会使 gRPC 性能更高,未来有可能成为主要的传输层。
  • 实验环境:实验运行在 Google 的标准 Linux 内核上,没有使用 Google 特定的内核优化,所有 io_uring 功能均基于开源版本。

总结

在本节课中,我们深入探讨了将 io_uring 集成到 gRPC-C++ 以优化性能的实践经验。我们了解了 io_uring 异步 I/O 模型的核心机制,分析了其在高压力场景下减少系统调用和提升 CPU 利用率的能力,并展望了通过其高级特性进一步优化性能的未来方向。对于构建高性能网络服务的开发者而言,理解并合理利用像 io_uring 这样的底层优化技术至关重要。

020:gRPC 新功能与未来展望

在本节课中,我们将学习gRPC项目在未来一年的路线图,涵盖服务网格、可观测性和现代化三大支柱领域的一系列新功能和改进计划。

三大核心支柱

在深入具体功能之前,我们先了解驱动gRPC项目未来发展的三大主题。

  • 服务网格:自2019年起,gRPC致力于构建无需Sidecar代理的服务网格,并在2020年正式发布。这一方向将持续引领新功能的开发。
  • 可观测性:gRPC全面拥抱OpenTelemetry,致力于让用户更便捷地收集和导出日志、指标与追踪数据。
  • 现代化:为了跟上技术发展的步伐,gRPC将持续更新其支持的语言,并拥抱新兴的流行语言。

接下来,我们将基于这三大支柱,逐一介绍具体的功能规划。

服务网格增强功能

上一节我们介绍了gRPC发展的核心方向,本节中我们来看看服务网格相关的两项关键新功能。

EXT Proc 与 EXT Authz

以下是两项基于全新插件模型的服务网格功能:

  • EXT Proc:此功能用于修改服务器的入站请求和出站响应,类似于gRPC拦截器。其独特之处在于采用gRPC调用外部服务器的模型来实现功能,而非将逻辑编译进服务器。这使得平台团队能够跨整个系统统一实施策略。
  • EXT Authz:此功能用于判断发送方是否有权发起特定请求。其工作方式与EXT Proc类似,应用服务器会向预配置的gRPC服务器发起调用以获取授权决策。

这种新的插件模型为系统构建提供了前所未有的灵活性。EXT Authz功能目前仍在设计阶段。

HTTP CONNECT 隧道集成

gRPC自2017年起就支持通过HTTP CONNECT代理进行隧道传输。现在,我们正让此功能更易于使用,并与服务网格深度集成。

此前,基于HTTP CONNECT的代理需静态配置。新的计划是通过与XDS控制平面集成,实现动态配置。控制平面可以下发指令,告知客户端通过特定的HTTP CONNECT代理连接服务器。

这使用户能够轻松创建跨越本地环境和公有云的统一服务网格。该功能预计在今年晚些时候跨语言发布。

多SPIFFE信任域支持

SPIFFE是一个为工作负载分配和验证加密身份的系统。gRPC自2021年起支持通过XDS配置基于SPIFFE的mTLS,但仅限使用CA文件。

我们现在增加了对SPIFFE信任包的支持,这将允许利用多个SPIFFE信任域。

这在多种场景下非常有用,例如为开发、预发布和生产环境设置独立的信任链,或者允许公司内不同产品区域管理自己的身份,同时仍能跨信任域交互。

此功能预计今年在C++、Go和Java语言中落地。

调试与可观测性改进

在增强了服务网格的核心能力后,强大的调试和观测工具同样至关重要。本节我们将关注这方面的更新。

Channelz V2

Channelz是一个长期的gRPC调试工具,能提供进程中各种gRPC资源的详细信息。

我们现已设计了Channelz V2,它比原版更灵活。新版从底层设计就是通用的,为各个实现留出了通过Channelz导出相关独特细节的空间。同时,它允许gRPC团队在不修改Channelz协议的情况下,演进gRPC资源层次结构。

总之,Channelz V2将使gRPC应用的调试更快、更灵活。首个实现将于今年晚些时候在C++中推出。

与Istio的深度集成

gRPC Proxyless自2021年起已在Istio中提供实验性支持。我们正在进行一些改进,使其与Istio的结合更加自然。

目前,Istio集成依赖于与sdsud代理的不安全连接。我们正进行针对性更改,以完全消除对此代理的需求,使Istio和Proxyless gRPC成为更自然的组合。

OpenTelemetry指标增强

服务网格和可观测性的交叉领域同样火热。我们正在为以下组件添加OpenTelemetry指标:

  • 加权轮询负载均衡策略
  • Pick First负载均衡策略
  • XDS客户端

除了这12个新指标,我们还添加了横切标签来指示与指标关联的XDS地域信息。此外,我们最近引入了非每次调用指标的框架,这使我们能够丰富gRPC Proxyless服务网格用户的可观测性数据。

这些功能共同使得运行gRPC服务网格比以往更加容易和可靠。

高级功能与现代化

除了核心的服务网格和观测能力,gRPC也在不断引入高级功能并拥抱现代化生态。本节我们将了解速率限制、无服务器集成和AI领域的新进展。

XDS全局速率限制

这是gRPC Proxyless中一项非常令人兴奋的新能力。全局速率限制适用于服务器可能频繁被请求淹没的场景。

大多数速率限制方案都需要某种代理。借助XDS全局速率限制,gRPC服务器可以直接在gRPC库中,结合控制平面实现此功能。

该功能利用了现有的XDS协议,以及一个全新的RQS协议。RQS协议专门用于从服务器聚合负载信息,并从全局控制平面异步下发速率限制策略。

这意味着你既能获得全局速率限制决策的优势,又能享受完全去中心化数据平面的好处。这将形成一个高性能、全局感知的高流量软件管理系统。相关的协议和gRPC库实现都是开源的。

无服务器平台集成

服务网格运营商不仅运行在Kubernetes上,其组织内的团队也经常在各种无服务器平台上运行工作负载。

历史上,这两类平台之间在服务网格,特别是gRPC Proxyless服务网格方面的互操作性并不理想。我们针对Google Cloud Run设计和实现了一系列功能,使其成为一等公民的体验:

  1. 增加了XDS主机重写功能,支持在XDS目标中使用虚URL。
  2. 实现了基于JWT令牌的、针对出站服务网格RPC的授权。
  3. 支持基于SPIFFE身份的mTLS。

将这些功能结合起来,无服务器平台与Proxyless服务网格之间的集成现已达到真正的一流水平。

AI领域集成

过去一年,两个用于为AI模型丰富外部数据和能力的协议迅速流行起来:MCP和A2A。

  • MCP:使开发者能够通过MCP服务器,为LLM赋予任何工具的能力。
  • A2A:使长期运行的AI代理能够以结构化的、非英语的方式进行协作。

A2A从一开始就是基于gRPC构建的。而MCP最初仅通过JSON-RPC工作。我们收到了许多让MCP通过gRPC及其丰富功能运行的请求。因此,我们很高兴地宣布,我们将与作者合作,使gRPC成为MCP的一等传输方式。

gRPC已在AI领域确立了关键组件的地位。

官方支持的Rust语言

Rust凭借其默认安全性和零成本抽象,在过去几年变得极其流行。没有一流的gRPC支持,一门语言就不算完整。

我们正在确保这种支持到位。我们基于已经相当流行的Tonic实现进行构建,使其达到与C++、Java和Go等其他一等语言同等的水平。这意味着完整的XDS实现,以便你可以在Proxyless服务网格部署中使用gRPC Rust。

我们正与Tonic的作者直接合作,以确保为现有的Tonic用户和新的gRPC Rust用户提供最佳体验。预计很快将发布预览版。

总结

本节课中我们一起学习了gRPC在未来一年的详细路线图。我们看到了服务网格方面如EXT Proc、HTTP CONNECT集成和多信任域支持等增强功能;可观测性方面如Channelz V2和OpenTelemetry指标的改进;以及现代化方面如全局速率限制、无服务器集成、AI协议支持和全新的官方Rust语言实现。

随着gRPC加速进入第二个十年,它没有显示出任何放缓的迹象。一如既往,你可以通过我们的网站、YouTube频道、线下聚会和邮件列表关注项目进展。

021:使用基于gRPC的MCP构建生产就绪的LLM集成

概述

在本节课中,我们将学习如何利用成熟的gRPC技术栈来构建和部署生产就绪的模型上下文协议(MCP)服务器,以实现大型语言模型(LLM)与各种资源和API的集成。


MCP简介:什么是模型上下文协议?

上一节我们介绍了课程主题,本节中我们来看看MCP是什么。

如图所示,一个由AI驱动的聊天应用或IDE(称为MCP主机)通常通过API(如OpenAI)连接到LLM。MCP服务器则通过MCP协议与这个主机应用通信,并提供资源。

MCP协议支持多种传输方式,目前主要是标准I/O和HTTP流。标准I/O类似于传统的FastCGI模式。该协议定义了三种核心原语来交换数据。

以下是MCP定义的三种核心数据原语:

  1. 资源:指代一个具体的对象,如日历条目、文件、图像或数据库记录。每个资源都有一个唯一标识符(URI)。MCP主机可以通过资源API列出和读取服务器提供的资源,并将其作为上下文提供给LLM。
    • 公式/代码表示资源 -> { uri: string, content: any }
  2. 提示:用于向应用提供一组预定义的命令或操作,类似于“/”命令。用户触发操作并给出参数,MCP服务器将其转换为字符串后馈送给LLM。
  3. 工具:这是MCP调用操作的核心。工具API类似于gRPC反射,服务器可以描述一系列方法,MCP主机获取这些描述后可以调用相应的操作。这代表了API设计的一种新范式。

为什么在MCP中考虑gRPC?

了解了MCP的基本构成后,我们来看看为何要引入gRPC。

gRPC是一个经过大规模验证的、可信赖的技术栈。许多公司已经建立了完善的gRPC基础设施。在已有gRPC生态中引入新协议会增加复杂度,而直接利用gRPC则能继承其诸多优势。

以下是gRPC能为MCP带来的关键优势:

  • 性能与健壮性:gRPC拥有高性能和健壮性,是基于HTTP/2的最受测试的栈之一。
  • 核心功能:内置健康检查、截止时间传播、熔断和强类型等核心功能,这对MCP非常有益。
  • 多语言支持:无需等待特定语言的MCP SDK,可以用任何语言快速创建MCP服务器。
  • 基础设施集成:天然支持服务发现、负载均衡策略(可通过xDS控制)等大规模基础设施组件。
  • 版本控制:支持API版本控制,而当前MCP规范在此方面有所欠缺。
  • 认证与安全:可以复用gRPC成熟的认证流程,而MCP服务器设计初期常忽略此点。
  • 可观测性:可以收集统计数据和跟踪链路,监控整个基础设施的延迟。

部署模型:如何结合gRPC与MCP?

既然gRPC有这么多优势,本节我们探讨如何将两者结合,将基础设施中运行的gRPC服务暴露为MCP。

我们提出三种部署模型:

  1. 本地二进制文件:一个通过标准I/O与MCP主机通信的本地MCP服务器,但其内部通过gRPC与远程后端服务通信。
  2. gRPC到MCP网关:一个HTTP服务器,作为MCP主机与后端gRPC服务之间的翻译层。这类似于现有的gRPC网关转码解决方案。
  3. Envoy插件:在Envoy代理层面实现协议转换,这与转码器的功能非常匹配。

需要指出的是,gRPC并不限定序列化格式。你可以通过gRPC传输相同的JSON对象,从而获得之前提到的绝大部分优势。

那么,我们是否需要转码来映射MCP的请求和响应呢?这存在权衡。当前MCP规范可能仍在变化,为整个协议定义转码规范未来可能不适用。一个折中的方案是:对目前相对固定的资源和提示API进行转码,而对于工具API的输入输出,暂时保持JSON格式以获取灵活性,应对未来规范的变化。

这与通过gRPC网关暴露REST API的问题相似,但又不完全相同。首先,MCP不是REST API,更像是JSON-RPC。其次,MCP本质上是状态性的,并依赖双向流。这使得网关需要管理状态,并调用后端双向流RPC,这是主要的不同点。


实施与未来展望

我们已经讨论了部署模型,接下来看看具体的实施和社区进展。

我们已经实现了类似第二种模型(网关)的原型,并计划很快分享。如果您感兴趣,可以给我们发送邮件,我们会在可用时通知您。

关于传输协议,目前公共MCP客户端主要支持标准I/O或HTTP,而非gRPC。因此,我们描述的服务器是一个在两者之间进行翻译的HTTP服务器。未来,推动gRPC成为MCP的一等传输协议是很有价值的,因为它非常适合双向流。

社区(包括Google)正在积极推动相关工作,目标是引入真正的传输抽象层,并减少协议的默认状态性,使其在需要时才显式创建会话。这样有望实现一个一流的gRPC MCP传输方案。

在部署方面,我们目前具体实现的是中间的网关模型。最右边的Envoy插件模型尚未开始实施,但这与现有gRPC生态系统(如通过xDS统一控制面)结合会非常有趣,是我们计划探索的方向。

关于服务发现,XDS和服务发现作用于gRPC客户端层面。入站的MCP流在边缘无法直接看到XDS数据,由网关根据XDS信息将流量路由到对应的gRPC服务实例。由于状态性,网关可能需要维护与客户端HTTP流和与后端服务gRPC流的映射关系。


总结

本节课我们一起学习了如何利用gRPC构建生产就绪的MCP服务器。我们首先介绍了MCP协议及其核心原语(资源、提示、工具),然后分析了gRPC在性能、类型安全、多语言支持和基础设施集成方面为MCP带来的巨大优势。接着,我们探讨了三种结合二者的部署模型:本地二进制、HTTP网关和Envoy插件,并讨论了协议转码的权衡。最后,我们了解了相关原型实现和社区在推动gRPC成为MCP一等传输协议上的努力。通过将成熟的gRPC生态应用于新兴的MCP领域,我们可以更稳健、高效地构建LLM集成应用。

022:深入理解负载均衡机制 🚀

在本节课中,我们将要学习gRPC中的负载均衡机制。负载均衡对于运行多服务器后端和多客户端的系统至关重要,它能确保流量公平、合理地分布,从而保障服务的高可用性、可扩展性和高性能。

负载均衡概述

负载均衡的核心目标是将客户端流量公平、适当地分配到多个服务器后端。这有助于避免服务器集群中出现热点,从而保证服务的高可用性、可扩展性和高性能。

gRPC的负载均衡发生在应用层,即每个请求级别,而不是网络层的每个连接级别。

负载均衡的两种方式

上一节我们介绍了负载均衡的基本概念,本节中我们来看看实现负载均衡的两种主要方式:服务器端负载均衡和客户端负载均衡。

  • 服务器端负载均衡:客户端将所有流量发送到一个代理,由代理负责将流量分发到各个服务器后端。这种方式的优点是客户端逻辑简单,所有负载均衡逻辑集中在代理中。缺点是代理可能成为性能和可扩展性的瓶颈,且维护代理集群有额外成本。
  • 客户端负载均衡:客户端直接与服务器后端通信,消除了代理瓶颈,性能更佳。但负载均衡逻辑被移入客户端,导致客户端变得复杂,且需要在多种语言中实现相同的逻辑。

使用gRPC时,你可以免费获得客户端负载均衡能力。gRPC团队负责在多种语言的客户端中实现并维护复杂的负载均衡逻辑。

gRPC中的负载均衡策略

在gRPC中,执行负载均衡的组件被称为负载均衡策略。它由两个主要部分组成:

  1. 连接管理组件:负责从名称解析器接收后端地址,并创建和管理到这些后端的连接。
  2. 调用管理组件(Picker):在每个RPC请求时被调用,负责为该特定RPC选择一个后端。

gRPC采用插件化架构,负载均衡策略是其中之一。gRPC定义了LB策略需要实现的接口,策略实现后向gRPC注册。运行时,gRPC根据名称解析器返回的服务配置来选择具体的LB策略。

gRPC内置了多种LB策略,也允许用户引入自己的策略。

以下是gRPC支持的部分LB策略示例:

  • pick_first:按顺序连接给定的地址,直到找到一个可达的后端,之后将所有流量发送到该单个后端。
  • round_robin:并行连接到所有给定的后端地址,并尝试在所有可达的后端之间分发RPC请求。

为你的服务选择最合适的LB策略非常重要。例如,当客户端流量经过反向代理,或需要软亲和性(如特定服务器的缓存了客户端数据)时,pick_first策略很合适。在大多数其他情况下,round_robin或加权轮询策略更为合适。

gRPC客户端架构中的负载均衡

现在我们已经了解了负载均衡的高层概览,让我们深入gRPC客户端架构,看看负载均衡是如何融入其中的。

  1. 客户端应用程序创建一个gRPC通道,并传入目标服务的URI。
  2. 通道启动后,会创建一个名称解析器。
  3. 名称解析器返回地址服务配置。服务配置中包含该通道应使用的LB策略名称及其配置。
  4. 根据服务配置,gRPC通道创建LB策略,并将后端地址和LB配置传递给它。
  5. LB策略创建子通道。子通道是到后端地址的实际连接。
  6. LB策略根据子通道的连接状态,向gRPC通道发送更新。更新包含通道的总体连接状态和一个用于RPC的Picker。
  7. 当客户端发起RPC时,通道要求Picker为该RPC选择一个子通道,然后RPC通过该子通道发出。

名称解析器可以随时返回更新的地址和服务配置,子通道的连接状态也可能随时变化,这些异步事件都会触发LB策略生成新的Picker。

深入理解子通道

子通道是到特定后端地址的潜在或已建立的连接。其实现由gRPC客户端提供,但由LB策略决定何时创建及如何管理其生命周期。

子通道在其生命周期中会经历不同的连接状态,状态转换由gRPC通道通知给LB策略。

以下是子通道的状态转换流程:

  1. IDLE:所有子通道的初始状态。
  2. CONNECTING:当LB策略要求连接时进入此状态。
  3. READYTRANSIENT_FAILURE:连接尝试成功则进入READY,失败则进入TRANSIENT_FAILURE。
  4. READY状态的连接失败后,会回到IDLE状态。
  5. TRANSIENT_FAILURE状态会进行退避,退避结束后回到IDLE状态。
  6. 子通道在任何状态都可以被关闭,进入SHUTDOWN状态。

大多数LB策略会管理多个子通道,因此需要一种算法来聚合这些子通道的状态,以向通道返回一个总体状态。

以下是gRPC中许多内置LB策略使用的常见聚合算法:

if (至少有一个子通道处于 READY) {
    总体状态 = READY
} else if (至少有一个子通道处于 CONNECTING) {
    总体状态 = CONNECTING
} else if (至少有一个子通道处于 IDLE) {
    总体状态 = IDLE
} else {
    总体状态 = TRANSIENT_FAILURE
}

客户端应用程序可以通过gRPC客户端API查询此总体连接状态。

负载均衡策略API

最后,让我们来看看负载均衡策略的API。需要注意的是,该API在Java和Go中是实验性的,在C++中尚未公开。

API定义了几个核心接口:

Builder接口
每个LB策略都需要实现此接口并在初始化时向gRPC注册。

  • build():创建LB策略的新实例。
  • name():返回LB策略的注册名称。运行时,此方法返回的值需要与服务配置中指定的名称匹配,该策略才会被选中。

Balancer接口
此接口定义了LB策略需要实现的实际功能,主要响应来自名称解析器的事件。

  • updateClientConnState():当名称解析器返回新地址或服务配置时调用。LB策略可以借此关闭旧子通道、创建新子通道等。
  • resolverError():当名称解析器报告错误时由gRPC调用。
    gRPC保证此接口上的方法不会被同时调用,这简化了LB策略的实现。

ClientConn接口
此接口由gRPC客户端实现,供LB策略与gRPC通道通信。

  • newSubConn():LB策略使用此函数创建新的子通道。
  • updateState():LB策略使用此方法向通道报告新状态,包含通道的总体连接状态和用于RPC的Picker。

Picker接口
此接口用于每个RPC调用,只有一个方法pick()

  • pick():该方法接收RPC的相关信息(如方法名、头部元数据、超时设置等),并需要快速返回一个结果,不能阻塞。
    Picker可以返回四种类型的结果之一:
    1. COMPLETE:表示有一个有效的子通道可用于此RPC。
    2. QUEUE:表示当前没有活跃子通道,但正在获取中。RPC会被排队,并在LB策略生成新Picker时重试。
    3. FAIL:表示没有活跃子通道,且已耗尽所有后端地址。RPC将失败(除非使用了wait_for_ready调用选项,此时RPC会被排队重试)。
    4. DROP:与FAIL类似,但即使使用了wait_for_ready的RPC也会失败,且重试逻辑也不会再进行任何尝试。

总结

本节课中我们一起学习了gRPC的负载均衡机制。我们从负载均衡的基本概念和两种实现方式(服务器端与客户端)入手,了解了gRPC如何通过插件化的负载均衡策略在客户端实现高效的流量分发。我们深入探讨了gRPC客户端架构中负载均衡的工作流程,特别是子通道的状态管理。最后,我们概述了构建自定义负载均衡策略所需的API接口。正确理解和配置负载均衡策略,对于构建高性能、高可用的gRPC服务至关重要。

023:为高性能调整gRPC - 截止时间、批处理和心跳 🚀

在本节课中,我们将学习如何为生产环境中的高负载系统调优gRPC。我们将重点探讨三个关键的调优杠杆:截止时间、批处理和心跳机制。理解并正确配置这些设置,对于构建一个不仅“能用”,而且能在低延迟、高吞吐量和规模化场景下保持稳定的系统至关重要。

gRPC已成为高性能分布式系统的基石,它提供了强类型、高效的Protobuf序列化以及内置的流式支持等关键特性。然而,当系统在生产环境中大规模扩展时,一些挑战开始显现:延迟累积、重试行为不当、连接频繁创建和销毁等。通过调整截止时间、批处理和心跳设置,我们可以有效应对这些挑战。

截止时间 ⏱️

截止时间是gRPC最重要的配置之一,它规定了客户端在放弃请求前愿意等待的最长时间。我们可以将其视为一个“请求预算”。当客户端发起请求时,会分配一个预算(例如10秒),这个预算会在整个服务调用链中被消耗。一旦预算耗尽,调用将被取消,并返回上下文截止时间超时错误。

一个关键点是,截止时间会向下游服务传递。在一个多层服务调用链中,每一跳看到的剩余时间都会更短。如果调用链很深,最下游的服务可能几乎没有时间来完成实际工作。

配置截止时间

在客户端,我们可以设置一个截止时间。如果服务器未能在规定时间内响应,客户端将收到截止时间超时错误。

// 客户端设置1.5秒的截止时间
ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond)
defer cancel()
response, err := client.SomeRPC(ctx, request)

在服务器端,一个最佳实践是进行“快速失败”检查。如果剩余时间已经不足以完成有意义的操作,服务器应直接返回错误,而不是开始工作,以避免浪费资源。

// 服务器端检查剩余时间
if time.Until(ctx.Deadline()) < 50*time.Millisecond {
    // 剩余时间不足50毫秒,快速失败
    return status.Error(codes.DeadlineExceeded, "not enough time left")
}
// 继续处理请求

截止时间确保了系统行为的可预测性,并为整个服务调用链提供了一致的时间预算。它也与重试行为紧密相关,我们接下来会看到。

截止时间与重试的协调

当截止时间到期时,gRPC不会重试该调用,这是合理的,因为客户端已经放弃了。然而,在配置截止时间和重试策略时,需要注意以下几点:

  • 过短的截止时间与多次重试:如果客户端设置了很短的截止时间,却允许多次重试,那么所有重试最终都可能失败,这是没有意义的。
  • 过长的截止时间:如果设置了很长的截止时间,重试请求会不断堆积,给服务增加不必要的负载。
  • 截止时间不匹配:一个常见问题是上游服务设置的截止时间比下游服务长。例如,客户端设置10秒,但下游服务只强制执行2秒。这会导致上游客户端不断重试,而下游服务因时间不足无法处理,最终白白消耗CPU和内存。

处理超时精度损失与重试放大

gRPC通过一个相对时间值的头部(grpc-timeout)来传递剩余时间。这个值在穿越代理、边车、负载均衡器等中间件时,可能会因计算和舍入而损失精度。每个中间件微小的舍入误差累积起来,可能导致到达后端时剩余时间所剩无几。

解决方案

  1. 在服务间传递绝对截止时间戳,并在每一跳重新计算相对超时值。
  2. 将超时值钳制在一个安全的最小值,防止舍入误差使其变为零。

另一个问题是重试和对冲请求的放大效应。如果端到端截止时间很长,但每次尝试的超时很短,客户端会进行大量快速失败的重试,浪费资源。对冲请求(同时发送多个相同请求副本)也可能在剩余时间不足时淹没下游服务。

解决方案

  1. 为每次尝试设置合理的、充足的超时预算。
  2. 限制最大重试次数。
  3. 在启用对冲请求前,检查剩余时间是否至少大于一次请求往返的中位延迟时间。如果不够,则不应发起对冲请求。

上一节我们详细探讨了截止时间的作用及其与重试的复杂交互。接下来,我们看看如何通过批处理来提升系统效率。

批处理 📦

批处理的概念很简单:不是在每个项目到达时立即发起一个RPC请求,而是在客户端维护一个微小的队列,将多个请求捆绑在一起,一次性发送。服务器处理这个批量请求后,返回一个包含所有结果的响应。

批处理在以下两个方面带来显著收益:

  1. CPU路径优化:Protobuf编码/解码、对象分配、头部传递等操作都需要时间。批处理使得这些固定成本只需支付一次,而不是为每个请求支付,从而让CPU能更专注于实际计算工作。
  2. 网络路径优化:更少的往返次数意味着更少的请求/响应头部、更少的控制流事件。对于中间的网络设备和服务器来说,网络拥塞和 chatter 会减少,尾部延迟也会得到改善。

批处理通过在客户端引入一个微小的队列延迟作为代价,换取了整体性能的大幅提升。

批处理的应用场景

批处理对于CPU密集型工作负载尤其重要。例如,在CPU上进行机器学习推理时,与其一次推送一个样本通过模型,不如将一批样本(如16或64个)组合在一起。这样,内核可以运行向量化操作,更充分地利用计算资源,实现高吞吐量。

对于流式流量,消息通常不是均匀到达的,而是可能突发性地到来。如果每个消息都作为一个独立的RPC发送,系统可能会被大量的小调用淹没。使用微批次(micro-batching)可以创建一个时间或大小窗口来聚合消息。

实现批处理

以下是实现批处理的一个简单策略示例。我们维护一个缓冲区来收集任务,每个任务包含请求负载和一个用于返回结果的回调函数。触发批量发送可以基于两个条件:

  • 基于大小:当缓冲区的任务数量达到阈值(如64个)时触发。
  • 基于时间:设置一个定时器(如每10毫秒),时间到则发送当前缓冲区中的所有任务。

当批量请求被刷新(发送)时,我们构建一个包含所有请求的单一RPC调用。如果RPC调用失败,则批量中所有任务都失败;如果成功,则通过回调函数将各自的响应返回给每个调用者。

通过批处理,我们显著减少了系统开销。然而,要保持高性能,稳定的网络连接也至关重要。下一节,我们将讨论如何使用心跳机制来维持连接健康。

心跳机制 ❤️

心跳机制就像是系统的早期预警系统。它可以帮助我们检测网络故障、空闲连接断开、移动设备休眠等问题。同时,它对于保持网络地址转换(NAT)设备和负载均衡器的连接映射表处于活跃状态至关重要。

负载均衡器或NAT设备会维护一个客户端与服务器之间的连接映射表。表中的每个条目都有一个空闲超时(例如60秒)。如果在此期间没有数据包流动,设备会认为连接已死亡并将其从表中删除。

配置心跳

通过配置心跳,客户端会定期(例如每45秒)发送一个小的HTTP/2 PING帧,并期望收到一个ACK确认。负载均衡器会将此PING视为有效流量,从而重置其空闲计时器,保持连接映射的温暖。

在gRPC中调优心跳设置时,首要原则是使其与实际网络路径的特性对齐。

  1. 对齐超时时间:了解网络中NAT、负载均衡器或代理的空闲超时设置(例如60秒)。将客户端的心跳间隔设置为这个最短空闲超时的70%到80%(例如45秒),并设置一个合理的超时窗口。
  2. 避免惊群效应:如果成千上万的客户端都在精确的45秒发送心跳,会导致下游服务被瞬间的流量脉冲冲击。可以引入一个小的随机抖动(例如±15%),让心跳请求在时间上更加分散。
  3. 优先使用gRPC内置心跳:gRPC提供了内置的HTTP/2 PING机制,它比操作系统级别的TCP Keepalive更轻量、更精确。如果两者都用,应避免将它们设置为相同的频率,以免相互干扰。

服务器端防护措施

在服务器端,应对心跳行为实施防护措施,防止客户端滥用或错误配置。

  • 设置最小PING间隔:拒绝过于频繁的心跳请求。
  • 谨慎允许无流心跳permit-without-stream 设置应默认关闭。这意味着客户端不应该在完全空闲的连接上发送心跳。对于控制平面、长连接订阅者或移动网络等需要长期保持空闲连接的特殊情况可以例外。
  • 发送GOAWAY信号:如果客户端违反了心跳策略,服务器可以发送HTTP/2的GOAWAY帧,告知客户端连接因策略不符而关闭,客户端应使用正确设置重新连接。
  • 动态调优:在某些场景下(如手机切换网络),可以动态缩短心跳间隔,以便更快地检测到路径中断并重建连接。

总结 📝

本节课我们一起学习了三个关键的gRPC性能调优技术。

首先,我们探讨了截止时间,它作为请求的全局预算,对于协调重试行为、防止资源浪费和确保系统稳定性至关重要。关键在于设置合理的端到端和每次尝试的预算,并注意上下游服务的截止时间匹配。

其次,我们介绍了批处理,它通过将多个请求聚合发送,显著降低了CPU和网络开销,尤其适用于CPU密集型工作负载和流式数据传输场景。实现时可以采用基于大小或基于时间的触发策略。

最后,我们讲解了心跳机制,它是维持连接健康、防止NAT/负载均衡器超时断开的有效工具。配置时需要与网络基础设施的超时设置对齐,并添加抖动避免惊群效应,同时在服务器端实施必要的防护策略。

正确理解和应用截止时间、批处理和心跳,能够帮助你的gRPC服务从“可以运行”提升到“高性能、高稳定、可扩展”的生产级水准。

024:将gRPC服务作为MCP服务器暴露

概述

在本节课中,我们将学习如何将现有的gRPC服务暴露为MCP服务器,以便大型语言模型能够调用这些服务。我们将深入探讨MCP协议的工作原理、gRPC在此场景中的优势,并介绍一个名为protoc-gen-go-mcp的工具,它能自动从Protobuf定义生成MCP工具规范,从而简化集成过程。

MCP协议简介与工作原理

上一节我们介绍了课程概述,本节中我们来看看什么是MCP以及它为何有用。

MCP是一个开放协议,它标准化了应用程序如何向大型语言模型提供上下文并执行操作。标准化是MCP的核心价值之一。

大型语言模型仅了解其训练数据和当前上下文窗口中的信息。它们本身无法执行搜索网络、调用API或操作系统命令等操作。像Cursor和Claude这类应用之所以能实现这些功能,依赖于“工具调用”机制。

工具调用的本质是函数调用。其工作流程如下:

  1. 你有一个实际的功能实现,例如get_current_temperature
  2. 你有一个描述该功能的JSON Schema,定义了字段类型等信息。
  3. 你将用户提示(例如“伦敦的天气如何?”)以及所有可用工具的定义发送给LLM。
  4. LLM决定是否需要调用工具来回答问题。如果需要,它会返回一个工具调用请求。
  5. 你的应用程序执行该工具调用,并将结果返回给LLM。
  6. LLM根据工具返回的结果,生成最终的回答给用户。

这个过程可以在不使用MCP的情况下实现。MCP的价值在于它提供了一个标准化的、可插拔的客户端-服务器架构。它定义了模式、传输协议,并解决了打包和分发问题,使得SaaS供应商可以轻松地将其服务提供给LLM使用。

目前MCP支持两种传输方式:标准I/O(类似于LSP,通过生成子进程进行通信)和HTTP/1。社区也期待未来能支持gRPC传输。

gRPC在MCP集成中的优势

了解了MCP的基本原理后,我们来看看gRPC如何在这个生态中发挥作用。

将gRPC集成到MCP中有几个显著优势:

  • 保持Protobuf的权威性:Protobuf拥有庞大的生态系统和丰富的工具链。通过从Protobuf定义生成MCP规范,我们可以复用现有的API定义和工具,无需为MCP维护另一套API副本。
  • 保障API质量:高质量的API对于LLM的稳定调用至关重要。gRPC及其相关最佳实践(如Google的AIP规范)为构建结构良好、语义清晰的API提供了坚实基础,包括资源导向设计、错误处理、增量更新等。
  • 避免重复工作:自动生成工具可以节省大量手动维护MCP接口的时间和精力,实现“一次定义,多处使用”。

使用protoc-gen-go-mcp生成MCP工具

上一节我们探讨了gRPC的优势,本节中我们将介绍一个具体的实现工具。

protoc-gen-go-mcp是一个Protobuf编译器插件,它可以从gRPC服务定义自动生成MCP工具规范。目前它主要支持Go语言。

其工作原理非常简单。你只需要在Protobuf编译命令中引入这个插件。例如,对于一个简单的测试服务定义,插件会生成一个新的Go包,其中包含:

  • 为每个gRPC方法生成的MCP工具定义。
  • 一系列用于注册这些工具到MCP服务器的函数。

生成的代码风格类似于gRPC-Gateway,你需要将你的gRPC处理器与生成的MCP工具进行连接。工具包中还包含一个运行时包来处理实际的转换工作。

你可以选择两种集成方式:

  1. 进程内处理:直接将MCP服务器与你本地的gRPC处理器连接。
  2. 客户端转发:让MCP服务器作为一个客户端,将接收到的请求转发给远程的gRPC服务器(甚至可以通过Connect-RPC)。

在你的应用程序主函数中,你只需要启动MCP服务器,注册生成的工具,然后启动服务即可。所有的协议转换都由生成的代码和运行时库自动完成。

注意事项与LLM兼容性

成功生成工具后,我们还需要注意一些实践中的挑战,特别是与不同LLM的兼容性问题。

虽然LLM的API大体相似,但在JSON Schema的支持细节上存在差异,这可能导致问题:

  • oneOf/anyOf支持:Claude几乎支持所有JSON Schema特性,但OpenAI的API不完全支持oneOf。我们通常通过添加提示性注释(如“请只选择其中一项”)作为变通方案。
  • 必填字段:OpenAI要求所有字段都必须标记为required。为了表示可选字段,我们需要将类型定义为该类型与null的联合类型。
  • 动态映射:OpenAI不支持map类型。替代方案是使用键值对数组。
  • 递归模式与嵌套深度:复杂的递归模式可能不被支持,有时需要回退到字符串类型。OpenAI对嵌套深度也有限制(最多6层),对于更深的嵌套结构需要截断处理。

最关键的一点是:提供丰富的错误信息至关重要。 LLM对低质量的API容忍度更低,容易产生困惑或错误行为。务必遵循gRPC最佳实践,进行严格的输入验证,并充分利用gRPC的错误详情机制来提供清晰的错误反馈。

未来展望

最后,让我们看看这个领域还有哪些可以改进和发展的方向。

protoc-gen-go-mcp工具和整个gRPC-MCP集成方案仍在演进中,未来可能的工作包括:

  • 集成Proto Validate:将Protobuf验证规则(如正则表达式约束)提升并转换到生成的JSON Schema中。
  • 支持自定义选项:允许开发者通过Protobuf自定义选项来跳过某些RPC、指定自定义的工具名称或描述,以提供更灵活的集成控制。
  • 直接gRPC工具调用:探索不经过MCP协议,直接在AI代理等场景中进行gRPC工具调用的可能性,这可以减少一层间接性。
  • 拦截器支持:解决在通过转发模式调用远程处理器时,gRPC拦截器无法生效的问题。

总结

本节课中我们一起学习了如何将gRPC服务暴露为MCP服务器。我们首先了解了MCP协议如何通过标准化工具调用来扩展LLM的能力,然后探讨了利用现有gRPC API和Protobuf生态来构建高质量MCP集成的优势。通过protoc-gen-go-mcp工具,我们可以自动从Protobuf定义生成MCP规范,极大地简化了集成工作。同时,我们也注意到了在不同LLM提供商之间保持兼容性所面临的挑战,并强调了提供高质量API和错误处理的重要性。随着工具和协议的不断成熟,gRPC与MCP的结合将为构建强大的、由AI驱动的应用接口提供坚实的基础。

025:全面指南

在本教程中,我们将系统性地学习gRPC在过去一年中在可观测性方面取得的最新进展。我们将重点介绍与OpenTelemetry的深度集成,并概览生态系统中的其他关键工具,帮助你构建更透明、更易维护的分布式系统。

与OpenTelemetry的集成

上一节我们概述了课程内容,本节中我们来看看gRPC与OpenTelemetry集成的核心。OpenTelemetry是一个开源的观测性框架,是OpenCensus和OpenTracing的继任者。

通过手动修改代码配置插件,你可以将所有数据,包括深入的gRPC状态,导出到任何支持OpenTelemetry导出器的后端。例如,如果你使用Google Cloud,可以设置Google Cloud Monitoring或Tracing导出器来展示所有观测性数据。

熟悉OpenTelemetry的人会知道,OpenTelemetry有自己的RPC语义约定。当我们开始支持gRPC的OpenTelemetry时,维护者认为这些约定过于通用,不够具体。任何RPC系统都可能有其独特的细微差别,这些理想情况下需要通过指标来捕获。为了满足gRPC的需求,我们需要能够定义适用于gRPC的指标和追踪。因此,我们使用了gRFC(gRPC的RFC版本)来定义gRPC的OpenTelemetry插件将如何工作,并且已经实现了一系列指标和追踪。我们仍在与OpenTelemetry社区持续合作,并探讨gRPC帮助开发OpenTelemetry RPC语义约定的可能性。

这样做的考虑是,我们或许能够为RPC系统提供一些开箱即用的观测性,因此未来可能会有某种兼容性方案。

新增功能:OpenTelemetry追踪

现在,让我们开始介绍新内容:OpenTelemetry追踪。我们目前还没有用户友好的指南,但gRFC已可供感兴趣的人查阅。

简而言之,这为我们提供了大量关于请求在分布式系统环境中生命周期内发生的信息。通常,会采样少量请求(例如万分之一到十万分之一)。对于每个采样请求,追踪帮助你获得一个树状结构,其中包含代表独立任务的各个跨度。例如,一个用户请求到达服务器,服务器进一步向其他服务发出请求,你将能够在同一追踪中看到新的RPC,以及显示事件发生时间的时间图。

此前,我们已批准了用于OpenTelemetry追踪的gRFC。在过去的一年里,我们已在Java、C++和Go中实现了它,但它目前仍处于实验阶段。我们正在努力完善细节,进行跨语言互操作性测试,以确保其按预期工作,然后才会宣布其稳定,希望很快能实现。

我们现在还在C++中实现了TCP追踪。除了之前的出站消息事件外,我们现在还能看到四个事件,它们显示TCP数据包何时传递给内核、内核何时调度发送消息系统调用、数据包实际发送的时间以及被确认的时间。此外,我们还获得了一些统计数据,例如:

  • 交付速率
  • 最小RTT
  • 重传
  • 拥塞

我们通过随发送消息系统调用向下发送控制消息来实现这一点,以告知内核我们对这些时间戳感兴趣。

我们在Google内部的调试工作中非常成功地使用了此功能,以确定是否遇到网络问题,或者反过来,排除网络故障作为高延迟的原因。目前,实现此功能的机制仅在Linux内核上的C++中可用,但未来我们或许能够将其添加到其他语言中。

深入指标世界

上一节我们介绍了追踪,本节中我们来看看指标的世界。与gRPC中的许多事物一样,指标也是一个不断发展的领域。

但在讨论“是什么”之前,让我们先谈谈“为什么”。我们为什么首先要关心指标?可以这样想:指标是我们的服务彼此交流的方式。它们是理解gRPC应用程序健康状况和性能的基础。它们为我们提供了监控和优化客户端与服务器之间实际发生情况所需的可见性,帮助我们定位性能瓶颈。这也充当了一个早期预警系统,允许我们通过跟踪延迟和错误率等指标,在问题影响服务之前主动发现问题。归根结底,使用指标是构建更好、更可扩展、更可靠、真正值得信赖的应用程序的基石。

今天,我们将尝试分享gRPC中通过OpenTelemetry可用的指标的全貌。

以下是现有的一些客户端指标。当你查看本幻灯片上的客户端指标时,可能会发现两个看起来很相似的指标:“每次尝试”和“每次调用”。问它们之间的区别是什么是一个很好的问题。

答案实际上突显了我们选择引入自己的指标语义,而不是使用标准OpenTelemetry语义的关键原因。为了理解原因,我们来分解一下。在gRPC中,你在客户端应用程序中进行的单次调用实际上可能导致向服务器进行多次尝试,尤其是在使用重试或对冲等功能时。这引出了一个自然的问题:为什么服务器没有“每次尝试”的指标?原因是,“尝试”这个概念本质上是客户端的故事。客户端是包含“哎呀,失败了,让我再试一次”逻辑的组件。从服务器的角度来看,这些尝试中的每一次,即使是重试,看起来都像是一个全新的独立请求。服务器的工作只是处理它收到的每个传入请求,因此其指标旨在反映这种“每次请求”的现实。

新增指标详解

现在,让我们转换话题,谈谈我们一直在添加的一些新指标。我们一直在忙于推出一系列新指标,以便更清晰地展示你的gRPC应用程序正在做什么。

首先是关于重试和对冲的指标。如果你以前使用过OpenCensus,这可能会看起来很熟悉。我们引入了一些强大的功能,现在你可以在新的OpenTelemetry实现中跟踪重试、对冲甚至它们之间的延迟。

我们上次提到了WRR和xDS,我很高兴地说,WRR指标已在核心、Java和Go中实现,而xDS指标已在C++和Java中实现。

接下来是新的子通道指标。这是连接可见性方面的一大改进。与之前被替代的“Pick First”指标相比,旧的指标有些令人困惑,因为即使没有明确配置为负载均衡策略,子通道指标也会显示为“Pick First”。此外,使用那些指标无法获取实际的断开连接错误。现在,你可以判断断开连接的原因,例如套接字错误、连接超时等,这为调试提供了更清晰的视图。

现在,我想重点介绍两个我特别兴奋的新增功能。

第一是这些异常检测指标。这是我们Dropbox的朋友们做出的一个很棒的贡献。它是社区如何让gRPC可观测性对每个人变得更好的完美范例。所以,向他们致以巨大的感谢。

第二是这个新的可选标签:grpclb_backend_service。那么它具体是做什么的呢?它保存了你正在调用的后端服务的名称。如果你有一个客户端与许多不同的后端通信,这个标签就是救星。它让你可以轻松地按服务切分和深入分析指标,从而准确查看每个服务的情况。

需要快速提醒的是,我们正在努力将这些新指标引入到所有gRPC语言中。但它们可能还没有全部到位。我们正在逐步推出它们,因此最好查看你所用语言的文档,以了解当前可用的内容。

指标的未来展望

那么,指标的下一个发展方向是什么?我们想要更深入,直达传输层本身。我们正在为一些新的TCP级别指标制定提案,这些指标将帮助你理解网络上实际发生的情况。目前,如果你遇到棘手的网络问题,可能会感觉有点像黑盒。这些新指标旨在照亮那里。例如,我们计划添加诸如:

  • TCP RTT:为你提供网络延迟的最佳情况视图。
  • TCP 交付速率:显示你获得的实际数据吞吐量。

而这里才是真正强大的地方:用于数据包、重传数据包甚至虚假重传的指标。这将为你提供数据包级别发生情况的超级详细细分,用于诊断那些间歇性的、难以发现的网络问题。

现在我想明确一点:这些仍处于提案阶段,所以你还不能直接使用它们。可以将此视为对我们前进方向的一次预览。

生态系统中的补充工具

以上是我们过去一年在OpenTelemetry方面所做的所有工作。现在,让我们看看生态系统中其他一些工具,以补充我们从指标和追踪中获得的观测性。

让我们来谈谈一个非常方便的工具:gRPC二进制日志记录。本质上,它是一个允许你以二进制格式记录RPC的功能。这非常有用,有几个关键原因。

首先,它对于故障排除非常有用。当你试图调试一个棘手的问题时,这些日志为你提供了请求、响应和状态的完美记录,帮助你找到问题的根本原因。你还可以通过分析生产环境中的真实流量模式,将这些日志用于负载测试,以查看你的服务表现如何。

但你能做的最强大的事情之一是重放RPC。想象一下,从生产环境捕获日志,然后在开发环境中重放完全相同的操作序列。这是重现和修复错误的绝佳方式。

设置它非常简单。你只需要设置一个环境变量,通常是类似 GRPC_BINARY_LOG_CONFIG 的东西,具体取决于你使用的语言。在该配置中,你可以非常具体地告诉它要记录哪些服务和方法,甚至可以设置消息大小的限制。

现在,你可能会想到安全性。这是一个很好的问题。我们设计二进制日志记录时就考虑了安全性。日志格式有意将元数据(如头部)与实际消息负载分开。这使得设置过滤器来控制记录的内容变得更加容易,从而避免意外泄露密码或加密密钥等敏感数据。

接下来,让我们看看社区提供的一个很棒的工具:grpcurl。简单来说,grpcurl 是一个命令行工具,让你可以直接与gRPC服务器交互。这个名字几乎就说明了它的用途:它是用于gRPC的curl。这意味着你可以直接从终端发送gRPC请求并获取响应。

我想提一下,虽然 grpcurl 不是由核心gRPC团队官方维护的工具,但它对你的开发工作流非常有用。

首先,它能极大地加速测试和调试。你可以快速向端点发送一个RPC,并查看确切的服务器响应,而无需编写任何客户端代码。它对于API探索也非常棒。如果你的服务器启用了反射,你可以直接将 grpcurl 指向它并询问:“嘿,你有什么服务和方法?”它甚至会显示请求和响应的确切模式。最后,由于它是一个命令行工具,它非常适合脚本编写。你可以轻松地将其集成到shell脚本中,用于自动化测试或健康检查等任务。

管理服务与调试工具

接下来是Channelz。我假设你们中的许多人已经熟悉这个工具。它包含两个服务:Channelz和CSDS。这两个服务都可以添加到服务器中,如屏幕上的示例所示。一旦这些服务开始运行,你就可以像往常一样使用RPC调用它们。

具体来说,Channelz提供关于通道、子通道、服务器套接字等信息。它会回答诸如“我的通道当前状态如何?”、“给我我的通道或子通道上的所有最近事件”或“如果我的RPC调用失败,特定子通道是否出了问题?”等问题。

此外,为了简化操作,作为gRPC生态系统的一部分,我们有一个辅助UI工具,可以为你获取数据并以易于理解的格式呈现数据。这些图像来自上面链接的博客本身,这是一份由Eshaan编写的非常有用的指南,展示了如何使用Channelz和UI工具。

我们还有一个进行中的Channelz V2的gRFC,Richard之前也提到过。其目标是使Channelz更加通用,以便各种节点之间的关系更加松散,并且我们可以记录更多类型,而不仅仅是服务器、通道和套接字。但请注意,这仍在进行中,所以请关注gRFC的进展。

我们想提到的下一个工具是 grpcdebug,这是一个命令行实用程序,允许你查询正在运行gRPC的进程。它的工作方式是充当一个gRPC客户端,查询由gRPC应用程序公开的服务。它支持的命令包括Channelz(如果你无法使用UI,可以使用这个)、健康检查(使用健康检查服务,因此你可以检查服务器是否正在服务)以及XDS(使用前面提到的CSDS或客户端状态发现服务)。因此,如果你有一个支持XDS的gRPC应用程序正在运行,你可以使用它来检查各种XDS资源的状态并获取配置的转储。

总结与展望

以上是目前我们在生态系统中拥有的所有工具。我们认为它们还没有达到最终形态。随着我们继续使用gRPC,我们会遇到不足之处,并尝试改进可观测性以解决这些问题。我们非常高兴能够与社区互动,获取反馈和想法。

作为对所讨论内容的快速总结,这是我们短期(想象几个季度内)要完成的路线图:

  • 附加指标:包括实现前面讨论的TCP级别指标,并将新的子通道指标和异常检测指标推广到更多语言,包括C++、Go和Java。
  • 追踪稳定化:完成跨语言互操作性测试等工作,使追踪功能稳定。
  • 延迟分析工具:这是一个在C++(以及Go)中的性能分析工具,帮助你可视化和分析gRPC程序的延迟,并以被诸如Perfetto等工具识别的格式输出数据。这已经在一定程度上实现了,但目前获取数据的方式对用户还不够友好,因此这方面的工作正在进行中。

在本教程中,我们一起学习了gRPC可观测性的最新进展。我们从与OpenTelemetry的深度集成开始,涵盖了新的追踪功能、丰富的指标集(包括客户端尝试、子通道、异常检测等),以及像二进制日志、grpcurl、Channelz和grpcdebug这样的生态系统工具。这些工具共同为你提供了监控、调试和优化gRPC应用程序所需的全面视角。请记住,这是一个持续发展的领域,社区反馈对于塑造未来功能至关重要。

026:Reddit如何扩展弹性服务发现

在本节课中,我们将学习Reddit如何构建一个不依赖传统代理的、基于XDS(通用数据平面API)的现代服务发现系统。我们将探讨其架构决策、核心组件以及如何实现平滑迁移。

概述

Reddit是一个大型网站,用户在此聚集交流、搜索社区并寻找他们关心的内容。为了支撑如此庞大的规模,Reddit的技术栈广泛使用了Kubernetes和gRPC,每天处理数百亿次请求。

随着“World Reddit”项目的推进,Reddit需要在全球范围内建立更多集群和区域,这使得原有的、基于Kubernetes DNS的简单服务发现抽象开始变得难以维护。服务所有者需要手动管理跨集群调用,这带来了巨大的复杂性和操作负担。

从Kubernetes服务发现开始

在深入探讨XDS服务发现之前,我们先回顾一下Kubernetes原生的服务发现机制是如何工作的。

在Kubernetes中,Pod是计算的基本单位,由Deployment管理。Service为这些Pod提供网络抽象和负载均衡。Kubernetes通过其DNS解析器为服务提供DNS地址。例如,运行在service-foo命名空间中的服务,其DNS地址为service-foo.namespace.svc.cluster.local

根据服务的配置,DNS解析可能返回服务的集群IP(一个虚拟IP),也可能直接返回后端Pod的IP列表(无头服务)。

公式示例:Kubernetes服务DNS解析

# 服务DNS记录
<service-name>.<namespace>.svc.cluster.local

# 解析结果示例(集群IP模式)
-> 10.96.123.456

# 解析结果示例(无头服务模式)
-> 10.244.1.10
-> 10.244.2.20
-> 10.244.3.30

这种机制在初期简单易用。但随着Reddit规模扩大和集群数量增多,其局限性开始显现:服务调用方需要明确知道目标服务是在同一集群内(使用集群本地DNS)还是在不同集群(需要使用其他集群的地址)。随着基础设施的演进和服务迁移,这种手动管理变得异常复杂。

此外,为了优化利用率和性能,服务的客户端和服务器端还需要手动管理许多细节,例如无头服务、Pod生命周期等。不同的服务可能有不同的负载均衡策略需求,而我们不希望为此修改所有客户端代码。

架构决策:代理 vs. 无代理

为了解决上述问题,我们首先需要决定是采用代理方案还是无代理方案。以下是我们的决策考量,这些标准也适用于其他类似场景。

如果采用代理方案(例如使用Envoy),其优势在于对gRPC的良好支持,并且能够支持更多种类的协议。然而,代理方案也存在显著挑战:

  1. 语言支持成本高:如果Reddit未来想使用小众编程语言,需要为其提供C-core库支持,成本较高。而进程外代理可以支持所有语言。
  2. 隔离性:在大型系统中,隔离性是重要考量。无代理方案中,应用运行在自己的进程中,问题易于排查。引入代理(无论是节点级还是边车代理)会引入一个共享资源,多个应用可能竞争同一代理资源。
  3. 开销:无代理方案开销极低。引入代理意味着需要运行多个代理实例,这会消耗大量计算资源。
  4. 运维成本:应用开发者习惯于查看自己应用的日志和指标。无代理方案对此非常直观。引入代理后,基础设施团队需要额外构建仪表盘和运维流程来区分问题是出在代理还是上游服务,这带来了很高的运维成本。

基于以上权衡,我们最终选择了构建一个无代理(Proxy-less)的解决方案,将服务发现逻辑直接集成到gRPC客户端中。

核心架构:控制平面

我们构建的控制平面非常简单,包含三个核心组件。其中,执行服务发现的XDS控制平面是核心,但它的实现相对标准,已有许多开源方案。因此,我们将重点介绍另外两个更具创新性的组件:验证准入Webhook配置映射注入器

以下是控制平面的架构图,它清晰地展示了各组件的关系:

1. XDS控制平面

这是一个简单的Kubernetes控制器,它监听来自Gateway API项目的gRPC路由(GRPCRoute)、端点切片(EndpointSlice)和服务(Service)资源。控制器根据这些信息创建XDS配置快照(Snapshot),并通过XDS协议将其发布给gRPC客户端。

代码示例:控制器核心逻辑(概念性)

// 伪代码,展示控制器如何监听资源并生成快照
for {
    select {
    case event := <-grpcRouteWatcher:
        updateRouteConfig(event)
    case event := <-endpointSliceWatcher:
        updateEndpointConfig(event)
    case event := <-serviceWatcher:
        updateClusterConfig(event)
    }
    // 生成新的XDS快照
    snapshot := generateSnapshot()
    // 将快照分发给所有连接的客户端
    xdsServer.pushSnapshotToClients(snapshot)
}

2. 配置映射注入器

使用gRPC的XDS实现时,客户端需要一个引导文件(bootstrap.json)来配置初始的XDS服务器地址。这带来了一个挑战:我们希望能够在不修改客户端代码的情况下,动态改变客户端连接的XDS服务器地址。例如,当某个客户端变得非常庞大时,我们可能希望将其隔离到专用的控制平面实例(惩罚箱),或者避免单点故障。

为了实现这个目标,我们构建了名为配置映射注入器的控制器。它针对Kubernetes命名空间工作,自动创建一个包含引导文件的ConfigMap。这个引导文件由控制器动态生成,因为它可以访问Kubernetes控制平面,从而智能地填充所需值(如XDS服务器地址)。

应用Pod只需要加载这个ConfigMap即可。这为平台团队提供了独立运营的能力,并且即使注入器出现故障,ConfigMap仍然存在,客户端可以回退到一个已知可用的静态配置。

3. 验证准入Webhook

在Kubernetes世界中,验证准入Webhook通常被API服务器用来提供更好的用户体验:如果用户即将创建的资源被控制平面视为无效,Webhook会拒绝该请求。

我们除此之外,还创新性地利用验证准入Webhook来保护我们的控制平面免受“死亡查询”攻击。试想,如果一个聪明的用户创建了一个我们未曾预料到、无法正确解析的GRPCRoute资源,这可能会导致我们的控制平面崩溃(段错误)。

我们的验证准入Webhook运行着与控制平面相同的逻辑,并回答相同的问题:“我能否使用这个GRPCRoute来构建一个有效的XDS快照?” 如果不能,或者这个操作因为我们的bug而导致Webhook崩溃,那么用户将无法再创建新的配置,但关键的控制平面服务仍然保持运行。这为我们的控制平面提供了一个非常优雅的保护机制。

实现平滑迁移与可观测性

构建系统是相对容易的部分,而如何让业务方更容易地采纳和使用新系统才是真正的挑战。以下是我们在提升可观测性和推动迁移方面所做的关键工作。

客户端可观测性

我们非常感谢gRPC Go社区,他们提供了我们所需的大部分指标。我们最终构建并暴露了gRPC客户端指标,这对于了解客户端视角下的控制平面状态非常有用。

此外,我们使用自定义指标和链路追踪来更好地理解数据路径。随着网络对应用开发者变得更加透明(他们无需关心服务在哪个集群),我们提供了更强的可观测性,以便他们在调试时能够清晰地了解数据实际经过的路径。

管理端点

我们利用了客户端发现服务(CDS)的管理端点。这允许我们连接到任何客户端,查询其当前的XDS配置状态。同时,我们在控制平面也提供了类似的管理端点,用于展示其当前感知的全局状态。这在调试配置错误时是终极真相来源。

采样客户端:渐进式迁移与回滚

本次迁移的一个主要目标是渐进式易于回滚。为此,我们构建了一个采样客户端,允许任何gRPC调用选择是使用我们构建的XDS寻址,还是回退到默认的DNS解析。

采样策略看似简单,但需要根据底层gRPC实现和服务的规模进行权衡。主要有两种方式:

  1. 按RPC采样:每个RPC调用可以独立选择走XDS路径还是DNS路径。优点是控制粒度细,回滚可以瞬间完成。缺点是开销大,因为客户端需要维护两套连接池,意味着双倍的连接开销,对于大型服务成本很高。
  2. 按连接采样:在建立连接时决定是建立XDS连接还是DNS连接。缺点是,如果需要回滚(比如将采样率调为0),可能需要重启Pod,因为连接通常在启动时建立并长期复用。这种方式开销较小。

下图展示了采样策略的权衡:

性能优化与问答精粹

在系统运行过程中,我们针对性能和一些技术细节进行了优化,并在社区问答中分享了经验。

增量更新与资源隔离

gRPC目前仅支持全量状态推送,而不支持增量更新(Delta XDS)。虽然我们很希望实现增量更新(因为它可能更简单),但当前的全量模式工作良好。

为了提升效率,我们在控制平面侧将端点(Endpoints)的配置快照与其他资源(如路由和集群)的快照分开。因为端点的变化非常频繁,而其他资源相对稳定。这样,我们可以更高效地管理更新。

更新抖动控制

为了避免在端点发生剧烈变化时“轰炸”客户端,我们实施了更新批处理。控制平面有一个收集窗口(例如5-10秒),在此期间收集端点的更新,然后一次性发送给客户端。这个值目前是手动选取的,与我们之前DNS缓存的TTL设置保持一致,未来可能会根据实际情况调整。

总结

在本节课中,我们一起学习了Reddit如何构建一个无代理的、基于XDS的现代服务发现系统。我们从Kubernetes原生服务发现的局限性出发,探讨了代理与无代理架构的权衡,并最终详细介绍了Reddit自研控制平面的三大核心组件:XDS控制平面、配置映射注入器和验证准入Webhook。我们还了解了如何通过增强可观测性、提供管理接口和实现采样客户端来推动系统的平滑迁移与安全运维。这套架构帮助Reddit在全球化扩展中,实现了更弹性、更透明和更易管理的服务通信。

posted @ 2026-03-29 09:15  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报