阅读视图

发现新文章,点击刷新页面。
🔲 ☆

GPU 计算的起源

本文永久链接 – https://tonybai.com/2026/04/17/the-origins-of-gpu-computing

大家好,我是Tony Bai。

在今天的人工智能时代,GPU 已成为数据中心的核心算力引擎,但它的崛起并非一夜之间的奇迹。ACM通讯文章《The Origins of GPU Computing》回溯了 GPU 计算的三十年发展史,揭示了从并行计算、图形系统到流处理等关键技术如何在政府资助的学术研究中逐步成熟,并最终汇聚成推动深度学习革命的基础设施。文章不仅梳理了技术脉络,也展示了学界与产业之间如何通过人才与思想的流动,共同塑造了现代 GPU 计算的格局。

本文是这篇文章的译文,供大家学习参考(格式有调整,更适合公众号阅读)。


政府资助的并行计算、流处理、实时着色语言和可编程图形处理单元(GPU)的学术研究直接推动了 GPU 计算的发展。GPU 被广泛应用于现代数据中心,并促成了当前的人工智能(AI)革命。生产 GPU 的英伟达(Nvidia)现已成为世界上最有价值的公司。这种计算变革及其产生的经济价值,得益于超过 30 年的政府资助研究。政府资助不仅有助于发展许多关键的技术创新,还培养了大量将这些技术带入行业的学生。

本文追溯了 GPU 计算的起源。我们首先描述了 GPU 计算所构建的技术(并行计算、并行图形系统、可编程着色器(shaders)和流处理)的发展,然后详细介绍了这些技术是如何转移到英伟达和其他公司,并最终应用于现代机器学习的。

赋能技术

GPU 计算建立在并行计算、并行图形系统和流处理的早期工作基础上。这些技术是通过超过 30 年的政府资助学术研究发展而来的。

并行计算

当你学习计算时,你了解到的是中央处理器(CPU)按顺序执行一系列指令。

实际上,芯片包含数十亿个并行切换并由导线连接的晶体管。开关和导线是物理计算机的基本构建块,它们同时运行。

此外,晶体管切换消耗的能量很少,而沿导线的通信消耗的能量要多得多。

通信需要功率来将信号从一点发送到另一点;功率随着距离的增加而增加,如果是在芯片之间进行信号传输,功率消耗将非常巨大。

虽然顺序计算机可能比并行计算机更容易理解,但顺序计算机必须通过同时切换的晶体管和同时传输信息的导线来实现。顺序计算机使用许多晶体管并行计算结果,然后仔细地以与顺序执行一致的方式组装这些结果。

创建这种执行是顺序的“幻觉”,在功率和性能上都是低效的。随着可用晶体管数量的增加,这种低效性也随之增加。在现代半导体技术中构建计算机的自然方式是设计并行计算机。GPU 比 CPU 更高效,因为它们是大规模并行计算机。

GPU 计算建立在并行计算的早期工作之上。与所有并行计算机一样,在 GPU 上运行的并行任务或线程必须相互同步和通信。

线程需要通信来使用由另一个线程产生的数据。同步是必要的,以在数据可用时发出信号,确保消耗的是正确的值。

并行计算、同步和通信的许多基础知识是由政府资助的学术研究开发的。由加州理工学院 Chuck Seitz 领导的 DARPA 资助的“宇宙立方”(Cosmic Cube)项目发展了并行计算的许多基础知识。在该项目上开发的硬件是英特尔 iPSC、Delta 和 Paragon 机器的蓝图,以及几台早期的能源部 ASCI 机器。“Cosmic-C”编程语言引入了异步消息传递和集合通信,后来以消息传递接口(MPI)的形式成为编程大型并行机器的标准。

麻省理工学院(MIT)的 DARPA 资助的 J-Machine 和 M-Machine 项目开发了用于通信和同步的低开销机制,以及现代互连网络的许多关键方面。这些机制使得并行性可以在非常细的粒度上被利用,最少只需 10 或 20 条指令即可作为一个可调度的工作单元。J-Machine 的许多特性被 Cray T3D 和 T3E 计算机直接采用。

并行计算有着超越这一特定历史分支的丰富历史。由于篇幅有限,我们无法进行完整的综述。Culler 等人的文章提供了一个很好的回顾。

GPU 计算与所有高性能计算一样,深受这一遗产的影响。它使用 MPI 进行节点间的通信,使用互连网络连接这些节点,并且在此研究过程中开发的许多通信和同步机制被用于协调并行计算。

并行图形系统

虽然不如传统的并行计算和超级计算机广为人知,但并行图形和成像计算机有着悠久的历史。

处理和生成图像需要巨大的计算量。例如,如果一台每秒处理一百万条指令的计算机(1MIPS)对百万像素图像的每个像素应用一次算术运算,计算机需要一秒钟来处理一张图像。

渲染电影和游戏中的 3D 虚拟世界比图像处理每像素需要的计算量大几个数量级。例如,为现代电影生成的图像每个像素需要大约十亿次浮点运算。因此,为了在实践中有用,图形和成像需要高性能的并行超级计算机。这些计算机在大规模数据集合上并行计算。

一个早期的 DARPA 资助研究项目是吉姆·克拉克(Jim Clark)在斯坦福大学领导的几何引擎(Geometry Engine)。

几何引擎促成了硅谷图形公司(Silicon Graphics)的成立,该公司率先开发了 3D 图形工作站。SGI 硬件架构和 OpenGL 软件库定义了现代 GPU 架构。

另一个值得注意的政府资助研究项目是亨利·福克斯(Henry Fuchs)及其合作者在北卡罗来纳大学领导的 Pixel Planes 系列高性能图形系统。事实上,Pixel Planes 5 是一台相当通用的单指令多数据(SIMD)计算机,它在 128 x 128 图像上运行并行计算。其他早期并行图形和图像计算机的例子包括 NASA 的大规模并行处理器(MPP)、Ikonas 图形系统和 Pixar 图像计算机。

早期 GPU 实现了类似于早期 SGI 工作站的固定功能图形流水线。当整个 OpenGL 图形流水线可以在单个芯片上实现时,英伟达引入了“GPU”一词。1999 年推出的英伟达 Geforce 256 由 1700 万个晶体管组成,是第一款商用 GPU。

在此之前,在皮克斯(Pixar)工作期间,Hanrahan 开发了 RenderMan,这是一个生成照片级逼真图像的系统。该系统彻底改变了电影行业,因为它能够生成可以与相机拍摄的实景无缝结合的图像。RenderMan 的一个关键组件是着色语言,它使用户能够扩展系统以模拟复杂的材质和光照。

虽然最初的 GPU 实现了固定功能流水线,但它们是由可编程组件构成的。不幸的是,这些处理单元因系统而异,因代而异。需要的是一种可移植的编程模型。由于 GPU 的主要应用是电脑游戏,因此将 RenderMan 着色语言适配到 GPU,以便游戏开发者可以创造新的光照和着色效果似乎是自然而然的。

