我在微服务世界中看到的灾难 - joaoqalves


当Martin Fowler在2014年发表有关微服务的文章时,我工作的团队已经在构建面向服务的架构。这个概念的炒作席卷了世界上几乎每个软件团队。“ Netflix OSS堆栈”是当时最酷的东西,它使全球的工程师都可以在分布式系统中利用Netflix的经验。六年多以后,如果我们现在研究软件工程工作,那么大多数人都在谈论微服务的体系结构。 
 
炒作驱动的发展
在2010年代初期,许多组织在其软件开发周期方面都面临着挑战。与其他50、100或200名工程师一起工作的人们在开发环境,繁琐的QA流程和计划的部署中苦苦挣扎。尽管马丁·福勒(Martin Fowler)的《持续交付》一书阐明了许多这样的团队,但他们开始意识到雄伟的整体/单体Monolith正在为他们带来组织上的问题。因此,微服务吸引了软件工程师。在大型项目中引入持续交付或部署要比从开始做起更具挑战性。 
因此,团队开始分离出几十或上百种微服务。他们中的大多数使用“ HTTP上的JSON”(其他人可能会说RESTful)来在这些组件之间进行远程调用的API。人们非常了解HTTP协议,这似乎是将整体转换成较小片段的相对简单的方法。此时,团队开始在不到15分钟的时间内将代码部署到生产中。不再有“哦,A团队中断了CI管道,我无法部署我的代码”,感觉很棒!
但是,大多数工程师忘记了在软件体系结构级别解决组织问题时,他们也引入了很多复杂性。分布式系统复杂性变得越来越明显和快速,让对这些团队头疼。
 
现实反击
进行重大的体系结构更改不是免费的。团队开始意识到共享数据库是单点故障。然后,他们意识到将自己的领域分开创造了一个全新的世界:最终的一致性就变成一件需要注意的事情。当您要提取数据的服务关闭时该怎么办?问题的数量开始堆积。寻找错误,事件,数据一致性问题等,使实现高速开发步伐的希望破灭了。另一个问题是,工程师集中观察的日志需要跨越数十项服务,这样菜鸟菜鸟并纠正其中Bug。
 
灾难1:服务太小
由20名工程师组成的团队维护着50项服务。每人不止一项服务!维护每项服务都是有代价的。这些服务是在不同的时间点启动的,它们具有不同的体系结构以及业务逻辑和所使用的框架之间的纠缠。
当有人告诉我在服务A中部署新功能时也需要同时在服务B中进行部署。人们不仅可以在其IDE中查看一个项目,而且还需要同时打开多个项目才能理解所有这些混乱情况。 
 
灾难2:开发环境
跨分布式系统的开发环境存在多个问题,尤其是在大规模环境中:

  1. 在云提供商中运行200个服务需要多少费用?你能做到吗?您还可以提升运行它们所需的基础结构吗?
  2. 这样做需要花费多少时间?如果移动工程师开始开发功能时,该新版本中有一组服务,而当它们完成时,有十个新版本部署到生产中怎么办?
  3. 那测试数据呢?您是否有所有服务的测试数据?它在整个团队中是否连贯,因此用户和其他实体匹配吗?
  4. 如果要开发多租户,多区域应用程序,那么配置和功能标志呢?您如何与生产保持同步?如果同时更改默认值怎么办?

 
灾难3:端到端测试
端到端测试与开发环境存在类似的问题。以前,使用虚拟机或容器创建新的开发环境相对容易。使用Selenium创建一个测试套件来遍历业务流程并断言它们在部署新版本之前是有效的,这也非常简单。
使用微服务后,即使我们可以通过设置环境解决上述所有问题,也无法声明系统正在运行。
 
