发布在即!企业 AIGC 应用程度测评,3 步定制专属评估报告。抢首批测评权益>>> 了解详情
写点什么

Segment 放弃了微服务

  • 2019-05-27
  • 本文字数:5393 字

    阅读完需:约 18 分钟

Segment放弃了微服务

除非你一直生活在石器时代,否则你可能已经知道微服务是当前流行的架构。随着这一趋势的发展,Segment在早期就将其作为一种最佳实践,在某些情况下,这对我们很有帮助,但你很快就会了解到,在其他情况下效果并不好。


简单来说,微服务是一种面向服务的软件架构,在这种架构中,服务器端应用程序是通过组合许多单用途、低空间占用的网络服务来构建的。人们极力宣扬的好处是改进模块化、减少测试负担、更好的功能组合、环境隔离和开发团队自治。与之相反的是单体架构,大量的功能存在于一个服务中,该服务作为一个单元进行测试、部署和扩展。


2017 年初,Segment 的一个核心产品达到了一个临界点。这就像我们从微服务的树上掉下来,并在下落的过程中砸到每根树枝一样。小团队没有让我们更快地前进,相反,我们发现自己陷入了复杂性爆炸的泥潭。这种架构的基本好处变成了负担。我们的速度急剧下降,我们的缺陷率却呈现爆炸式增长。


团队最终发现,他们无法取得进展,3 名全职工程师为了维持系统的运行花费了大部分的时间。有些事情必须改变。这篇文章讲述的是我们如何后退一步,采用一种可以很好地满足我们的产品需求和团队需求的方法。

为什么微服务曾经有效?

Segment 的客户数据基础设施每秒接收数十万个事件,并将它们转发给合作伙伴 API,我们称之为服务器端目标。这些目标有 100 多种类型,比如谷歌 Analytics、Optimizely 或自定义 webhook。


几年前,当产品最初发布时,架构很简单。有一个 API 接收事件并将其转发到分布式消息队列。在本例中,事件是 Web 或移动应用程序生成的 JSON 对象,其中包含关于用户及其操作的信息。下面是一个有效载荷示例:


{  "type": "identify",  "traits": {    "name": "Alex Noonan",    "email": "anoonan@segment.com",    "company": "Segment",    "title": "Software Engineer"  },  "userId": "97980cfea0067"}
复制代码


当从队列中消费事件时,将检查客户管理设置,以确定哪些目标应该接收事件。然后将事件一个接一个地发送到每个目标 API,这非常有用,因为开发人员只需要将事件发送到单个端点,即 Segment 的 API,而不需要构建几十个可能的集成。Segment 向每个目标端点发出请求。


如果对一个目标的某个请求失败,有时我们将尝试稍后再次发送该事件。有些失败可以安全地重试,而有些则不能。可重试错误是指目标可能在不做任何更改的情况下接受的错误。例如,HTTP 500、速率限制和超时。不可重试错误是我们可以确定目标永远不会接受的请求。例如,具有无效凭据或缺少必需字段的请求。



此时,一个队列既包含了最新的事件,也包含所有目标的那些可能已经多次重试的事件,这些事件导致了队头阻塞。在这种特殊情况下,如果一个目标变慢或宕机,重试将淹没队列,导致所有目标延迟。


假设目标 X 遇到一个临时问题,每个请求都有一个超时错误。现在,这不仅创建了尚未到达目标 X 的大量请求的积压列表,而且还将每个失败事件放回队列中重试。虽然我们的系统会根据负载的增加自动向上扩展,但是队列深度的突然增加会超过我们的扩展能力,从而导致最新事件的延迟。所有目标的交付时间都将因为目标 X 发生了短暂停机而增加。客户依赖于该交付的及时性,因此,我们无法承受在管道中的任何地方增加等待时间。



为了解决队头阻塞问题,团队为每个目标创建了单独的服务和队列。这个新的架构包括一个额外的路由器进程,它接收入站事件并将事件的副本分发到每个选定的目标。现在,如果一个目标遇到问题,只有它的队列会积滞,其他目标不会受到影响。这种微服务风格的架构将目标彼此隔离,当有目标经常遇到问题时,这一点至关重要。


