MoreRSS

site iconhuizhou | 萝卜修改

Golang 分布式相关的主题,读书笔记
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

huizhou | 萝卜的 RSS 预览

软件工程师的身份危机 by Annie Vella

2025-03-26 19:17:56

Featured image of post 软件工程师的身份危机 by  Annie Vella

原文地址: https://annievella.com/posts/the-software-engineering-identity-crisis/

我们很多人选择成为软件工程师,是因为我们在亲手创造事物中找到了自我价值——而非仅仅管理或监督已有事物。

但这种身份认同正面临挑战。

人工智能(AI)编码助手不仅仅改变了我们编写软件的方式,它们还在从根本上动摇了我们对自我身份的认知。我们正在从创造者转变为编排者,从建造者转变为监督者。从工程师变成了看起来非常像……管理者的人。

这种讽刺意味,直击痛点:多年来,我们一直认为软件工程超越了单纯的编码。需求、设计、测试、运维——这些都被认为是我们的专业技能的一部分。然而,行业却将我们推向了相反的方向。我们将这些职责交给了专业人士——产品负责人、架构师、质量工程师、平台工程师——而我们则加倍投入到我们的编码专业知识中。我们成为了代码大师,成为了现代魔法的骄傲使用者。

现在,就在我们完善这项技能的同时,人工智能正威胁着要夺走它。

我们即将失去的快乐

让我们坦诚地谈谈这里存在着什么风险。我们中的许多人不仅仅是编写代码——我们热爱编写代码。我们的身份融入到我们精心设计的每一个优雅的解决方案中,融入到我们通过的每一个测试中,融入到我们通过纯粹的逻辑和创造力解决的每一个问题中。这不仅仅关乎职业或技艺,而是我们身份的真实写照。

想想那些充满成就感的时刻:当你最终追踪到那个困扰生产环境的难以捉摸的bug时,当你计算出如何优化那个缓慢的算法并看到响应时间从几秒降到几毫秒时,当你把一个迷宫般的遗留代码改造成干净且可维护的东西时。这些不仅仅是成就——它们是我们作为工程师的表达。它们是提醒我们为什么选择这条道路的时刻。

现在想象一下人工智能接管这些精雕细琢的时刻。这些工具的创造者描绘了一幅乐观的图景——他们说我们将花更多的时间在定义意图高层架构系统思考上。但仔细听听他们真正想说的是什么:我们将成为监督者而不是创造者,管理者而不是建造者。
软件工程师正在进化为系统架构师?

软件工程师正在转变为系统思考者和高层架构师吗?

这种转变引发了关于我们作为建造者的身份的重要问题:监督是驱动我们的动力吗?是它让我们早上从床上跳起来,渴望解决下一个难题吗?

身份转变:它已经到来

我们现在讨论的并不是什么理论上的未来——而是我们正在经历的现实。当谷歌最近透露人工智能生成了他们超过四分之一的新代码时,这仅仅是个开始。Y Combinator 的首席执行官 Garry Tan 透露,对于他们大约四分之一的初创公司来说,95% 的代码现在是由人工智能编写的——标志着一个真正意义重大的转变。我自己的硕士研究也揭示了类似的景象:77% 的人花费在编写代码上的时间变少了,几乎一半的人认为我们的核心技能可能会退居二线,让位于提示工程(prompt engineering)。想想这种转变:从设计解决方案到设计提示。

提示工程会取代传统的编码技能吗?

提示工程会取代传统的编码技能吗?

当被问及如何培养提示工程技能时,软件工程师强调要提高沟通技巧。让 AI 按照你想要的方式行事,意味着能够清晰地表达事情——提供恰到好处的上下文,并清楚地描述任务。你与生成式人工智能(Gen AI)沟通得越好,输出结果就越有可能符合你的期望。有些人甚至建议对这些工具礼貌一些,像对待团队成员一样对待它们——好像你在引导另一个人为你做某事。

这些变化如此深刻,以至于我们正在创造新的术语来描述我们正在成为什么样的人。以 vibe coding(氛围编码,一种依赖 AI 建议进行编码的方式)为例,这是 Andrej Karpathy 最近在 推特 上创造的一个词。它捕捉到了我们编写软件方式的深刻转变。

在这种方式的一端是传统的方式——工匠的方式。我们有目的地编写每一行代码,每一个函数名和架构决策都反映了我们对系统的深刻理解。

在另一端呢?我们让 AI填补空白,与它的建议“产生共鸣”。我们关注的是“是什么”,而不是“怎么做”。正如 Karpathy 所说:“完全沉浸在氛围中,拥抱指数增长,忘记代码的存在。”

最后一部分让我们停顿了一下——如果我们忘记了所有关于代码的事情,我们还是工程师吗?

在最近的一次结对编程会议中,工程领域的思想领袖 Gene KimSteve Yegge 演示了这在实践中是什么样子的。他们使用 AI 编码助手,将一个 3500 行的遗留 Ruby 脚本移植到 Kotlin——这项任务通常需要一周的时间——仅用了一个小时。人工智能不仅仅翻译了代码,还改进了它,添加了他们多年来一直想要的模块化架构和单元测试,但却无法证明花费的时间是合理的。

甚至连 DevOps 的教父 Patrick Debois 也认为这种转变正在重塑我们的身份。在他最近对 AI 原生开发模式的分析中,他概述了我们工作方式的四个根本性转变:

Patrick Debois:AI 原生开发的 4种模式

Patrick Debois:AI 原生开发的 4 种模式

这些模式揭示了一个深刻的转变:我们正在从 AI 系统的生产者转变为管理者,从详细的实现转变为表达意图,从交付转变为通过快速实验进行发现,以及从内容创建转变为知识管理。我们的角色正在演变为将创造与组织、构建与监督相结合。

总的来说,我认为可以公平地说,我们职业身份的本质正在发生核心变化。

塑造我们身份的技艺

要理解这种身份危机,我们需要看看编码的技艺对我们产生了多么深刻的影响。从本质上讲,编写代码是关于掌握和控制——我们花费了多年时间来完善的技能。现代编程语言比过去使用的那些语言高级得多,但它们仍然需要深厚的技术理解。如今,很少有开发人员处理指针和内存管理的细节,但我们仍然以了解事物在底层是如何运作的而自豪。即使框架承担了更多繁重的工作,我们仍然保持着我们作为工匠的身份,我们对自己的工具了如指掌。

今天的编程更多的是以创造性的方式将 API、框架和库拼接在一起,以构建有意义的东西。事实上,谷歌最近的一项研究表明,软件工程中的创造力主要集中在巧妙的重用而不是纯粹的创新这一概念上。这对我来说很有意义——我经常评论说,我们现在真的都只是“集成”工程师。

尽管如此,我们仍然以了解构建某些东西所需的所有奇怪语法而感到自豪。这就像一种只有我们才能理解的秘密语言。精通一门编程语言使你能够灵活驾驭、精准操控它。它非常详细——仅仅一个错误的字符就会破坏整个程序,而且可能需要大量的时间和耐心才能让它按照你想要的方式运行。

首先,必须完美地执行。在这方面,计算机也类似于传说中的魔法。如果一个字符,一个停顿,咒语的形式不严格正确,魔法就不会起作用。

——弗雷德里克·P·布鲁克斯,《人月神话》,第一章,Addison-Wesley,1975

其他 99% 的人认为我们理解代码是魔术,而且确实,可能需要多年的刻意练习才能掌握它。那些掌握不止一种编程语言的人有幸被称为“多面手”。我们中的许多人以编写干净、优雅的代码而感到自豪。我们热情地争论不同的风格和最佳实践,而且常常对此过于认真。

一位不甘心的管理者自述

让我分享一个关于身份演变的故事,它可能会引起共鸣。

在做了十年个人贡献者之后,我遇到了技术职业道路上臭名昭著的天花板。“高级首席软件工程师”——这就是技术职业道路的尽头。当时 Staff+ Engineering(资深工程师之上的职级)还不存在,而我所在公司的唯一架构师职位也已有人选。我面临着一个会改变我身份的选择:继续做一名建造者,还是成为一名监督者。

我选择了管理。不情愿地。这就是这条路引导我的方向。我告诉自己这仍然是工程,只是在一个不同的层面上。管理系统与管理人员并没有什么不同。我仍然可以在其他任务之间继续编写代码。

听起来很熟悉吗?这其中的相似之处令人惊叹。正如我不得不将直接解决问题的工作换成会议和文档工作一样,我们现在也被要求用提示工程来代替编码。那些定义我们作为工程师的技能——掌握语法、优雅地构建我们的代码、捕获和处理边缘情况、调试复杂问题——正在被降级到人工智能。相反,我们被告知要专注于听起来非常像管理的技能:清晰的沟通、系统思考、问题定义。

但这里没有人谈论的是:身份危机。当你意识到你不再用自己的双手建造东西时的那种深深的失落感。当你的技术专长变得不如你“管理”工具的能力重要时。当你的技能变成监督时。

组织人工智能能给我们带来同样的身份认同感吗?一种作为建造者、创造者、问题解决者的感觉?