灾难4:庞大的共享数据库
在保持整体架构的数据一致性的同时,摆脱整体架构的一种简单方法是继续使用共享数据库。它不会增加运维负荷,并且使逐步分割整块变得容易。但是,它也具有相当大的缺点。除了明显的单点故障之外,它还打破了一些面向服务的体系结构的原则,还有更多。您是否为每个服务创建一个用户?您是否具有细粒度的权限,因此服务A只能从特定表读取或写入?如果有人无意中删除了索引该怎么办?我们如何知道有多少服务正在使用不同的表?那么缩放呢?
弄清所有这些问题本身就成为一个全新的问题。从技术上讲,考虑到数据库的寿命往往超过软件,这可能并不容易。使用数据复制解决问题(无论是Kafka,AWS DMS还是其他方法),都需要工程团队了解数据库的具体信息以及如何处理重复的事件等等。
 
灾难5:API网关
API网关是面向服务的体系结构中的典型模式。它们有助于使后端与前端使用者脱钩。当在整个系统上实施端点聚合,速率限制或身份验证时,它们也很有用。最近,行业一直倾向于后端-前端(backend-for-frontend)的体系结构,在这些体系结构中,为每个前端消费者(iOS,Android,Web或桌面应用程序)部署这些网关,从而使它们的发展彼此分离。
就像这个世界上的一切一样,人们开始为此有了新的,创造性的用例。有时,使移动应用程序向后兼容是一个小技巧。突然,您的“ API网关”成为了单点故障,因为人们发现在一个地方更容易处理身份验证,并且其中包含一些意外的业务逻辑。
现在,您有了一个自制的Spring Boot服务来获取所有流量,而不再是一个整体架构来获取所有流量!(这个SpringBoot服务又变成单点)。
API网关灾难的根源在于它消耗了未分页或返回大量响应的端点。或者,如果在没有适当的回退机制的情况下进行聚合,则进行单个API调用会消耗网关的资源。
 
灾难6:超时,重试和弹性
分布式系统不断在部分故障模式下。服务A无法联系服务B会怎样?我们可以重试我们的请求,对吧?但这立即导致我们陷入困境。我见过团队使用断路器,然后增加了对下游服务的HTTP调用的超时。但它会产生二阶效应。
现在,断路器将取消所有持续很长时间的请求,如果流量增加,则会有越来越多的请求排队,从而导致比您要修复的情况更糟的情况。我已经看到工程师难以理解队列理论以及为何存在超时。当团队开始讨论其HTTP客户端的线程池之类的事情时,会发生同样的事情。
从故障中恢复时的一个棘手的事情是并非所有的人都是幂等的。在某些情况下,我们可能期望我们的消费者即使用者是幂等的,但这意味着我们需要主动决定在每种故障情况下应该采取的措施。消费者是幂等的吗?我可以重试这个调用吗?我已经看到许多工程师忽略了这些,因为这是一个“极端情况”,后来才意识到他们有一个巨大的数据完整性问题。
重试甚至比所有这些都复杂,即使您设置了备用机制。想象一下,您的移动应用程序中有500万用户,并且用于更新用户首选项的消息总线已经停止工作了一段时间。您为这种情况设置了一个后备机制,该机制通过HTTP API调用用户的首选项服务。现在,此服务突然出现了巨大的流量高峰,并且可能无法应付所有流量。甚至比这更糟:您的服务可能能够获得所有这些新请求,但是如果重试机制没有实现指数退避和抖动( exponential backoff and jitter),您可能会遇到来自移动应用程序的分布式拒绝服务。
 
看到所有这些灾难,您仍然爱上分布式系统吗?
如果我告诉你我只写了一部分我所见的灾难怎么办?分布式系统很难掌握,只有最近大多数软件工程师才始终如一地接触它们。
好消息是,我所谈论的许多灾难都有很好的答案,并且业界已创建了更好的工具,以使它们可以由FAANG以外的组织解决。 (banq注:以上问题基本是微服务边界设计和运维问题,业界通过引入DDD上下文和服务网格等方式分别来解决,当然也额外带来复杂性。催生了DevOps)
我仍然喜欢分布式系统,并且我仍然认为微服务是解决组织问题的很好的解决方案。但是,当我们将失败视为“边缘案例”或我们认为永远不会发生的事情时,问题就来了。这些极端情况在一定程度上成为新常态,我们应该应对它们。