在斯坦福大学的一个 DARPA 资助项目下,为当时的 GPU 设计并实现了一种实时着色语言(RTSL)。着色语言程序现在被称为着色器(shaders)。博士后学者 Bill Mark 领导了斯坦福 RTSL 的设计,后来加入了英伟达。他与另一位前斯坦福研究生 Kurt Akeley 一起增强了该技术,并创建了 Cg 着色语言。Cg 导致了微软 HLSL 和 OpenGL GLSL 的开发。

人们很快意识到,这些早期的着色语言足够灵活,可以实现科学计算中的许多算法。研究人员采用了诸如矩阵乘法、线性求解器、流体动力学求解器和分子动力学等算法在着色器上运行。这导致了 GPGPU(通用 GPU)计算运动的兴起。

流处理

DARPA 和 DOE 在斯坦福大学资助的关于 Imagine 流处理器和 Merrimac 流式超级计算机的工作发展了流处理,这是一种导致算术强度(计算与带宽之比)增加的并行计算形式。

如前所述,处理器消耗的大部分功率是在通信上。在芯片之间发送信号尤其耗电。芯片外通信也比芯片内通信慢得多。

流处理包含两个减少内存带宽需求的主要思想。

第一个是利用生产者-消费者局部性,使得一个阶段(生产者)将其结果转发给下一个阶段(消费者),而无需写入和读取内存。

第二个主要思想是将计算组织成称为内核(kernels)的函数。每个内核获取一个数据包,对该包执行函数,并输出另一个数据包。函数中的算术运算数量大于对内存的读写次数。这两种技术显著减少了内存访问次数,并提高了流处理架构的效率。

在流处理器中,计算被组织成产生和消耗数据流的内核。产生内核会将输出流写入流寄存器文件(SRF)。消费内核会从 SRF 读取输入,而数据无需写入或从内存中读取。通过适当的调度来匹配流的批处理大小与 SRF 的容量,这种组织使得应用程序能够维持非常高的算术强度(算术与内存带宽之比)。

一个设计和构建 Imagine 流处理器的 DARPA 资助项目于 1997 年在 MIT 启动,并于同年晚些时候转移到斯坦福大学。Imagine 是一台用于信号和图像处理工作负载的图形和媒体处理器。它由许多带有本地寄存器文件的并行算术单元、一个中央流寄存器文件和一个内存系统组成。内核从流寄存器文件读取流,通过本地寄存器文件传递中间结果,并将输出流写回流寄存器文件,供下一个内核读取。

Stream-C 编程语言被开发用于编程 Imagine。它扩展了 C 编程语言,增加了描述内核和流的构造。开发了众多的图形、信号处理和图像处理应用程序来调整和评估该架构。它在纹理映射光栅图形上的性能与当时的固定功能 GPU 相当。

在一次 DARPA 主要研究人员会议上,本文作者意识到这项技术可以应用于高性能计算,并构思了 Merrimac 项目。斯坦福 DOE ASCI 中心的计算机科学(CS)部分被重定向以追求这种高性能计算方法。该中心的年度报告提供了流处理发展史的详实记录。

Merrimac 架构被定义为将流处理适配到科学应用。与 Imagine 相比,主要变化是增加了科学计算所需的数据类型(如 FP64),将架构扩展到通过互连网络连接的多个节点以处理大规模问题,并增加了许多弹性特征,以支持在具有合理故障率的情况下进行大规模计算。

Stream-C 编程语言演变成了 Brook。Brook 背后的关键思想是将流编程的想法与更传统的数据并行计算合并。内核函数成为保持高算术强度的关键处理原语。

Brook 被适配以针对 2000 年代初的 GPU。这些 GPU 运行可编程顶点和片段着色器。着色器实现了内核,但指令数量有限且寄存器很少。常见的数据并行编程原语(如 map、reduce/scan、filter、gather 和 scatter)是通过在低级图形着色器之上构建虚拟数据并行计算机来实现的。这种抽象使得大量现有的并行算法可以在 GPU 上运行,并且早期着色器的局限性逐渐被消除。

早期利用内核执行高算术强度计算的一个很好的例子是稠密矩阵-矩阵乘法,它是现代神经网络算法的基础。在执行矩阵-矩阵乘法时,需要读取两个 n×n 矩阵并写入一个 n×n 矩阵。矩阵乘法需要 n³ 次乘加运算。因此,算术强度为 O(n)。这一事实众所周知,并导致了针对带有缓存的 CPU 进行矩阵乘法分块的有效方法。分块在 GPU 上运行时也非常有效。

斯坦福 ASCI 中心的数值科学家将几种科学代码移植到 Brook,以便在 Merrimac 模拟器上运行。这些代码包括计算流体动力学、磁流体动力学和 n 体模拟。n 体模拟是高效 GPU 应用的一个很好的例子。原子对之间的力由天体物理模拟中的引力定律给出,但非结合原子之间的相互作用由 Lennard-Jones 势(甚至更复杂的经验势)近似。这些函数需要许多算术运算。对于这些模拟,相邻原子存储在“邻居列表”中。分子动力学模拟立即成为 GPU 的主要应用。

GPU 和流处理器的一个关键特征是它们具有多种形式的硬件并行性。

每个 GPU 由许多核心组成。每个核心包含一个 SIMD 处理单元(通常为 32 宽)。

此外,每个核心都是多线程的。

回想一下,GPU 是为图形应用程序开发的,其性能取决于将纹理应用于三角形的效率。

纹理映射涉及计算三角形内每个像素片段的纹理坐标,然后使用这些坐标从图像中获取。这些纹理获取具有空间局部性,但时间局部性很小。空间局部性可以通过小型缓存来处理,但由于缺乏一致性,缓存无法处理时间局部性。

高效的纹理映射要求 GPU 隐藏这些纹理获取的延迟。早期 GPU 通过让片段请求纹理、挂起该片段的执行,并立即切换到处理另一个片段来实现这一点。这是多线程的简化版本,这意味着 GPU 需要有许多并行线程同时运行。任务总数是核心数乘以 SIMD 算术单元数(称为 warp)乘以线程数。Blackwell B200 GPU 拥有 384 个流多处理器(SMs)。每个 SM 有 64 个驻留 warp,每个 warp 有 32 个线程。因此,该 GPU 上有 786,432 个任务同时执行。

技术转移

流处理架构和编程系统通过人员流动从斯坦福转移到了英伟达。英伟达的一位架构师 John Nickolls 听说过流处理,并招募了 Bill Dally 在 2003 年为英伟达的 NV50 架构提供咨询。(NV50 于 2006 年作为 G80 发布)。流处理器的许多特性被合并到了该架构中。NV50 的“共享内存”发挥了 Imagine 和 Merrimac 中 SRF 的作用。

Ian Buck(Merrimac 项目的研究生和 Brook 的主要开发人员)于 2004 年加入英伟达。Ian 与 John Nickolls 合作将 Brook 演进为 CUDA。CUDA 合并了 Brook 和 Cg(一种图形着色语言)的最佳特性,并采纳了 Brook 程序员的反馈。关于该技术如何从斯坦福转移到英伟达的故事在一篇演示文稿中进行了描述。Mike Houston(该项目的另一位研究生)加入了 AMD,并直接使用 Brook 作为其 GPU 的编程语言。G80(NV50)和 CUDA 于 2006 年在超级计算大会上发布。