单代码库的情况

每个目标 API 使用不同的请求格式,需要自定义代码转换事件以匹配这种格式。一个基本的例子是目标 X 需要在有效载荷中发送生日 traits.dob,而我们的 API 以 traits.birthday 接收。目标 X 中的转换代码应该是这样的:


const traits = {}traits.dob = segmentEvent.birthday
复制代码


许多现代化的目标端点都采用了 Segment 的请求格式,这使得一些转换相对简单。但是,根据目标 API 的结构,这些转换可能非常复杂。例如,对于一些较老且分布最广的目标,我们得自己将值硬塞进手工编写的 XML 有效负载中。


最初,当目标被划分为单独的服务时,所有代码都存在于一个库中。一个非常令人沮丧的地方是,一个失败的测试导致所有目标的测试失败。当我们想要部署一个变更时,我们必须花费时间来修复受损的测试,即使变更与最初的变更没有任何关系。针对这个问题,我们决定将每个目标的代码分解为各自的库。所有的目标都已经被划分为各自的服务,这种转换很自然。


分割库使我们能够轻松地隔离目标测试套件。这种隔离允许开发团队在维护目标时快速前进。

扩展微服务和代码库

随着时间的推移,我们增加了 50 多个新目标,这意味着 50 个新的库。为了减轻开发和维护这些代码库的负担,我们创建了共享库,使跨目标的通用转换和功能(如 HTTP 请求处理)更容易、更统一。


例如,如果我们希望从事件中获得用户名,则可以在任何目标的代码中调用event.name()。共享库检查事件的属性键 name。如果不存在,它将检查名字,检查属性 firstName、first_name 和 firstName。它对姓氏执行相同的操作,检查大小写并将两者组合起来形成全名。


