为何需要微服务特性?
在微服务架构中,应用程序是由多个相互连接的服务组成的,这些服务协同工作以实现所需的业务功能。
所以,一个典型的企业级微服务架构如下所示:
最初,我们可能认为使用微服务架构实现一个应用程序是很容易的事情。但是,要恰当地完成这一点并不容易,因为我们会面临一些新的挑战,而这些挑战是单体架构所未曾遇到的。举例来讲,这样的挑战包括容错、服务发现、扩展性、日志和跟踪等。
为了应对这些挑战,每个微服务都需要实现在 Red Hat 被称为“微服务特性(microservicility)”的内容。这个术语指的是除了业务逻辑之外,服务必须要实现的一个横切性关注点的列表,总结起来如下图所示:
业务逻辑可以使用任何语言(Java、Go 或 JavaScript)或任何框架(Spring Boot、Quarkus)来实现,但是围绕着业务逻辑,我们应该实现如下的关注点:
API:服务可以通过一组预先定义的 API 操作进行访问。例如,在采用 RESTful Web API 的情况下,会使用 HTTP 作为协议。此外,API 还可以使用像Swagger这样的工具实现文档化。
发现(Discovery):服务需要能够发现其他的服务。
调用(Invocation):在服务发现之后,需要使用一组参数来调用它,并且可能会返回一个响应。
弹性(Elasticity):微服务架构很重要的特性之一就是每个服务都是有弹性的,这意味着它可以根据一些参数(比如系统的重要程度或当前的工作负载)独立地进行扩展和伸缩。
回弹性(Resiliency):在微服务架构中,我们在开发时应该要考虑到故障,特别是与其他服务进行通信的时候。在单体架构中,应用会作为一个整体进行启动和关闭。但是,当我们把应用拆分成微服务架构之后,应用就变成由多个服务组成的,所有的服务会通过网络互相连接,这意味着应用的某些部分可能在正常运行,而其他部分可能已经出现了故障。在这种情况下,很重要的一点就是遏制故障,避免错误通过其他的服务进行传播。回弹性(或称为应用回弹性)是指一个应用/服务能够对面临的问题作出反应的能力,在出现问题的时候,依然能够提供尽可能最好的结果。
管道(Pipeline):服务应该能够独立部署,不需要任何形式的部署编排。基于这一点,每个服务应该有自己的部署管道。
认证(Authentication):在微服务架构中,涉及到安全性时,很重要的一个方面就是如何认证/授权内部服务之间的调用。Web token(以及通用的 token)是在内部服务之间声明安全性的首选方式。
日志(Logging):在单体应用中,日志是很简单的事情,因为应用的所有组件都在同一个节点中运行。现在,组件以服务的形式分布在多个节点上,因此,为了全面了解日志跟踪的情况,我们需要一个统一的日志系统/数据收集器。
监控(Monitoring):要保证基于微服务的应用正确运行,很重要的一个方面就是衡量系统的运行情况、理解应用的整体健康状况并在出现问题的时候发出告警。监控是控制应用程序的重要方面。
跟踪(Tracing):跟踪用来可视化一个程序的流程和数据进展。当我们需要检查用户在整个应用中的操作时,它对开发人员或运维人员尤其有用。
Kubernetes 正在成为部署微服务的事实标准工具。它是一个开源的系统,用来自动化、编排、扩展和管理容器。
但是在我们提到的十个微服务特性中,通过使用 Kubernetes 只能覆盖其中的三个。
发现(Discovery) 是通过_Kubernetes Service_理念实现的。它提供了一种将_Kubernetes Pod_(作为一个整体)进行分组的方式,使其具有稳定的虚拟 IP 和 DNS 名。要发现一个服务只需要在发送请求的时候使用 Kubernetes 的服务名作为主机名即可。
使用 Kubernetes 调用(Invocation) 服务是非常容易的,因为平台本身提供了所需的网络来调用任意的服务。
弹性(Elasticity) (或者说扩展性)是 Kubernetes 从一开始就考虑到的问题,例如,如果运行kubectl scale deployment myservice --replicas=5
命令的话,myservice deployment 就会扩展至五个副本或实例。Kubernetes 平台会负责寻找合适的节点、部署服务并维持所需数量的副本一直处于运行状态。
但是,剩余的微服务特性该怎么处理呢?Kubernetes 只涵盖了其中的三个,那么我们该如何实现剩余的哪些呢?
根据所使用的语言或框架,我们有很多可遵循的策略,但是在本文中,我们会看到如何使用Quarkus来实现其中某些微服务特性。
什么是 Quarkus?
Quarkus是一个全栈、Kubernetes 原生的 Java 框架,适用于 Java 虚拟机(JVM)和原生编译环境,针对容器环境对 Java 的进行了专门的优化,使其成为一个可用于无服务器、云和 Kubernetes 环境的高效平台。
Quarkus 没有重复发明轮子,而是使用了由标准/规范支撑的知名企业级框架,并使它们可以借助GraalVM编译成二进制文件。
什么是 MicroProfile?
Quarkus 集成了MicroProfile规范,将企业级 Java 生态系统转移到了微服务架构中。
在下图中,我们可以看到构成 MicroProfile 规范的所有 API。其中有些 API 是基于Jakarta EE(也就是以前的 Java EE)规范的,比如 CDI、JSON-P 和 JAX-RS,其他的则是由 Java 社区开发的。
接下来,我们就使用 Quarkus 来实现 API、调用、回弹性、认证、日志、监控和跟踪等微服务特性。
如何使用 Quarkus 实现微服务特性
起步
开始使用 Quarkus 的最快捷方式就是通过起始页面,在这里我们可以添加所需的依赖。就本例来讲,我们要注册如下的依赖以满足微服务特性的需求:
API:RESTEasy JAX-RS、RESTEasy JSON-B 和 OpenAPI
调用:REST Client JSON-B
回弹性:Fault Tolerance
认证:JWT
日志:GELF
监控:Micrometer metrics
跟踪:OpenTracing
我们可以手动选择这些依赖,也可以导航至如下的链接Microservicilities Quarkus Generator,在这里所有的依赖都已经选择好了。然后点击“Generate your application”按钮以下载包含脚手架应用的压缩文件。
服务
在本例中,我们会创建一个非常简单的应用,它只包含两个服务。其中一个服务名为_Rating service_,它会返回给定一本书的评分,另外一个服务名为_Book service_,它会返回某本书的信息及其评分。服务之间的所有调用必须要进行认证。
在下图中,我们可以看到完整系统的概览:
_Rating service_已经开发完成并且能够以 Linux 容器的形式供我们使用。我们可以使用如下的命令在 9090 端口启动该服务:
为了校验该服务,我们可以发送请求到http://localhost:9090/rate/1
这里的状态码是401 Unauthorized
,这是因为我们没有在请求中以 bearer token(JWT)的形式提供认证信息。带有_group_ Echoer
的合法 token 才能访问_rating service_。
API
Quarkus 使用大家熟知的 JAX-RS 规范来定义 RESTful web API。在底层,Quarkus 使用了 RESTEasy 实现,直接与 Vert.X 框架协作,而不是使用 Servlet 相关的技术。
现在我们为 book service 定义 API,实现最常见的操作:
我们要注意的第一件事情就是这里定义了四个不同的端点:
GET /book/{bookId}
使用 GET HTTP 方法来返回图书的信息,其中包括它的评分。返回元素会自动反编组(unmarshal)为 JSON。POST /book
使用 POST HTTP 方法插入来自请求体内容的一本图书。请求体的内容会自动从 JSON 编组(marshal)为 Java 对象。DELETE /book/{bookId}
使用 DELETE HTTP 方法以根据 ID 删除某本图书。GET /book/search?description={description}
根据描述搜索图书。
要注意的第二件事就是返回的类型,有时候我们返回的是一个 Java 对象,有时候返回的是javax.ws.rs.core.Response
的实例。当使用 Java 对象的时候,我们会将 Java 编组为@Produces
注解所设置的媒体类型。具体到本服务中,输出是 JSON 文档。如果使用Response
对象的话,对于返回什么内容给调用者,我们会有更细粒度的控制,例如,我们可以设置返回给调用者的 HTTP 状态码、头信息或内容。至于该优选选择哪种方式,这取决于具体的使用场景。
调用
定义完访问_book service_的 API 之后,我们就该开发调用_rating service_服务以获取图书评分信息的代码了。
Quarkus 使用MicroProfile Rest Client规范来访问外部的(HTTP)服务。它提供了一种类型安全的方式借助 HTTP 协议访问 RESTful 服务,在这个过程中,它会使用 JAX-RS 2.0 的一些 API 以实现一致性和更简单的重用。
我们要创建的第一个元素是代表远程服务的接口,它会用到 JAX-RS 的注解。
当getRate()
方法被调用的时候,会触发一个对/rate/{bookId}
的远程 HTTP 调用,在这个过程中bookId
会被替换为方法参数中的值。很重要的一点就是,要为接口添加@RegisterRestClient
注解。
然后,RatingService
接口需要注入到BookResource
中以执行远程调用。
@RestClient
注解会注入对应接口的一个代理实例,从而提供了客户端的实现。
最后需要配置的就是服务的位置(_hostname_部分)。在 Quarkus 中,配置属性是在src/main/resources/application.properties
文件中设置的。要配置服务的位置,我们需要使用 Rest Client 接口的全限定名并结合 URL 作为键,然后使用实际的位置作为值:
要想正确访问_rating_服务并摆脱401 Unauthorized
的问题,我们还需要解决相互认证的问题。
认证
基于 token 的认证机制允许系统基于一个安全 token 进行认证、授权和身份验证。Quarkus 集成了MicroProfile JWT RBAC Security规范,以使用 JWT Bearer Token 来保护服务。
要使用 MicroProfile JWT RBAC Security 来保护一个端点,我们只需要为方法添加@RolesAllowed
注解即可。
随后,我们还需要在application.properties
文件中配置 token 的 issuer 以及公钥文件的位置,以便于校验 token 的签名:
该扩展会执行如下的校验:token 是合法的;issuer 是正确的;token 没有被修改过;签名是合法的;它还没有过期。
现在,_book service_和_rating service_都使用相同的 JWT issuer 和秘钥进行保护,所以服务之间的通信需要用户进行认证,这是通过在Authentication
头信息中提供一个合法的 bearer token 实现的。
rating service_运行起来之后,我们就可以使用如下的命令启动_book service:
最后,我们可以发送请求来获取图书信息并提供一个合法的 JSON Web Token 作为 bearer token。
至于 token 如何生成超出了本文的范围,我们假设 token 已经能够生成:
响应再次提示禁止访问的错误:
你可能会想在提供了合法的 token 之后,为何还会遇到这个错误。如果我们探查一下 book service 的控制台,就会看到如下的异常:
出现这个异常的原因在于我们已经认证并授权访问_book service_,但是这个 bearer token 并没有传递到_rating service_中。
为了让Authorization
头信息能够从传入的请求自动传播至 rest-client 请求,我们需要进行两项修改。
第一项修改是更新 Rest Client 接口并为其添加org.eclipse.microprofile.rest.client.inject.RegisterClientHeaders
注解。
第二项修改是配置哪些头信息要在请求之间进行传递,这是在application.properties
文件中进行配置:
我们再次使用相同的 curl,就会得到正确的输出了:
回弹性
在微服务架构中,服务具备容错性是非常重要的,这样可以避免一个故障从某个服务传播至它的所有直接和间接的调用者。Quarkus 将MicroProfile Fault Tolerance规范与如下的注解集成到了一起,以便于处理故障相关的问题:
● @Timeout
:定义在抛出异常之前,某个服务最长的持续时间。
● @Retry
:如果调用失败的话,会再次进行尝试执行。
● @Bulkhead
:并发执行的限制,这样的话,该区域出现的故障不会导致整个系统超载。
● @CircuitBreaker
:当执行反复失败时,该服务会自动地快速失败。
● @Fallback
:当执行失败的时候,提供一个替代方案/默认值。
在访问_rating service_的时候,如果出现错误,我们会进行三次重试并在每次重试之间添加一秒钟的睡眠计时器。
现在,我们关掉_rating service_并执行请求,将会抛出如下的异常:
显然,这里会有错误,但是需要注意在抛出异常之前,我们经历了三秒钟的时间,这是因为执行了三次重试,并且每次重试间有一秒钟的延迟。
在这种情况下,_rating service_已经被我们停掉了,所以不可能恢复,但是在现实世界的例子中,_rating service_可能只会停机很短的时间,或者服务部署了多个副本,这样的话,简单的重试操作可能就足以恢复并提供一个合法的响应。
但是,当重试不足以解决问题并且抛出异常的时候,我们可以将错误传播至调用者,也可以为调用提供一个替代值。这个替代值可以来自对其他系统的调用(如分布式缓存),也可以是一个静态值。
就本例来讲,当连接_rating service_失败的时候,我们会返回一个值为 0 的评分值。
为了实现这个回退(fallback)逻辑,我们首先要做的就是实现org.eclipse.microprofile.faulttolerance.FallbackHandler
接口,并将返回值设置为相同的类型,因为回退策略方法的作用是返回一个替代值。在本例中,会返回一个默认的Rate
对象。
最后要做的就是为getRating()
方法添加@org.eclipse.microprofile.faulttolerance.Fallback
注解,配置在无法进行恢复的时候要执行的回退类。
如果我们重复前面的请求,此时不会抛出异常,而是会返回一个合法的输出,其中评分字段的值被设置成了 0。
相同的方式也可以用于该规范提供的其他模式。比如,如果使用断路器模式的话::
如果在滚动时间窗口中,四个连续的请求中有_三个(即 4 x 0.75)_出现了故障,那么断路器就会打开 1000 毫秒的时间,然后会回到半开状态。当断路器处于半开状态时,如果调用成功了,那么会再次关闭。否则的话,它会继续保持打开的状态。
日志
在微服务架构中,推荐将所有服务的日志收集到一起,以便于高效使用和理解。
其中有个解决方案就是使用Fluentd,这是一个开源的数据收集器,能够用来实现 Kubernetes 中统一的日志层。Quarkus 使用 Graylog 扩展日志格式(Graylog Extended Log Format,GELF)与 Fluentd 进行了集成。
具体的集成是非常简单的。首先,像其他的 Quarkus 应用那样使用日志逻辑:
接下来,启用 GELF 格式并设置 Fluentd 服务器的地址:
最后,我们可以发送请求给实现日志功能的端点:
在输出方面并没有任何变化,但是日志已经被传输到了 Fluentd 上。如果我们使用Kibana来可视化数据的话,就会看到如下所示的日志行:
监控
监控是另外一个我们需要在微服务架构中实现的微服务特性。Quarkus 集成了Micrometer实现应用监控。Micrometer 为几乎所有流行的监控系统提供了一个简单的入口,从而能够让我们在避免供应商锁定的前提下 instrument 基于 JVM 的应用。
对于本例来讲,我们使用Prometheus格式作为监控输出,但是 Micrometer(和 Quarkus)也支持其他的格式,比如 Azure Monitor、Stackdriver、SignalFx、StatsD 和 DataDog。
我们可以注册如下的 Maven 依赖以提供 Prometheus 输出:
Micrometer 扩展默认会注册一些与系统、JVM 或 HTTP 相关的指标。所收集到的指标的一个子集可以通过/q/metrics
端点进行访问,如下所示:
但是,我们还可以使用 Micrometer API 实现应用特定的指标。
在这里,我们实现一个自定义的指标来衡量评分最高的图书。
要注册一个指标,也就是本例中的一个 gauge,是通过使用io.micrometer.core.instrument.MeterRegistry
类来完成的。
我们发送一些请求并校验 gauge 是否被正确地更新了。
我们还可以设置一个计时器,记录从 rating service 获取评分信息所耗费的时间。
我们发送一些请求并校验是否收集了评分服务的耗时。
Micrometer 使用MeterFilter
实例来自定义由MeterRegistry
实例所发出的指标。Micrometer 扩展将会探测到MeterFilter
CDI bean 并使用它们来初始化MeterRegistry
实例。
例如,我们可以定义一个通用的标签来设置应用运行的环境(prod、testing、staging 等)。
发送一个新的请求并校验指标是否添加了标签。
请注意,标签env
包含的值为prod
。
跟踪
Quarkus 应用使用OpenTracing规范来为互相交互的 Web 应用提供分布式跟踪能力。
接下来,我们配置 OpenTracing 连接一个 Jaeger 服务器,并将服务的名字设置为 book-service 以标识跟踪信息:
现在,我们发送一个请求:
访问 Jaeger UI 来校验调用过程被进行了跟踪:
结论
开发和实现微服务架构要比开发单体应用更具挑战性。我们相信,微服务特性能够促使你在应用基础设施方面正确地开发服务。
我们在这里所阐述的微服务特性(除 API 和管道之外)都是新的理念,或者说在单体应用中会以不同的方式来实现。其中的原因在于,现在应用被拆分成了多个组成部分,所有的这些组成部分需要在网络中进行相互连接。
如果你打算开发微服务并将它们部署到 Kubernetes 的话,那么 Quarkus 是一个很好的解决方案,因为它可以很平滑地与 Kubernetes 进行集成,实现大多数的微服务特性都非常简单,只需要几行代码就能实现。
用来阐述本文的源码都可以在github上找到。
作者简介:
Alex Soto 是红帽公司的开发者体验总监。他对 Java 领域、软件自动化充满热情,他相信开源软件模式。Soto 是Manning的《Testing Java Microservices》和O’Reilly的《Quarkus Cookbook》两本书的共同作者,他还是多个开源项目的贡献者。自 2017 年以来,他一直是 Java Champion,是国际演讲者和 Salle URL 大学的教师。
英文原文:
Implementing Microservicilities with Quarkus and MicroProfile
评论