当 CUDA 于 2006 年发布时,很少有人了解并行编程,更不用说 GPU 流编程了。为了克服这一劳动力短缺,Wen-Mei Hwu 和 David Kirk 通过为教授讲授 CUDA 编程课程来推广 GPU 计算。参加这些课程的教师随后教授了成千上万的学生使用 CUDA 进行并行编程。从 Cosmic Cube、J-Machine 和 M-Machine 借来的并行计算技术既被应用于 GPU 内部(以协调多个 SM),也被应用于跨 GPU(构建多节点 GPU 系统以解决大型问题)。

赋能 AI

现代机器学习依赖于三个关键要素——海量数据集、具有许多层和权重的庞大模型,以及优化权重的计算能力。核心算法(深度神经网络、卷积网络、使用反向传播的训练和随机梯度下降)自 20 世纪 80 年代或更早以来就一直存在。大型标注数据集,例如 PASCAL 和 Imagenet,出现在 21 世纪初。最近的进展,例如将文本嵌入到向量空间中,使得自然语言深度学习成为可能。Transformers(“注意力就是你所需要的”)用带有历史记录的易于训练的神经网络取代了难以训练的循环神经网络。GPU 计算使得大规模数据集的网络训练在经济上变得可行。一旦展示了这种能力(Alexnet, GPT),AI 的能力就得到了迅速提升。AI 的快速采用为改进 GPU 计算系统提供了更大的动力。

英伟达的机器学习也得益于学术界与产业界的协同效应。2010 年,作者之一(Dally)与吴恩达(Andrew Ng)的一次早餐交谈促成了一个英伟达与斯坦福之间的联合项目,旨在 GPU 上构建深度神经网络。Bryan Catanzaro 领导了该项目的英伟达部分。在此项目中开发的软件成为了 CuDNN,它为英伟达 GPU 上的深度学习提供了一个现成的库——从而推动了深度学习的普及。

结论

GPU 计算背后的技术(已促成了现代机器学习)主要归功于 30 年的政府资助学术研究。

并行计算、并行图形系统和流处理的研究为 GPU 计算奠定了基础。在这些研究项目中培养的许多学生后来进入行业,转移了这些技术并利用其开发了创新产品。

从斯坦福流处理项目到 GPU 计算的转移非常直接,学术上的 Brook 语言演变为 CUDA,流处理器的功能被整合到 G80 GPU 中。

GPU 提供的高效、易于编程且性能极高的计算平台,通过计算着色器促成了当前的机器学习革命——提供了缺失的成分,以补充早已可用但一直缺乏计算能力的算法和数据。

资料链接:https://cacm.acm.org/federal-funding-of-academic-research/the-origins-of-gpu-computing/

关于作者

威廉·J·达利是美国加利福尼亚州圣克拉拉英伟达公司首席科学家兼高级副总裁,同时也是斯坦福大学电气工程与计算机科学的兼职教授。

帕特·汉拉汉是美国加利福尼亚州斯坦福大学电气工程与计算机科学的佳能荣休教授。


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

降低 74% 的 P99 尾延迟:揭秘 Go HTTP 客户端的“请求对冲”魔法

本文永久链接 – https://tonybai.com/2026/03/30/reduced-p99-latency-by-request-hedging-in-go

大家好,我是Tony Bai。

在微服务和分布式系统的世界里,我们常常会遇到一个令人头疼的现象:服务在大部分时间(如 P50 或 P90 指标)表现得非常丝滑,但总有那么一小撮请求(P99 甚至 P99.9 指标)慢得令人发指。

近日,在 Reddit 的 r/golang 社区中,一位开发者分享了他将 Go 服务的 P99 延迟降低了 74% 的经验。令人惊讶的是,他所使用的绝招并非升级硬件或重构业务逻辑,而是引入了一个名为 Request Hedging(请求对冲) 的策略。

面对高延迟,我们本能的反应是“重试(Retry)”。但正如这位开发者所发现的:单纯的重试不仅无助于解决长尾延迟,反而可能在系统高负载时雪上加霜。真正有效的方法是处理“落后者”,而不是“失败者”。

本文将带你重温 Google 关于分布式系统的经典论文,深入剖析 Request Hedging 的原理,并手把手教你如何仅使用 Go 标准库,为你的 HTTP 客户端插上“对冲”的翅膀。

尾延迟的诅咒:为什么重试不是万能药?

在深入 Hedging 之前,我们必须先理解什么是尾延迟(Tail Latency)

2013 年,Google 的两位大神 Jeffrey Dean 和 Luiz André Barroso 在《Communications of the ACM》上发表了一篇神级论文:《The Tail at Scale》。在这篇Paper中,他们详细阐述了在大规模分布式系统中,为什么长尾延迟是不可避免的。

哪怕你拥有世界上最优秀的工程师,底层硬件的物理特性(如 CPU 降频、网络拥塞)、操作系统的后台任务(如 IO 调度)、以及语言运行时的特性(如 Go 的 GC 停顿),都会导致某些请求的处理时间远高于平均值。

当你的服务需要并行调用多个下游服务时,这种局部的延迟波动会被急剧放大。 假设一个服务需要调用 100 个叶子节点,如果单个节点响应时间超过 1 秒的概率是 1%,那么整个请求超过 1 秒的概率将飙升至 63%!

注:节点总数 n = 100 ,已知单个节点响应时间超过 1 秒的概率 为1%。单个节点响应时间不超过 1 秒(即正常响应)的概率为1-1% = 99% = 0.99。由于 100 个请求是并行的且相互独立,整个请求“正常”的前提是所有 100 个节点都必须在 1 秒内返回。这种概率为0.99^100=0.366。这样只要这 100 个节点中有任何一个掉链子,整个请求(作为整体)的耗时就会超过 1 秒。其概率为1-0.366≈0.63=63%。


图:来自《The Tail at Scale》

这张图直观地展示了随着服务器数量(Fan-out)增加,哪怕单机变慢的概率极低,整体响应时间变慢的概率也会陡峭上升。

面对超时的请求,传统的做法是实施超时重试(Timeout & Retry)。但重试存在致命缺陷:

  1. 你必须等待超时发生。 如果超时设置为 1 秒,那么重试的请求至少要经历 1 秒的延迟,这根本无法改善 P99 延迟。
  2. 加剧雪崩。 当下游服务因为负载过高而变慢时,大量的重试请求会瞬间淹没下游,导致系统彻底崩溃。

Request Hedging:优雅地跑赢时间

为了解决长尾延迟,Google 论文中提出了一种极具工程智慧的策略:Hedged Requests(请求对冲/对冲请求)

其核心思想非常简单直白:

客户端首先向目标服务器发送一个请求。如果该请求在预期的时间(即“对冲延迟阈值”,Hedging Delay)内没有返回,客户端不会等待其超时或失败,而是立即向另一个副本(或者同一个负载均衡器后的其他实例)发送一模一样的备份请求。客户端将使用最先返回的那个成功响应,并主动取消其余的未决请求。

这种方法之所以有效,是因为导致请求变慢的因素通常是瞬时的且与特定机器相关的(如某台机器刚好在做 GC,或者刚好被一个大查询阻塞了队列)。第二个请求很大概率会被路由到一台健康的、空闲的机器上,从而快速返回。

