微服务的黑暗面:什么可能会出错?

似乎最近所有人都开始研究微服务,而单体架构一夜之间被抛弃了。

获得的公众关注的某种趋势通常会被夸大,并且并不会反应实际使用中的问题,但是这一次似乎更是一种共识。微服务遵守基本的关注点分离的原则,同时由解决实际问题的工程师团队为服务命名。

本文我们会提出质疑,分享一些社区和我们用户的声音,探讨生产环境里微服务的首要问题——以及可以如何解决这些问题。

 

微服务的另一面

关注点分离并不是什么新概念,分布式计算也不是。优势很明显,但是它们的代价通常是时间和金钱上更高的运维成本。将这两者混合在一起,就会遇到所有类型的问题。将其上生产环境,问题会变成四倍。调试修复,但是等一下—— 调试并不能让问题消失。

就像Bryan Cantrill在其QCon演讲里所指出的,“调试已经退化成了口头传说,期望问题能够神话般消失。”事实上,调试更像一门科学,要理解系统是如何工作的,而不是我们认为它是如何工作的。

调试不仅仅是微不足道的边缘任务,而是个根本性问题。Sir Maurice Wilkes,创造出首批程序之一的调试者,那时候就已经认识到调试会成为开发人员需要承担的主要职责。

“在开始编程的1949年,我们惊讶地发现让程序像我们认为地那样工作并不容易。必须找到调试方法。我还能记得那一刻,我意识到从今之后我生命的很大一部分会花在寻找自己编写的程序的错误上。”

我们曾经认为这是为了解决问题。但是实际挑战是要理解系统到底是如何工作的。

 

问题#1:如果监控单体应用还不够困难

无论你是逐步将单体应用分解成微服务,还是从头构建一个新系统,现在你都有更多的服务需要监控。每个都很有可能:

  • 使用不同的技术/语言
  • 在不同机器/容器里
  • 有自己的版本

 

要点是智慧地监控,系统是高度碎片化的,迫切需要中央化监控和日志,才能够理解到底发生了什么事情。

比如,在最近持续讨论播客描述的一个场景里是需要回滚的差版本。这是单体应用最直接的方式。但是——现在我们有微服务了。因此需要确定哪个服务需要回滚,这样的回滚对其他服务会有什么影响,或者可能只是需要添加一些功能,但是也可能就直接将问题推到另一个服务里。

要点#1:如果你认为监控单体架构很难,那么微服务会更难10倍,并且需要预先计划更多的投资。

问题#2:日志分布在服务间

日志 日志 日志。服务器每天都会产生数GB的非结构化文本。IT界和二氧化碳排放等价的东西,是溢出的硬盘和疯狂的Splunk账单/ELK存储费用。另外,如果想学习Splunk或者ELK,可以看看我们最新的电子书:《Splunk vs ELK:日志管理工具决策指导》。

在单体架构下,日志很可能已经分散在不同的地方,单体思维里就得使用日志记录在不同地方的几层设计。在微服务里 - 日志更加分散。现在当研究一些用户事务相关的场景时,不得不从所有可能使用到的服务那里将所有的不同日志收集到一起,才能理解什么地方出错了。

在Takipi里,我们的团队通过使用Takipi解决这样的问题。对于来自生产JVM里的所有日志错误或者报警,我们在日志里注入了可以指引事件分析的链接。包括每一帧完整的stack trace和变量状态,即使它们分布在一定数量的服务/机器上。

要点#2:微服务是指将东西分解成单个组件。这么做的副作用是,运维和监控也随之分解到每个服务里,并且失去了作为整体的系统的力量。这里的挑战是使用合适的工具重新将这些中央化。

问题#3:一个服务导致的问题,会造成其他地方的问题

如果跟踪某个特定服务的故障事务,你无法保证正在查看的服务就是出问题的地方。假定服务间有一些消息传递机制,比如RabbitMQ、ActiveMQ或者可能使用的是Akka。  

即使服务行为正常,没有找到什么问题,也可能会发生如下场景:

  • 它接收到的输入是错的,那么你需要理解是什么导致前一个服务的异常行为
  • 其结果的接收方返回了一些异常响应,那么你需要理解下一个服务是如何工作的
  • 如果这些依赖条件比1:1更加复杂会如何?或者多个服务受益于该问题呢?

无论问题是什么,微服务下的第一步都是要知道从哪里开始寻找答案。数据完全分散,很有可能完全不在日志和仪表盘里。  

要点#3:单体应用里,你通常能够知道检查的方向是正确的,微服务让理解问题的来源在哪里,以及应该到哪里得到数据变得更加困难。

问题#4:找到问题的根本原因

好的,让我们继续调查。现在的出发点是我们已经找到了有问题的服务,拿到了需要的数据,从日志里找到了stack trace和一些变量值。如果使用的是APM(就像New Relic、AppDynamics或者Dynatrace,我们也有文章介绍,在这里和这里),你可能还得到其他一些数据,有关一些方法的高速处理时间/对问题的严重性做了一些基本的评估。

但是——实际问题是什么呢?真正的根本原因?要找到实际出错的代码。

大多数情况下,从日志里首先得到的变量数据并不是真正需要的数据。它们通常指到下一个线索,要求你发现更多的真相,并且添加更多的日志声明。部署变更,期望问题能够重现,或者不重现,因为——有时候仅仅添加一条日志声明似乎就能解决问题。

要点#4:当某个微服务的根本原因影响多个服务时,有可用的中央化根本原因检测工具至关重要。如果你使用Java/其他JVM语言,一定要来看看我们Takipi是怎么做的。

如何优化微服务性能 微服务调优有哪些方式_如何优化微服务性能

Takipi的错误分析仪表盘——每一帧的变量值放在实际代码之上

问题#5:版本管理和服务间的循环依赖

持续讨论博客里提到的另一个问题是从典型单体架构的单层模型到微服务的图模型。  

这里可能发生的两个问题和依赖有关。

  1. 如果在服务间存在循环依赖,当某个事务可能死锁在循环里时,就会出现溢出错误。
  2. 如果两个服务共享某个依赖,并且你以会影响它们的方式更新了其他服务的API,那么就需要一次更新所有这三个服务。这会带来这样的问题,你应该先更新哪个?怎么才能让这样的更新平稳过渡?

更多的服务意味着每个服务都有不同的发布周期,大大增加了这里的复杂度。当问题在一个版本消失,在新版本又出现时,重现问题就会很复杂。  

要点#5:在微服务架构里,你更容易遇到依赖相关的问题。

最后的思考

立刻调试让你希望解决问题,这和调试这个词的直接意思是一致的。当在系统上下文中思考这些时,这里的内容比临时的问题解决要多得多。这有关于当做整体理解系统,系统如何工作,以及实际上是如何的,而不是你希望它如何。 最后,这完全取决于你使用的工具以及工作流。这是我们在构建Takipi所思考的,解决这些实际问题,以及调优应用程序日志来理解生产应用程序的实际状态。