从微服务开始(三):基本模式与最佳实践

原文链接:https://blogs.oracle.com/the-cloud-front/getting-started-with-microservices%2c-part-3%3a-basic-patterns-and-best-practices

在这个系列当中的第一部分讨论了微服务的主要优势,并且接触到了一些使用微服务时需要考虑的问题;第二部分考虑了如何将容器加入到微服务体系中。在本文中,我们将看一下实施微服务的几个基本模式和最佳实践。

简介

应用于基于微服务应用的审计与实施模式,根据特定的应用场景而千差万别。仅仅凭一篇博客无法涵盖所有可能的场景和其对应的设计模式。在最近与客户的接触当中,我们发现他们在开始使用微服务的时候,会遇到一些常见的问题。本文的目的是提供一些基本模式和最佳实践的该来,来解决这些常见问题,在开始使用微服务时,提供一个良好的起点。

常用的最佳实践

在你开始考虑架构与实施的最佳实践之前,确保我们能够为微服务应用的成功交付应用一些基本原则是非常重要的。为了让服务团队获得最大的成功,他们必须能够真正的独立,能够根据他们自己的节奏进行服务发布。他们也必须能够使用最符合他们需要的语言、框架和产品。很多企业并没有建立起这些,所以阻碍了他们的成功。这些都是微服务项目成功的关键因素。一些企业每天对服务的发布和更新会达到上百次,自动化就成为关键需求。通常,从持续集成到持续交付到持续部署,所有的事情都是自动化的。下一篇博客当中,我们将更详细的介绍DevOps和自动化。

随着每个团队自由度的增加,在标准方面(比如通用日志格式、命名约定、API文档等)进行投入,来避免混乱,是非常重要的。想象一下,如果每一个团队都使用它们自己的日志格式,会发生什么情况。如何能够将跨越多个服务关联不同的事件或事务。下面,我们将介绍一些与实施相关的最佳实践。

无状态计算的设计

我们先对无状态进行定义,很少有应用真的是无状态的。在大多数情况下,应用或者服务都会有状态,比如Session状态,应用和配置数据等等。以往,特别是在非动态环境中,状态都是随服务实例存储,比如存储在内存。之前提过,微服务世界是非常动态的,所以在你可能不知道服务在扩展时或者失败时所生成的新的实例在什么地方。如果服务状态与实例一起存储,你就会丢失状态,新的实例也无法使用它。针对这种情况推荐的最佳实践是,将数据推送到高可用的受管服务当中。在这系列博客的最后一部分,你将看到我们正在构建的一些非常酷的服务,来启用这些场景。

故障处理的设计

在分布式系统当中,你应该总是假设服务调用应因为故障而失败。除了错误的代码,还有很多因素会引起故障。比如,因为网络问题或者基础架构问题造成的故障。有两种类型的故障:瞬态故障和非瞬态故障。瞬态故障可能在任何时候发生,大多数时候,操作会在几次重试后成功。非瞬态故障会更持久,比如,当你尝试访问一个被删除的目录。故障处理的设计意味着要写代码来处理这些类型的故障,来保证应用总是能够响应,并且给用户返回一些东西。下面的表格显示了一些能够使用的错误处理模式。

模式 

场景示例

重试

在微服务世界,一个微服务第一次调用一个其他服务时,可能会失败,不论是另一个微服务还是云环境中的一个受管服务。这种失败可能是非常短暂的,但是没有任何重试的策略,请求服务将会被迫进入故障处理模式。有重试策略,底层架构会在不通知请求服务的情况下进行重试,因此提供了改进的故障处理。可用的重试策略有如下几种:

  • 固定时间间隔:以固定的时间间隔重试。选择在很短的时间间隔内进行大量重试的时候需要注意,这有可能被认为是对服务的Dos攻击。
  • 指数后退:逐渐增加两次重试之间的等待时间。
  • 随机:定义随机的重试间隔

断路器

与重试模式让服务进行重试操作相比,断路器模式阻止服务执行一个可能失败的操作。比如,当一个下游服务没有正常工作时,客户端服务能够使用断路器阻止进一步的网络远程调用。它也能够避免因为一个服务调用失败进行重试造成的访问高峰形成网络拥挤,也能够避免级联失败。自我修复断路器定期检查下游服务,并且在下游服务功能正常后对断路器进行重置。

隔板

隔板通过对系统的划分,避免整个系统因为单个组件的故障而宕机。假设我们有一个基于请求的多线程引用,使用了三个不同的组件,ABC。如果对组件C的请求开始挂起,最终所有的请求线程都将挂起,等候C的响应。这将会使整个应用无法响应。隔板模式仅使用特定数量的线程保留给C使用,所以不会有所有的线程都来等待C,来避免整个系统的宕机。