Hedging 与 Retry 的本质区别:

  • Retry:针对的是失败(Failure)。必须等第一个请求彻底失败或超时,才发起第二个。
  • Hedging:针对的是慢(Slowness)。第一个请求还在运行(没报错),第二个请求就已经出发了。它们是并行竞争的关系。

虽然这听起来像是在浪费服务器资源,但 Google 的实践证明,如果将 Hedging Delay 设置为 P95 延迟(即 95% 的请求都能在这个时间内完成),那么只有 5% 的请求会触发对冲。这仅仅增加了 5% 的系统负载,却能将 P99 或 P99.9 的长尾延迟削减大半!

在现代微服务生态中,gRPC 已经在 Service Config 中原生支持了 Hedging 策略,但对于广泛使用的 HTTP/REST 接口,我们通常需要自己实现。

实战:构建可压测的 Hedging HTTP Client

为了验证 Hedging 的威力,我们将使用 Go 原生标准库,从零实现一个带有对冲机制的 http.RoundTripper,并构建一个完整的压测实验环境。

项目布局

首先,创建一个新的 Go 项目:

mkdir go-hedging-demo
cd go-hedging-demo
go mod init hedging-demo

我们将创建三个文件:

  • hedge.go:包含核心的 Hedging 逻辑实现。
  • server.go:一个模拟真实分布式环境、带有随机高延迟的测试服务器。
  • main.go:客户端压测入口,用于对比普通请求和 Hedging 请求的性能差异。
go-hedging-demo/
├── go.mod
├── hedge.go
├── server.go
└── main.go

核心实现:hedge.go

我们将通过实现 http.RoundTripper 接口,优雅地将对冲逻辑无缝注入到 Go 标准库的 http.Client 中。

// hedge.go
package main

import (
    "context"
    "errors"
    "net/http"
    "sync"
    "time"
)

// HedgedTransport 实现了 http.RoundTripper 接口
type HedgedTransport struct {
    Transport   http.RoundTripper // 底层真正的 Transport
    MaxAttempts int               // 最大并发请求数(包括最初的1次)
    HedgeDelay  time.Duration     // 触发对冲的延迟时间
}

func (ht *HedgedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    // 如果没有设置,使用默认行为
    transport := ht.Transport
    if transport == nil {
        transport = http.DefaultTransport
    }
    attempts := ht.MaxAttempts
    if attempts <= 0 {
        attempts = 1
    }

    // 使用带有取消功能的 context 控制整个对冲生命周期
    ctx, cancel := context.WithCancel(req.Context())
    defer cancel()

    // 结果通道,用于接收第一个成功的响应或错误
    type result struct {
        resp *http.Response
        err  error
    }
    resCh := make(chan result, attempts)
    var wg sync.WaitGroup

    // 启动一个请求的闭包函数
    doRequest := func() {
        wg.Add(1)
        go func() {
            defer wg.Done()
            // 克隆请求,防止并发修改
            cloneReq := req.Clone(ctx)
            resp, err := transport.RoundTrip(cloneReq)

            // 只有当请求不是因为 context 取消而失败时,才尝试写入结果
            if !errors.Is(err, context.Canceled) {
                select {
                case resCh <- result{resp: resp, err: err}:
                default:
                    // 通道已满或已不再需要,直接丢弃(如果 resp 不为空,需要关闭 Body 以防泄露)
                    if resp != nil && resp.Body != nil {
                        resp.Body.Close()
                    }
                }
            }
        }()
    }

    // 1. 发起第一个请求
    doRequest()

    // 2. 控制对冲的定时器和尝试次数
    timer := time.NewTimer(ht.HedgeDelay)
    defer timer.Stop()

    errs := make([]error, 0, attempts)
    requestsSent := 1

    for {
        select {
        case res := <-resCh:
            // 收到结果
            if res.err == nil {
                // 成功!立即取消其他还在飞行的请求
                cancel()
                // 等待后台 goroutine 清理完成 (可选,这里为了简单不阻塞)
                return res.resp, nil
            }
            // 如果这个请求失败了,记录错误
            errs = append(errs, res.err)
            // 如果所有发出的请求都失败了,且已经达到最大尝试次数,返回错误
            if len(errs) == attempts {
                return nil, errors.Join(errs...)
            }

            // 如果一个请求失败了,且还没达到最大尝试次数,我们不应该死等 Timer,
            // 而应该立刻触发下一个对冲请求(这里为了简化逻辑,依然依赖下一次 Timer 或失败循环)
            // 实际生产级实现可以在这里直接触发 doRequest()

        case <-timer.C:
            // 对冲延迟到达
            if requestsSent < attempts {
                // 触发对冲请求
                doRequest()
                requestsSent++
                // 重置定时器,准备下一次可能的对冲
                timer.Reset(ht.HedgeDelay)
            }

        case <-ctx.Done():
            // 整个请求超时或被调用方取消
            return nil, ctx.Err()
        }
    }
}

这里,我们使用了 req.Clone(ctx) 来复制请求,确保并发安全。通过 context.WithCancel 控制所有的下游请求,一旦有一个请求成功返回(res.err == nil),立即调用 cancel() 取消其余正在运行(in-flight)的请求。

测试服务器:模拟“长尾效应” server.go

为了看到效果,我们编写一个简单的 HTTP 服务。它在 90% 的情况下在 50ms 内快速响应,但在 10% 的情况下会遇到长达 500ms 到 1s 的长尾延迟。

// server.go
package main

import (
    "fmt"
    "math/rand"
    "net/http"
    "time"
)

func startServer() {
    http.HandleFunc("/data", func(w http.ResponseWriter, r *http.Request) {
        // 模拟 10% 的长尾延迟
        if rand.Float32() < 0.1 {
            // 长尾延迟:500ms - 1000ms
            delay := 500 + rand.Intn(500)
            time.Sleep(time.Duration(delay) * time.Millisecond)
        } else {
            // 正常响应:10ms - 50ms
            delay := 10 + rand.Intn(40)
            time.Sleep(time.Duration(delay) * time.Millisecond)
        }

        fmt.Fprintln(w, "OK")
    })

    go func() {
        err := http.ListenAndServe(":8080", nil)
        if err != nil {
            panic(err)
        }
    }()
    time.Sleep(100 * time.Millisecond) // 等待服务器启动
}

压测入口:对比见真章 main.go

最后,我们编写压测代码,分别使用普通 Client 和 Hedged Client 发送 1000 个并发请求,并统计 P99 延迟。

// main.go
package main

import (
    "fmt"
    "io"
    "net/http"
    "sort"
    "sync"
    "time"
)

const RequestCount = 1000

func main() {
    startServer()

    fmt.Println("开始压测普通 HTTP Client...")
    normalClient := &http.Client{
        Timeout: 2 * time.Second,
    }
    normalLatencies := runBenchmark(normalClient)

    fmt.Println("\n开始压测 Hedged HTTP Client...")
    hedgedClient := &http.Client{
        Timeout: 2 * time.Second,
        Transport: &HedgedTransport{
            Transport:   http.DefaultTransport,
            MaxAttempts: 3,                 // 最多发送3个请求
            HedgeDelay:  80 * time.Millisecond, // P95 延迟设为触发点(我们服务器正常响应 < 50ms)
        },
    }
    hedgedLatencies := runBenchmark(hedgedClient)

    // 打印统计结果
    printStats("Normal Client", normalLatencies)
    printStats("Hedged Client", hedgedLatencies)
}