Identify.prototype.name = function() {  var name = this.proxy('traits.name');  if (typeof name === 'string') {    return trim(name)  }    var firstName = this.firstName();  var lastName = this.lastName();  if (firstName && lastName) {    return trim(firstName + ' ' + lastName)  }
复制代码


共享库使构建新目标变得更快。一组统一的共享功能带来的熟悉度使维护变得不那么麻烦。


然而,一个新的问题开始出现。测试和部署这些共享库的变更影响了我们所有的目标。它开始需要相当多的时间和精力来维护。我们知道,通过变更来改进我们的库需要测试和部署几十个服务,这是一个冒险的提议。如果时间紧迫,工程师们将只在单个目标的代码库中包含这些库的更新后版本。


随着时间的推移,这些共享库的版本开始在不同的目标代码库之间产生差异。曾经,减少目标代码库之间的定制让我们获得了巨大的好处,现在情况开始反转。最终,它们都使用了这些共享库的不同版本。我们本可以构建一些工具来自动化滚动变更过程,但在这一点上,不仅开发人员的生产效率受到影响,而且我们开始遇到微服务架构的其他问题。


另外一个问题是,每个服务都有不同的负载模式。一些服务每天处理少量事件,而另一些服务每秒处理数千个事件。对于处理少量事件的目标,当出现意外的负载高峰时,运维人员必须手动扩展服务以满足需求。


虽然我们实现了自动伸缩,但是每个服务都有不同的 CPU 和内存资源,这使得自动伸缩配置调优更像是艺术而不是科学。


目标的数量继续快速增长,团队平均每月增加三个目标,这意味着更多的代码库、更多的队列和更多的服务。在我们的微服务架构中,我们的运维开销随着目标的增加而线性增加。因此,我们决定后退一步,重新考虑整个管道。

抛弃微服务和队列

清单上的第一项是将现有的 140 多个服务合并为一个服务。管理所有这些服务的开销对我们的团队来说是一个巨大的负担。我们几乎为此失眠,因为我们这些随叫随到的工程师要经常处理负载峰值。


然而,当时的架构使迁移到单个服务变得颇具挑战性。由于每个目标都有一个单独的队列,每个工作进程必须检查每个队列是否有工作,这将给目标服务增加一层复杂性,而我们对这种复杂性感到不适。这是Centrifuge的主要灵感来源。Centrifuge 将替换所有单独的队列,并负责将事件发送到一个单体服务。


迁移到单一代码库

假设只有一个服务,那么将所有目标的代码移动到一个代码库中很容易理解,这意味着将所有不同的依赖关系和测试合并到一个代码库中。我们知道这会很乱。


对于 120 个独一无二的依赖项中的每一个,我们承诺为所有目标提供一个版本。当我们迁移目标时,我们会检查它使用的依赖项,并将它们更新到最新版本。我们修复了与新版本发生冲突的所有目标。


通过这次迁移,我们不再需要跟踪依赖项版本之间的差异。我们所有的目标都使用相同的版本,这大大降低了整个代码库的复杂性。现在,目标维护变得更省时、风险更小。


我们还需要一个测试套件,它允许我们快速、轻松地运行所有的目标测试。在更新我们前面讨论的共享库时,运行所有测试是主要的障碍之一。


幸运的是,目标测试都具有类似的结构。它们有基本的单元测试来验证我们的自定义转换逻辑是否正确,并将执行到合作伙伴端点的 HTTP 请求来验证事件是否如预期的那样出现在目标中。


回想一下,将每个目标代码库划分到它自己的代码库中,最初的动机是为了隔离测试失败。然而,事实证明这是一个不成立的优势。发出 HTTP 请求的测试仍然以一定的频率失败。由于目标被划分到它们自己的代码库中,所以我们几乎没有动力去清理失败的测试。这种不良习惯导致了源源不断的技术债务。通常,一个原本只需要一两个小时就能完成的小变更,最终可能需要几天到一周的时间才能完成。

构建一个有弹性的测试套件

测试运行期间对目标端点的出站 HTTP 请求是测试失败的主要原因。不相关的问题,比如凭证过期,不应该导致测试失败。根据经验,我们还知道,有些目标端点比其他端点慢很多。有些目标运行测试的时间长达 5 分钟。我们有超过 140 个目标,我们的测试套件可能需要一个小时来运行。


为了解决这两个问题,我们创建了 Traffic Recorder。它基于yakbak构建,负责记录和保存目标的测试流量。每当测试第一次运行时,任何请求及其相应的响应都会被记录到一个文件中。随后的测试将回放文件中的请求和响应,而不是向目标端点发送请求。这些文件被检入代码库中,以保证每次变更时测试都是一致的。现在,测试套件不再依赖于互联网上的这些 HTTP 请求,我们的测试变得更有弹性,这是迁移到单个代码库的必要条件。


我还记得,在我们集成了 Traffic Recorder 之后,第一次针对每个目标运行测试。完成针对所有 140 多个目标的测试需要几毫秒的时间。在过去,一个目标可能就需要几分钟才能完成。感觉就像魔法一样。

为什么单体架构有效?

一旦所有目标的代码都存在于一个代码库中,就可以将它们合并到一个服务中。由于每个目标都存在于一个服务中,我们的开发人员的工作效率得到了显著提升。我们不再需要因为变更一个共享库而部署 140 多个服务。一个工程师可以在几分钟内完成服务部署。


证据就在于改进的速度。2016 年,当我们还在使用微服务架构时,我们对共享库进行了 32 次变更。而今年,我们已经做了 46 项改进。过去 6 个月,我们对库的改进比 2016 年全年都要多。


这种变化也使我们的运营从中受益。由于每个目标都存在于一个服务中,我们很好地组合了 CPU 密集型和内存密集型目标,这使得扩展服务以满足需求变得非常容易。大型工作池可以承受负载峰值,因此,我们不再为处理少量负载的目标分页。

妥协

从微服务架构到整体的单体架构是一个巨大的改进,但是,也有一些妥协:


  1. 故障隔离很困难。由于所有内容都在一个整体中运行,如果在一个目标引入了导致服务崩溃的 Bug,那么所有目标服务都会崩溃。我们有全面的自动化测试,但测试有其局限性。我们目前正在研究一种更加健壮的方法,以防止一个目标使整个服务宕掉,同时又保持所有目标都在一个单体中。

  2. 内存缓存的效率较低。以前,每个目标一个服务,我们的低流量目标只有少数几个进程,这意味着它们的控制平面数据的内存缓存将保持热状态。现在,缓存被分散到 3000 多个进程中,所以它命中的可能性要小得多。我们可以用像 Redis 这样的东西来解决整个问题,但这是另一个我们需要考虑的扩展点。最后,我们接受了这种效率的损失,因为它带来了巨大的运营效益。

  3. 更新依赖项的版本可能会破坏多个目标。虽然将所有内容都迁移到一个代码库中解决了之前的依赖关系混乱问题,但这意味着如果我们想要使用库的最新版本,我们可能必须更新其他目标。然而,在我们看来,这种方法的简单性是值得做出这种妥协的。通过全面的自动化测试套件,我们可以很快地看到更新的依赖项版本带来了什么破坏。

小结

我们最初的微服务架构在一段时间内是有效的,通过将目标彼此隔离来解决管道中的即时性能问题。然而,我们并没有做好扩展准备。当需要大量更新时,我们缺乏测试和部署微服务的适当工具。结果,我们的开发人员的生产效率迅速下降。


迁移到一个单体架构中,可以在显著提高开发人员生产力的同时,消除运维问题。不过,我们并没有轻率地实施这次迁移。我们知道,如果要成功,有些事情是必须考虑的。


  1. 我们需要一个健壮的测试套件,把所有的东西都放在一个代码库中。如果没有这个,我们就会和当初决定把它们分开时一样。在过去,不断失败的测试损害了我们的生产力,我们不希望这种情况再次发生。

  2. 我们接受了单体架构中需要做出的妥协,并确保每个方面都有一个好的故事。我们必须适应这种变化带来的一些牺牲。


在决定采用微服务还是单体服务时,需要考虑不同的因素。在我们的基础设施的某些部分,微服务工作得很好,但是我们的服务器端目标是这种流行趋势如何实际损害生产力和性能的一个完美示例。原来,我们的解决方案是一个单体架构。


Stephen MathiesonRick BransonAchille RousselTom Holmes等人促成了向单体架构的转变。


特别感谢Rick Branson在每一个阶段都帮助审阅和编辑这篇文章。


查看英文原文:Goodbye Microservices: From 100s of problem children to 1 superstar


2019-05-27 11:5118112
用户头像

发布了 683 篇内容, 共 391.1 次阅读, 收获喜欢 1498 次。

关注

评论 20 条评论

发布
用户头像
左转、右转,都有道理的,需要折腾,过几年换个包装还会折腾的,只要活得够久就能看到。
2021-06-24 11:09
回复
用户头像
先吐槽一下翻译水平,都是什么鬼。。。。看作者描述的主要问题是1、公共库的维护和测试 2、不同微服务的自动伸缩没有一个统一的标准和方案(然而为什么要有统一的方案呢,微服务不就是为了区别对待吗) ,各位同仁在解决以上问题的时候有什么好的方案?
2019-06-06 10:17
回复
用户头像
异步可以解决流量峰值问题,除此以外,微服务架构(或者说任何分布式架构)其实都是严重损害伸缩性的,或者说大幅度提高了伸缩的成本的。 所以说 为了可伸缩性而才有微服务 根本就是无稽之谈!
2019-06-05 22:14
回复
用户头像
不要滥用微服务,没有银弹。使用微服务前先想想业务场景适合吗,项目规模够大吗,技术人员准备好了吗,领域能划分清楚吗。千万不要给自己挖坑,说多了都是泪,推荐看下这篇文章https://adamdrake.com/enough-with-the-microservices-chinese-translation.html
2019-06-05 17:01
回复
用户头像
正如作者所说的“当需要大量更新时,我们缺乏测试和部署微服务的适当工具。结果,我们的开发人员的生产效率迅速下降。” 其实,没有什么架构风格是银弹,对于微服务而言,没有这些自动化的基础设施,实施起来远比单体系统痛苦得多。
2019-06-04 21:07
回复
用户头像
使用微服务架构的一个理由是无法预估某些服务的峰值请求,但是这种情况在单体架构下依然存在,只不过是峰值请求被大一些的容器给分担了而已。
2019-06-03 17:29
回复
用户头像
“构建微服务的唯一充分理由是组织结构,并且这也应该是大多数组织构建微服务的唯一原因。
随着你的组织架构复杂到一定程度、团队规模大到了一定程度,你应该采用与之对应的技术架构——而那很可能就是微服务架构。
这样做的主要原因是最大限度地减少团队之间的人员沟通成本,一个超过10个或12个人的团队无法在一个工程项目上成功协作,它与人员沟通结构和工作授权有很大关系。因此,将项目团队映射到微服务可以减少人与人之间的沟通开销,从而提高开发速度,这是一个选择微服务的合理和现实的理由。”
观点引自《谷歌失败案例赏析:那些年在微服务上踩的坑》
所谓合久必分 分久必合,你还真别瞧不上十年前的单体架构,没准儿那还真就是最适合你的组织的,只不过不能给你的个人简历增光添彩罢了。
2019-06-03 16:42
回复
用户头像
迷信滥用微服务的,可以看看这个《谷歌失败案例赏析:那些年在微服务上踩的坑》https://mp.weixin.qq.com/s?__biz=MjM5MDE0Mjc4MA==&mid=2651016151&idx=2&sn=caa40d813b176a8325d61ca0c1040d19&chksm=bdbeb3848ac93a9224ed992f0e11e4a75626b7107d796c17a4f71335d7d735279ff6d6461453&mpshare=1&scene=2&srcid=0516RlQ5lWjzTuPJVXbVaQi9&from=timeline&ascene=14&devicetype=Windows+10&version=62060834&nettype=WIFI&abtest_cookie=BAABAAoACwASABMABgAjlx4AVpkeAL%2BZHgDcmR4A%2BJkeAAOaHgAAAA%3D%3D&lang=zh_CN&pass_ticket=LfHxNUl7SwZG7Z1n44peGZmpJaHjoCODSzjll2Ue5js56djBerqgdkiMMPyl52ln&wx_header=1&key=352cdcf02fb917f673e45fb4275045c3b58b65083d89610bd90f882d2137ffc758055e05c11003661ac996059d62020cb570435780ec36c571ee0428b2c6e85b13a8794f3da9d9cbb3055159ee8b94a0&uin=MTExNDgwNjM2MQ%3D%3D
展开
2019-06-03 16:33
回复
用户头像
感觉是领域划分和依赖关系没设计好
2019-06-03 12:12
回复
用户头像
感觉是领域划分和依赖关系没设计好
2019-06-03 12:12
回复
用户头像
不是说....微服务架构的精髓在切分么
自己乱切出了一堆问题后回退...
2019-06-03 11:59
回复
用户头像
感觉是遇到问题,然后没有深入思考和解决,然后就fall back了。再过一段时间,单体架构的弊端开始显现,然后再调整一次大方向。。。
2019-06-03 11:07
回复
用户头像
应该只是业务场景不合适吧,并不是完全否定微服务
2019-06-03 09:16
回复
用户头像
感觉最后罗列的很多tradeoff反而证明了微服务的好处。不恰当的业务抽象和服务拆分才是复杂度的根源,合并在一起本身不会解决任何问题。作者拿共享lib变更不需要发布N个服务作为理由,怎么说呢,这种把模块下沉做成服务或者搞成代码插件热部署也行啊,也许它们的基础设施太过薄弱了吧。很难相信这种回归单体的方案居然推下去了???
2019-05-29 23:18
回复
用户头像
感觉最后罗列的tradeoff反而证明了微服务的好处。不恰当的业务抽象和服务拆分才是复杂度的根源,合并在一起本身没有解决任何问题。拿共享lib变更不需要发布N个服务作为理由,怎么说呢也许它的基础设施太过薄弱。很难相信这种回归单体的方案居然推下去了?
2019-05-29 23:15
回复
用户头像
放弃了微服务。 这个标题就是哗众取宠, 仅此而已!!
文章的原作者如真有本事,想做个更牛B的标题党的话,你把标题改为 “Segment 的架构,重回十年前的单体时代” 。
2019-05-29 19:13
回复
用户头像
说白了就是为了赶时髦用了新技术,很多搞技术的人有这个毛病,有个新技术,我们用吧,有个新架构,很好,我们来重构吧。其实技术最后是为了业务服务,能用就好,不能本末倒置,光追求时髦了
2019-05-29 12:29
回复
用户头像
明显是架构没设计好,共享的模块为何会频繁变更,肯定是混入了一些业务层的代码,最后又回到单体应用,回到原点,以前的问题还是问题,没有一点改进,差评!
2019-05-29 09:27
回复
用户头像
微服务的粒度很难把握,平衡很多的因素才行
2019-05-27 22:32
回复
用户头像
微服务针对横向业务较复杂时的隔离手段,如果业务相对简单,不隔离也无所谓;
2019-05-27 16:28
回复
没有更多了
发现更多内容

区块链架构的层级:第 0、1、2、3 层介绍

devpoint

区块链 10月月更

Photoshop软件应用项目(三)

张立梵

设计师 ps 10月月更

golang中的接口

六月的

golang interface

在线问题反馈模块实战(六):接口文档定义

bug菌

springboot 项目实战 10月月更

计算机体系结构“圣经”新版,图灵奖得主扛鼎之作,影响无数技术人

图灵教育

计算机体系结构 图灵奖

在线问题反馈模块实战(四):封装通用字段类

bug菌

springboot 项目实战 10月月更

【资损】资损防控的系统规范之服务接口类设计

小明Java问道之路

架构 微服务 金融 10月月更 资损

feign的动态代理

急需上岸的小谢

10月月更

Feign的整体流程

急需上岸的小谢

10月月更

【一Go到底】第二十天---闭包

指剑

Go golang 10月月更

在线问题反馈模块实战(五):实现对通用字段内容自动填充功能

bug菌

springboot 项目实战 10月月更

Centos7 搭建Jupyter NoteBook教程

Yeats_Liao

后端 虚拟机 10月月更

Centos7 搭建单机Spark分布式集群

Yeats_Liao

后端 虚拟机 10月月更

利用Babel自动生成“Attribute”文档

小鑫同学

前端 markdown 插件开发 babel 10月月更

创建容器镜像:如何编写正确、高效的Dockerfile

okokabcd

Docker

一文搞懂CAN和CAN FD总线协议

不脱发的程序猿

汽车电子 通信协议 CAN总线 CAN和CAN FD总线协议 CAN FD总线

2022年8月银行APP月活跃人数盘点

易观分析

手机银行 8月

Flash软件应用项目(三)

张立梵

设计师 Flash 10月月更

华为云从入门到实战 | AI云开发ModelArts入门与WAF应用与部署

TiAmo

华为 华为云 云开发 10月月更

RefectiveFeign

急需上岸的小谢

10月月更

数据中台坠落神坛,数据服务平台闪亮登场,阿里、快手又整烂活?

雨果

数据中台

golang中的init初始化函数

六月的

golang init

【愚公系列】2022年10月 Go教学课程 033-结构体方法重写、方法值、方法表达式

愚公搬代码

10月月更

cstdio的源码学习分析11-格式化输入输出函数fprintf---format解析跳转表逻辑分析

桑榆

c++ 源码刨析 10月月更

公司没钱了,工资发不出来,作为员工怎么办?

石云升

离职 职场经验

树莓派4B安装docker-compose(64位Linux)

程序员欣宸

Docker 10月月更 树莓派4

数据湖(五):Hudi与Hive集成

Lansonli

10月月更 Hudi与Hive集成

命名规范与原则

Appleex

代码人生 命名规范

CAN与CAN FD通信之间存在的问题

不脱发的程序猿

汽车电子 CAN总线 CAN FD总线 CAN与CAN FD通信问题 CAN与CAN FD通信

Windows Update MiniTool 20.12.2016 控制Window更新下载及使用教程

Yeats_Liao

工具 windows 10月月更

群主发红包带你深入了解继承和super、this关键字

共饮一杯无

Java 关键字 10月月更

Segment放弃了微服务_架构_Alexandra Noonan_InfoQ精选文章