当机器挑战我们的身份时

现在,我们身份危机的根源变得清晰起来。我们花费多年时间完善的技能——那些给予我们目标、意义和自豪感的技能——现在正被机器以更快、更便宜和更大规模的方式完成。当然,质量不如你手写的代码(但目前而言)。但是现在编写代码的速度是惊人的,企业都在争先恐后地参与进来。

这就是一线希望出现的地方。还记得那种讽刺吗——我们是如何将更广泛的技能方面交给专家的?人工智能正在推动我们重新获得我们曾经知道的东西:软件工程超越了单纯的编码。这个核心真理依然存在——最终,软件工程是关于解决问题、创造解决方案、构建重要的东西。

这些更广泛的技能——Addy Osmani 在他关于人工智能辅助编码中人类 30% 的文章中称之为“持久的工程技能”——一直将伟大的工程师与优秀的工程师区分开来。沟通、大局观思考、处理歧义——这些在人工智能驱动的世界中变得更加重要。

然而,这种对更广泛技能的强调在我们的社区中引发了争论。对于某些人来说,这听起来很像重新包装过的管理。而且他们并没有完全错——最近的一篇 CIO 文章 证实,开发团队已经在进行重组,以专注于监督而不是创造。这篇文章设想未来的团队由一个产品经理、一个用户体验设计师和一个主要使用人工智能生成原型的软件架构师组成。这些架构师或高级开发人员必须“理解内容……了解客户是谁以及我们试图实现什么”——这是被重新包装成技术工作的经典管理职责。

披着技术外衣的管理

披着技术外衣的管理

这种演变引发了关于我们作为工程师的身份的根本性问题:随着传统职业阶梯的转变,下一代软件工程师将如何发展他们的技能?我们如何在拥抱这些新工具的同时,保留塑造我们职业的深厚技术理解和技能?也许最令人不安的是——随着人工智能能力的指数级进步,我们作为工匠的角色是否会像工业革命期间的手工织布工一样过时?

前进的道路

也许答案不在于抵制这种转变,而在于通过历史的视角来理解它。这些身份危机——这些通过我们的工作来定义我们自己的根本性转变——并不是什么新鲜事。它们是技术重塑一个职业时重复出现的一种模式的一部分。

工业革命期间,工匠们也面临着类似的危机。他们经过几代人磨练的传统技能正在被机器取代。但接下来发生的事情令人着迷:许多人适应了,成为了可以修理和改进这些威胁要取代他们的机器的专业人士。其他人则找到了应用他们对材料和工艺的深刻理解来改进整个工厂运营的方法。

如果我们把这种类比应用到我们的人工智能时代,就会出现一条类似的道路。软件工程的核心——解决问题和创造价值——仍然没有改变。我们的工具正在发展,随之而来的是有效使用它们所需的技能。

问题不在于我们是否会成为机器的管理者——而在于我们是否能在这种技能的演变中找到同样的满足感。

工程师的困境

那么,这会把我们带到哪里?我们是否注定要成为人工智能智能体的监督者,而不是代码的编写者?这是一个应该抵制还是拥抱的未来?

真相,一如既往,是细致入微的。正如一些工程师自然而然地倾向于管理,而另一些工程师则更喜欢保持亲力亲为一样,我们可能会看到在如何与人工智能互动方面出现类似的范围。有些人会擅长组织人工智能系统,专注于高层设计,并使这些系统更高效和可靠——指挥一场技术交响乐,而不是进行独奏。另一些人则会在人类专业知识仍然至关重要的领域找到自己的使命——可能是在安全敏感的应用程序、人工智能缺乏训练数据的新领域,或性能和可靠性至关重要的系统中。关键不是抵制这种演变,而是在其中找到自己的位置。

显而易见的是,“软件工程师”的定义正在扩大,而不是缩小。使某人有价值的技能正在多样化。这既带来了挑战,也带来了机遇。

对于那些热爱编码技能的人来说,这种转变可能会让人感到威胁。但请记住,人工智能工具仍然只是工具。它们不了解代码背后的“为什么”、业务背景或所服务的人类需求。它们无法真正意义上进行创新,至少目前还不能。而且据我们所知,它们无法感受到解决复杂问题的满足感或创造新事物的乐趣。

也许在这个新领域中最有价值的技能不是提示工程或系统架构,而是适应性——愿意进化、学习新技能,并在一个快速变化的领域中找到自己独特的位置。

光明的一面

尽管存在这些挑战,但我们需要承认一些重要的事情:这些人工智能工具可以非常强大。借助像WindsurfCursor 这样将软件开发提升到一个全新水平的自主智能体集成开发环境(agentic IDE),就像拥有一个始终在你身边的支持性结对编程伙伴一样,随时准备帮助你解决以前可能看起来令人望而却步的问题。

对于初级开发人员或我们这些可能感到有些生疏的人来说,人工智能助手可以增强信心——在你盯着一个空白文件时帮助你入门,在你犹豫不决时验证你的方法,或者以一种对你有意义的方式解释复杂的概念。对于经验丰富的开发人员来说,它们就像拥有一个不知疲倦的助手,可以处理日常任务,而你可以专注于问题的更具挑战性的方面。

如今,我们能迅速搭建原型、探索各种方法,甚至在几分钟内掌握新技术——这速度着实令人震撼。可能需要数周的研究和反复试验才能完成的事情通常可以在几小时甚至几分钟内完成。这就像拥有超能力一样——能够放大我们的能力,并比以往更快地将我们的想法变成现实。

现实检验

但是,能力越大,责任越大。最近一项全面的 GitClear 研究 分析了 2.11 亿行代码,揭示了一些令人担忧的趋势,因为人工智能代码生成工具变得越来越普遍:

  • 复制粘贴的代码增加了 17.1%,这是人工智能辅助的代码重复首次超过重构(移动)的代码。
  • 重复代码块增加了 8 倍,现在有 6.66% 的提交包含重复的代码段。
  • 代码改动增加了 26%,所有代码更改中有 5.7% 在两周内被修改或删除。GitClear:代码更改趋势

GitClear:代码更改趋势

虽然我们生成代码的速度比以往任何时候都快,但我们也花费更多的时间来修复人工智能生成的错误并处理更难维护的代码。这不仅仅是速度问题——而是关于编写可持续、可维护软件的技能。

隐藏的身份危机

然而,在这些表面上的变化之下,隐藏着一个更深层次的挑战——一个触及我们作为工程师的核心的挑战。新兴的人机协作领域正在揭示关于我们未来的令人不安的真相。2024 年的一项研究表明,当人类和人工智能一起工作时,结果往往达不到预期。不是因为人工智能缺乏能力,而是因为信任在机器和人类之间的运作方式不同。

我们与人工智能建立信任的方式与我们与人类团队成员建立信任的方式不同。

对于人类来说,信任是通过共同的成功逐渐建立起来的。一起解决的每一个问题都会加强这种联系。即使是处理得当的失败也能加深信任。对于人工智能来说,信任通常开始时很高,但会迅速瓦解。

每一个不正确的回答、每一个幻觉般的错误修复、每一次放错地方的信心都会削弱我们对机器的信任。与人类关系中信任通常会随着时间的推移而增长不同,人工智能的信任通常会在早期达到顶峰并下降。

当信任消失时,生产力也会下降。

该研究揭示了原因:

  • 人工智能在解释我们的意图方面存在固有的不可预测性
  • 它缺乏使人类协作流畅的上下文意识
  • 它的决策通常缺乏透明度,因此一旦失去信任就很难重建

这些挑战反映了我们许多人在转变为技术领导者时所经历的事情。正如新的工程经理必须学会信任他们团队的工作而无需自己动手一样,我们现在也面临着与人工智能类似的转变——学会指导和验证,而不是自己编写每一行代码。

现实是严峻的:尽管人工智能具有原始能力,但团队在有人工智能的情况下通常比没有人工智能的情况下表现更差。正如团队的生产力在无效的领导下会受到影响一样,当我们不了解如何使用我们的人工智能工具时,我们的效率也会降低。

重塑你的身份

从我作为一名不情愿的经理的历程以及我对这种人工智能转型的研究中,我看到了三种我们可以保留我们作为建造者身份的方式:

  1. 抵制——有些人会选择专注于人类创造力和深厚技术专业知识仍然至关重要的领域
  2. 适应——另一些人会拥抱人工智能编排,成为一种新型技术交响乐的指挥家3. 平衡——还有许多人,像我一样,会寻求一条中间道路——使用人工智能来完成日常任务,同时保留直接解决问题的乐趣

然后我意识到一个改变我观点的事实:我们不必只选择一条道路

身份的钟摆

也许我们身份危机的答案在于工程师/经理的钟摆。我自己在这些角色之间转换的经历教会了我一些关于身份的关键知识:

  • 管理并没有取代我的工程师身份——它扩展了它
  • 回归亲力亲为的工作并不是倒退——而是身份的更新
  • 钟摆的摆动本身成为了我的一部分——适应性强、不断成长、不断进化