回退

当相关服务故障时,回退模式为请求服务提供了替代解决方案。比如,一个电影客户端调用保存在远端服务中的最流行的电影列表失败。如果故障出现在远端服务,会有一个替代服务,可能会显示一些普通的电影列表,或者显示最近缓存的列表。

向后与向前兼容性的设计

因为服务是完全独立自主的进行部署,你需要确保在你的服务代码更新时,不需要中断那些现有的,与你的服务进行通讯的服务。这意味着,你的代码需要能够向后和向前进行兼容。那这意味着什么呢?

在图1中,Processing Service针对Order Servicev1.1版本进行工作。现在,我们将Order Service升级到v1.2以实现一些新的功能。向后兼容性意味着,这些变化将不会破坏Processing Service的功能。API中添加的新字段或者是可选项,或者有合理的默认值。绝对不对已有的字段进行重新命名。建议在发布新的版本之前,使用旧的消息格式对新版本的API进行测试,以便确定没有问题。图1展示了这种场景。

从微服务开始(三):基本模式与最佳实践_第1张图片

1向后兼容性

向前兼容性对于微服务来说不是一个普遍的需求,但是,如果你需要确保功能性的回滚,就必须要实现它。向前兼容性意味着服务使用相同的方式工作,即便是对它自己的新版本,也不需要对它进行更新。通常,遵循伯斯塔尔法则,“发送时保守,接收时开放”。忽略传递的任何额外字段,不要抛出错误。

除了实现API的向后和向前兼容性,对API和它们的版本历史进行适当的文档化也非常重要。对你的微服务使用Semantic Versioning(语义版本控制,major.minor.patch)是一个很好的实践。这不仅能够帮助服务消费者更快的开始对API进行消费,也能够提供API消费和使用的最佳实践。Swagger是一个非常成功的工具,被OAI(Open API Initiative)所使用,专注于一个供应商中立的API描述格式的创建、发展和提升。你可以使用它来作为API的交互式文档和客户端SDK的生成与发现。

高效服务通讯的设计

在高层次上,可以区分外部服务通讯和内部服务通讯。本系列博客的第二部分讨论了外部服务通讯的一个例子,通过API网关,允许其他事情(比如AuthNAuthZ,流量控制或者协议转换),将来自客户端的流量路由到微服务。大多数情况下,HTTP被用于客户端与API网关或者其他外部服务进行通讯的协议。现在你可能会问,为什么要区分外部和内部服务的通讯的?让我们考虑的更多一点。

大型的微服务应用会有成百上千个服务,服务越多,服务间的通讯和数据交换就越多。因此,协议的选择成为影响性能的重要因素。当HTTP(HTTP/2)成为客户端通过Internet访问你的服务的首选协议,而内部服务的通讯则可以采用TCP/UDP协议,来提升性能。

另一个经常被忽略的方面是数据的序列化和反序列化对整体性能的影响,在最坏的情况下,它会成为瓶颈。要避免数据序列化和反序列化所造成的影响,除了选择一个好的JSON序列化器之外,如果下游服务需要对相同的对象进行操作,还要考虑你是否需要进行反序列化。相反,你可以仅仅增加反序列化对象,并将它传递给表单中的另一个服务。另一个提升性能的选择是使用二进制格式,比如协议缓冲区。

适当的异步消息传递的设计