func runBenchmark(client *http.Client) []time.Duration {
    var wg sync.WaitGroup
    latencies := make([]time.Duration, RequestCount)

    for i := 0; i < RequestCount; i++ {
        wg.Add(1)
        go func(index int) {
            defer wg.Done()

            start := time.Now()
            resp, err := client.Get("http://localhost:8080/data")
            if err != nil {
                fmt.Printf("Request failed: %v\n", err)
                return
            }
            io.Copy(io.Discard, resp.Body)
            resp.Body.Close()

            latencies[index] = time.Since(start)
        }(i)
    }

    wg.Wait()
    return latencies
}

func printStats(name string, latencies []time.Duration) {
    // 去除可能的失败请求(0值)
    valid := make([]time.Duration, 0, len(latencies))
    for _, l := range latencies {
        if l > 0 {
            valid = append(valid, l)
        }
    }

    sort.Slice(valid, func(i, j int) bool {
        return valid[i] < valid[j]
    })

    if len(valid) == 0 {
        fmt.Printf("No valid responses for %s\n", name)
        return
    }

    p50 := valid[len(valid)/2]
    p95 := valid[int(float64(len(valid))*0.95)]
    p99 := valid[int(float64(len(valid))*0.99)]

    fmt.Printf("\n=== %s 统计 ===\n", name)
    fmt.Printf("请求总数: %d\n", len(valid))
    fmt.Printf("P50 延迟: %v\n", p50)
    fmt.Printf("P95 延迟: %v\n", p95)
    fmt.Printf("P99 延迟: %v\n", p99)
}

运行与验证

在本地 MacBook Pro 的终端上执行 go run .,我得到了以下真实的性能对决:

$go run .
开始压测普通 HTTP Client...

开始压测 Hedged HTTP Client...

=== Normal Client 统计 ===
请求总数: 1000
P50 延迟: 115.226929ms
P95 延迟: 850.768537ms <-- 注意看这里
P99 延迟: 1.045720114s <-- 长尾效应严重

=== Hedged Client 统计 ===
请求总数: 1000
P50 延迟: 138.930108ms <-- P50 轻微损耗
P95 延迟: 360.607686ms <-- 巨大的改善!
P99 延迟: 376.98949ms  <-- P99 降低了将近 70%!

正如你所见:

  • P99 巨幅改善:对冲机制成功将 P99 延迟降低了 64%。原本需要 1 秒以上的极端慢请求,现在被控制在了 400ms 以内。
  • P50 轻微损耗:由于请求克隆、Context 管理以及本地 CPU 调度多出一倍请求的竞争,P50 上升了约 23ms。

结论:在典型的分布式系统中,这种权衡是极度划算的。我们用极小的平均延迟上升,换取了尾部延迟的高稳定性。

生产环境的避坑指南

Request Hedging 虽好,但绝非能随意滥用的“银弹”。在将其部署到生产环境之前,你必须考虑以下几个核心约束:

  1. 绝对的幂等性(Idempotency):对冲意味着同一笔请求可能同时发送给后端的两个节点。如果这是个 POST 扣款请求,而你的后端没有做好幂等性控制,这将会是一场灾难。Hedging 最好只用于幂等的只读请求(如 GET),或者有严格全局事务 ID 兜底的写入操作。
  2. Hedge Delay 的设定:这是最考验架构师的参数。设得太短,所有的请求都会变成双倍发送,瞬间打挂后端(这叫放大攻击);设得太长,起不到降低长尾的作用。最佳实践是通过 Prometheus 等监控工具,计算出该接口过去的 P95 响应时间,将其作为 Hedging Delay 的基准值。
  3. 熔断与限流(Throttling):如果下游服务整体宕机,所有的请求都会变慢,此时触发所有的对冲请求只会加速死亡。因此,正如 gRPC 规范中要求的,Hedging 必须与限流(Throttling)结合。例如,计算一个“对冲令牌池”,只有当成功请求大于失败请求达到一定比例时,才允许发送对冲请求。

小结

软件工程是一门关于权衡的艺术。在追求极致性能的道路上,我们往往将目光局限于优化数据库索引、压缩 JSON 序列化,却忽视了分布式系统固有的宏观不确定性。

Request Hedging 是从宏观架构层面给出的一记漂亮的防守反击。通过上面几百行的 Go 代码,我们成功复现了 Google 级别的架构优化。下一次,当你的监控大盘上 P99 曲线再次异常抖动时,不妨收起单纯的“超时重试”,尝试给你的 Go 客户端加一点“对冲”的魔法吧。

本文中涉及的代码可以在这里下载。https://github.com/bigwhite/experiments/tree/master/go-hedging-demo

资料链接:

  • https://www.reddit.com/r/golang/comments/1s4mb10/reduced_p99_latency_by_74_in_go_learned_something/
  • https://grpc.io/docs/guides/request-hedging/
  • https://research.google/pubs/the-tail-at-scale/

你的 P99 达标了吗?

尾延迟是分布式系统中最难缠的对手。在你的项目中,主要的长尾延迟来源是什么?你会为了降低那 1% 的极端慢请求,而接受 5% 的额外系统负载吗?

欢迎在评论区分享你的性能调优“必杀技”!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

OpenAI 创始人盛赞 Rust,却遭开发者反驳:Go 才是大模型眼里的“香饽饽”!

本文永久链接 – https://tonybai.com/2026/03/23/go-is-the-best-programming-language-for-llm

大家好,我是Tony Bai。

在这个大模型重塑编程范式的当下,如果你想开发一个自主运行的智能体(Agent),或者想让大模型(LLM)帮你生成上万行的核心业务代码,你会选择哪门编程语言?

如果你去问 OpenAI 的总裁兼联合创始人 Greg Brockman,他的答案非常直接:

“Rust is a perfect language for agents, given that if it compiles it’s ~correct.”
(Rust 是开发 Agent 的完美语言,因为只要它能编译通过,它就基本是正确的。)

这句话听起来极其硬核且有道理。Rust 引以为傲的所有权模型和严苛的编译器,就像一个极度刻薄的审查员。既然大模型经常“胡言乱语”,那不如交给 Rust 编译器来兜底。

但有趣的是,Greg 的这番高论,最近在推特(X)上遭到了不少一线资深开发者的强烈反驳。其中,一条阅读量近 7 万的推文直指核心痛点,甚至抛出了一个让无数 Gopher(Go 开发者)极度舒适的反直觉结论:

“别吹 Rust 了,在大模型眼里,语法简单、风格统一的 Go 才是真正的‘香饽饽’!”

今天,我们就来扒一扒这场顶级“语言战争”背后的神仙打架,看看为什么 Go 语言身上那些曾经被全网群嘲的“缺点”,如今却成了大模型时代最无敌的护城河。