就在那时,我突然意识到:这正是我们需要的人工智能时代的模型。如果我们不必被迫成为永久的“人工智能经理”,而是可以在以下角色之间切换,那会怎么样呢?

  • 深入的技术工作,我们可以直接编写和完善代码
  • 战略编排,我们可以指导人工智能系统
  • 将这两种方法结合起来的创造性问题解决

这种平衡的方法与我从其他工程师那里听到的声音产生了深刻的共鸣。我的研究表明了一个明确的信息:保持强大的工程基础比以往任何时候都更加重要。我们需要深厚的技术知识才能有效地审查、验证和调整人工智能生成的代码——因为它通常不太正确。当被问及他们对人工智能编码助手的担忧时,软件工程师将代码质量和安全性排在工作保障之上。

对人工智能编码助手的主要担忧

软件工程师对人工智能编码助手的主要担忧

这告诉我一些深刻的事情:我们把自己视为工程卓越的守护者,确保人工智能生成的解决方案遵循可靠的软件工程原则。我们并不是想把我们的专业知识委托给人工智能——我们正在进化以新的方式应用我们的技能。

你的行动

当我们驾驭这种转型时,一个基本的真理浮出水面:我们的身份危机实际上根本不是关于人工智能的。对人机协作的研究、与管理转型的相似之处、角色的钟摆——它们都指向更深层次的东西。除了在建造者或监督者之间做出选择之外,还存在着我们作为创造者的核心。

现在我们又回到了原点:人工智能并没有抢走我们的工作,而是给了我们一个机会来重新获得我们交给专家的那些更广泛的角色方面。回到软件工程不仅仅意味着编写代码的时代。当它意味着理解整个问题空间时,从用户需求到业务影响,从系统设计到卓越运营。

钟摆的比喻在这里为我们提供了智慧。正如我们中的许多人在工程和管理角色之间摇摆一样,我们可以以类似的方式拥抱人工智能的流动性。有些时候,我们会深入研究代码,体验设计优雅解决方案的快感。其他时候,我们会退一步来指导人工智能系统——不是作为监督者,而是作为了解他们技能的每一个部分的大师级建造者。就像工业革命的工人成为优化改变他们技能的机器的专家一样,我们可以掌握这些人工智能系统——使它们成为我们创造力的工具,而不是我们创造力的替代品。

在人工智能时代,最重要的是保留我们本质的东西:构建事物、解决难题、使某些东西完全正确运行的纯粹乐趣。我们的工程卓越不仅仅是验证人工智能的工作——它源于对系统如此熟悉,以至于我们可以塑造它们、改进它们、改变它们。

选择不是人工智能是否会改变我们的行业——它已经在改变了。真正的选择是我们如何与它一起进化。我们是会坚持对成为一名工程师的过时观念吗?还是会重新获得我们的技能,不是作为单纯的编码员,而是作为人工智能增强型系统的大师级建造者?

钟摆正在摆动——你会坚守阵地,还是随之而动?


  • 本文长期链接
  • 如果您觉得我的博客对你有帮助,请通过 RSS订阅我。
  • 或者在X上关注我。
  • 如果您有Medium账号,能给我个关注嘛?我的文章第一时间都会发布在Medium。

【译】:gRPC丑陋的部分

2025-03-14 09:59:10

原文链接:https://kmcd.dev/posts/grpc-the-ugly-parts/
这篇文章是gRPC:好与坏系列的一部分。

gRPC无疑是微服务领域中的一把利器,它带来了效率和性能上的优势,但gRPC也有其丑陋的一面。作为一个在gRPC上花费了大量时间的人,我想揭示这项技术的一些不那么美好的方面。我已经讨论过gRPC的优点缺点,现在让我们来谈谈它的丑陋之处。

代码生成

首先,我不得不说一下从protobuf生成的代码有多么丑陋。这些代码通常很冗长、复杂且难以阅读。尽管它并不是为了手动编辑而设计的,但这会影响代码的可读性和可维护性,尤其是在将gRPC集成到大型项目中时。最近在大多数语言中,这种情况已经有所改善,但仍然存在一些粗糙的地方。

语言特定的怪癖

protobufgRPC的初始实现常常偏离语言特定的规范,尤其是在HTTP处理方面。这在一定程度上源于强制支持HTTP/2的决定,这一决定后来被证明限制了gRPC在前端的应用。我们现在从gRPC-Web中了解到,trailer并不是像gRPC这样的协议的硬性要求。在这一决定之后,我们现在需要改进protobufgRPC的语言实现,使其更符合每种语言的习惯。

对于Go语言来说,避免使用net/http包是一个艰难的决定,因为这使得在与其他类型的HTTP API一起使用gRPC端点时变得更加困难,并且难以复用HTTP中间件。他们最终在grpc-go中添加了一个ServeHTTP()接口,作为使用Go标准库中的HTTP服务器的一种实验性方法,但使用这种方法会导致显著的性能损失。也许他们这样做是出于性能考虑?如果是这样,这无疑是一个将gRPC与Go生态系统其他部分割裂的权衡。

有时,语言特定的怪癖实际上会影响你如何设计protobuf类型。如果你遵循Buf的风格建议,枚举的名称应该以枚举名的大写蛇形版本作为前缀,就像这样:

1
2
3
4
5
enum FooBar {
  FOO_BAR_UNSPECIFIED = 0;
  FOO_BAR_FIRST_VALUE = 1;
  FOO_BAR_SECOND_VALUE = 2;
}

这在buf的lint规则描述中有更好的解释,但这种风格指南之所以如此,是因为C++的枚举作用域规则,这使得在同一包中无法有两个具有相同枚举值名称的枚举值。虽然这种约定源于C++的作用域规则,但它影响了你应该如何设计所有的protobuf文件。为什么枚举内部的作用域不足以让C++编译器生成唯一的名称?为什么这种缺陷会影响风格指南,并进而影响所有目标语言?对我来说,这有点丑陋,因为某些语言实现的怪癖以不直观的方式冒了出来。

生成的代码甚至不够快

生成代码的一个好处是,你可以生成一些正常人不会写的代码,以获得一些性能优化。然而,如果你查看一些从protobuf生成的代码,你会发现大量使用了运行时反射。为什么?在某种程度上,我是在说生成的代码不够丑陋。让我们看一个具体的例子。请注意,这将是一个非常Go特定的部分,因为我大部分关于protobuf的经验都是在Go中。然而,相同的策略已经应用于大多数语言。

让我们来看一个Go中的超级简单示例。以下是protobuf:

1
2
3
message Hello {
  string name = 1;
}

这是由protoc生成的类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type Hello struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}

// With these methods, contents are stripped
func (*Hello) Reset()
func (*Hello) String() string
func (*Hello) ProtoMessage()
func (*Hello) ProtoReflect() protoreflect.Message

实际上,并没有为这个类型定义专门的Marshal()Unmarshal()函数。这意味着序列化是通过运行时反射来实现的。反射通常被认为较慢,因为它确实较慢。我觉得奇怪的是,没有为Go生成优化的、类型特定的序列化代码。话虽如此,你可以通过使用一个名为vtprotobuf的单独protoc插件来获得这一点,该插件将为每个protobuf类型生成专门的marshal和unmarshal函数。它还允许使用类型特定的内存池,这也有助于减少分配并提高性能。根据我的测试,只需添加vtprotobuf而不做任何代码更改,就可以将性能提高2-4%。这基本上是“免费”的2-4%,所以我觉得很奇怪,这竟然不是标准编译器的一部分。你可能不喜欢它,但这就是峰值性能的样子。无论如何,这个项目需要更多的关注和支持。

请注意,还有其他一些项目声称在标准protobuf库的基础上取得了惊人的性能提升。他们确实通过做出一些权衡来实现这些性能提升,但很多时候,额外的复杂性是值得的。

你可能会读到这一部分并想:“好吧,这会增加生成的代码量,增加二进制文件或包的大小,在某些环境中,你可能不希望这样。”这是事实,这就是为什么protobuf有一个optimize_for选项,所以你可以注释以下之一:

  • option optimize_for = SPEED; - 更冗长、更快的代码
  • option optimize_for = CODE_SIZE; - 更小的代码
  • option optimize_for = LITE_RUNTIME; - 旨在在较小的运行时上运行,省略了描述符和反射等功能。

请参阅官方protobuf文档中关于optimize_for的完整描述。虽然这些选项存在,但它们实际上并没有用于大多数目标语言。将来,我完全希望看到大部分vtprotobuf被整合到Go的标准protobuf编译器中,并在optimize_for = SPEED时使用。将类似vtprotobuf的优化整合到标准protobuf编译器中,可以为Go带来显著的性能提升,其他语言也可能存在类似的机会。

必填字段

Protobuf的维护者在必填字段方面学到了一些艰难的教训。他们觉得自己犯了一个严重的错误,以至于他们推出了一个新版本的protobuf,即proto3,只是为了从规范中删除必填字段。为什么?“必填字段有害”宣言的作者在一篇冗长的Hacker News评论中谈到了这一点,但重要的是:

现实世界的实践也表明,许多最初被认为是“必填”的字段随着时间的推移往往会变成可选的,因此有了“必填字段有害”的宣言。在实践中,你希望将所有字段声明为可选的,以便为变化提供最大的灵活性。

这一点在官方protobuf风格指南中得到了呼应,他们建议添加注释来指示某个字段是必填的。如果我们讨论的是将消息从A传递到B,我完全同意这种思路。然而,仅仅因为某些字段被认为是“必填”的会随着时间的推移而变化,并不意味着必填字段不存在。仍然需要代码来强制执行这一要求,老实说,我不想编写这些代码。因此,我认为在不编写大量空检查的情况下处理必填字段的最佳方法是使用protovalidate或类似的库,这些库具有protobuf选项,允许你注释哪些字段是必填的。然后在服务器和/或客户端上有代码可以使用库来强制执行这些要求。在我看来,这兼具了两者的优点:你仍然可以以不会完全破坏消息完整性的方式声明必填字段。

我不喜欢这样:

1
2
3
message User {
  int32 age = 1; // required.
}

我喜欢这样:

1
2
3
message User {
  int32 age = 1 [(buf.validate.field).required = true];
}

我是protovalidate的忠实粉丝,我已经多次使用它并为其做出了贡献。一般来说,我认为protobuf字段的自定义选项是protobuf的一个未被充分利用的超能力。

难以入门

尽管gRPC具有不可否认的优势,但其学习曲线可能很陡峭。对于新手来说,开始使用protobuf、理解工具链以及设置必要的基础设施可能会让人望而生畏,这使得初始采用的障碍比使用更简单的基于JSON的API更高。为什么它如此陡峭?嗯,它在大多数语言中引入了非惯用的工具链。有一些语言支持的例子使得protobuf生成变得无缝。Grpc.Tools for .NET就是一个闪亮的例子,展示了如何将protobuf工具链更紧密地集成到标准语言工具链中。我们需要更多这样的例子。

当许多使用和依赖protobuf和gRPC的人积极不希望gRPC扩展到前端,并认为推动这一方向会导致不了解情况的人侵入后端领域时,陡峭的学习曲线并没有帮助,他们认为只有他们足够聪明才能在后端工作。这是精英主义的守门行为,不幸的是,这种行业普遍存在。我相信gRPC在Web前端中与在微服务中一样有其地位。

我通过帮助其他人使用protobuf学到了很多。你可能会在Buf的Slack频道或相关讨论中看到我,因为我确实从中受益匪浅。许多文章的想法直接来自于在那里回答问题。如果我看到某个问题出现的频率足够高,我可能会写一篇文章来讨论它。我认为protobuf和gRPC社区需要更多这种态度。

我相信陡峭的学习曲线(可以通过工具链来缓解),加上一些后端开发者的抵制(可以通过……同理心来缓解?),已经减缓了它在Web开发中的广泛采用。

gRPC有其历史

gRPC最初专注于微服务,并且与HTTP/2的紧密联系阻碍了它在Web开发中的广泛采用。即使有了gRPC-Web的出现,仍然有一种看法认为它在前端生态系统中并不是一等公民。与TanStack Query等流行前端库缺乏强大的集成进一步巩固了这种看法。

我认为通过改进工具链,有真正的机会让更多前端开发者对gRPC感到兴奋。目前,整个行业正在围绕“前端”和“后端”之间的界限进行一场巨大的讨论,我认为无论结果如何,我们都会看到更多使用gRPC的TypeScript代码。

gRPC中的“g”

虽然gRPC项目声称 gRPC中的“g”是一个反向缩略词,代表“gRPC”,但它最初代表Google,因为是Google开发并发布了protobuf和gRPC。
google 的墓碑

关于Google对gRPC和protobuf的长期承诺,始终存在一个悬而未决的问题。他们会继续投资于这些开源项目,还是会在优先级发生变化时突然撤资?请记住,Google最近裁掉了Flutter、Dart和Python团队的大部分成员。Protobuf社区正在发展,但它是否足够自给自足以应对这种情况?

它尚未完成

其他人说gRPC不成熟,不是因为它的年龄,而是因为它的生态系统发展程度。我倾向于同意,因为它缺少我期望在一个成熟生态系统中看到的功能和工具。

缺少包管理器

在没有专门工具的情况下,跨多个项目或仓库共享protobuf定义是一个持续的挑战。虽然像BazelPantsBuf的BSR这样的解决方案存在,但我在“现实世界”中使用protobuf的经验……参差不齐。有一些由Google开发的开源项目,它们使用bash脚本拼凑在一起,在手动调用protoc之前下载依赖项。想象一下,一个编程语言没有管理依赖项的解决方案。这太疯狂了。我认为BazelBuf工具链很好地解决了这个问题,但我只是感到沮丧,我遇到的每个使用protobuf的仓库都以最定制化的方式解决了这个问题。社区需要团结起来改进这一点。有一个名为Buffrs的开源仓库似乎正在解决这个问题。我还没有亲自使用过它,但到目前为止它看起来还不错。

关于依赖项,我想指出的是,Google的“众所周知的”protobuf类型享有被内置到protoc中的特权。虽然这些类型非常有用且无价,但它们的特权使得其他有用的protobuf类型库难以存在和繁荣。仅仅将这些protobuf定义内置到protoc(和其他工具链中)是对没有真正和一致的依赖管理故事的逃避。

编辑器支持

Protobuf代码生成的编辑器集成还有很多不足之处。如果编辑器能够智能地将生成的代码链接回其protobuf源,那将非常有帮助。这将提供更无缝的体验,但工具链还不够智能。此外,我认为每个人都应该使用Buf的编辑器支持。如今,开发者期望在编辑器中内置linter和自动格式化工具。而对于protobuf来说,有非常真实的原因需要遵循linter的建议。

tRPC这样的项目展示了紧密集成和意见化设计选择的好处——这是protobuf由于其性质无法完全复制的。然而,我仍然希望protobuf生态系统能够发展,提供类似的简化开发者体验。

丑陋的文档

我从未见过从protobuf生成的文档不是超级丑陋的。我认为,由于gRPC历来是后端服务,后端开发者从未真正努力使用protoc插件生成漂亮的文档输出。我通过制作一个protoc插件解决了这个问题,该插件可以从给定的protobuf文件生成OpenAPI。然后我使用众多漂亮的工具之一来显示OpenAPI规范。这远比让我设计一个像样的文档容易得多。从protobuf生成OpenAPI的另一个附带好处是能够利用该生态系统,因为它不仅仅是文档。

让我们看一个真实的例子。这是使用少数几个从protobuf生成文档的工具之一,protoc-gen-doc生成的文档:

将其与一些OpenAPI工具链进行比较。这是使用Elements生成的,但还有许多其他同样精美的替代方案:

指责单个插件并说默认模板不如OpenAPI替代品好看有点不公平,因为实际上你确实在protoc-gen-doc中有更多的灵活性。它允许你指定自己的模板,因此它可以像你希望的那样漂亮。然而,这确实符合我的观点:在REST世界中,工具链比gRPC更完善和精致。这是一个可以解决的问题,但我们需要让前端开发者和设计师对gRPC感到兴奋,或者后端工程师需要开始磨练他们的设计技能。

我还想指出,OpenAPI/Swagger接口通常有一种方法可以直接从文档网站测试端点。这在gRPC世界中的等效工具中是完全缺失的。此外,使用大多数OpenAPI文档工具,你可以清楚地看到哪些字段是必填的,并会显示具有约束的字段。因此,它不仅更漂亮,而且功能也更强大。

结论

gRPC虽然在许多方面是一个强大的工具,但仍然有成长的空间。生成代码的不理想之处,加上依赖项管理和protobuf模式演变的挑战,可能会给开发者带来摩擦。缺乏直观的编辑器集成以及历史上对后端服务的关注,也阻碍了它在Web开发中的广泛采用。

然而,我认为gRPC的未来是光明的,并且可以变得不那么丑陋。社区正在积极应对这些挑战,开发诸如buf CLIprotovalidateprotoc-gen-connect-openapi等工具来弥合差距并增强开发者体验。随着gRPC的成熟和其生态系统的扩展,我们可以期待改进的工具链、更好的编辑器支持以及更顺畅地融入前端世界。


  • 本文长期链接
  • 如果您觉得我的博客对你有帮助,请通过 RSS订阅我。
  • 或者在X上关注我。
  • 如果您有Medium账号,能给我个关注嘛?我的文章第一时间都会发布在Medium。

【译】grpc:好的部分

2025-03-12 10:32:24


原文地址: https://kmcd.dev/posts/grpc-the-good-parts/

虽然 REST API 仍是 Web 服务开发的主流选择,但 gRPC 正凭借其卓越的性能、效率和开发体验,受到越来越多的青睐。你可能看过我的文章《gRPC 的不足之处》,其中提到了我对 gRPC 的一些不满。根据那篇文章的众多反馈,我本可以再写一篇续集来继续吐槽。但今天,我们换个角度,来探讨 gRPC 优秀 的一面。
显然,许多人没有读完上篇文章的结尾——我曾指出,文中提到的许多问题如今已不复存在。因此,我决定专门写一篇文章,聚焦 gRPC 的优势。
让我们深入探讨 gRPC 在现代 Web 开发中的关键价值。

