近十年来,Spring 因其提供的依赖注入功能而广受 Java 开发者的欢迎,因其可以帮助大家开发出松散耦合的系统。简单来说,用户只需要专注于接口所提供的抽象,即可获得具体的实例。如今随着云计算越发流行,如何充分利用云环境提供的自动伸缩能力,并与特定云供应商实现松散的耦合,这已成为一个非常有趣的挑战。“云原生”这个概念应运而生。我们先来看看“云原生”和“微服务”到底是什么?
云原生和微服务
“云原生”你了解吗?
很多人认为,云原生仅仅是用云提供商的服务来运行你的现有应用?
答案不是这么简单。云原生会全面改变应用程序的设计、实现、部署和运维过程。
现在比较流行的云原生定义如下:
在开发 Spring 框架和云平台的软件公司 Pivotal 看来,云原生的定义是:
云原生是一种构建和运行应用程序的方法,这种方法可以完全发挥出云计算模型的优势。
致力于创建和推动云原生编程范式的组织 CNCF (Cloud Native Computing Foundation),对云原生的定义是:
云原生技术可以帮助企业通过公有云、私有云、混合云等现代化的动态环境构建并运行可伸缩的应用。例如:容器、服务网格(Service Mesh)、微服务、不可变基础架构(Immutable infrastructure)以及声明式 API。
总结一下,云原生应用能充分利用云计算模型带来的各种优势, 而微服务是其中的一种实现形式。下面的定义会帮助你更清晰地理解:
云原生应用是专门面向云计算环境设计的应用,而非简单的应用迁移上云。
“微服务”来啦!
“微服务架构风格是一种将单个应用程序开发成一套小型服务的方法,每个小型服务都在自己的进程中运行,并通过轻量级机制(通常为 HTTP 资源 API)相互通信。这些服务围绕业务功能构建,可通过全自动部署机制独立部署。此外,这些服务至少应该能集中管理,可以用不同编程语言编写,并可以使用不同的数据存储技术。”
——知名软件工程师,敏捷开发方法创始人之一,Martin Fowler
由此可见微服务是一种可以协同工作的小型、专注、自治的服务。
小型、专注体现了微服务的单一职责(Single Responsibility)。一个服务只需要将一件事做好就够了。自治则意味着容错能力,每个服务可以彼此独立地演化和部署。
微服务就其本质来说与云计算平台的关联极为密切,但微服务这个概念本身并不是新事物。这个概念多年前就出现了。
微服务的概念虽然出现已久,但并未真正流行。主要是因为传统的本地化部署使微服务的落地相当困难。而云计算的出现提供了更好的扩展性、可靠性和可维护性,所以基于云计算的微服务实现和维护更加便利,也就流行起来。目前我们可以从微服务里获得收益包括但不限于这些:
- 弹性:一个组件出现问题不会拖累整个系统,这通常是通过清晰定义的服务边界实现的。
- 可伸缩性:如果只有一个组件的性能达到上限,可单独进行扩容,而无需对整个系统进行扩容。
- 易于部署:可只改动一个服务,进而加快发布周期并简化排错过程。
- 可组合性:每个服务只做一件事,因此可以像 Unix 管道那样轻松实现服务的复用。
- 可替换性:每个小型的服务都可以更容易地使用更好的实现或技术来替换。
Spring Cloud
微服务有很多优点,但是构建微服务的过程依然相当复杂。为了使微服务架构轻松实现,业界定义了一些通用的模式。比较知名的有,集中化的中心化配置管理、服务注册和发现、异步消息驱动以及分布式追踪。Spring Cloud 将这些微服务架构模式融合到具体实践中,帮助我们更好地遵循云原生最佳实践。如下图:
Spring Cloud 独特的价值主要体现在这几方面:
- 为常用模式定义了通用抽象。这也是 Spring 解耦哲学的另一个精彩应用:每种模式并不与具体的实现紧密耦合。以 Config Server 为例,我们可以在不影响其他服务的前提下随意更改配置所用的后端存储。Discovery 和 Stream 也采用了类似方式。
- 模块化组件。很多人第一印象认为 Spring Cloud 是一种重量级的全家桶式的解决方案。但实际上,这并不是一种全有或全无的解决方案,我们可以只选择一个模块并将该模块用作一个微服务,其他服务依然可以灵活选择其他任何框架。这就像乐高积木,我们只需要选择自己需要的模块,并确保接口兼容就行了。
那么 Spring Cloud 的模块是如何融入微服务模式的?
Spring Cloud Config:集中化配置管理
为满足Store config in environment
(存储配置于环境中)以及微服务架构的要求,我们需要将所有服务的配置存储在一个集中的位置。此外还需要具备下列功能:
- 支持开发、测试、生产等多种环境。这样就可以为所有环境提供同一个软件包,配置从环境中获取。
- 以透明的方式引入配置。配置可以无需编写代码自动获取。
- 属性变化后自动刷新。服务应该能获得变化的通知并重新载入新的属性。
维持变更历史并能轻松回退至老版本。这是一个很实用的功能,可以帮我们撤销生产环境中错误的改动。
Spring Cloud Config 通过两个简单的注解来支持这些功能。我们只需要在 Config Server 中注解 @EnableConfigServer,并在其他服务中包含 Starter 即可启用客户端。详情可参阅 Spring Cloud Config 文档 ( http://cloud.spring.io/spring-cloud-config/single/spring-cloud-config.html )。
Spring Cloud Discovery:服务发现
服务发现是大部分分布式系统和面向服务的架构中一个重要的组件。这个问题看起来很简单:客户端如何确定运行于多个主机上的服务所使用的 IP 和端口。但随着在云环境中部署的服务越来越多,问题开始变得复杂了。
确定服务位置,包含两部分:
- 服务注册:指服务在中央注册表中注册自己位置的过程,此时通常需要注册主机和端口号,有时候还需要注册身份验证凭据、协议、版本号和 / 或环境细节。
- 服务发现:指客户端应用通过查询中央注册表确定服务位置的过程。
那么又该如何选择服务发现解决方案?此时有很多因素需要考虑:
- 监控:如果已注册的服务失败了然后会怎样?有时会立即撤销注册,或者超时后撤销注册,或者被其他进程撤销注册。因此通常需要为服务实现心跳检测机制来确保服务始终在线,并确保客户端通常可以正确、可靠地处理服务失败。
- 负载均衡:如果一个服务注册了多个节点,客户端该如何实现负载均衡?如果存在主(Master)服务,客户端可以正确识别吗?
- 集成方式:注册表是否只提供了与少数语言的绑定(例如仅 Java)?集成过程是否需要在应用程序中嵌入实现注册和发现的代码?还是需要提供守护进程?
- 可用性问题:失去一个节点后是否依然可以正常运行?是否可以在不造成中断的前提下升级?注册表是整个架构的核心,这是否会导致单点故障?
Spring Cloud 为注册和发现提供了通用的抽象,我们只需要使用 @EnableDiscoveryClient,相应的客户端会自动绑定。Spring Cloud Discovery Eureka 和 Spring Cloud Discovery Zookeeper 提供了服务发现的具体实现。我们需要根据具体地业务场景选择不同的实现,详情可参阅 Spring cloud Discovery 文档 ( https://cloud.spring.io/spring-cloud-commons/multi/multi__spring_cloud_commons_common_abstractions.html )。
Spring Cloud Stream:消息驱动的架构
既然我们有了众多的微服务,那么它们肯定需要互相通信。传统的同步调用显然不能满足微服务多变的复杂环境,所以异步的消息驱动是必然趋势。其实,一切请求都可以视作是消息,因此也就诞生了使用不同格式和 API 的消息中间件。让这些消息中间件相互通信无异于一场噩梦。解决这个问题其实很简单,只需要定义统一的消息接口,随后为每个中间件提供适配器,让它们知道如何在自己的消息和标准格式之间进行转换就行了。这就是 Spring Integration 的核心设计理念。
Spring Integration 的目标主要在于:
- 为复杂的企业级集成方案提供一种更简单的模型。
- 推行异步的消息驱动行为。
- 促进现有 Spring 用户渐进地采用这一技术。
同时 Spring Integration 的设计主要遵循了下列原则:
- 为了提高模块化程度和可测试性,组件必须松散耦合。
- 框架应在业务逻辑和集成逻辑之间强制实施关注点隔离(Separation of concerns)。
- 扩展点是抽象的,并且有合理地边界,以此来促进复用和可移植能力。
详情可参阅 Spring Integration 文档 ( https://docs.spring.io/spring-integration/reference/htmlsingle/ )。
不过目前 Spring Integration 依然专注于底层,并包含很多晦涩难懂的术语。所以这种编程模型不像其他 Spring 技术那么易用,于是又诞生了 Spring Cloud Stream。Spring Cloud Stream 基于标准消息格式和 Spring integration 提供的多种适配器,可以工作在高层抽象中,借此以更简单的方式产生、处理和使用消息。它也类似于 Unix 管道,我们只需要关注如何处理消息,消息将按照我们的预期来之,去之。详情可参阅 Spring Cloud Stream 文档 ( https://docs.spring.io/spring-cloud-stream/docs/current/reference/htmlsingle/ )。
Spring Cloud Sleuth & Zipkin:分布式追踪
在微服务架构下,由于服务进行了拆分,一个请求很可能涉及多个服务,这些服务可能部署在不同的机器之上。尽管很多解决方案都实现了集中的日志存储和查询,但是如果遇到调用失败,想快速定位问题仍然非常困难。一般的做法是多次反复在日志中查询相关的关键字来寻找线索。这种方法费时费力,而且容易出错。实际上,我们需要的是一个可以帮助我们整合一次调用链的所有相关信息的系统。
Spring Cloud Sleuth 通过引入 Span 和 Trace 的概念来实现这种聚合。通俗地讲,一个 Span 就是一次服务的调用,而 Trace 就是包含多个 Span 的树形结构,比如一次分布式调用会包含多个服务调用。Sleuth 会把相应的 SpanId 和 TraceId 记录在对应的日志中。如下图:
实际中,会被记录的操作包括:
- MVC controller 收到的 HTTP 请求
- 通过 RestTemplate 发送的请求
- 通过 Spring Cloud Stream Binder 发送和接受的请求
- 其他在 Spring 生态系统中的请求和回复
然而,有了这些信息还远远不够,我们还需要把这些信息整合,处理,并以简单直观的方式展现出来。这就是 Zipkin 发挥作用的地方。它提供的存储模块和 UI 界面可以帮助我们理解整个调用链。如下图所示。
Spring Cloud Azure
Spring Cloud 虽然对通用的模式提供了很好的实现,但如果想在实际的云服务上面使用还有一定的距离。所以,Spring Cloud Azure 遵循了 Spring Cloud 提供的最佳实践和通用抽象,并在此基础上更进一步提供了自动化的资源配置和自动配置 Azure 服务相关属性的能力。借此用户只需要从较高层面了解 Azure 服务即可顺利使用,而无需涉及有关配置和 SDK API 的底层细节。以 Azure EventHub 为例,我们只需要知道这是一种在设计上与 Kafka 类似的消息服务,随后即可使用 Azure EventHub 的 Spring Cloud Stream Binder 生成并使用消息。
Spring Cloud Azure 的设计理念主要包括:
- 简化 Spring Cloud 与 Azure 的集成。用户无需修改现有代码即可轻松使用 Azure 服务,并且只需要提供最少的依赖和配置。
- 极简配置。为此可以充分利用 Spring boot auto config,根据 Azure 资源管理器 API 预配置默认属性值,但用户也可以用自己的配置覆盖默认值。
- 自动管理资源。如果资源不存在,将能在用户指定的订阅和资源组中自动创建所需资源。
- 与云供应商解耦。用户可以轻松地使用 Azure 服务以及 Spring Cloud 提供的便利性,无需受制于某个特定的云供应商。
Azure 资源管理器:自动配置和资源管理
配置,这可能是开发者最不愿做的工作之一。配置每个属性前,开发者必须完整阅读相关文档并全面了解每个属性的含义,随后小心谨慎地从一个位置复制每个属性,然后粘贴到应用的属性文件中。然而麻烦还没完,他们还需要为每个属性提供必要的备注,以便让其他开发者明白在每种场景下需要更改哪个属性,并且怎样做才不会出错。我们想要解决这个痛点,因此基于 Spring boot 提供了自动配置功能。
举例来说,如果想使用 Azure EventHub,此时并不需要了解连接字符串是什么,只需输入 EventHub 的名称空间(类似于 Kafka 的集群名称)和 EventHub 名称(类似于 Kafka 的话题名称),其他都会自动配置。当然,我们也可以通过自定义配置覆盖默认值。
云最大的优点之一在于可以通过可编程的 API 创建并查询自己的资源。这也是实现自动化的关键。Spring Cloud Azure 可借助 Azure 资源管理器实现自动化的资源配置。其实资源的范畴很大,例如 AzureEventHub 的 Consumer group。当我们有一个新服务使用另一个新的 Consumer Group 时,无需手工创建。
Spring Cloud Stream Binder 和 Azure EventHub
在了解了 Spring Cloud Stream 的优势后,假设你开始使用它,但希望将其迁移至 Azure,此时该怎么做?你可能已经使用了 Kafka 或 RabbitMQ Binder,但 Azure 似乎并未提供此类托管式的 Kafka 或 RabbitMQ 服务。那么怎样用最快捷的方法来迁移?
实际上我们并不需要关注自己到底使用了哪种消息中间件,只要有一个组件能提供类似的功能并满足性能要求就行了。因此我们只需要将依赖项从 Kafka Binder 改为 Azure EventHub Binder 就行,完全不用更改任何代码即可平滑迁移。Azure EventHub Binder 提供了丰富地高级功能,如下:
Consumer Group:
与 Apache Kafka 类似,EventHub 也为轻量级 Consumer 组提供了类似的支持,但实现上略有差异。Kafka 会将所有已提交的偏移量(Offset)存储在 Broker 中,但 EventHub 偏移量的存储工作需要手工进行。EventHub SDK 提供了在 Azure 存储帐户中保存偏移量的功能。
分区支持:
EventHub 提供了与 Kafka 类似的物理分区概念,但与 Kafka 会在 Consumer 和分区之间自动再均衡的做法不同,EventHub 使用了一种抢占模式。Azure 存储帐户可以充当「租约」来决定每个分区是哪个 Consumer 所拥有的。当新 Consumer 启动后,它会尝试着从负载最重的 Consumer「抢占」一部分分区,借此实现负载均衡。
检查点支持:
在分布式发布 - 订阅消息系统中,主要存在三种消息语义:最少一次(At-least-once)、最多一次(At-most-once)以及严格一次(Exactly-once)。目前我们只考虑了使用方:
- 最少一次:Consumer 接收并处理消息,但在消息成功处理完毕之前,并不向 Broker 发出确认。如果因为某些原因导致处理工作未能完成,例如 Consumer 节点故障,同一条消息将被(作为分区中的下一条可用事件)重新处理,借此确保消息最少可以被使用一次。这种情况下,(在消息被成功处理完成之前)Consumer 可能会将同一条消息处理多次,
Manual
检查点模式为消息处理完毕之后的手工检查点操作提供了支持。 - 最多一次:Consumer 收到消息并立即向消息 Broker 发送确认,随后开始处理该消息。但如果 Consumer 在处理过程中遇到故障,该消息将无法重新处理,因为 Broker 会认为 Consumer 已经收到了这条消息。这种情况下,Consumer 最多将一条消息处理一次,但可能因为处理过程中的错误而丢失某些消息。这也是 EventHub Binder 默认的
Batch
检查点模式。 - 严格一次:在严格一次语义中,我们会通过唯一的消息 ID 对已经处理过的消息进行去重。或者也可以实施幂等的消息处理机制。详情可参阅 Exactly once ( https://www.confluent.io/blog/exactly-once-semantics-are-possible-heres-how-apache-kafka-does-it/ )。
通过使用自定义消息头暴露 Checkpointer,Azure EventHub Binder 即可支持不同的消息使用语义。详情可参阅 Spring Cloud Stream Event Hub binder 文档 或者也可以通过范例自行尝试。
Spring 资源和 Azure 存储 Blob
Spring 资源为UrlResource
、ClassPathResource
和FileSystemResource
等基于流的资源的操作提供了通用接口。很明显,Azure 存储 Blob 很适合充当这种BlobResource
。在这种Resource
中,所有实现方面的细节均已隐藏,不存在的文件也可以自动创建。
详情可参阅Spring Resource with Azure Storage 文档,或通过范例自行尝试。
Spring Cloud Azure Playground:一键运行微服务
尽管 Spring Cloud 提供了很好的微服务构建支持,但是对于首次尝鲜微服务的小白,想迅速搭建一个基于 Spring Cloud 的可以运行的微服务仍然很有挑战,通常包含以下工作:
- 需要为每个微服务创建单独的项目和依赖。尽管 Spring Initializr 在这方面提供了很好的支持,但是微服务的数量很多,每个分别创建也是很繁琐的工作。
- 确保微服务之间的版本和依赖互相兼容。
- 不同的微服务有不同的配置,而且有些有相互的依赖关系。手动配置很容易遗漏和犯错。
- Spring Cloud 提供的基础微服务有各自的注解和配置来启动微服务。手工配置需要参考各自的样例。
- 很多用户希望可以一键在本地运行微服务来验证可行性。手工编写 docker file 也需要一定的时间和对各个服务关系的理解。
为了解决上述问题,我们构建了 Spring Cloud Azure Playground 来帮助大家轻松构建微服务。提供的功能如下:
- 基于 Spring Initializr,可以一次创建全套的可运行微服务
- 每个微服务包含完整的依赖,注解,配置和样例。通过相应的 docker file 可以一键运行。
- 用户可以自定义微服务的端口和名字,以避免与本地运行的其他服务冲突。
- 用户可以选择下载到本地或者推送到 GitHub。方便团队共享。
你可以通过以下链接访问: https://aka.ms/springcloud 。如下图:
目前,Spring Cloud Azure 已全面开源并已发布至Github ( https://github.com/Microsoft/spring-cloud-azure )。欢迎感兴趣的朋友进一步了解、使用或提出意见建议。
参考链接:
Spring Resource with Azure Storage 文档: https://github.com/Microsoft/spring-cloud-azure/tree/release/1.0.0.M2/spring-cloud-azure-starters/spring-azure-starter-storage
Spring Cloud Stream Event Hub binder 文档: https://github.com/Microsoft/spring-cloud-azure/tree/release/1.0.0.M2/spring-cloud-azure-eventhub-stream-binder
作者简介
朱仲玮,Azure 资深工程师,主要致力于改进 Java 开发者在 Azure 的体验,Spring Cloud Azure 的主要贡献者。曾就职于惠普,eBay 和 Bing,对分布式架构,微服务和大数据有丰富的经验。
感谢张婵对本文的审校。
评论 3 条评论