大模型写 Rust,真的安全吗?

发起反驳的开发者 Emil Privér 一针见血地指出了用大模型写 Rust 的最大陷阱:“逃课”心理

Greg Brockman 认为 Rust 编译器能阻止大模型犯错。但这有一个前提:大模型必须老老实实地去解生命周期(Lifetime)和所有权(Ownership)的方程。

然而现实是,大模型也是会“偷懒”的。

Emil 敏锐地指出,当现代 LLM 在生成复杂的 Rust 业务逻辑,且实在绕不过编译器的各种借用检查报错时,它们会极其鸡贼地使出大招:直接套上一层 unsafe {} 块,或者无脑使用 .unwrap() 来强行绕过编译器的安全审查!

你在指望编译器兜底,大模型却在底下悄悄开了“后门”。

就像评论区一位开发者吐槽的那样:“当你看到大模型为了图省事,把一段关键操作包在 unsafe 里,并且依然能顺利编译通过时,你还敢说它‘只要编译通过就基本正确’吗?”

虽然有开发者反驳说,可以通过配置强制禁止 unsafe。但大模型的“逃课手段”防不胜防,比如疯狂滥用 RefCell 导致运行时 Panic,这在编译器眼里是合法的,但在生产环境下却是灾难。

Go 的“无趣”,成了最顶级的生产力

既然 Rust 太“聪明”导致大模型容易弄巧成拙,那大模型到底喜欢什么样的语言?

Emil 给出的答案是:Go。

他的底层逻辑非常硬核。

他认为,大模型(LLMs)的本质是基于大量预训练语料进行下一个 Token 的概率预测。对于这种预测机制来说,一段代码的上下文看起来越“同质化(Looks the same)”,大模型生成的准确率就越高。

这就牵扯到了 Go 语言一个常年被群嘲的“缺点”:啰嗦、缺乏表现力、没有花里胡哨的语法糖。

在 Go 里,如果你想写一个循环,你只有一种办法:for 循环。

没有 while,没有 do-while,没有 foreach,更没有各种炫技的函数式流处理。

而在 Rust 或者 JavaScript 等语言里,你想遍历一个数组,至少有 5 种写法。甚至在不同的开源库里,大家的编码风格都千奇百怪。

在人类看来,Go 语言简直“无趣”到了极点。但在大模型这种无情的“概率预测机器”眼里,Go 简直就是天堂!

因为 Go 语言有着近乎暴君般的强制格式化工具 gofmt,以及全宇宙最少、最没有歧义的语法关键字。无论你是 Google 的顶级工程师,还是刚入门三个月的新手,写出来的 Go 代码结构几乎是一模一样的。

这种极度“收敛”和“无聊”的代码风格,恰恰完美契合了大模型的预测机制。

当所有的 Go 项目看起来都像是一个模子里刻出来的,大模型在生成上下文时就不需要去猜测“这个项目的主人喜欢用哪种流派”。它闭着眼睛往下预测,准确率就能轻易碾压其他语言。

Go,这种“一眼望到底”的特性,让它成为了大模型眼里的头号“香饽饽”。

AI 时代的软件工程师,该选什么语言?

推特评论区里,争论依然在继续。

但透过这场口水战,我们作为一线的软件工程师,应该看透一个更深层次的时代演进:

在过去十年,程序员们热衷于发明各种奇技淫巧,比拼谁的代码写得更短、更具“魔法”;但在未来,当 80%以上 的代码都将由 AI Agent 自动生成时,“可读性”与“无歧义”将成为一门编程语言最核心的生产力。

Go 语言的联合缔造者 Rob Pike 当年顶着巨大的压力,坚持不给 Go 加各种复杂的特性。很多人觉得他固执、老派。但在十多年后的今天,当大模型海啸席卷而来时,我们才突然惊觉:

Go 语言那种“强迫你用最笨、最直白的方式写代码”的设计哲学,不仅让它在微服务时代大杀四方,更让它在 AGI 时代,成为了大模型最忠实、最可靠的合作伙伴。

当大模型吐出一段复杂的 Rust 代码,你可能还要花十分钟去审查它有没有隐藏的逻辑陷阱;

但当大模型吐出一段 Go 代码,那满屏极其直白的 if err != nil,让人类工程师一眼就能看穿它的核心逻辑。

没有魔法,才是大模型时代最强的防御。

资料链接:https://x.com/emil_priver/status/2034971247348535399


今日互动探讨:

在日常开发中,你让 ChatGPT/Claude 帮你写过哪种语言的代码?你觉得它写 Go、Python 还是 Rust 时的准确率最高?

欢迎在评论区分享你的实战感受!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧!

我们致力于打造一个高品质的 Go 语言深度学习AI 应用探索 平台。在这里,你将获得:

  • 体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏,夯实你的 Go 内功。
  • 前沿 Go+AI 实战赋能: 紧跟时代步伐,学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等,掌握 AI 时代新技能。
  • 星主 Tony Bai 亲自答疑: 遇到难题?星主第一时间为你深度解析,扫清学习障碍。
  • 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术,碰撞思想火花。
  • 独家资源与内容首发: 技术文章、课程更新、精选资源,第一时间触达。

衷心希望「Go & AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

img{512x368}


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

Kelsey Hightower 退休后的冷思考:为什么 10 年过去了,我们还在谈论容器?

本文永久链接 – https://tonybai.com/2026/01/22/why-are-we-still-talking-about-containers-in-ai-age

大家好,我是Tony Bai。

“如果你在 2014 年告诉我,十年后我们还在讨论容器,我会觉得你疯了。但现在是 2025 年,我们依然在这里,谈论着同一个话题。”

在去年中旬举行的 ContainerDays Hamburg 2025 上,早已宣布“退休”的云原生传奇人物 Kelsey Hightower 发表了一场发人深省的主题演讲。在这个 AI 狂热席卷全球的时刻,他没有随波逐流地去谈论大模型,而是回过头来,向所有技术人抛出了一个灵魂拷问:

为什么我们总是在追逐下一个热点,却从来没有真正完成过手头的工作?

烂尾工程的诅咒——技术圈的“海啸”循环

Kelsey 首先回顾了他职业生涯中经历的三次技术浪潮:Linux 取代 Unix(AIX、Solaris等)、DevOps 的兴起、以及 Docker/Kubernetes 的容器革命。

他敏锐地指出,技术圈似乎陷入了一个无休止的“海啸循环”:

  1. 热点爆发:一个新的技术(如 Docker)出现,VC 资金涌入,所有人都在谈论它。
  2. 疯狂追逐:为了抢占市场,大家都只做“足够发布”的工作,追求速度而非完美。
  3. 未竟而散:还没等这项技术真正成熟、稳定、标准化,下一个热点(如 AI)就来了。于是,半数工程师跳船去追新热点,留下一地鸡毛。

“我们就像一群踢足球的孩子,看到球滚到哪里,所有人就一窝蜂地冲过去,连守门员都离开了球门。结果是,球门大开,后方空虚。”

这就是为什么 10 年过去了,我们还在谈论容器。因为我们当年并没有真正“完成”它。我们留下了无数的复杂性、不兼容和“企业级发行版”,却忘了初衷。