性能

这一点可能会引发争议,但 Protocol Buffers 确实比 JSON 和 XML 更高效。多项测试都证明了这一点。Protobuf 的高效性主要体现在以下几个方面:

  • 字段名不包含在消息中:Protobuf 使用数字标识字段,而 JSON 需要存储完整的字段名。通常,Protobuf 的字段编号仅占 1-2 字节,而 JSON 的字段名可能远超这个大小。
  • VARINT 类型优化:小整数即使声明为 int64 也只需 1 字节。在大多数应用场景中,我们很少使用大整数,这种优化能显著减少数据占用,相比 ASCII 编码的数字更加高效。
  • 压缩优化:虽然 Protobuf 在字符串和字节数组方面并无特别优势,但 gRPC 支持数据压缩,使其至少能与 HTTP/JSON 方案持平。

在实际应用中,改用 Protobuf 编码后,我亲眼见证了数据传输量减少 50% 的效果。
当然,仍然有人质疑 Protobuf。对我而言,最“致命”的缺陷是「map 的值不能是另一个 map」。从实现角度来看,这本应是可行的,但实际上并不被支持。例如:

1
2
3
4
5
6
7
message MapMessage {
  map<string, MapValue> values = 1;
}

message MapValue {
  map<string, string> nested = 1;
}

最令人困惑的是,我不明白为什么 value_type 不能是 map。最终只能通过包装类型来嵌套 map,虽然可行,但略显繁琐。这类问题在 gRPC 中时有发生。哎,这明明是一篇夸奖 gRPC 的文章,我们回到正题。

总的来说,Protobuf 在许多方面优于 JSON。当然,如果你更喜欢 JSON,gRPC 也完全支持 JSON。虽然 gRPC 消息前会有少量二进制帧字节(不可读),但如果你真的在意这些,可以参考下文 ConnectRPC 章节。

此外,大多数 gRPC 实现都支持自定义编码,因此你甚至可以采用自定义的序列化方案。

API 契约

告别松散的 API 类型推测。gRPC 依靠 protobuf 定义,提供了严谨的客户端-服务端契约,带来诸多优势:

  • 更少错误:明确的数据类型要求减少了数据不匹配的风险。
  • 更好的代码生成:支持多语言客户端/服务端代码自动生成,节省开发时间。
  • 更顺畅的 API 演进:有了稳定的契约,API 迭代时不易破坏已有客户端。
  • 自动化文档生成:API 定义即文档,始终与实现保持同步。

API 契约的强大之处,在我的另一篇文章《用契约构建 API》中有更深入的讨论。

流式通信

gRPC 提供了一流的流式通信支持,消除了许多场景下的轮询需求,特别适用于:

  • 实时聊天应用:支持双向消息流,确保低延迟。
  • 实时更新:无需轮询,服务器可主动推送数据。
  • 需要持续通信的场景:如游戏、金融数据传输等。

如果你来自网络开发领域,可能知道基于 gRPC 的 gNMI 已取代 SNMP。通过 gNMI 订阅计数器更新,无需每分钟轮询网络设备。更多讨论可见《2024 年为何要选择 gNMI 而非 SNMP》。

跨语言支持

gRPC 天然支持多语言,几乎涵盖所有主流编程语言。借助代码生成工具,你可以在不同技术栈之间无缝集成。

这一特性极大提升了团队协作效率,也让开发者能自由选择最适合的工具。

推动 HTTP/2 发展

gRPC 是 HTTP/2 普及的有力推动者,借助 HTTP/2 提供:

  • 多路复用:单连接支持多个请求/响应,提升传输效率。
  • 头部压缩:减少冗余数据,提高传输速度。
  • 整体性能优化:HTTP/2 是更现代的 Web 通信方式。

HTTP/3 进展

gRPC 正在推进对 HTTP/3 的支持。尽管官方进展缓慢,但已有多个社区实现,如:

  • .NET 的 dotnet-grpc
  • Rust 的 Tonic(基于 Hyper)
  • Go 语言的 ConnectRPC 与 quic-go

HTTP/3 进一步优化了连接建立速度、解决了队头阻塞问题,并改善了丢包恢复能力。

逐步替换

若想逐步采用 gRPC 或需支持现有 REST 客户端,当前已有成熟方案:

JSON/HTTP 转码

使用 gRPC-GatewayGoogle Cloud EndpointsEnvoy 等工具,可以在后端享受 gRPC 优势的同时暴露 REST 风格接口。例如定义如下服务:

1
2
3
4
5
6
7
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloResponse) {
    option (google.api.http) = {
      get: "/v1/greeter/{name}"
    };
  }
}

即可通过 REST 端点访问:

1
curl http://localhost:8080/v1/greeter/world

这种自动转换能大幅减少支持多种 API 格式的工作量。

gRPC-Web

由于浏览器对 HTTP trailers 的支持限制,传统 gRPC 无法直接在 Web 使用。gRPC-Web 协议解决了这个问题,使浏览器也能使用 gRPC,并为仍在使用 HTTP/1.1 的平台(如某些 Unity 版本)提供支持。

ConnectRPC

ConnectRPC 能够从 gRPC 定义自动生成 JSON/HTTP API,同时保持与 gRPC/gRPC-Web 兼容。Connect 协议更严格遵循 HTTP 标准,支持如下直观的 curl 调用:

1
2
3
4
curl \
    --header "Content-Type: application/json" \
    --data '{"name": "world"}' \
    http://localhost:8080/greeter.v1.GreeterService/SayHello

Twirp

Twirp 由 Twitch 开发,采用类似思路。其规范通过 protobuf 生成更符合 HTTP 惯例的 API,但不直接支持 gRPC 协议,需要额外工作实现互操作。

工具生态

虽然官方工具链仍有不足,但社区生态蓬勃发展:

Buf CLI

Buf 公司推出的 Buf CLI 完全取代了官方的 protoc 编译器。它通过配置化管理 proto 文件依赖和代码生成,提供:

  • lint 检查:强制代码规范
  • 破坏性变更检测:防止协议不兼容修改
  • 简化工作流:替代 Makefile 等临时方案

第三方插件与工具

gRPC 拥有丰富的插件体系,比如:

结语

gRPC 以卓越的性能、强类型契约、流式通信、跨语言能力和 HTTP/2 基础,成为现代 Web 开发的强大工具。无论你是要优化 API 交互,还是构建高效可扩展的系统,gRPC 都值得一试。
随着生态的持续发展,gRPC 的未来充满可能。如果你追求快速、可靠的 API 设计,不妨深入了解并尝试 gRPC,它或许能彻底改变你的开发方式。


  • 本文长期链接
  • 如果您觉得我的博客对你有帮助,请通过 RSS订阅我。
  • 或者在X上关注我。
  • 如果您有Medium账号,能给我个关注嘛?我的文章第一时间都会发布在Medium。

网页图标(Icon)那些事

2025-02-18 11:30:38

Featured image of post 网页图标(Icon)那些事

昨天涛叔的博客 发布了一篇关于友情链接的博客,我毛遂自荐向涛叔请求添加友情链接。涛叔很快回应了我,并且在邮件中友好的提醒我,可以给博客添加一个favicon(icon),这样方便RSS订阅用户快速的区分博客。当时我心想 favicon 是什么?(后端程序员伤不起)

后面我咨询了DeepSeek:
在网页设计中,图标(icon)是一个小而重要的元素。它不仅帮助用户快速识别网站,还能提升用户体验。

常见使用场景:

  • 浏览器标签页:显示在网页标题旁边。
  • 书签栏:用户收藏网页时显示。
  • 主屏幕图标:移动设备将网页添加到主屏幕时显示。
  • PWA(渐进式网页应用):作为应用图标使用。
    比如这样
    在浏览器标签页展示图标
    在浏览器标签页展示图标。
    在书签栏显示图标。
    在书签栏显示图标。

甚至在安卓手机 上,使用chrome浏览器的将网页添加到主屏幕功能。可以显示icon图标。
使用 Android Chrome 将网页添加到手机主屏幕

如何设置

设置icon 最简单的方式是在 网页的 <head> 中添加 一行。

1
<link rel="icon" type="image/png" href="/favicon.png">

如果您是使用 hugo 或者其他工具的话,可能会有favicon的设置。
一些大型网站 比如 google.comapple.com 它们可能需要考虑的问题更多,设置也并不完全一样。

一些需要注意的地方

为了优化使用体验,在各个场景下都达到最佳的显示效果, icon的的尺寸也是有说法。

常见尺寸:

  • 16x16:浏览器标签页图标。
  • 32x32:书签栏图标。
  • 64x64:高分辨率屏幕图标。
  • 180x180:iOS 设备主屏幕图标。
  • 192x192 和 512x512:PWA 图标。
    所以我们在一些网站上会看到设置多个icon 的现象。比如 Hugo官网
    Hugo 官网设置了两个 icon

现代浏览器都支持根据不同的场景,屏幕的PPI 选择不同尺寸的图标,尽量做到所有场景下都达到最好的显示效果。