微服务应用当中的每一个服务实例都是一个进程。所以,服务必须使用进程间通讯(IPC)机制进行互操作。NGINX发表过一个很不错的博客文章描述用来在微服务架构中实现IPC几种方式(https://www.nginx.com/blog/building-microservices-inter-process-communication/)

IPC常用的一种方式是使用消息发送。尽管大家都知道如何在队列当中读取或者写入消息,然而,在你的应用当中使用异步消息发送作为IPC机制时,需要考虑并记住几个设计实践。就像通常使用异步调用一样,使用异步消息发送的最大的好处之一是在等待从另一个服务的响应时,服务不会阻塞。另一方面,异步消息发送也引入了一些挑战。与你的消息发送解决方案相关,消息的顺序可能是随机的。有时候这不是个问题,但是一旦你的服务必须活着期望它的消息有特定的顺序,你需要对你的服务应用一些逻辑来实现排序。

另一个挑战是需要对重复的消息进行处理;一个消息有可能被发送了不止一次,比如作为重试逻辑的一部分。在这种情况下,确保你的服务能够检测到重复消息并且能够进行适当的处理非常重要。幂等性设计是一个好的实践,将会在本文后面详细介绍。

另一个会经常发生的问题,是如何处理无法被处理的消息。比如,消息是错误的或者包含损坏的数据,可能会总成接收方的异常。一种好的方法是丢弃它们,将它们放入一个指定的队列进行异常处理,这样接收方就不会继续尝试处理这种“有毒”的消息了。这些有毒的消息之后会被从指定的队列中取出,并进行诊断。这些只是一些在使用异步消息发送的时候可能会遇到的一些挑战。很明显,消息发送是一个丰富的话题,能够很容易的填满整个文章,但是好消息是很多消息发送系统,比如Apache Kafka或者RabbitMQ提供了能够解决我们上述问题的功能。

幂等性的设计

幂等(idempotentidempotence)是一个数学与计算机学概念,常见于抽象代数中。在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的.更复杂的操作幂等保证是利用唯一交易号(流水号)实现.

不论是处理消息还是数据,我们都应该尝试幂等性设计。比如,基于失败的接收者、重试策略等,可能造成消息被接收和处理不止一次。理想的接收者应该能够通过一种幂等性的方式处理消息,所以重复的调用产生相同的结果。

下面的例子说明这实际的意思。我们假设一个服务需要向一个账户中增加一些钱。消息如下所示。

{

  “credit” : {

“forAccount” : “12345”

“amount” : “100”,

  }  

}

我们假设因为网络问题,造成第一次操作失败,接收者没有收到这个消息。就像你之前看到的,一个好的方法是进行重试。结果,发送者重新提交了这个消息。现在你得到了两个相等的消息。如果接收者现在拿到了这两个消息,并对他们都进行了处理,那么这个账号的入账就不是$100,而是$200。要避免这种情况,你需要确保幂等性。一种常用的确保幂等性的方法是通过对消息增加唯一的标识号,确保服务仅在标识号不匹配的情况下才对消息进行处理。下面是一个相同的消息的例子,但是增加了标识号。

{

  “credit” : {

“creditID” :“124e456-e89b-12d3-a456-426655440000”

       “forAccount”: “12345”

“amount” : “100”,

  }  

}

现在接收者可以检查消息是否已经被处理过了。通常这也被称为去重。在数据更新时,也可以使用相同的原则。底线是,操作应该被设计成幂等的,这样每一步都可以在不影响系统的情况下进行重复。

最终一致性的设计

基于CAP原则,在需要容忍网络分区的地方,必须在一致性和可用性之间做出选择。大多数的微服务应用通常被设计为高可用,这就意味着放弃了强一致性。问题是,业务交易通常要跨越多个服务,涉及到不同的数据存储。像事件发起连同交易补偿模式这样的模式已经被证明非常有效,但是它需要归结到特定的应用场景,来找到实现最终一致性的最佳方式。

CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

面向运维的设计

微服务实现的最重要方面是能够对系统和服务进行监控,并进行事件的诊断。除了对主机和容器的监控,好的策略是必须要对服务进行型监控与诊断。监控与诊断的关键要素是数据收集,通常使用日志来完成。在之前提到,所有的服务都需要使用一种通用的日志格式,为跨服务的诊断打下良好的基础。以Opentracing(http://opentracing.io/)为例,为分布式追踪提供了一种供应商中立的开放标准,用于多语言的微服务应用环境。除了通用的日志格式,你还需要考虑记录哪些日志。总会有那个经典的问题,什么是太多的日志,什么是不够的日志。与许多事情一样,取决与你的场景,下面的列表基于许多用户的场景给你一个好的起点:

  • 健康状态数据:记录你的系统在一个健康状态下的行为非常重要。这些数据可以作为以后进行异常比较的基准。其他的可收集的数据包括服务启动事件、心跳等。
  • 记录那些不在工作的:这看起来很直白,但是除了错误信息和堆栈追踪之外,还应该记录语义信息,比如用户请求等。
  • 记录关键路径上的性能数据:在性能指标上聚合百分位数据,能够识别系统范围的长尾性能问题。

最后,最重要的最佳实践之一是使用关联或活动ID。这个ID是在向应用发起请求的时候生成的,并在所有的下游服务中传递。即使请求跨越多个独立的服务,也可以通过这个ID对请求进行全程的追踪。

从微服务开始(三):基本模式与最佳实践_第2张图片

2基于活动ID的瀑布图

分布式追踪解决方案,比如Zipkin(http://zipkin.io/)已被证明在监控和诊断基于微服务应用时非常有效。

总结

就像在简介中说的,使用哪种模式取决于你的特定场景。这篇博客中提到了一些你开始设计和开发为服务应用时需要记住的基本的设计原则。

下一篇博客将涉及容器化微服务的DevOps



你可能感兴趣的:(SOA技术,开发技术,云计算)