Apple 的“非性感”工作——这才是未来

在演讲中,Kelsey 分享了他最近的一个惊人发现:Apple 正在 macOS 中原生集成容器运行时。

这不是 Docker Desktop,也不是虚拟机套娃,而是操作系统级别的原生支持。这就是 GitHub 上的一个名为 apple/container 的 Apple 开源项目:

Kelsey 提到 contributors 中有 Docker 元老 Michael Crosby ,Michael Crosby 正在 Apple 做着这件“不性感”但极其重要的事情。

Kelsey 认为,这才是容器技术的终局

  • 标准化:容器运行时将成为像 TCP/IP 协议栈一样的操作系统标配,无论你是 Linux、macOS 还是 Windows。
  • 隐形化:你不再需要安装 Docker,不再需要关心运行时。它就在那里,像水和电一样自然。
  • 应用商店的重构:未来,App Store 分发的可能就是容器镜像,彻底解决依赖冲突和安全沙箱问题。

这正是那些没有去追逐 AI 热点,而是选择留在“球门”前的人,正在默默完成的伟大工程。

关于 AI——不要做“盲目的复制者”

作为 Google 前员工,Kelsey 对 AI 并不陌生。但他对当前的 LLM 热潮保持着清醒的警惕。

他现场演示了一个有趣的实验:询问一个本地运行的 LLM “FreeBSD Service Jails 需要什么版本?”
* AI 的回答:FreeBSD 13(一本正经的胡说八道)。
* 真相:FreeBSD 15(尚未发布)。

Kelsey 指出,现在的 AI 就像一个热心但糊涂的路人,它不懂装懂,只想取悦你。

他的建议是

  1. 不要迷信生成:不要因为 AI 生成了代码就直接用,就像你不会盲目复制 Stack Overflow 的代码一样。
  2. 上下文为王:AI 不是魔法,它只是一个强大的搜索引擎。如果你想得到正确答案,你必须先给它提供正确的上下文(Context)
  3. 先训练自己,再训练模型:在成为“提示词工程师”之前,先成为一名合格的工程师。只有当你自己深刻理解了问题,你才能判断 AI 的回答是天才还是垃圾。

给技术人的最后忠告

演讲的最后,Kelsey 回答了关于开源、职业发展和未来的提问。他的几条忠告,值得每一位技术人铭记:

  • 关于职业:“你的职业生涯不应该是一场马拉松,而应该是一场接力赛。当你到达巅峰时,想的应该是如何把接力棒交给下一个人,而不是霸占着位置直到倒下。”
  • 关于开源:“不要被商业公司的许可证游戏迷惑。如果代码是公开的,你可以 fork,可以学习。真正的开源精神在于分享和协作,而不在于谁拥有控制权。”
  • 关于专注:像那家只做钳子的德国公司(Knipex)一样,专注做好一件事。技术圈不缺追风者,缺的是能够沉下心来,把一项技术打磨到极致、直到它变得“无聊”和“隐形”的工匠。

小结

Kelsey Hightower 的这场演讲,是对当前浮躁技术圈的一剂清醒剂。

他提醒我们,技术的真正价值,不在于它有多新、多热,而在于它是否真正解决了问题,是否被完整地交付了。在所有人都在谈论 AI 的今天,或许我们更应该关注那些被遗忘的“球门”,去完成那些尚未完成的伟大工程。

资料链接:https://www.youtube.com/watch?v=x1t2GPChhX8


你的“烂尾”故事

Kelsey 的“海啸循环”论断让人深思。在你的职业生涯中,是否也经历过这种“还没做完旧技术,就被迫去追新热点”的无奈?你认为在这个 AI 时代,我们该如何保持“工匠精神”?

欢迎在评论区分享你的经历或思考!让我们一起在喧嚣中寻找内心的宁静。

如果这篇文章让你停下来思考了片刻,别忘了点个【赞】和【在看】,并转发给那些还在焦虑中奔跑的同行!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

Go 语言的“魔法”时刻:如何用 -toolexec 实现零侵入式自动插桩?

本文永久链接 – https://tonybai.com/2026/01/19/unleashing-the-go-toolchain

大家好,我是Tony Bai。

“Go 语言以简洁著称,但在可观测性(Observability)领域,这种简洁有时却是一种负担。手动埋点、繁琐的初始化代码、版本升级带来的破坏性变更……这些都让 Gopher 们痛苦不已。


可观测性的三大支柱

相比之下,Java 和 Python 开发者享受着“零代码修改”的自动插桩福利。Go 开发者能否拥有同样的体验?

在 GopherCon UK 2025 上,来自 DataDog 的资深工程师 Kemal Akkoyun 给出了肯定的答案。他通过挖掘 Go 工具链中一个鲜为人知的特性,不仅实现了这一目标,还将其开源为一个名为 Orchestrion 的工具。今天,就让我们一起揭秘这背后的“黑魔法”。

痛点:Go 语言的“反自动化”体质

在 Go 中集成分布式追踪(如 OpenTelemetry),通常意味着你需要:

  • 手动修改代码:在 main 函数中初始化 Tracer Provider。
  • 到处传递 Context:在每个函数签名中添加 ctx context.Context。

  • OpenTelemetry Go SDK难于集成。

  • 样板代码爆炸:在每个关键路径上通过 defer span.End() 开启和结束 Span。

这种手动方式不仅效率低下,而且容易出错。如果有遗漏,追踪链路就会断裂;如果库升级,你可能需要重写大量代码。

与 Java Agent 的字节码注入或 Python 的动态装饰器不同,Go 是静态编译语言,运行时极其简单,没有虚拟机层面的“后门”可走。这似乎是一个死局。

Gopher强烈希望 Go 也能像其他语言那样,轻松实现插桩从而注入追踪(trace)能力:

破局:编译时“大挪移”

Kemal 及其团队发现,Go 虽然没有运行时魔法,但在编译时却留了一扇窗:-toolexec 标志

$go help build|grep -A6 toolexec
    -toolexec 'cmd args'
        a program to use to invoke toolchain programs like vet and asm.
        For example, instead of running asm, the go command will run
        'cmd args /path/to/asm <arguments for asm>'.
        The TOOLEXEC_IMPORTPATH environment variable will be set,
        matching 'go list -f {{.ImportPath}}' for the package being built.

这是一个鲜为人知的 go build 参数。它允许你指定一个程序,拦截并包装构建过程中的每一个工具调用(如 compile、link、asm 等),让你可以在真正的compile、link 等之前对Go源码文件 (以compile等命令行工具的命令行参数形式传入) 做点什么。

为了让大家直观感受 -toolexec 的作用,我们先来看一个最简单的“拦截器”示例。

假设我们写了一个名为 mytool 的小程序,它的作用仅仅是打印出它接收到的命令,然后再原样执行该命令:

// mytool.go
package main

import (
    "fmt"
    "os"
    "os/exec"
)

func main() {
    // 注意:将日志打印到 Stderr,避免干扰 go build 读取工具的标准输出(如 Build ID)
    fmt.Fprintf(os.Stderr, "[Interceptor] Running: %v\n", os.Args[1:])

    // 原样执行被拦截的命令
    cmd := exec.Command(os.Args[1], os.Args[2:]...)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    if err := cmd.Run(); err != nil {
        os.Exit(1)
    }
}