图标格式的选择

icon 可以使用不同的图片格式,通过 type指定即可,常见的图标格式包括:

  • ICO:传统格式,兼容性好,支持多尺寸。
  • PNG:现代格式,支持透明背景,适合高分辨率屏幕。
  • SVG:矢量格式,无限缩放不失真,适合响应式设计。

多合一 Icon

如果觉得需要维护多个 icon 文件 比较麻烦的话,可以使用多合一icon(Multi-Resolution ICO 或 Multi-Size ICO)是一种包含多种尺寸和色深的图标文件。允许在一个文件中存储多个位图(BMP 或 PNG 格式)。每个位图可以具有不同的尺寸。
ICO 文件包含:

  • 文件头(Header):定义 ICO 文件类型及包含的图像数量。
  • 图像目录(Image Directory):描述每个图像的尺寸、色深、偏移量等信息。
  • 图像数据(Image Data):实际存储图像像素数据。
    浏览器需要一个图标时,它会从 ICO 文件中选择最合适的尺寸。缺点就是包含多个尺寸的 .ico 文件可能会比单个尺寸的文件大。
    本篇文章没有继续深挖,比如 Android Chrome 独有的 manifest.json 苹果设备 apple-mobile-web-app-capable 等。 感兴趣的朋友可以继续深挖。

  • 本文长期链接
  • 如果您觉得我的博客对你有帮助,请通过 RSS订阅我。
  • 或者在X上关注我。
  • 如果您有Medium账号,能给我个关注嘛?我的文章第一时间都会发布在Medium。

【译】Wise工程:2025年技术栈更新

2025-02-13 09:46:39

原文链接:https://medium.com/wise-engineering/wise-tech-stack-2025-update-d0e63fe718c7

Wise工程:2025年技术栈更新

截至2024财年,Wise已经为1280万活跃客户提供服务,每季度处理的跨境转账金额高达300亿英镑。超过60%的转账实现了即时到账,我们的Wise平台为全球银行和非银行机构提供支付服务。这一成就离不开我们以技术为核心的理念、稳健的架构以及专注的工程团队。

Wise的工作方式

Wise在全球主要地点拥有850多名工程师,他们被组织成独立的小组和部落。这些团队被赋予了创新和独立决策的权力,促进了透明度、信任和协作。

本文基于我们2022年的技术栈,涵盖了Wise技术栈的最新改进,帮助我们实现无国界的资金流动——即时、便捷、透明,最终实现免费。

使用Wise转账

我们的网页和移动应用

我们的网页应用基于CRAB(Wise特有的抽象层,构建在流行的Next.js框架之上),包含40个独立的应用程序,每个应用负责特定的产品功能,使得部署更加安全和易于管理。

在测试方法上,我们最大的变化之一是引入了Storybook,用于在开发过程中可视化单个React组件。Storybook与Chromatic配合使用,能够在每次更改后捕获快照,并突出组件的视觉差异。这些快照在代码更改过程中非常有效,帮助我们防止错误影响到客户。

Wise移动应用:更快、更智能、更高效

我们的iOS工程师通过将250多个Xcode模块从Xcodegen迁移到Tuist,并将Cocoapods切换到Swift Package Manager(SPM),升级了基础设施,从而实现了构建缓存的改进。团队还提高了灵活性,将零变更构建时间从28秒减少到2秒。借助先进的构建缓存,开发变得更加顺畅,并朝着使用SPM的Swift可组合架构方向发展。

我们的Android工程师则专注于大规模应用开发。主要Android代码库包含300多个Gradle模块和大约100万行代码,涵盖2个生产应用、6个示例应用、17个JVM模块、221个Android模块和65个多平台模块。我们提高Android开发速度的努力集中在以下几个关键领域:

  • 使用更多的BFFs在Android、iOS和网页团队之间共享代码逻辑。
  • 基于KSP构建代码生成工具。
  • 探索Kotlin多平台的应用。

在用户界面方面,我们已经全面转向Compose——首先用于设计系统,现在用于整个屏幕和导航。我们迅速采用了Kotlin 2.0和2.1版本。为了处理异步任务,我们使用协程和流,而我们的架构遵循标准的MVVM模式,并得到Google的Jetpack库的支持。

后端服务

Wise总共运行超过1000个服务。在后端,我们主要使用Java和Kotlin。自上次更新以来,我们专注于通过开发内部工具来提高自动化和效率,从而加快开发速度,并提供跨不同服务使用的标准库。

更快构建优秀应用

自上次更新以来,我们一直专注于通过自动化代码更新和可扩展的依赖管理解决方案来实现大规模工程。为此,我们:

  • 引入了一个内部微服务底层框架,基于最小配置原则构建,并作为构件发布,使我们能够更快地构建标准微服务。它配置了服务使用的常见功能,提供推荐的默认设置:安全性、可观察性、数据库通信、与Kafka的交互等,使团队能够专注于业务逻辑。
  • 通过内部Gradle插件集合改善构建管道的标准化。一个显著的例子是我们的插件,它标准化了GitHub Actions工作流。这使得通过简单的插件版本更新实现组织范围内的工作流更改变得轻而易举,使得在700多个Java代码库中推出SLSA等倡议变得轻松。
  • 引入了一种语言无关的自动化服务,使我们能够在大规模上对代码库进行复杂更改,并为拥有团队创建拉取请求进行审查。通过使用该服务,我们进一步推进了集中式Java依赖管理平台,通过自动化Java服务的依赖升级。

直接与本地支付系统集成

我们已在菲律宾上线了即时支付系统InstaPay,并获得了加入日本即时支付系统Zengin的许可。我们还获得了巴西PIX的接入权限。

在Wise,我们投入了大量精力来创建尽可能一致的架构,网络通过AWS Transit Gateways集中管理。英国、匈牙利和澳大利亚的物理数据中心集成的细节存在显著差异。我们的澳大利亚数据中心是AWS Outpost Servers的首次部署之一,使我们能够在尽可能多的基础设施中保持一致的AWS工具。

允许企业使用我们的API

我们的公共API允许企业直接集成Wise的跨境支付服务,使用安全的REST API,支持OAuth认证。这为企业提供了转账、货币兑换和账户管理的功能,以及全面的文档和开发者工具,以简化集成过程。

Wise平台支持超过70种货币和多种支付路线,提供无缝的全球连接解决方案。该平台包括内置的合规功能,允许在利用Wise广泛的全球基础设施的同时,实现无缝的跨境操作。

扩展Wise的基础设施平台

为了适应快速增长,我们专注于重建基础设施,以确保效率和灵活性,同时减少团队的运营负担。

引入我们的新Kubernetes支持的计算运行时平台

**计算运行时平台(CRP)**是我们新的可扩展平台,利用Kubernetes,使工程团队能够轻松托管应用程序,而无需管理复杂的基础设施设置。

发展我们的Kubernetes栈

自2018年以来,Wise一直依赖于使用Terraform、JSONNET和ConcourseCI构建的Kubernetes,以支持服务网格控制(Envoy)、PCI-DSS合规性和无摩擦的部署。虽然这一模型为我们提供了良好的服务,但我们需要一种更可扩展和标准化的方法。这就是我们引入CRP的原因:

  • Terraform仍然负责基础设施的配置,但我们从头开始重写了代码库,以提高灵活性和可维护性。
  • RKE2处理集群引导,Rancher管理整体集群状态。
  • Helm取代JSONNET,以提高可维护性和上游兼容性。
  • ArgoCD与自定义插件确保完全自动化的配置和一致性。
  • 我们的Envoy服务代理现在包括服务之间的无缝集成和发现,提高了平台的灵活性、弹性和监督。

因此,我们的Kubernetes集群数量从6个增长到超过20个,同时保持可维护性和效率。

更智能的自动扩展和成本优化

除了更好地配置和维护基础设施的能力外,我们还通过CRP引入了效率改进:

  • 我们正在构建一个灵活的、可选择的自动扩展解决方案,以降低云成本和团队的认知负担。
  • 自动化容器CPU调整大小(通过垂直Pod自动扩展器)现在在非生产环境中上线,并正在向非关键工作负载的生产环境推广。
  • 完全托管的边车容器(如Envoy代理)现在简化了产品团队的部署。
  • 扩展水平扩展,使用KEDA,根据每日和每周的流量模式优化工作负载。

对成本优化的关注使Wise更接近于零任务

构建可扩展、可靠和智能的数据基础设施

在Wise,我们的许多工作都与数据的移动和理解有关。无论是转账、更新实时仪表板,还是为后台的机器学习模型提供动力,我们的系统都在不断处理和分发大量信息。随着我们全球足迹的扩大,我们对更快、更安全和更灵活的数据处理方式的需求也在增加。以下是我们如何发展数据技术栈,以继续为客户提供可靠、便捷和高效的体验的快速概述。

为我们的数据骨干提供动力

