在德国柏林举办的 microXchg 2016 会议上,Richard Rodger 做了一个“在微服务中生存(Surviving Microservices)”作为主题的演讲,对于希望保持微服务“健康和高性能”的开发人员来说,这是一个实用的指南。在这个演讲中,所讨论的核心话题包括面向消息系统的益处、服务间通信的模式匹配、故障处理以及微服务框架Seneca.js 的简介。
InfoQ 最近邀请到了 Rodger ,他是 nearForm 的 CTO 和联合创始人,我们更加详细地讨论了这个演讲的核心观点,还讨论了创建 Seneca.js 框架的驱动力(以及该框架在当前微服务平台中的位置)、在分布式系统中有效处理故障的方法和使用面向消息通信的收益。
Rodger 分享了多个很有意思的观点:微服务通常会实现为分布式的系统,这种方式会有一些“严重的失败模式(deliciously evil failure modes)”;微服务的核心属性是松耦合的组合形式,不应该产生由面向对象语言所导致的不必要的复杂性;如果在微服务社区中,试图寻找和实现某种“标准”的话,我们将会重蹈传统面向服务架构(SOA)的覆辙。
InfoQ:Richard,感谢您接受 InfoQ 的采访。您能为我们的读者快速介绍一下 microXchg 演讲的整体内容吗?
Rodger:微服务并不是金锤(通常指放之四海皆准的方案——译者注)!在我们的行业中,有一种不太好的特征就是软件工程实践所带来的收益会被“非理性地繁荣膨胀”所稀释。如果你在实践中真正使用微服务的话,就会发现它像所有的分布式系统一样,会产生一些严重的失败模式。在实践中,我们曾经见过这样的情景,所以,我想讨论一下我们的经验。
应该讲,我们的经验也表明这些失败模式并不能掩盖微服务架构所带来的明显优势。我们使用微服务是因为它能够让我们行动更加迅速并且能够消除技术债。它使得实现持续交付以及可扩展性更加容易。最后,作为开发人员,会发现使用微服务会更加有乐趣。如果要在本地校验一个新特性的话,我们不必等待整个系统完成,只需重启相关的服务即可。你的编码 - 构建 - 测试迭代周期能够按秒来进行计算,而不是按照分钟。
这个演讲使用一个小型的示例性系统来阐述实现服务的方式,还有更重要的,那就是服务间通过消息进行交互的方式。我们可以将消息分为同步的(预期接受一个响应)和异步的(不会接受预期的响应)。针对消息,我们可以将服务分为使用者(单独发挥作用)或观察者(允许其他服务来响应消息)。这意味着我们就会有四种交互形式。然后,我们就可以问一下这些交互会出现什么样的故障。这个演讲就利用该交互模式来讨论故障模型,并且讨论了该如何缓解这些故障,这会更加有价值。
微服务架构不像传统的单体架构那样具有确定性,所以传统的生存本能(良好的测试、经过验证的模式以及项目管理的方法论)就不会那么有效了。我们必须要采用这样一种理念,那就是将接受故障作为每天都能遇到的常态现象。你所构建的系统要能容忍故障。这意味着它会影响到架构的各个层,我在演讲中基于每种故障模型,对其进行了讨论。
InfoQ:在会议上,你介绍了 Seneca.js 微服务框架,你能更为详细地介绍一下创建这种工具集的驱动力吗?
Rodger:这个框架是我们( nearForm.com )从 2010 年开始使用的!在最近的两年中,它成为了真正的开源社区,我们也看到了它的健康发展,这一点是很棒的。该框架起初作为组合业务逻辑功能的一种方式。这是什么意思呢?软件组件很难实现得恰到好处。如果将所有的设置项都暴露出来,并且为了涵盖每种场景而提供大量的 API,这样其实并没有什么好处。这种方式需要学习更多的内容,也提供了更多的地方滋生 bug。在实践中,真正可行的组件模型要归功于它们支持 _ 组合 _,如 UNIX 的管道。这是一个简单的理念,基于该理念,我们能够通过小的东西,构建出更大的东西。
技巧在于,要让小东西组合在一起的过程变得简单,而这只能通过统一的、简单的接口模型来实现。现代的面向对象语言对它来讲太过复杂了,它们所有的特性实际上都是缺点。这就是让对象作为组件模型失败的原因所在。GoF(Gang-of-Four)《设计模式》一书的出现,恰好充分证明了这种失败。我们为什么首先需要一组安全的构造呢?这是因为组件组合的所有内容都围绕着如何将它们按照任意古老的方式拼装在一起!
我曾经将无模式的 JSON 文档作为组件间面向消息通信的媒介,在这个过程中,我一直致力于解决的就是标识的问题。组件 A 为了使用组件 B,A 必须要知道 B,这样的话,它就必须要知道 B 的标识。这是一个很大的问题,因为造成了组件之间太多的耦合。在面向对象的世界中,要调用某个对象的方法,需要首先获得该对象的引用。对于大型的系统来讲,随着复杂性的增加,最终可能会需要像依赖注入(Dependency Inversio)这样的东西,才能使功能运行起来。
我采用了一种不同的方式。为什么不使用模式匹配(pattern-matching)呢?如果某个组件对一条消息感兴趣的话,那么可以通过查看的方法来进行确定。你可以使用某个模式(JSON 结构的一些模板)来对消息进行匹配。采用这种方式它就会变得非常简单——只需与顶层属性的字面值进行匹配即可。这样就足以使用它来构建整个系统了,我们所得到的组件模型非常易于进行组合。你可以看到 Seneca 实际上已经有了超过 200 个插件,借助它们基本可以访问任意的数据库、消息总线、服务注册器以及所有类型的其他内容。基于这些,我们可以实现用户账号、项目甚至 hypercard 数据模型的业务逻辑组件。
一旦这个模型建立起来,下一步的任务就很明显了,那就是使其通过网络,以可扩展的方式运行起来。要实现这一点,我们采用了传输独立的理念。Seneca 微服务不关心消息是如何从 A 传送到 B 的。它可能是 HTTP REST,可能是消息总线,也可能是发布 / 订阅模式,这都没关系。从消息的角度来说,我们让消息传送进来,并能让消息发送出去,这就你所要关心的所有的事情。我们假设这些消息始终会满足多种多样的网络形式。
Seneca 并不会将消息伪装成是在本地的——它假定会发生故障。例如,这里有一个内部的断路器(circuit breaker),用来应对重复的消息和消息循环。通过网络拓扑与服务实现的完全分离,我们可以达到超乎寻常的灵活性。例如,如果将函数调用作为传输机制,Seneca 微服务能够很容易地变为单体(monolith)架构。这对于本地部署来讲,是很不错的,对于那些想起初使用单体架构的人来说,这也是一个好消息。总而言之,Seneca 对于微服务如何部署并没有什么既定的立场。
InfoQ:在微服务框架领域,眼下似乎正在经历一种快速的发展,尤其是在 Go(lang) 和 JavaScript 语言方面更是如此。关于这种现象,能分享一下您的观点吗?您认为会产生明确的胜利者吗,或者这个过程是否能够推动标准的产生?
Rodger:我们最不需要的就是标准。我们绝对不能重复 SOA 架构的错误。微服务并不需要什么所谓的标准。想一下吧,假设我们希望 Seneca 与纯的 HTTP REST 微服务对话。只需很少的代码将 Seneca 消息转换为 HTTP 请求就可以了。从另一个方面来讲,只需一点代码就能按照其他的方式来进行转换。对于像 Akka 这样的框架或使用 Kafka 作为统一消息日志的架构来讲,道理是完全相同的。对于消息传输来说,这并不是高难的火箭技术。当然,你要应用 Postel 规则——“对于输入要灵活,而对产出的内容要严格限制”。这就是不采用严格的模式为何如此重要——它们会真切地破坏架构所带来的收益。
和平共处——在很大程度上来讲,这就是微服务的哲学。欢迎任何人加入这个盛宴。
InfoQ:在演讲中,你讨论了基于微服务的系统会导致失败的一些场景(参考了分布式计算的八大谬误)。你能分享一些严重的故障吗,最好介绍一下是如何解决这些问题的?
Rodger:不行,关于这一点我不能分享!我们构建 nodezoo.com 这样的示例系统就是因为我们的客户希望保持最高级别的保密性。我们会在战略方面对他们提供帮助,在竞争性的市场中这会有很多的利益关系。这一点很令人沮丧,我很羡慕那些面向客户公司中的开发人员,如 Netflix 和 Uber,因为他们能够直接谈论真实的系统,我们却不能这样做。
但是,我们可以大致讨论一下内存泄露问题,每个人都会遇到这样的问题。它们导致很多运行生产系统的人彻夜难眠。在进入生产阶段之前,它们往往难以识别。在这方面,100% 的单元测试覆盖率帮不上什么忙。经常遇到的一种噩梦场景就是你不断地重启系统,但是在几分钟之后,它们就会再次出现故障,所造成的响应延迟将会给很多用户带来糟糕的体验。你的系统其实并不是完全地死掉,但是也算不上还活着。有一点毫无疑问,那就是会造成金钱方面的损失。
借助微服务,我们可以解决这个问题。我们不再假设服务是长期存活的,而是将其设计为会经常并且快递地死掉。将高负载服务分散到众多的实例中。假设,将其半衰期(half-life)设置为 1 个小时。值得一提的是,使用随机数是非常重要的——这样的话,能够避免引入系统性的反馈回路(systematic feedback loop),它会导致整个系统出现故障。半衰期的理念是从物理学中借鉴来的,这就意味着如果你起始启动了 100 个服务,等待 1 个小时之后,就只剩 50 个服务了(如果你较为挑剔的话,可以将其设置为最大的存活周期)。对于内存泄露问题来讲,这样就能在很大程度上免于它的困扰。我们几乎不用再关心代码中是否还有这样的问题,因为任意的单个服务都不会持续太长的时间,所以这些问题就不会出现。
对于服务器的内存泄露,微服务架构同样能够提供帮助。你可以搭建很多有问题服务的实例。相对于搭建大量单体架构的实例,这种方式有很大的差异,并且成本更低。通过将大量的实例设置为较短的半衰期,我们能够得到一些时间来解决问题。通常来讲,快捷的解决办法就是重新部署服务的一个较早版本,这个版本是确定不存在问题的,然后让存在内存泄露的新版本慢慢消亡。当然,我们失去了刚刚构建的新特性,但是系统能够保持运行,我们也有时间去排查到底哪里出现了问题。微服务能够借助阶段性部署帮助我们更容易地实践持续交付,这样在它影响整个系统之前,我们就能意识到出现了问题。
关于软件部署,Zach Holman 有一篇很棒的文章,他之前就职于 github,就这个话题来说,这篇文章非常重要。尽管该文是从单体架构角度来进行描述的,但是我们可以很容易地看到微服务架构能够让这些理念更易于实现。尤其是,特性标记(feature flag)就没有必要使用了,因为它们对应于特定微服务的部署状态。
InfoQ:在 microXchg 的演讲中,你特别强调了面向消息的系统。对于这种前提假设,你能更为详细地描述一下吗,对于评估借助这种风格实现微服务通信的读者来讲(比方说,与 REST/RPC 进行对比),你能提供一些建议吗?
Rodger:很遗憾的一点是它的名字叫做微服务架构,其实消息是一等公民并且具有同等的重要性。下面介绍了我们是如何构建微服务系统的。每周我们都会交付一个可运行的系统,这叫做一个迭代。每个迭代都可以通过添加的服务列表以及移除的服务列表来进行完整的描述。微服务架构非常具有动态性,也能快速响应业务的需求。
我们接受到这些业务需求,然后使用它们来识别业务活动。这些活动对应于消息交互,可能是单条消息,也可能是一个消息链。这样我们就有了一个消息列表。然后,可以将这些列表进行逻辑分组,这些分组会形成服务。所以,我们从消息开始,并根据它们衍生出服务。
在起始的时候,我们不必定义出所有的服务。即便有些服务定义出现了错误,我们也可以按照有序的方式通过使用转换,将其从系统中移除掉。我们永远不会陷入绝境。
要使这些运行起来,模式匹配至关重要。它使得转换和多版本部署成为了可能。如果你将模式列出来的话,就能形成一个模式语言。
InfoQ:今天同你的交流非常棒,你还有其他的内容想与 InfoQ 的读者分享吗?
Rodger:如果可以的话,我再插播一个广告!我正在编写一本书,“ The Tao of Microservices ”,这本书深入介绍了在过去的几年中,我们构建微服务系统所取得的经验。在 Manning 上,它目前处于早期发布阶段。
我非常乐意与大家在 twitter 上交流微服务,我的账号: @rjrodger 。
Richard Rodger 在 microXchg 演讲的视频,“ Surviving Microservices ”,可以在该会议的 YouTube 频道找到。
查看英文原文:"Surviving Microservices" with Richard Rodger at microXchg: Messages, Pattern Matching and Failure
评论