现在,当我们使用 -toolexec 参数来编译一个普通的 Go 程序时:

# 先编译我们的拦截器
go build -o mytool mytool.go

# 使用拦截器来编译目标程序
go build -toolexec="./mytool" main.go  // 这里的main.go只是一个"hello, world"的Go程序

你会看到类似这样的输出:

[Interceptor] Running: /usr/local/go/pkg/tool/darwin_amd64/compile -o ...
[Interceptor] Running: /usr/local/go/pkg/tool/darwin_amd64/link -o ...

看到了吗?go build 并没有直接调用编译器,而是先调用了我们的 mytool,并将真正的编译器路径和参数作为参数传给了它。之后再调用回原命令,在上面示例执行完go build -toolexec=”./mytool” main.go后,我们同样看到了编译成功后的可执行二进制文件main。

这就给了我们一个惊人的机会:既然我们拦截了编译指令,我们当然可以修改它,甚至修改它即将编译的源文件!

但是,仅仅打印几个日志、拦截一下命令,离真正的“自动插桩”还有很远的距离。要在真实复杂的 Go 项目中,安全、准确地修改成千上万行代码,同时还要处理依赖管理、缓存失效、语法兼容等棘手问题,绝非易事。

这正是 Orchestrion 登场的时刻。它不仅将 -toolexec 的潜力发挥到了极致,更将这套复杂的流程封装成了一个开箱即用的产品。

深度解构:Orchestrion 的“编译时手术”

Orchestrion 是什么?

简单来说,它是 DataDog 开源的一个编译时自动插桩工具。它的名字来源于一种模仿管弦乐队声音的机械乐器(Orchestrion),寓意它能像指挥家一样,协调并增强你的代码,而无需你亲自演奏每一个音符。

有了 -toolexec 这把钥匙,Orchestrion 就开启了一场编译时的“精密手术”。这不仅仅是简单的拦截,而是一场与 Go 编译器配合默契的“双人舞”。

安装下面图片中步骤,你就可以自动完成对你的go程序的插桩:

Kemal 在演讲中展示了一个复杂的时序图,Orchestrion 的工作流远比我们想象的要精细:

  1. 精准拦截:
    当 go build 启动时,Orchestrion 守在门口。它并不关心链接器(linker)或汇编器(asm),它的目光紧紧锁定在 compile 命令上。每当 Go 编译器准备编译一个包(Package),Orchestrion 就会叫停。

  2. AST 级解析与“无损”操作:
    它读取即将被编译的 .go 源文件,将其解析为 AST(抽象语法树)。

  3. 手术式注入 (Injection):
    根据预定义的规则(YAML 配置),Orchestrion 开始在 AST 上动刀:

    • 添加 Import:自动引入 dd-trace-go 等依赖包。
    • 函数入口插桩:在函数体的第一行插入 span, ctx := tracer.Start(…)。
    • 函数出口兜底:利用 defer span.End() 确保追踪闭环。
      甚至,它还能识别 database/sql 的调用,自动将其替换为带有追踪功能的 Wrapper。
  4. 狸猫换太子:
    手术完成后,Orchestrion 将修改后的 AST 重新生成为 .go 文件,保存在一个临时目录中。
    最后,它修改传递给编译器的参数,将原始源文件的路径替换为这些临时文件的路径。

  5. 透明编译:
    真正的 Go 编译器(compile)被唤醒,它毫不知情地编译了这些被“加料”的代码。

最终生成的二进制文件,包含了完整的、生产级的可观测性代码,而你的源代码仓库里,依然是那份清清爽爽、没有任何第三方依赖的业务逻辑。


Orchestrion:将“魔法”产品化

Orchestrion 不仅仅是一个概念验证,它是 DataDog 已经在生产环境中使用的成熟工具(现已捐赠给 OpenTelemetry 社区)。它解决了一系列工程难题:

1. 像 AOP 一样思考

Orchestrion 引入了类似 AOP(面向切面编程) 的概念。通过 YAML 配置文件,你可以定义“切入点”(Join Points)和“建议”(Advice)。

例如,你可以定义一条规则:
* 切入点:所有调用 database/sql 包 Query 方法的地方。
* 建议:在调用前后包裹一段计时和记录代码。


2. 解决 Context 丢失的终极“黑魔法”

Go 的许多老旧库或设计不规范的代码并没有在参数中传递 context.Context。为了在这些地方也能传递追踪 ID,Orchestrion 做了一件极其硬核的事情:它修改了 Go 的运行时(Runtime)!

通过修改 runtime.g 结构体,它引入了类似 GLS (Goroutine Local Storage) 的机制。这允许在同一个 Goroutine 的不同函数调用栈之间隐式传递上下文,彻底解决了 Context 断链的问题。虽然这听起来很危险,但在受控的编译时注入环境下,它变得可行且强大。

3. 零依赖与容器化友好

Orchestrion 支持通过环境变量注入。这意味着平台工程师可以构建一个包含 Orchestrion 的基础镜像,只需要在 CI/CD 流水线中设置几个环境变量,就可以让所有基于该镜像构建的 Go 应用自动获得可观测性能力,而无需应用开发者修改一行代码。

未来:社区驱动的标准

DataDog 已将 Orchestrion 捐赠给 OpenTelemetry,并与阿里巴巴(其有类似的 Go 自动插桩工具)合作,共同在 OpenTelemetry Go SIG 下推进这一技术的标准化。

这意味着,未来 Go 开发者可能只需要执行类似 otel-go-instrument my-app 的命令,就能获得与 Java/Python 同等便捷的监控体验。

小结:工具链的无限可能

Kemal 的演讲不仅展示了一个工具,更展示了一种思维方式:当语言本身的特性限制了你时,不妨向下看一层,去挖掘工具链本身的潜力。

虽然“编译时注入”听起来像是一种对 Go 简洁哲学的“背叛”,但在解决大规模微服务治理、遗留代码维护等现实难题时,它无疑是一剂强有力的解药。

对于那些渴望从重复劳动中解脱出来的 Gopher 来说,这或许就是你们一直在等待的“魔法”。

参考资料

  • https://www.youtube.com/watch?v=8Rw-fVEjihw
  • https://www.datadoghq.com/blog/go-instrumentation-orchestrion/
  • https://x.com/felixge/status/1865034549832368242
  • https://github.com/DataDog/orchestrion
  • https://datadoghq.dev/orchestrion/docs/architecture
  • https://github.com/open-telemetry/opentelemetry-go-compile-instrumentation

你的插桩之痛

自动插桩无疑是未来的方向。在你的项目中,目前是如何处理链路追踪埋点的?是忍受手动埋点的繁琐,还是已经尝试过类似的自动化工具?你对
这种修改 AST 甚至 Runtime 的“黑魔法”持什么态度?

欢迎在评论区分享你的看法或踩坑经历! 让我们一起探索 Go 可观测性的最佳实践。

如果这篇文章为你打开了 Go 编译工具链的新大门,别忘了点个【赞】和【在看】,并转发给你的架构师朋友,让他也来学两招!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

❌