在Wise,我们的数据库是我们所有工作的基础之一,因此我们在使其既稳健又易于管理方面投入了大量精力。在幕后,我们的数据库工程师正在解决一些引人入胜的技术挑战,推动金融数据管理的可能性。

  • 我们努力将大部分MariaDB和Postgres工作负载从EC2迁移到Amazon RDS。这一转变减少了维护任务,降低了运营开销,并提供了更强大的安全功能。
  • 同样,我们正在将自托管的MongoDB迁移到MongoDB Atlas,这使我们能够专注于构建新功能,而不是与扩展作斗争。
  • Redis继续为我们的内存工作负载提供支持。
  • 我们还在探索分布式数据库,以实现更大的关系可扩展性。

更智能的工作流编排和可观察性

  • 我们采用了一种名为Temporal的工作流引擎,以自动化关键任务,如切换和恢复测试。这有助于我们将停机时间降至最低,并保持符合严格的弹性法规。
  • 像RDS性能洞察和Percona监控与管理(PMM)这样的工具为我们提供了清晰的数据库运行状况视图,使我们能够及早解决问题。
  • 我们还在尝试使用直接的云SDK来管理我们的基础设施——逐步从Terraform Enterprise转向简化我们的配置流程。

保持数据流动

  • Kafka支撑着我们大多数实时数据的移动——无论是服务之间的异步消息传递、日志收集,还是分析的流式更新。
  • 我们的Kafka集群容量显著增长,并引入了诸如机架感知备用副本等功能,以提高容错能力。
  • 我们的内部数据移动服务帮助将信息从Kafka或数据库引导到Snowflake、S3 Parquet、Iceberg或其他目标。
  • 配置过程中的自动检查减少了人为错误,其日益增长的使用表明团队发现设置新管道变得更简单、更快捷。
  • 另一个内部服务,数据归档,现在在多个数据库中归档超过1000亿条记录。这不仅节省了成本,还使我们的数据库更易于备份和恢复。

将数据转化为洞察

Wise各团队使用我们的商业智能工具做出战略性、数据驱动的决策,以提升客户体验——从欺诈检测到个性化营销和预测分析。

  • 尽管我们仍然依赖Snowflake作为分析的核心组成部分,但我们已经在Amazon S3上建立了数据湖的基础,使用Apache Iceberg。得益于其强大的开放表格式,Apache Iceberg使我们能够更高效地在S3上存储大量数据。它允许我们在不需要重写所有数据的情况下修改表结构,从而加快查询速度并控制存储成本。此外,其活跃的开源社区不断推动改进,惠及我们的长期可扩展性。
  • 在我们的数据源和商业智能工具之间是Trino,它使我们能够在一个地方查询Iceberg表、Snowflake或甚至Kafka流。
  • 一个新的Trino网关处理工作负载分离和容错查询,而复杂的工作流仍由Airflow和dbt-core管理。有关此主题的深入了解,请观看我们数据工程师最近的会议演讲
  • 报告和仪表板使用Looker或Superset构建——团队选择最适合的工具集。

推动智能解决方案

我们的机器学习架构旨在支持探索和生产,无缝集成机器学习功能到产品中,以改善客户入职和欺诈预防,并利用负责任的人工智能技术。

  • 我们的数据科学家在Amazon SageMaker Studio中工作,选择JupyterLab或VSCode来构建实验和探索数据。
  • 大规模处理在EMR上使用Spark进行,而Airflow则协调数据收集、清理、模型训练和定期再训练,以保持每个步骤按计划进行。
  • 我们使用SageMaker特征存储来保持数百个特征在训练和推理中的同步,MLflow跟踪实验、指标和模型版本。这种设置简化了模型变体的比较或在需要时的回滚。
  • 当模型准备好投入生产时,我们通过基于Ray Serve的内部预测服务进行部署。
  • 多亏了MLflow插件,我们的数据科学家可以以最小的摩擦推出模型——加快欺诈检测、KYC或其他用例的推理时间,在这些情况下每毫秒都至关重要。
  • 自动检查有助于在数据漂移或特征不一致变成严重问题之前捕捉到它们。

解锁新的人工智能能力

我们创建了一个安全的网关,连接多个大型语言模型提供商,包括Anthropic(Claude)、AWS(Bedrock)、Google(Gemini)和OpenAI(gpt和o系列)。这种方法使我们能够在不处理单独凭证或复杂合规检查的情况下实验不同的模型。一个受LangChain启发的Python库封装了这些API,以加快原型设计。

对于需要引用内部文档、知识库或用户数据的情况,我们提供了一个自定义的检索增强生成(RAG)服务。它在生成响应之前从各种数据存储中提取最新信息——这是总结复杂文档或自动化客户服务工作流的便捷功能。

智能数据管理

我们的数据架构既庞大又复杂,因此我们建立了一个全面的库存系统和专门的治理门户,以显示数据存储的位置及其分类。

我们已在整个数据资产中实现了自动化数据发现,以了解创建了什么数据;谁创建了它;以及数据的类别是什么。我们正在利用我们的数据库存来支持数据删除、数据合规和数据发现的工作。这种设置不仅支持审计和法规的合规工作,还提高了开发者的生产力。

随着越来越多的工程师加入治理工作,我们能够推出更严格的政策、增强的隐私检查和自动化的数据生命周期管理。

开发者赋能——Wise的CI/CD演变

为了加强我们的交付管道和开发者体验,我们不断发展我们的CI/CD平台,以使开发者能够比以往更快、更可靠地将功能交付给客户。

CI改进:速度和安全性

从CircleCI迁移到GitHub Actions带来了优化的新可能性。通过实施详细的指标跟踪,我们发现了构建性能的关键见解。例如,通过预填充常用容器的缓存,我们将构建时间缩短了15%在我们每月50万次构建的规模下,这相当于每月节省超过1000小时。

我们一直在有条不紊地在我们的构建过程中实施SLSA框架,逐步加强我们的供应链安全。

CD转型:从Octopus到Spinnaker

在我们之前的文章中提到的CI/CD管道状态之后,我们的部署策略随着从Octopus(我们的内部工具)转向Spinnaker而发生了变化。这不仅仅是工具的更换——它代表了一种范式转变,从将部署视为简单的事务转变为将其视为有序事件序列。

这一转变使我们能够减少工程师在部署管理上花费的时间,并最小化缺陷到达客户的风险。这提高了开发者的交付速度,使我们能够更快地为客户提供服务,而不牺牲质量和稳定性。

高级金丝雀测试

Spinnaker的自动金丝雀分析已成为我们部署管道的基石。该过程在其简单性中优雅而强大:

  • 仅5%的流量路由到新服务版本进行测试
  • 对技术和业务指标进行全面的30分钟分析
  • 对重大异常触发自动回滚

因此,仅在2024年,这一系统自动阻止了数百次可能导致事件的部署,节省了数千小时的工程时间。

目前,Wise的超过一半服务已在Spinnaker上运行,预计到2025年中期将完成全面迁移,我们准备迈出下一步:实施托管交付,以协调整个SDLC,包括测试和数据管理。

LGTM堆栈的可观察性

我们改善了可观察性生态系统,以更好地监控、理解和优化Wise产品。可靠性工程师专注于构建一个更强大、高效和富有洞察力的可观察性平台,以应对我们快速扩展环境中的关键挑战。

专用的可观察性基础设施

我们实施了专用的可观察性CRP集群。这为在不同环境中运行的服务提供了开箱即用的可观察性。因此,我们简化了监控设置,减少了手动配置的负担。

统一的指标和监控堆栈

为了解决可扩展性问题,我们已从Thanos迁移到Grafana Mimir。这意味着我们现在完全运行在LGTM堆栈上:Loki用于日志,Grafana用于仪表板和可视化,Tempo用于跟踪,Mimir用于指标。作为我们在可观察性方面持续改进的一部分,我们正在试点测试Grafana Pyroscope,以对选定服务进行分析,探索性能洞察和优化的新维度。
我们的指标堆栈每秒接收约600万个指标样本,并处理我们最大指标租户的1.5亿个活动系列。
通过统一我们的堆栈,我们:

  • 在整个技术生态系统中标准化可观察性。
  • 增强日志、指标、跟踪和仪表板之间的关联。
  • 改善监控基础设施的性能和可扩展性。

可观察性的成本优化和效率

最后,我们继续投资于优化我们的可观察性堆栈。我们能够降低运营成本,提高资源利用率,并最终拥有更可持续的长期可观察性战略。请查看我们之前的文章,详细介绍了我们在这些倡议上所做的工作。

这一战略演变使我们的工程团队能够获得更深入、更具可操作性的洞察,同时确保我们的可观察性基础设施既强大又具有成本效益。

结论

总之,我们2025年的技术栈证明了Wise如何引领潮流,为全球1280万活跃客户提供最快、最可靠和最具成本效益的资金转移方式。对标准化和集成的高度关注意味着我们的系统旨在高效扩展,同时确保稳健的风险和合规管理。

我们的工程团队继续在所有领域精炼我们的基础设施,从移动和网页应用到后端服务和机器学习。这些努力简化并加速了跨境资金流动,确保我们为当前需求和未来增长做好准备。

我们致力于长期投资,构建最佳基础设施,以无缝管理您在全球范围内的资金。随着每一次技术增强和与支付系统的新直接连接,我们正稳步朝着实现无国界资金流动的愿景迈进。

Go1.24: mutex自旋优化,最大提升70%的性能

2025-02-11 11:16:36

Featured image of post Go1.24: mutex自旋优化,最大提升70%的性能

背景

Rhys Hiltner 在 2024 年提出了改进互斥锁的性能优化诉求。现在这个优化已经合并到即将发布的Go1.24中,在锁竞争激烈的场景下最多会提升70%的性能。
source https://github.com/golang/go/issues/68578

在基准测试 ChanContended 中,作者发现随着 GOMAXPROCS 的增加,mutex 的性能明显下降。
Intel i7-13700H (linux/amd64):

  • 当允许使用 4 个线程时,整个进程的吞吐量是单线程时的一半。
  • 当允许使用 8 个线程时,吞吐量再次减半。
  • 当允许使用 12 个线程时,吞吐量再次减半。
  • GOMAXPROCS=20 时,200 次channel操作平均耗时 44 微秒,平均每 220 纳秒调用一次 unlock2,每次都有机会唤醒一个睡眠线程。
    另一个角度是考虑进程的 CPU占用时间。
    下面的数据显示,在 1.78 秒的Wall-Clock Time内,进程的20个线程在lock2调用中总共有27.74秒处于CPU(自旋)上。

这些 lock2 相关的线程并没有休眠,而是一直在自旋,这将消耗大量的CPU资源。

新提案:增加spinning状态

通过上述的分析,作者发现在当前的lock2实现中,虽然理论上允许线程睡眠,但实际上导致所有线程都在自旋,导致了更慢的锁传递,带来了不少的性能损耗。
于是提出了新的设计方案《Proposal: Improve scalability of runtime.lock2》

核心优化点

mutex 的状态字添加了一个个新的标志位,称为 “spinning”

1
2
3
4
5
6
7
https://github.com/golang/go/blob/608acff8479640b00c85371d91280b64f5ec9594/src/runtime/lock_spinbit.go#L57
const (
	mutexLocked = 0x001
	mutexSleeping = 0x002
	mutexSpinning = 0x100
	...
)

使用这个 spinning位来表示是否有一个等待的线程处于 “醒着并循环尝试获取锁” 的状态。线程之间会互相排除进入 spinning状态,但它们不会因为尝试获取这个标志位而阻塞。

metux 的介绍可以参考以前的文章
https://pub.huizhou92.com/go-source-code-sync-mutex-3082a25ef092

Mutex 获取锁分析

1. 快速路径尝试获取锁

1
2
3
4
5
6
7
8
9
//https://github.com/golang/go/blob/adc9c455873fef97c5759e4811f0d9c8217fe27b/src/runtime/lock_spinbit.go#L160
k8 := key8(&l.key)
v8 := atomic.Xchg8(k8, mutexLocked) 
if v8&mutexLocked == 0 {
    if v8&mutexSleeping != 0 {
        atomic.Or8(k8, mutexSleeping)
    }
    return
}

fast 模式跟以前变化不大。如果成功(锁之前未被持有)则快速返回。这是最理想的情况,无竞争时的快速路径。

2. 自旋等待阶段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//https://github.com/golang/go/blob/adc9c455873fef97c5759e4811f0d9c8217fe27b/src/runtime/lock_spinbit.go#L208
if !weSpin && v&mutexSpinning == 0 && atomic.Casuintptr(&l.key, v, v|mutexSpinning) {
    v |= mutexSpinning 
    weSpin = true
}

if weSpin || atTail || mutexPreferLowLatency(l) {
    if i < spin {
        procyield(mutexActiveSpinSize)  //主动自旋
        // ...
    } else if i < spin+mutexPassiveSpinCount {
        osyield() //被动自旋
        // ...
    }
}
  • 如果快速路径失败,进入自旋等待阶段。
  • 通过 mutexSpinning 标志控制同时只允许一个 goroutine 自旋
  • 自旋分为procyield与osyield,两者的区别是:procyield会持续占有CPU,响应会更快,适合极短时间的等待,osyield会临时释放CPU,响应较慢,但是不会占用较多CPU,适用于较长时间的等待。
    这种两阶段自旋设计能够在不同竞争强度下都保持较好的性能表现。
  • 轻度竞争时主要使用主动自旋,保证低延迟
  • 重度竞争时快速进入被动自旋,避免CPU资源浪费

休眠等待阶段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//https://github.com/golang/go/blob/adc9c455873fef97c5759e4811f0d9c8217fe27b/src/runtime/lock_spinbit.go#L231
// Store the current head of the list of sleeping Ms in our gp.m.mWaitList.next field
gp.m.mWaitList.next = mutexWaitListHead(v)

// Pack a (partial) pointer to this M with the current lock state bits
next := (uintptr(unsafe.Pointer(gp.m)) &^ mutexMMask) | v&mutexMMask | mutexSleeping
if weSpin {
    next = next &^ mutexSpinning
}

if atomic.Casuintptr(&l.key, v, next) {
    weSpin = false
    semasleep(-1)
    atTail = gp.m.mWaitList.next == 0
    i = 0
}

如果自旋失败,goroutine 将进入休眠等待,然后将当前 M 加入等待队列(通过 mWaitList 链表),通过信号量(semasleep)使当前 goroutine 进入休眠,等待持有锁的 goroutine 在解锁时唤醒。

当某个线程解锁互斥锁时,如果发现已经有线程处于 “醒着并旋转” 的状态,就不会唤醒其他线程。在 Go 运行时的背景下,这种设计被称为 spinbit
这个设计的核心目的是:通过让一个线程负责 “旋转尝试获取锁”,避免所有线程都同时竞争资源,从而减少争用和不必要的线程切换

效果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
goos: linux
goarch: amd64
pkg: runtime
cpu: 13th Gen Intel(R) Core(TM) i7-13700H
                │     old     │                  new                  │
                │   sec/op    │    sec/op     vs base                 │
ChanContended      3.147µ ± 0%    3.703µ ± 0%   +17.65% (p=0.000 n=10)
ChanContended-2    4.511µ ± 2%    5.280µ ± 7%   +17.06% (p=0.000 n=10)
ChanContended-3    5.726µ ± 2%   12.125µ ± 2%  +111.75% (p=0.000 n=10)
ChanContended-4    6.574µ ± 1%   13.356µ ± 4%  +103.16% (p=0.000 n=10)
ChanContended-5    7.706µ ± 1%   13.717µ ± 3%   +78.00% (p=0.000 n=10)
ChanContended-6    8.830µ ± 1%   13.674µ ± 2%   +54.85% (p=0.000 n=10)
ChanContended-7    11.07µ ± 0%    13.59µ ± 2%   +22.77% (p=0.000 n=10)
ChanContended-8    13.99µ ± 1%    14.06µ ± 1%         ~ (p=0.190 n=10)
ChanContended-9    16.93µ ± 2%    14.04µ ± 3%   -17.04% (p=0.000 n=10)
ChanContended-10   20.12µ ± 4%    14.12µ ± 1%   -29.80% (p=0.000 n=10)
ChanContended-11   23.96µ ± 2%    14.44µ ± 3%   -39.74% (p=0.000 n=10)
ChanContended-12   29.65µ ± 6%    14.61µ ± 3%   -50.74% (p=0.000 n=10)
ChanContended-13   33.98µ ± 7%    14.69µ ± 3%   -56.76% (p=0.000 n=10)
ChanContended-14   37.90µ ± 1%    14.69µ ± 3%   -61.23% (p=0.000 n=10)
ChanContended-15   37.94µ ± 4%    14.89µ ± 5%   -60.75% (p=0.000 n=10)
ChanContended-16   39.56µ ± 0%    13.89µ ± 1%   -64.89% (p=0.000 n=10)
ChanContended-17   39.56µ ± 0%    14.45µ ± 4%   -63.47% (p=0.000 n=10)
ChanContended-18   41.24µ ± 2%    13.95µ ± 3%   -66.17% (p=0.000 n=10)
ChanContended-19   42.77µ ± 5%    13.80µ ± 2%   -67.74% (p=0.000 n=10)
ChanContended-20   44.26µ ± 2%    13.74µ ± 1%   -68.96% (p=0.000 n=10)
geomean            17.60µ         12.46µ        -29.22%


source https://github.com/golang/go/issues/68578#issuecomment-2256792628

虽然在竞争较少的情况下,性能有降低,但是在竞争比较多的地方性能提升显著。平均来说,大约获得 29%的性能提升。期待后续能够优化这种情况吧。

mutex本次修改没涉及API层面改动,所以只要等 Go1.24 正式发布就能自动使用了。该特性通过GOEXPERIMENT=spinbitmutex 来控制,默认是开启的,也可以将它关闭,来使用原来的Mutex。


  • 本文长期链接
  • 如果您觉得我的博客对你有帮助,请通过 RSS订阅我。
  • 或者在X上关注我。
  • 如果您有Medium账号,能给我个关注嘛?我的文章第一时间都会发布在Medium。