mirror of
https://github.com/DocsHome/microservices.git
synced 2025-12-08 19:25:13 +00:00
commit
e4ba5e89ec
@ -10,7 +10,7 @@
|
||||
## 1.1、构建单体应用
|
||||
我们假设,您开始开发一个打车应用,打算与 Uber 和 Hailo 竞争。经过初步交流和需求收集,您开始手动或者使用类似 Rails、Spring Boot、Play 或者 Maven 等平台来生成一个新项目。
|
||||
|
||||
该新应用是一个模块化的六边形架构,如图 1-1 所示:
|
||||
该新应用有一个模块化的六边形架构,如图 1-1 所示:
|
||||
|
||||
![图 1-1、一个简单的打车应用][image-1]
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
```
|
||||
GET api.company.com/productdetails/productId
|
||||
```
|
||||
```
|
||||
|
||||
负载均衡器将请求路由到几个相同应用程序实例中的其中一个。之后,应用程序查询各个数据库表并返回响应给客户端。相比之下,当使用微服务架构时,产品详细页面上展示的数据来自多个微服务。以下是一些微服务,可能拥有给定产品页面展示的数据:
|
||||
|
||||
@ -72,7 +72,7 @@ https://serviceName.api.company.name
|
||||
|
||||
API 网关负责请求路由、组合和协议转换。所有的客户端请求首先要通过 API 网关,之后请求被路由到适当的服务。API 网关通常会通过调用多个微服务和聚合结果来处理一个请求。它可以在 Web 协议(如 HTTP 和 WebSocket)和用于内部的非 Web 友好协议之间进行转换。
|
||||
|
||||
API 还可以为每个客户端提供一个定制 API。它通常会为移动客户端暴露一个粗粒度的 API。例如,考虑一下产品详细信息场景。API 网关可以提供一个端点 `/productdetails?productid=xxx`,如图 2-3 所示,一个使用了 API 网关的微服务。允许移动客户端通过一个单独的请求来检索所有产品详细信息。API 网关通过调用各种服务(产品信息、推荐、评价等)并组合结果。
|
||||
API 网关还可以为每个客户端提供一个定制 API。它通常会为移动客户端暴露一个粗粒度的 API。例如,考虑一下产品详细信息场景。API 网关可以提供一个端点 `/productdetails?productid=xxx`,如图 2-3 所示,一个使用了 API 网关的微服务。允许移动客户端通过一个单独的请求来检索所有产品详细信息。API 网关通过调用各种服务(产品信息、推荐、评价等)并组合结果。
|
||||
|
||||
一个很好的 API 网关案例是 [Netflix API 网关](http://techblog.netflix.com/2013/02/rxjava-netflix-api.html)。Netflix 流媒体服务可用于数百种不同类型的设备,包括电视机、机顶盒、智能手机、游戏机和平板电脑等。起初,Netflix 尝试为他们的流媒体服务提供一个[通用](http://www.programmableweb.com/news/why-rest-keeps-me-night/2012/05/15)的 API。后来,他们发现由于设备种类繁多,并且他们各自有着不同需求,所以并不是能很好地运作。如今,他们使用了 API 网关,通过运行特定设备适配代码来为每个设备提供一个定制 API。
|
||||
|
||||
@ -87,22 +87,22 @@ API 网关也存在一些缺点,它是另一个高度可用的组件,需要
|
||||
|
||||
<a id="implementing-an-api-gateway"></a>
|
||||
|
||||
## 2.5、实施 API 网关
|
||||
我们已经了解了使用 API 网关的动机与权衡。接下来让我们看看您需要考虑的各种设计问题。
|
||||
## 2.5、实现 API 网关
|
||||
我们已经了解了使用 API 网关的动机和利弊,接下来让我们看看您需要考虑的各种设计问题。
|
||||
|
||||
<a id="performance-and-scalability"></a>
|
||||
|
||||
### 2.5.1、性能与可扩展性
|
||||
只有少数公司能达到 Netflix 的运营规模,每天需要处理数十亿的请求。然而,对于大多数应用来说,API 网关的性能和可扩展性是相当重要的。因此,在一个支持异步、非阻塞 I/O 平台上构建 API 网关是很有必要的。可以使用不同的技术来实现一个可扩展的 API 网关。在 JVM 上,您可以使用基于 NIO 的框架,如 Netty、Vertx、Spring Reactor 或者 JBoss Undertow。一个流行的非 JVM 选择是使用 Node.js,它是一个建立在 Chrome 的 JavaScript 引擎之上的平台。此外,您还可以选择使用 NGINX Plus。
|
||||
只有少数公司能达到 Netflix 的运营规模,每天需要处理数十亿的请求。不管怎样,对于大多数应用来说,API 网关的性能和可扩展性是相当重要的。因此,在一个支持异步、非阻塞 I/O 平台上构建 API 网关是很有必要的。实现一个可扩展的 API 网关的技术多种多样。在 JVM 上,您可以使用基于 NIO 的框架,如 Netty、Vertx、Spring Reactor 或者 JBoss Undertow。一个流行的非 JVM 选择是使用 Node.js,它是一个建立在 Chrome 的 JavaScript 引擎之上的平台。此外,您还可以选择使用 NGINX Plus。
|
||||
|
||||
[NGINX Plus](https://www.nginx.com/solutions/api-gateway/) 提供了一个成熟、可扩展和高性能的 Web 服务器和反向代理,它易于部署、配置和编程。NGINX Plus 可以管理身份验证、访问控制、负载均衡请求、缓存响应,并且提供了应用程序健康检查和监控功能。
|
||||
|
||||
<a id="using-a-reactive-programming-model"></a>
|
||||
|
||||
### 2.5.2、使用响应式编程模型
|
||||
API 网关通过简单地把他们(请求)路由到适当的后端服务来处理一些请求。它通过调用多个后端服务并聚合结果来处理其他请求。对于某些请求,如产品详细信息请求,对后端服务请求而言是彼此独立的。为了把响应时间缩短到最小,API 网关应该并发执行独立请求。
|
||||
API 网关处理大部分请求只是简单的把它们路由到与之对应的后端服务。它通过调用多个后端服务并聚合结果来处理其他请求。对于某些请求,如产品详细信息请求,对后端服务请求而言是彼此独立的。为了把响应时间缩短到最小,API 网关应该并发执行独立请求。
|
||||
|
||||
然而,有时候,请求是相互依赖的。首先,API 网关可能需要在将请求路由到后端服务之前,通过调用验证服务来验证请求。同样,为了从客户的愿望清单中获取产品信息,API 网关首先必须检索包含该信息的客户资料,然后检索每个产品的信息。另一个有趣的 API 组合案例是 [Netflix 视频网格](http://techblog.netflix.com/2013/02/rxjava-netflix-api.html)。
|
||||
然而,有时候,请求是相互依赖的。首先,API 网关可能需要在将请求路由到后端服务之前,通过调用验证服务来验证该请求。同样,为了从客户的愿望清单中获取产品信息,API 网关首先必须检索包含该信息的客户资料,然后检索每个产品的信息。另一个有趣的 API 组合案例是 [Netflix 视频网格](http://techblog.netflix.com/2013/02/rxjava-netflix-api.html)。
|
||||
|
||||
使用传统的异步回调方式来编写 API 组合代码会很快使你陷入回调地狱。代码将会变得杂乱、难以理解并且容易出错。一个更好的方式是使用响应式方法以声明式编写 API 网关代码。响应式抽象的例子包括 Scala 的 [Future](http://docs.scala-lang.org/overviews/core/futures.html)、Java 8 中的 [CompletableFuture](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html) 和 JavaScript 中的 [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)。还有 [Reactive Extensions](http://reactivex.io/)(也称为 Rx 或 ReactiveX),最初由 Microsoft 为 .NET 平台开发。Netflix 为 JVM 创建了 RxJava,专门应用于其 API 网关。还有用于 JavaScript 的 RxJS,它可以在浏览器和 Node.js 中运行。使用响应式方式可让您能够编写出简单而高效的 API 网关代码。
|
||||
|
||||
@ -125,7 +125,7 @@ API 网关需要知道与其通信的每个微服务的位置(IP 地址和端
|
||||
<a id="handling-partial-failures"></a>
|
||||
|
||||
### 2.5.5、处理局部故障
|
||||
实施 API 网关时必须解决的另一个问题是局部故障问题。当一个服务调用另一个响应缓慢或者不可用的服务时,所有分布式系统都会出现此问题。API 网关不应该无期限地等待下游服务。但是,如何处理故障问题取决于特定的方案和哪些服务发生故障。例如,如果推荐服务在获取产品详细信息时没有响应,API 网关应将其余的产品详细信息返回给客户端,因为它们对用户仍然有用。建议可以是空的,也可以用其他代替,例如硬编码的十强名单。然而,如果产品信息服务没有响应,那么 API 网关应该向客户端返回错误。
|
||||
实现 API 网关时必须解决的另一个问题是局部故障问题。当一个服务调用另一个响应缓慢或者不可用的服务时,所有分布式系统都会出现此问题。API 网关不应该无期限地等待下游服务。但是,如何处理故障问题取决于特定的方案和哪些服务发生故障。例如,如果推荐服务在获取产品详细信息时没有响应,API 网关应将其余的产品详细信息返回给客户端,因为它们对用户仍然有用。建议可以是空的,也可以用其他代替,例如硬编码的十强名单。然而,如果产品信息服务没有响应,那么 API 网关应该向客户端返回错误。
|
||||
|
||||
如果可以,API 网关还可以返回缓存数据。例如,由于产品价格变化不大,当价格服务不可用时,API 网关可以返回被缓存的价格数据。数据可以由 API 网关缓存或存储在外部缓存中,如 Redis 或 Memcached。API 网关通过返回默认数据或缓存数据,确保系统发生故障时最小程度上影响到用户体验。
|
||||
|
||||
|
||||
@ -15,12 +15,12 @@
|
||||
<a id="interaction-styles"></a>
|
||||
|
||||
## 3.2、交互方式
|
||||
当为服务选择一种 IPC 机制时,首先需要考虑服务如何交互。有许多种客户端 — 服务交互方式。它们可以分为两个类。第一类是一对一交互与一对多交互:
|
||||
当为服务选择一种 IPC 机制时,首先需要考虑服务如何交互。有许多种客户端 — 服务交互方式。可以从两个维度对它们进行分类。第一个维度是交互方式是一对一还是一对多:
|
||||
|
||||
- **一对一** — 每个客户端请求都由一个服务实例处理。
|
||||
- **一对多** — 每个请求由多个服务实例处理。
|
||||
|
||||
第二类是同步交互与异步交互:
|
||||
第二个维度是交互是同步的还是异步的:
|
||||
|
||||
- **同步** — 客户端要求服务及时响应,在等待过程中可能会发生阻塞。
|
||||
- **异步** — 客户端在等待响应时不会发生阻塞,但响应(如果有)不一定立即返回。
|
||||
@ -41,10 +41,10 @@
|
||||
|
||||
客户端向服务发出请求并等待响应。客户端要求响应及时到达。在基于线程的应用程序中,发出请求的线程可能在等待时发生阻塞。
|
||||
- **通知(又称为单向请求)**
|
||||
|
||||
|
||||
客户端向服务发送请求,但不要求响应。
|
||||
- **请求/异步响应**
|
||||
|
||||
|
||||
客户端向服务发送请求,服务异步响应。客户端在等待时不发生阻止,适用于假设响应可能不会立即到达的场景。
|
||||
|
||||
一对多交互可分为以下列举的类型,它们都是异步的:
|
||||
@ -78,20 +78,20 @@
|
||||
## 3.4、演化 API
|
||||
服务 API 总是随着时间而变化。在单体应用程序中,更改 API 和更新所有调用者通常是一件直截了当的事。但在基于微服务的应用程序中,即使 API 的所有消费者都是同一应用程序中的其他服务,要想完成这些工作也是非常困难的。通常,您无法强制所有客户端与服务升级的节奏一致。此外,您可能需要[逐步部署服务的新版本](http://techblog.netflix.com/2013/08/deploying-netflix-api.html),以便新旧版本的服务同时运行。因此,制定这些问题的处理策略还是很重要的。
|
||||
|
||||
处理 API 变更的方式取决于变更的程度。某些更改是次要或需要向后兼容以前的版本。例如,您可能会向请求或响应添加属性。此时设计客户端与服务遵守[鲁棒性原则](https://en.wikipedia.org/wiki/Robustness_principle)就显得很有意义了。使用较旧 API 的客户端应继续使用新版本的服务。该服务为缺少的请求属性提供默认值,并且客户端忽略所有多余的响应属性。使用 IPC 机制和消息格式非常重要,他们可以让您轻松地演化 API。
|
||||
处理 API 变更的方式取决于变更的程度。某些更改是次要或需要向后兼容以前的版本。例如,您可能会向请求或响应添加属性。此时设计客户端与服务遵守[鲁棒性原则](https://en.wikipedia.org/wiki/Robustness_principle)就显得很有意义了。新版本的服务要能兼容使用旧版本 API 的客户端。该服务为缺少的请求属性提供默认值,并且客户端忽略所有多余的响应属性。使用 IPC 机制和消息格式非常重要,他们可以让您轻松地演化 API。
|
||||
|
||||
但有时候,您必须对 API 作出大量不兼容的更改。由于您无法强制客户端立即升级,服务也必须支持较旧版本的 API 一段时间。如果您使用了基于 HTTP 的机制(如 REST),则一种方法是将版本号嵌入到 URL 中。每个服务实例可能同时处理多个版本。或者,您可以部署多个不同的实例,每个实例用于处理特定版本。
|
||||
|
||||
<a id="handling-partial-failure"></a>
|
||||
|
||||
## 3.5、处理局部故障
|
||||
正如[第二章](2-using-an-api-gateway.md)中关于 API 网关所述,在分布式系统中存在局部故障风险。由于客户端进程与服务进程是分开的,服务可能无法及时响应客户端的请求。由于故障或者维护,服务可能需要关闭。也有可能因服务过载,造成响应速度变得极慢。
|
||||
正如[第二章](2-using-an-api-gateway.md)中关于 API 网关所述,在分布式系统中始终存在局部故障的风险。由于客户端进程与服务进程是分开的,服务可能无法及时响应客户端的请求。由于故障或者维护,服务可能需要关闭。也有可能因服务过载,造成响应速度变得极慢。
|
||||
|
||||
例如,请回想[第二章](2-using-an-api-gateway.md)中的产品详细信息场景。我们假设 Recommendation Service 没有响应。客户端天真般的实现可能会无限期地阻塞以等待响应。这不仅会导致用户体验糟糕,而且在许多应用程序中,它将消耗如线程之类等宝贵资源。以致最终,在运行时将线程用完,造成无法响应,如图 3-3 所示。
|
||||
例如,请回想[第二章](2-using-an-api-gateway.md)中的产品详细信息场景。我们假设推荐服务没有响应。低级的客户端实现可能会无限期地阻塞以等待响应。这不仅会导致用户体验糟糕,而且在许多应用程序中,它会消耗线程等宝贵的资源。最终导致运行时系统将线程消耗完,造成无法响应,如图 3-3 所示。
|
||||
|
||||

|
||||
|
||||
为了防止出现此类问题,您必须设计您的服务以处理局部故障。以下是一个由 [Netflix 给出的好办法](http://techblog.netflix.com/2012/02/fault-tolerance-in-high-volume.html)。处理局部故障的策略包括:
|
||||
为了防止出现此类问题,在设计服务时必须考虑处理局部故障。以下是一个由 [Netflix 给出的好办法](http://techblog.netflix.com/2012/02/fault-tolerance-in-high-volume.html)。处理局部故障的策略包括:
|
||||
|
||||
- **网络超时**
|
||||
|
||||
@ -99,7 +99,7 @@
|
||||
|
||||
- **限制未完成的请求数量**
|
||||
|
||||
对客户端拥有特定服务的未完成请求的数量设置上限。如果达到了上限,发出的额外请求可能是毫无意义的,因此这些尝试需要立即失败。
|
||||
对客户端请求的特定服务,设置未完成请求的数量上限。如果达到了上限,发出的额外请求可能是毫无意义的,因此这些尝试需要立即失败。
|
||||
|
||||
- **[断路器模式](http://martinfowler.com/bliki/CircuitBreaker.html)**
|
||||
|
||||
@ -116,7 +116,7 @@
|
||||
## 3.6、IPC 技术
|
||||
有多种 IPC 技术可供选择。服务可以使用基于同步请求/响应的通信机制,比如基于 HTTP 的 REST 或 Thrift。或者,可以使用异步、基于消息的通信机制,如 AMQP 或 STOMP。
|
||||
|
||||
还有各种不同的消息格式。服务可以使用人类可读、基于文本的格式,如 JSON 或 XML。或者,可以使用如 Avro 或 Protocol Buffers 等二进制格式(更加高效)。稍后我们将讨论同步 IPC 机制,但在此之前让我们先来讨论一下异步 IPC 机制。
|
||||
还有各种不同的消息格式。服务可以使用可读的、基于文本的格式,如 JSON 或 XML。或者,可以使用如 Avro 或 Protocol Buffers 等二进制格式(更加高效)。稍后我们将讨论同步 IPC 机制,但在此之前让我们先来讨论一下异步 IPC 机制。
|
||||
|
||||
<a id="asynchronous-message-based-communication"></a>
|
||||
|
||||
@ -147,7 +147,7 @@ Trip Management 服务通过向发布订阅通道写入 Trip Created 消息来
|
||||
客户端通过向相应的通道发送一条消息来简单地发出一个请求。服务实例对客户端而言是透明的。客户端不需要使用发现机制来确定服务实例的位置。
|
||||
- **消息缓冲**
|
||||
|
||||
使用如 HTTP 的同步请求/响应协议,客户端和服务在交换期间必须可用。相比之下,消息代理会将消息写入通道入队,直到消费者处理它们。这意味着,例如,即使订单执行系统出现缓慢或不可用的情况,在线商店还是可以接受客户的订单。订单消息只需要简单地排队。
|
||||
使用如 HTTP 的同步请求/响应协议,客户端和服务在交换期间必须可用。相比之下,消息代理会将消息排队写入通道,直到消费者处理它们。这意味着,例如,即使订单执行系统出现缓慢或不可用的情况,在线商店还是可以接受客户的订单。订单消息只需要简单地排队。
|
||||
- **灵活的客户端 — 服务交互**
|
||||
|
||||
消息传递支持前面提到的所有交互方式。
|
||||
@ -160,7 +160,7 @@ Trip Management 服务通过向发布订阅通道写入 Trip Created 消息来
|
||||
- **额外的复杂操作**
|
||||
|
||||
消息传递系统是一个需要安装、配置和操作的系统组件。消息代理程序必须高度可用,否则系统的可靠性将受到影响。
|
||||
- **实施基于请求/响应式交互的复杂性**
|
||||
- **实现基于请求/响应式交互的复杂性**
|
||||
|
||||
请求/响应式交互需要做些工作来实现。每个请求消息必须包含应答通道标识符和相关标识符。该服务将包含相关 ID 的响应消息写入应答信道。客户端使用相关 ID 将响应与请求相匹配。通常使用直接支持请求/响应的 IPC 机制更加容易。
|
||||
|
||||
@ -190,7 +190,7 @@ Trip Management 服务通过向发布订阅通道写入 Trip Created 消息来
|
||||
|
||||

|
||||
|
||||
乘客的智能手机通过向 Trip Management 服务的 `/trips` 资源发出一个 POST 请求来请求旅程。该服务通过向 Passenger Management 服务发送一个获取乘客信息的 GET 请求来处理该请求。在验证乘客被授权创建旅程后,Trip Management 服务将创建旅程,并向智能手机返回 201 响应。
|
||||
乘客的智能手机通过向 Trip Management 服务的 `/trips` 资源发出一个 POST 请求来请求旅程。该服务通过向 Passenger Management 服务发送一个获取乘客信息的 GET 请求来处理该请求。在验证乘客有权限创建旅程后,Trip Management 服务将创建旅程,并向智能手机返回 201 响应。
|
||||
|
||||
许多开发人员声称其基于 HTTP 的 API 就是 RESTful。然而,正如 Fielding 在这篇博文中所描述的那样,并不是都是这样。
|
||||
|
||||
@ -204,10 +204,10 @@ Leonard Richardson 定义了一个非常有用的 [REST 成熟度模型](https:/
|
||||
级别 1 的 API 支持资源概念。要对资源执行操作,客户端会创建一个 POST 请求,指定要执行的操作和参数。
|
||||
- **级别 2**
|
||||
|
||||
级别 2 的 API 使用 HTTP 动词(谓词)执行操作:使用 GET 检索、使用 POST 创建和使用 PUT 进行更新。请求查询参数和请求体(如果有)指定操作的参数。这使服务能够利用得到 Web 的基础特性,如缓存 GET 请求。
|
||||
级别 2 的 API 使用 HTTP 动词(谓词)执行操作:使用 GET 检索、使用 POST 创建和使用 PUT 进行更新。请求查询参数和请求体(如果有)指定操作的参数。这使服务能够利用得到 Web 的基础特性,如缓存 GET 请求。
|
||||
- **级别 3**
|
||||
|
||||
级别 3 的 API 基于非常规命名原则设计,HATEOAS(Hypermedia as the engine of application state,超媒体即应用程序状态引擎)。基本思想是 GET 请求返回的资源的表述,包含用于执行该资源上允许的操作的链接。例如,客户端可以使用发送 GET 请求检索订单返回的订单响应中的链接来取消订单。HATEOAS 的一个[好处](http://www.infoq.com/news/2009/04/hateoas-restful-api-advantages)是不再需要将 URL 硬编码在客户端代码中。另一个好处是,由于资源的表示包含可允许操作的链接,所以客户端不必猜测可以对当前状态的资源执行什么操作。
|
||||
级别 3 的 API 基于非常规命名原则设计,HATEOAS(Hypermedia as the engine of application state,超媒体即应用程序状态引擎)。基本思想是 GET 请求返回的资源的表述,包含用于执行该资源上允许的操作的链接。例如,发送 GET 请求检索订单,返回的订单响应中包含取消订单链接,客户端可以用该链接来取消订单。HATEOAS 的一个[好处](http://www.infoq.com/news/2009/04/hateoas-restful-api-advantages)是不再需要将 URL 硬编码在客户端代码中。另一个好处是,由于资源的表示包含可允许操作的链接,所以客户端不必猜测可以对当前状态的资源执行什么操作。
|
||||
|
||||
使用基于 HTTP 的协议有很多好处:
|
||||
- HTTP 简单易懂。
|
||||
@ -226,24 +226,26 @@ Leonard Richardson 定义了一个非常有用的 [REST 成熟度模型](https:/
|
||||
<a id="thrift"></a>
|
||||
|
||||
### 3.8.2、Thrift
|
||||
[Apache Thrift](https://thrift.apache.org/) 是 REST 的一个有趣的替代方案。它是一个用于编写跨语言 [RPC](https://en.wikipedia.org/wiki/Remote_procedure_call) 客户端和服务器 skeleton。Thrift 提供了一个 C 风格的 IDL 来定义您的 API。您可以使用 Thrift 编译器生成客户端 stub 和服务器端 skeleton。编译器可以生成各种语言的代码,包括 C++、Java、Python、PHP、Ruby、Erlang 和 Node.js。
|
||||
[Apache Thrift](https://thrift.apache.org/) 是 REST 的一个有趣的替代方案。它是一个用于编写跨语言 [RPC](https://en.wikipedia.org/wiki/Remote_procedure_call) 客户端和服务器框架。Thrift 提供了一个 C 风格的 IDL 来定义您的 API。您可以使用 Thrift 编译器生成客户端 stub 和服务器端 skeleton。编译器可以生成各种语言的代码,包括 C++、Java、Python、PHP、Ruby、Erlang 和 Node.js。
|
||||
|
||||
Thrift 接口由一个或多个服务组成。服务定义类似于一个 Java 接口。它是强类型方法的集合。Thrift 方法可以返回一个(可能为 void)值,或者如果它们被定义为单向,则不会返回值。返回值方法实现了请求/响应的交互方式,客户端等待响应,并可能会抛出异常。单向方式对应通知互动方式,服务器不发送响应。
|
||||
Thrift 接口由一个或多个服务组成。服务定义类似于一个 Java 接口。它是强类型方法的集合。
|
||||
|
||||
Thrift 支持多种消息格式:JSON,二进制和压缩二进制。二进制比 JSON 更有效率,因为其解码速度更快。而且,顾名思义,压缩二进制是一种节省空间的格式。当然,JSON 是人性化和浏览器友好的。Thrift 还为您提供了包括原始 TCP 和 HTTP 在内的传输协议选择。原始 TCP 可能比 HTTP 更有效率。然而,HTTP 是防火墙友好的、浏览器友好的和人性化的。
|
||||
Thrift 方法可以返回一个(可能为 void)值,或者如果它们被定义为单向,则不会返回值。返回值方法实现了请求/响应的交互方式,客户端等待响应,并可能会抛出异常。单向方式对应通知交互方式,服务器不发送响应。
|
||||
|
||||
Thrift 支持多种消息格式:JSON,二进制和压缩二进制。二进制比 JSON 更有效率,因为其解码速度更快。而且,顾名思义,压缩二进制是一种节省空间的格式。当然,JSON 是可读的和浏览器友好的。Thrift 还为您提供了包括原始 TCP 和 HTTP 在内的传输协议选择。原始 TCP 可能比 HTTP 更有效率。然而,HTTP 是防火墙友好的、浏览器友好的和可读的。
|
||||
|
||||
<a id="message-formats"></a>
|
||||
|
||||
## 3.9、消息格式
|
||||
我们已经了解了 HTTP 和 Thrift,现在让我们来看看消息格式的问题。如果您使用的是消息系统或 REST,则可以选择自己的消息格式。其他 IPC 机制如 Thrift 可能只支持少量的消息格式,甚至只支持一种。在任一种情况下,使用跨语言消息格式就显得非常重要了。即使您现在是以单一语言编写您的微服务,您将来也可能会使用到其他语言。
|
||||
我们已经了解了 HTTP 和 Thrift,现在让我们来看看消息格式的问题。如果您使用的是消息系统或 REST,则可以选择自己的消息格式。其他 IPC 机制如 Thrift 可能只支持少量的消息格式,甚至只支持一种。在任一种情况下,使用跨语言的消息格式都很重要了。即使您现在是以单一语言编写您的微服务,您将来也可能会使用到其他语言。
|
||||
|
||||
有两种主要的消息格式:文本和二进制。基于文本格式的例子有 JSON 和 XML。这些格式的优点在于,它们不仅是人类可读的,而且是自描述的。在 JSON 中,对象的属性由键值对集合表示。类似地,在 XML 中,属性由命名元素和值表示。这使得消息消费者能够挑选其感兴趣的值并忽略其余的值。因此,可以轻松地向后兼容作出微小更改的消息格式。
|
||||
有两种主要的消息格式:文本和二进制。基于文本格式的例子有 JSON 和 XML。这些格式的优点在于,它们不仅是人类可读的,而且是自描述的。在 JSON 中,对象的属性由一组键值对表示。类似地,在 XML 中,属性由命名元素和值表示。这使得消息消费者能够挑选其感兴趣的值并忽略其余的值。因此,稍微修改消息格式就可以轻松地向后兼容。
|
||||
|
||||
XML 文档的结构由 [XML 模式](https://www.w3.org/XML/Schema)(schema)指定。随着时间的推移,开发人员社区已经意识到 JSON 也需要一个类似的机制。一个选择是使用 [JSON Schema](http://json-schema.org/),无论独立或作为 IDL 的一部分,如 Swagger。
|
||||
XML 文档的结构由 [XML 模式](https://www.w3.org/XML/Schema)(schema)定义。随着时间的推移,开发人员社区已经意识到 JSON 也需要一个类似的机制。一个选择是使用 [JSON Schema](http://json-schema.org/),无论独立或作为 IDL 的一部分,如 Swagger。
|
||||
|
||||
使用基于文本的消息格式的缺点是消息往往是冗长的,特别是 XML。因为消息是自描述的,每个消息除了它们的值之外还包含属性的名称。另一个缺点是解析文本的开销。因此,您可能需要考虑使用二进制格式。
|
||||
|
||||
有几种二进制格式可供选择。如果您使用的是 Thrift RPC,您可以使用二进制 Thrift。如果您选择的消息格式,包括了流行的 [Protocol Buffers](https://developers.google.com/protocol-buffers/docs/overview) 和 [Apache Avro](https://avro.apache.org/)。这两种格式都提供了一种用于定义消息结构的类型 IDL。然而,一个区别是 Protocol Buffers 使用标记字段,而 Avro 消费者需要知道模式才能解释消息。因此,Protocol Buffers 的 API 演化比 Avro 更容易使用。这里有篇[博文](http://martin.kleppmann.com/2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html)对 Thrift、Protocol Buffers 和 Avro 作出了极好的比较。
|
||||
有几种二进制格式可供选择。如果您使用的是 Thrift RPC,您可以使用 Thrift 的二进制格式。如果你可以选择消息格式,比较流行的有 [Protocol Buffers](https://developers.google.com/protocol-buffers/docs/overview) 和 [Apache Avro](https://avro.apache.org/)。这两种格式都提供了一种类型化的 IDL 用于定义消息结构。然而,一个区别是 Protocol Buffers 使用标记字段,而 Avro 消费者需要知道模式才能解释消息。因此,Protocol Buffers 的 API 演化比 Avro 更容易使用。这里有篇[博文](http://martin.kleppmann.com/2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html)对 Thrift、Protocol Buffers 和 Avro 作出了极好的比较。
|
||||
|
||||
<a id="summary"></a>
|
||||
|
||||
|
||||
@ -25,9 +25,9 @@
|
||||
|
||||
服务实例的网络位置在服务注册中心启动时被注册。当实例终止时,它将从服务注册中心中移除。通常使用心跳机制周期性地刷新服务实例的注册信息。
|
||||
|
||||
[Netflix OSS](https://netflix.github.io/) 提供了一个很好的客户端发现模式示例。[Netflix Eureka](https://github.com/Netflix/eureka) 是一个服务注册中心,它提供了一个用于管理服务实例注册和查询可用实例的 REST API。[Netflix Ribbon](https://github.com/Netflix/ribbon) 是一个 IPC 客户端,可与 Eureka 一起使用,用于在可用服务实例之间使请求负载均衡。本章稍后将讨论 Eureka。
|
||||
[Netflix OSS](https://netflix.github.io/) 提供了一个很好的客户端发现模式示例。[Netflix Eureka](https://github.com/Netflix/eureka) 是一个服务注册中心,它提供了一组用于管理服务实例注册和查询可用实例的 REST API。[Netflix Ribbon](https://github.com/Netflix/ribbon) 是一个 IPC 客户端,可与 Eureka 一起使用,用于在可用服务实例之间使请求负载均衡。本章稍后将讨论 Eureka。
|
||||
|
||||
客户端发现模式存在各种优点与缺点。该模式相对比较简单,除了服务注册中心,没有其他移动部件。此外,由于客户端能发现可用的服务实例,因此可以实现智能的、特定于应用程序的负载均衡决策,比如使用一致性哈希。该模式的一个重要缺点是它将客户端与服务注册中心耦合在一起。您必须为服务客户端使用的每种编程语言和框架实现客户端服务发现逻辑。
|
||||
客户端发现模式存在各种优点与缺点。该模式相对比较简单,除了服务注册中心,没有其他移动部件。此外,由于客户端能发现可用的服务实例,因此可以实现智能的、特定于应用程序的负载均衡决策,比如使用一致性哈希。该模式的一个重要缺点是它将客户端与服务注册中心耦合在一起。您必须为您使用的每种编程语言和框架实现客户端服务发现逻辑。
|
||||
|
||||
现在我们已经了解了客户端发现,接下来让我们看看服务端发现。
|
||||
|
||||
@ -55,7 +55,7 @@ HTTP 服务器和负载均衡器(如 [NGINX Plus](https://www.nginx.com/produc
|
||||
|
||||
如之前所述,[Netflix Eureka](https://github.com/Netflix/eureka) 是一个很好的服务注册中心范例。它提供了一个用于注册和查询服务实例的 REST API。服务实例使用 POST 请求注册其网络位置。它必须每隔 30 秒使用 PUT 请求来刷新其注册信息。通过使用 HTTP DELETE 请求或实例注册超时来移除注册信息。正如您所料,客户端可以使用 HTTP GET 请求来检索已注册的服务实例。
|
||||
|
||||
Netflix 通过在每个 Amazon EC2 可用性区域(Availability Zone)中运行一个或多个 Eureka 服务器来[实现高可用](https://github.com/Netflix/eureka/wiki/Configuring-Eureka-in-AWS-Cloud)。每个 Eureka 服务器都运行在具有一个 [Elastic IP 地址](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html)的 EC2 实例上。DNS TEXT 记录用于存储 Eureka 集群配置,这是一个从可用性区域到 Eureka 服务器的网络位置列表的映射。当 Eureka 服务器启动时,它将会查询 DNS 以检索 Eureka 群集配置,查找其对等体,并为其分配一个未使用的 Elastic IP 地址。
|
||||
Netflix 通过在每个 Amazon EC2 可用性区域(Availability Zone)中运行一个或多个 Eureka 服务器来[实现高可用](https://github.com/Netflix/eureka/wiki/Configuring-Eureka-in-AWS-Cloud)。每个 Eureka 服务器都运行在具有一个 [弹性 IP 地址](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html)的 EC2 实例上。DNS TEXT 记录用于存储 Eureka 集群配置,这是一个从可用性区域到 Eureka 服务器的网络位置列表的映射。当 Eureka 服务器启动时,它将会查询 DNS 以检索 Eureka 群集配置,查找其对等体,并为其分配一个未使用的弹性 IP 地址。
|
||||
|
||||
Eureka 客户端 — 服务与服务客户端 — 查询 DNS 以发现 Eureka 服务器的网络位置。客户端优先使用相同可用性区域中的 Eureka 服务器,如果没有可用的,则使用另一个可用性区域的 Eureka 服务器。
|
||||
|
||||
@ -63,7 +63,7 @@ Eureka 客户端 — 服务与服务客户端 — 查询 DNS 以发现 Eureka
|
||||
|
||||
- **[etcd](https://github.com/coreos/etcd)**
|
||||
|
||||
一个用于共享配置和服务发现的高可用、分布式和一致的键值存储。使用了 etcd 的两个著名项目分别为 Kubernetes 和 [Cloud Foundry](http://pivotal.io/platform)。
|
||||
一个用于共享配置和服务发现的高可用、分布式和一致的键值存储。使用了 etcd 的两个著名项目分别为 Kubernetes 和 [Cloud Foundry](http://pivotal.io/platform)。
|
||||
- **[Consul](https://www.consul.io/)**
|
||||
|
||||
一个发现与配置服务工具。它提供了一个 API,可用于客户端注册与发现服务。Consul 可对服务进行健康检查,以确定服务的可用性。
|
||||
@ -72,7 +72,7 @@ Eureka 客户端 — 服务与服务客户端 — 查询 DNS 以发现 Eureka
|
||||
一个被广泛应用于分布式应用程序的高性能协调服务。Apache ZooKeeper 最初是一个 Hadoop 子项目,但现在已经成为一个独立的顶级项目。
|
||||
|
||||
另外,如之前所述,部分系统,如 Kubernetes、Marathon 和 AWS,没有明确的服务注册中心。相反,服务注册中心只是基础设施的一个内置部分。
|
||||
|
||||
|
||||
现在我们已经了解服务注册中心的概念,接下来让我们看看服务实例是如何被注册到服务注册中心。
|
||||
|
||||
<a id="service-registration-options"></a>
|
||||
@ -104,11 +104,11 @@ Eureka 客户端 — 服务与服务客户端 — 查询 DNS 以发现 Eureka
|
||||
|
||||

|
||||
|
||||
开源的 [Registrator](https://github.com/gliderlabs/registrator) 项目是一个很好的服务注册器示例。它可以自动注册和注销作为 Docker 容器部署的服务实例。注册器支持多个服务注册中心,包括 etcd 和 Consul。
|
||||
开源的 [Registrator](https://github.com/gliderlabs/registrator) 项目是一个很好的服务注册器示例。它可以自动注册和注销作为 Docker 容器部署的服务实例。注册器支持多种服务注册中心,包括 etcd 和 Consul。
|
||||
|
||||
另一个服务注册器例子是 [NetflixOSS Prana](https://github.com/Netflix/Prana)。其主要用于非 JVM 语言编写的服务,它是一个与服务实例并行运行的侧中应用。Prana 使用了 Netflix Eureka 来注册和注销服务实例。
|
||||
另一个服务注册器例子是 [NetflixOSS Prana](https://github.com/Netflix/Prana)。其主要用于非 JVM 语言编写的服务,它是一个与服务实例并行运行的附加应用。Prana 使用了 Netflix Eureka 来注册和注销服务实例。
|
||||
|
||||
服务注册器在部分部署环境中是一个内置组件。Autoscaling Group 创建的 EC2 实例可以自动注册到 ELB。Kubernetes 服务将自动注册并提供发现。
|
||||
服务注册器在部分部署环境中是一个内置组件。Autoscaling Group 创建的 EC2 实例可以自动注册到 ELB。Kubernetes 服务能够自动注册并提供发现。
|
||||
|
||||
第三方注册模式同样有好有坏。一个主要的好处是服务与服务注册中心之间解耦。您不需要为开发人员使用的每种编程语言和框架都实现服务注册逻辑。相反,仅需要在专用服务中以集中的方式处理服务实例注册。
|
||||
|
||||
@ -125,7 +125,7 @@ Eureka 客户端 — 服务与服务客户端 — 查询 DNS 以发现 Eureka
|
||||
|
||||
服务实例在服务注册中心中注册与注销有两种主要方式。一个是服务实例向服务注中心自我注册,即[自注册模式](http://microservices.io/patterns/self-registration.html)。另一个是使用其他系统组件代表服务完成注册与注销,即[第三方注册模式](http://microservices.io/patterns/3rd-party-registration.html)。
|
||||
|
||||
在某些部署环境中,您需要使用如 [Netflix Eureka](https://github.com/Netflix/eureka) 或 [Apache ZooKeeper](http://zookeeper.apache.org/) 等服务注册中心来设置您自己的服务发现基础设施。在其他部署环境中,服务发现是内置的,例如,[Kubernetes](https://kubernetes.io/) 和 [Marathon](https://mesosphere.github.io/marathon/docs/service-discovery-load-balancing.html),可以处理服务实例的注册与注销。他们还在每一个扮演服务端发现路由角色的集群主机上运行一个代理。
|
||||
在某些部署环境中,您需要使用如 [Netflix Eureka](https://github.com/Netflix/eureka) ,ectd 或 [Apache ZooKeeper](http://zookeeper.apache.org/) 等服务注册中心来设置您自己的服务发现基础设施。在其他部署环境中,服务发现是内置的,例如,[Kubernetes](https://kubernetes.io/) 和 [Marathon](https://mesosphere.github.io/marathon/docs/service-discovery-load-balancing.html),可以处理服务实例的注册与注销。他们还在每一个扮演服务端发现路由角色的集群主机上运行一个代理。
|
||||
|
||||
一个 HTTP 反向代理和负载均衡器(如 NGINX)也可以用作服务端发现负载均衡器。服务注册中心可以将路由信息推送给 NGINX,并调用一个正常的配置更新,例如,您可以使用 [Consul Template](https://www.hashicorp.com/blog/introducing-consul-template/)。NGINX Plus 支持[额外的动态重新配置机制](https://www.nginx.com/products/on-the-fly-reconfiguration/) — 它可以使用 DNS 从注册中心中提取有关服务实例的信息,并为远程重新配置提供一个 API。
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
因此,您的应用程序可以很容易地开始事务、更改(插入、更新和删除)多行记录,并提交事务。
|
||||
|
||||
使用关系型数据库的另一大好处是它提供了 SQL,这是一种丰富、声明式和标准化的查询语言。您可以轻松地编写一个查询组合来自多个表的数据,之后,RDBMS 查询计划程序将确定执行查询的最佳方式。您不必担心如何访问数据库等底层细节。因为您所有的应用程序数据都存放在同个数据库中,因此很容易查询。
|
||||
使用关系型数据库的另一大好处是它提供了 SQL,这是一种丰富、声明式和标准化的查询语言。您可以轻松地编写一个查询,组合来自多个表的数据,之后,RDBMS 查询计划程序将确定执行查询的最佳方式。您不必担心如何访问数据库等底层细节。因为您所有的应用程序数据都存放在同个数据库中,因此很容易查询。
|
||||
|
||||
很不幸的是,当我们转向微服务架构时,数据访问将变得非常复杂。因为每个微服务所拥有的数据[对当前微服务来说是私有的](http://microservices.io/patterns/data/database-per-service.html),只能通过其提供的 API 进行访问。封装数据可确保微服务松耦合、独立演进。如果多个服务访问相同的数据,模式(schema)更新需要对所有服务进行耗时、协调的更新。
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
|
||||
分区的数据存储混合持久化架构具有许多优点,包括了松耦合的服务以及更好的性能与可扩展性。然而,它也引入了一些分布式数据管理方面的挑战。
|
||||
|
||||
第一个挑战是如何实现维护多个服务之间的业务事务一致性。要了解此问题,让我们先来看一个在线 B2B 商店的示例。Customer Service (顾客服务)维护客户相关的信息,包括信用额度。Order Service (订单)负责管理订单,并且必须验证新订单,不得超过客户的信用额度。在此应用程序的单体版本中,Order Service 可以简单地使用 ACID 交易来检查可用信用额度并创建订单。
|
||||
第一个挑战是如何实现业务的事务在多个服务之间保持一致性。要了解此问题,让我们先来看一个在线 B2B 商店的示例。Customer Service (顾客服务)维护客户相关的信息,包括信用额度。Order Service (订单)负责管理订单,并且必须验证新订单,不得超过客户的信用额度。在此应用程序的单体版本中,Order Service 可以简单地使用 ACID 事务来检查可用信用额度并创建订单。
|
||||
|
||||
相比之下,在微服务架构中,ORDER (订单)和 CUSTOMER (顾客)表对其各自的服务都是私有的,如图 5-1 所示:
|
||||
|
||||
@ -36,12 +36,11 @@ Order Service 无法直接访问 CUSTOMER 表。它只能使用客户服务提
|
||||
## 5.2、事件驱动架构
|
||||
许多应用使用了[事件驱动架构](https://martinfowler.com/eaaDev/EventNarrative.html)作为解决方案。在此架构中,微服务在发生某些重要事件时发布一个事件,例如更新业务实体时。其他微服务订阅了这些事件,当微服务接收到一个事件时,它可以更新自己的业务实体,这可能导致更多的事件被发布。
|
||||
|
||||
您可以使用事件实现跨多服务的业务事务。一个事务由一系列的步骤组成。每个步骤包括了微服务更新业务实体和发布事件所触发的下一步骤。下图依次展示了如何在创建订单时使用事件驱动方法来检查可用信用额度。
|
||||
您可以使用事件实现跨多服务的业务的事务。一个事务由一系列的步骤组成。每个步骤包括了微服务更新业务实体和发布一个事件来触发下一步骤。下图依次展示了如何在创建订单时使用事件驱动方法来检查可用信用额度。
|
||||
|
||||
微服务通过 Message Broker (消息代理)进行交换事件:
|
||||
|
||||
- Order Service (订单服务)创建一个状态为 NEW 的订单,并发布一个 Order
|
||||
Created (订单创建)事件。
|
||||
- Order Service (订单服务)创建一个状态为 NEW 的订单,并发布一个 Order Created (订单创建)事件。
|
||||
|
||||

|
||||
|
||||
@ -55,16 +54,16 @@ Created (订单创建)事件。
|
||||
|
||||
更复杂的场景可能会涉及额外的步骤,例如在检查客户信用的同时保留库存。
|
||||
|
||||
假设(a)每个服务原子地更新数据库并发布事件,稍后再更新,(b)Message Broker 保证事件至少被传送一次,您可以实现跨多服务的业务事务。需要注意的是,这些并不是 ACID 事务。它们只提供了更弱的保证,如[最终一致性](https://en.wikipedia.org/wiki/Eventual_consistency)。该事务模型称为 [BASE 模型](http://queue.acm.org/detail.cfm?id=1394128)。
|
||||
假设(a)每个服务原子地更新数据库并发布一个事件 (稍后再详细说明),(b)Message Broker 保证事件至少被送达一次,然后您就实现了跨多服务的业务事务。需要注意的是,这些并不是 ACID 事务。它们只提供了更弱的保证,如[最终一致性](https://en.wikipedia.org/wiki/Eventual_consistency)。该事务模型称为 [BASE 模型](http://queue.acm.org/detail.cfm?id=1394128)。
|
||||
|
||||
您还可以使用事件来维护多个微服务预先加入所拥有的数据的物化视图(materialized view)。维护视图的服务订阅了相关事件并更新视图。图 5-5 展示了 Customer Order View Updater Service (客户订单视图更新服务)根据 Customer
|
||||
Service 和 Order Service 发布的事件更新 Customer Order View (客户订单服务)。
|
||||
属于多个微服务的数据构成的物化视图,你也可以用事件来维护。维护视图的服务订阅了相关事件并更新视图。图 5-5 展示了 Customer Order View Updater Service (客户订单视图更新服务)根据 Customer
|
||||
Service 和 Order Service 发布的事件更新 Customer Order View (客户订单视图)。
|
||||
|
||||

|
||||
|
||||
当 Customer Order View Updater Service 接收到 Customer 或 Order 事件时,它会更新 Customer Order View 数据存储。您可以使用如 MongoDB 之类的文档数据库实现 Customer Order View,并为每个 Customer 存储一个文档。Customer Order View Query Service(客户订单视图查询服务)通过查询 Customer Order View 数据存储来处理获取一位客户和最近的订单的请求。
|
||||
当 Customer Order View Updater Service 接收到 Customer 或 Order 事件时,它会更新 Customer Order View 的数据存储。您可以使用如 MongoDB 之类的文档数据库实现 Customer Order View,并为每个 Customer 存储一个文档。Customer Order View Query Service(客户订单视图查询服务)通过查询 Customer Order View 数据存储来处理获取一位客户和最近的订单的请求。
|
||||
|
||||
事件驱动的架构有几个优点与缺点。它能够实现跨越多服务并提供最终一致性事务。另一个好处是它还使得应用程序能够维护[物化视图](https://en.wikipedia.org/wiki/Materialized_view)。
|
||||
事件驱动的架构有几个优点与缺点。它能够实现跨越多服务并提供最终一致性的事务。另一个好处是它还使得应用程序能够维护[物化视图](https://en.wikipedia.org/wiki/Materialized_view)。
|
||||
|
||||
一个缺点是其编程模型比使用 ACID 事务更加复杂。通常,您必须实现补偿事务以从应用程序级别的故障中恢复。例如,如果信用检查失败,您必须取消订单。此外,应用程序必须处理不一致的数据。因为未提交的事务所做的更改是可见的。如果从未更新的物化视图中读取,应用程序依然可以看到不一致性。另一个缺点是订阅者必须要检测和忽略重复的事件。
|
||||
|
||||
@ -80,7 +79,7 @@ Service 和 Order Service 发布的事件更新 Customer Order View (客户订
|
||||
|
||||

|
||||
|
||||
Order Service 将一行记录插入到 ORDER 表中,并将一个 Order Created 事件插入到 EVENT 表中。Event Publisher(事件发布者)线程或进程从 EVENT 表中查询未发布的事件,之后发布这些事件,最后更新 EVENT 表以将事件标记为已发布。
|
||||
Order Service 将一行记录插入到 ORDER 表中,并将一个 Order Created 事件插入到 EVENT 表中。Event Publisher(事件发布者)线程或进程从 EVENT 表中查询未发布的事件,之后发布这些事件,最后更新 EVENT 表将事件标记为已发布。
|
||||
|
||||
这种方法有好有坏。好处是它保证了被发布的事件每次更新都不依赖于 2PC。此外,应用程序发布业务级事件,这些事件可以消除推断的需要。这种方法的缺点是它很容易出错,因为开发人员必须要记得发布事件。这种方法的局限性在于,由于其有限的事务和查询功能,在使用某些 NoSQL 数据库时,实现起来将是一大挑战。
|
||||
|
||||
@ -89,22 +88,22 @@ Order Service 将一行记录插入到 ORDER 表中,并将一个 Order Created
|
||||
<a id="mining-a-database-transaction-log"></a>
|
||||
|
||||
## 5.5、挖掘数据库事务日志
|
||||
不依靠 2PC 来实现原子性的另一种方式是使用线程或进程发布事件,该线程或进程对数据库的事务或者提交日志进行挖掘。当应用程序更新数据库时,更改信息被记录到数据库的事务日志中。Transaction Log Miner 线程或进程读取事务日志并向 Message Broker 发布事件。设计如图 5-7 所示。
|
||||
不依靠 2PC 来实现原子性的另一种方式,是用一个对数据库的事务或者提交日志进行挖掘的线程或进程来发布事件。当应用程序更新数据库时,更改信息被记录到数据库的事务日志中。事务日志挖掘器(Transaction Log Miner) 线程或进程读取事务日志并向 Message Broker 发布事件。设计如图 5-7 所示。
|
||||
|
||||

|
||||
|
||||
一个使用此方法的示例是 LinkedIn Databus 开源项目。Databus 挖掘 Oracle 事务日志并发布与更改相对应的事件。LinkedIn 使用 Databus 保持与记录系统一致的各种派生数据存储。
|
||||
一个使用此方法的示例是 LinkedIn Databus 开源项目。Databus 挖掘 Oracle 事务日志并发布与更改相对应的事件。LinkedIn 使用 Databus 保持与系统的记录一致的各种派生数据存储。
|
||||
|
||||
另一个例子是 AWS DynamoDB 中的流机制,它是一个托管的 NoSQL 数据库。DynamoDB 流包含了在过去 24 小时内对 DynamoDB 表中的项进行的更改(创建、更新和删除操作),其按时间顺序排列。应用程序可以从流中读取这些更改,比如,将其作为事件发布。
|
||||
|
||||
事务日志挖掘有各种好处与坏处。一个好处是它能保证被发布的事件每次更新都不依赖于 2PC。事务日志挖掘还可以通过将事件发布与应用程序的业务逻辑分离来简化应用程序。一个主要的缺点是事务日志的格式对于每个数据库来说都是专有的,甚至在数据库版本之间格式就发生了改变。而且,记录于事务日志中的低级别更新可能难以对高级业务事件进行逆向工程。
|
||||
事务日志挖掘有各种好处与坏处。一个好处是它能保证被发布的事件每次更新都不依赖于 2PC。事务日志挖掘还可以通过将事件发布与应用程序的业务逻辑分离来简化应用程序。一个主要的缺点是事务日志的格式对于每个数据库来说都是专有的,甚至在不同数据库版本之间格式就发生了改变。而且,记录于事务日志中的低级别更新可能难以对高级业务事件进行逆向工程。
|
||||
|
||||
事务日志挖掘消除了应用程序在做一件事时对 2PC 的依赖:更新数据库。现在我们来看看另一种可以消除更新并仅依赖于事件的不同方式。
|
||||
|
||||
<a id="using-event-sourcing"></a>
|
||||
|
||||
## 5.6、使用事件溯源
|
||||
[事件溯源](https://github.com/cer/event-sourcing-examples/wiki/WhyEventSourcing)通过使用完全不同的、不间断的方式来持久化业务实体,实现无 2PC 原子性。应用程序不存储实体的当前状态,而是存储一系列状态改变事件。该应用程序通过回放事件来重建实体的当前状态。无论业务实体的状态何时发生变化,其都会将新事件追加到事件列表中。由于保存事件是一个单一操作,因此具有原子性。
|
||||
[事件溯源](https://github.com/cer/event-sourcing-examples/wiki/WhyEventSourcing)通过使用不同于之前的、以事件为中心的方式来持久化业务实体,实现无 2PC 原子性。应用程序不存储实体的当前状态,而是存储一系列状态改变事件。应用程序通过回放事件来重建实体的当前状态。无论业务实体的状态何时发生变化,其都会将新事件追加到事件列表中。由于保存事件是一个单一操作,因此具有原子性。
|
||||
|
||||
要了解事件溯源的工作原理,以 Order(订单)实体为例。在传统方式中,每个订单都与 ORDER 表中的某行记录相映射,也可以映射到例如 ORDER_LINE_ITEM 表中的记录。
|
||||
|
||||
@ -112,7 +111,7 @@ Order Service 将一行记录插入到 ORDER 表中,并将一个 Order Created
|
||||
|
||||

|
||||
|
||||
事件被持久化在事件存储中,事件存储是一个事件数据库。该存储有一个用于添加和检索实体事件的 API。事件存储还与我们之前描述的架构中的 Message Broker 类似。它提供了一个 API,使得服务能够订阅事件。事件存储向所有感兴趣的订阅者派发所有事件。可以说事件存储是事件驱动微服务架构的支柱。
|
||||
事件被持久化在事件存储中,事件存储是一个事件的数据库。该存储有一个用于添加和检索实体事件的 API。事件存储还与我们之前描述的架构中的 Message Broker 类似。它提供了一个 API,使得服务能够订阅事件。事件存储向所有感兴趣的订阅者派发所有事件。可以说事件存储是事件驱动微服务架构的支柱。
|
||||
|
||||
事件溯源有几个好处。它解决了实现事件驱动架构的关键问题之一,可以在状态发生变化时可靠地发布事件。因此,它解决了微服务架构中的数据一致性问题。此外,由于它持久化的是事件,而不是领域对象,所以它主要避免了[对象关系阻抗失配问题](https://en.wikipedia.org/wiki/Object-relational_impedance_mismatch)。事件溯源还提供了对业务实体所做更改的 100% 可靠的审计日志,可以实现在任何时间点对实体进行时间查询以确定状态。事件溯源的另一个主要好处是您的业务逻辑包括松耦合的交换事件业务实体,这使得从单体应用程序迁移到微服务架构将变得更加容易。
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<a id="motivations"></a>
|
||||
|
||||
## 6.1、动机
|
||||
部署[单体应用](http://microservices.io/patterns/monolithic.html)程序意味着运行一个或多个相同副本的单个较大的应用程序。您通常会在每台服务器上配置 N 个服务器(物理或虚拟)并运行 M 个应用程序实例。单体应用程序的部署并不总是非常简单,但它比部署微服务应用程序要简单得多。
|
||||
部署[单体应用](http://microservices.io/patterns/monolithic.html)程序意味着运行一个或多个相同副本的单个较大的应用程序。您通常会配置 N 个服务器(物理或虚拟),每台服务器上会运行 M 个应用程序实例。单体应用程序的部署并不总是非常简单,但它比部署微服务应用程序要简单得多。
|
||||
|
||||
[微服务应用程序](http://microservices.io/patterns/microservices.html)由数十甚至上百个服务组成。服务以不同的语言和框架编写。每个都是一个迷你的应用程序,具有自己特定的部署、资源、扩展和监视要求。例如,您需要根据该服务的需求运行每个服务的一定数量的实例。此外,必须为每个服务实例提供相应的 CPU、内存和 I/O 资源。更具挑战性的是尽管如此复杂,部署服务也必须快速、可靠和具有成本效益。
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
|
||||
如果多个服务实例在同一进程中运行,那么将毫无隔离可言。例如,所有实例可能共享相同的 JVM 堆。行为不当的服务实例可能会轻易地破坏在同一进程中运行的其他服务。此外,您无法监控每个服务实例使用的资源。
|
||||
|
||||
这种方式的另一个重要问题是部署服务的运维团队必须了解执行此操作的具体细节。服务可以用多种语言和框架编写,因此开发团队必须与运维交代许多细节。这种复杂性无疑加大了部署过程中的错误风险。
|
||||
这种方式的另一个重要问题是部署服务的运维团队必须了解部署的具体细节。服务可以用多种语言和框架编写,因此开发团队必须与运维交代许多细节。这种复杂性无疑加大了部署过程中的错误风险。
|
||||
|
||||
正如您所见,尽管这种方式简单,但单主机多服务实例模式确实存在一些明显的缺点。现在让我们来看看可以绕过这些问题部署微服务的其他方式。
|
||||
|
||||
@ -57,11 +57,11 @@
|
||||
|
||||
[Boxfuse](https://boxfuse.com/) 公司有一种非常棒的方式用来构建虚拟机镜像,其克服了我将在下面描述的虚拟机的缺点。Boxfuse 将您的 Java 应用程序打包成一个最小化的 VM 镜像。这些镜像可被快速构建、快速启动且更加安全,因为它们暴露了一个有限的攻击面。
|
||||
|
||||
[CloudNative](https://cloudnative.io/) 公司拥有 Bakery,这是一种用于创建 EC2 AMI 的 SaaS 产品。您可以配置您的 CI 服务器,以在微服务通过测试后调用 Bakery。之后 Bakery 将您的服务打包成一个 AMI。使用一个如 Bakery 的 SaaS 产品意味着您不必浪费宝贵的时间来设置 AMI 创建基础架构。
|
||||
[CloudNative](https://cloudnative.io/) 公司拥有 Bakery,这是一种用于创建 EC2 AMI 的 SaaS 产品。您可以配置您的 CI 服务器,在微服务通过测试后调用 Bakery。之后 Bakery 将您的服务打包成一个 AMI。使用一个如 Bakery 的 SaaS 产品意味着您不必浪费宝贵的时间来设置 AMI 创建基础架构。
|
||||
|
||||
每个虚拟机一个服务实例模式有许多优点。VM 的主要优点是每个服务实例运行是完全隔离的。它有固定数量的 CPU 和内存,且不能从其他服务窃取资源。
|
||||
|
||||
将微服务部署为虚拟机的另一个优点是可以利用成熟的云基础架构。如 AWS 之类的云提供了有用的功能,例如负载平衡和自动扩缩。
|
||||
将微服务部署为虚拟机的另一个优点是可以利用成熟的云基础架构。如 AWS 之类的云提供了很多有用的功能,例如负载平衡和自动扩缩。
|
||||
|
||||
将服务部署为虚拟机的另一个好处是它封装了服务的实现技术。一旦服务被打包成一个虚拟机,它就成为一个黑匣子。VM 的管理 API 成为部署服务的 API。部署变得更加简单、可靠。
|
||||
|
||||
@ -84,17 +84,17 @@
|
||||
|
||||

|
||||
|
||||
要使用此模式,请将您的服务打包成一个容器镜像。容器镜像是由运行服务所需的应用程序和库组成的文件系统镜像。一些容器镜像由完整的 Linux 根文件系统组成。此外它更加轻便。例如,要部署一个 Java 服务,您可以构建一个包含了 Java 运行时的容器镜像,可能是一个 Apache Tomcat 服务器和编译好的 Java 应用程序。
|
||||
要使用此模式,请将您的服务打包成一个容器镜像。容器镜像是由运行服务所需的应用程序和库组成的文件系统镜像。一些容器镜像由完整的 Linux 根文件系统组成。其它的则更加轻便。例如,要部署一个 Java 服务,您可以构建一个包含了 Java 运行时的容器镜像,或许也包含一个 Apache Tomcat 服务器和编译好的 Java 应用程序。
|
||||
|
||||
将服务打包成一个容器镜像后,您将启动一个或多个容器。通常在每个物理或虚拟主机上运行多个容器。您可以使用集群管理工具(如 [Kubernetes](https://kubernetes.io/) 或 [Marathon](https://github.com/mesosphere/marathon))来管理容器。集群管理工具将主机视为一个资源池。它根据容器所需的资源和每个主机上可用的资源来决定每个容器放置的位置。
|
||||
|
||||
每个容器一个服务实例模式模式有优点和缺点。容器的优点与虚拟机的类似。它们将服务实例彼此隔离。您可以轻松地监控每个容器所消耗的资源。此外,与 VM 一样,容器封装了服务实现技术。容器管理 API 作为管理您的服务的 API。
|
||||
每个容器一个服务实例模式模式有优点和缺点。容器的优点与虚拟机的类似。它们将服务实例彼此隔离。您可以轻松地监控每个容器所消耗的资源。此外,与 VM 一样,容器封装了服务的实现技术。容器管理 API 作为管理您的服务的 API。
|
||||
|
||||
然而,与虚拟机不同,容器是轻量级技术。容器镜像通常可以非常快速地构建。例如,在我的笔记本电脑上,将一个 [Spring Boot](http://projects.spring.io/spring-boot/) 应用程序打包成一个 Docker 容器只需要 5 秒钟的时间。容器也可以很快地启动,因为没有繁琐的操作系统引导机制。当一个容器启动时,它所运行的就是服务。
|
||||
|
||||
使用容器有一些缺点。虽然容器基础架构正在快速发展走向成熟,但它并不像虚拟机的基础架构那么成熟。此外,容器不像 VM 那样安全,因为容器彼此共享了主机的 OS 内核。
|
||||
|
||||
容器的另一个缺点是您需要负责未划分的容器镜像管理重担。此外,除非您使用了托管容器解决方案[如 [Google Container Engine](https://cloud.google.com/container-engine/) 或 [Amazon EC2 Container Service](https://cloud.google.com/container-engine/)(ECS)],否则您必须自己管理容器基础设施以及可能运行的 VM 基础架构。
|
||||
容器的另一个缺点是您需要负责未划分的容器镜像管理的负担。此外,除非您使用了托管容器解决方案[如 [Google Container Engine](https://cloud.google.com/container-engine/) 或 [Amazon EC2 Container Service](https://cloud.google.com/container-engine/)(ECS)],否则您必须自己管理容器基础设施,可能还有运行容器的 VM 基础架构。
|
||||
|
||||
此外,容器通常部署在一个按单个 VM 收费的基础设施上。因此,如之前所述,可能会产生超额配置 VM 的额外成本,以处理负载峰值。
|
||||
|
||||
@ -107,7 +107,7 @@
|
||||
## 6.4、Serverless 部署
|
||||
[AWS Lambda](https://aws.amazon.com/lambda/) 就是一个 serverless 部署技术示例。它支持 Java、Node.js 和 Python 服务。要部署微服务,请将其打包成 ZIP 文件并将上传到 AWS Lambda。您还要提供元数据,其中包括了被调用来处理请求(又称为事件)的函数的名称。AWS Lambda 自动运行足够的微服务服务实例来处理请求。您只需根据每个请求所用时间和内存消耗来付费。当然,问题往往出现在细节上,您很快注意到了 AWS Lambda 的局限性。但是,作为开发人员的您或组织中的任何人都无需担心服务器、虚拟机或容器的任何方面 ,这非常有吸引力,足以令人难以置信。
|
||||
|
||||
Lambda 函数是无状态服务。它通常通过调用 AWS 服务来处理请求。例如,当图片上传到 S3 存储桶时Lambda 函数将被调用,可插入一条记录到 DynamoDB 图片表中,并将消息发布到 Kinesis 流以触发图片处理。 Lambda 函数还可以调用第三方 Web 服务。
|
||||
Lambda 函数是无状态服务。它通常通过调用 AWS 服务来处理请求。例如,当图片上传到 S3 存储桶时Lambda 函数将被调用,该函数可插入一条记录到 DynamoDB 图片表中,并将消息发布到 Kinesis 流以触发图片处理。 Lambda 函数还可以调用第三方 Web 服务。
|
||||
|
||||
有四种方法调用 Lambda 函数:
|
||||
|
||||
@ -116,9 +116,9 @@ Lambda 函数是无状态服务。它通常通过调用 AWS 服务来处理请
|
||||
- 通过 AWS API 网关自动处理来自应用程序客户端的 HTTP 请求
|
||||
- 按照一个类似 cron 的时间表,定期执行
|
||||
|
||||
正如您所见,AWS Lambda 是一个便捷的微服务部署方式。基于请求的定价意味着您只需为服务实际执行的工作支付。另外,由于您不需要对 IT 基础架构负任何责任,因此可以专注于开发应用程序。
|
||||
正如您所见,AWS Lambda 是一个便捷的微服务部署方式。基于请求的定价意味着您只需为服务实际执行的工作付费。另外,由于您不需要对 IT 基础架构负任何责任,因此可以专注于开发应用程序。
|
||||
|
||||
然而,其也存在一些明显的局限性。Lambda 函数不适用于部署长时间运行的服务,例如消耗第三方消息代理消息的服务。请求必须在 300 秒内完成。服务必须是无状态的,因为理论上,AWS Lambda 可能为每个请求运行一个单独的实例。他们必须使用受支持的语言之一来编写。服务也必须快速启动,否则,他们可能会因超时而终止。
|
||||
然而,其也存在一些明显的局限性。Lambda 函数不适用于部署长时间运行的服务,例如消耗第三方消息代理消息的服务。请求必须在 300 秒内完成。服务必须是无状态的,因为理论上,AWS Lambda 可能为每个请求运行一个单独的实例。他们必须使用受支持的语言来编写。服务启动速度也必须快,否则,他们可能会因超时而终止。
|
||||
|
||||
<a id="summary"></a>
|
||||
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
# 7、重构单体为微服务
|
||||
# 7、重构单体服务为微服务
|
||||
本书主要介绍如何使用微服务构建应用程序,这是本书的第七章,也是最后一章。[第一章](1-introduction-to-microservices.md)介绍了微服务架构模式,讨论了使用微服务的优点与缺点。随后的章节讨论了微服务架构的方方面面:[使用 API 网关](2-using-an-api-gateway.md)、[进程间通信](3-inter-process-communication.md)、[服务发现](4-service-discovery.md)、[事件驱动数据管理](5-event-driven-data-management-for-microservices.md)和[部署微服务](6-choosing-deployment-strategy.md)。在本章中,我们将介绍单体应用迁移到微服务的策略。
|
||||
|
||||
我希望这本电子书能够让您对微服务架构、其优点和缺点以及何时使用它有很好的了解。微服务架构也许很适合您的组织。
|
||||
|
||||
您正工作于大型复杂的单体应用程序上,这是相当不错的机会。然而,您开发和部署应用程序的日常经历是缓慢而痛苦的。微服务似乎是一个遥不可及的天堂。幸运的是,有一些战略可以用来逃离单体地狱。在本文中,我将描述如何将单体应用程序逐渐重构为一组微服务。
|
||||
您正工作于大型复杂的单体应用程序上,这是相当不错的机会。然而,您开发和部署应用程序的日常经历是缓慢而痛苦的。微服务似乎是一个遥不可及的天堂。幸运的是,有一些策略可以用来逃离单体应用的地狱。在本文中,我将描述如何将单体应用程序逐渐重构为一组微服务。
|
||||
|
||||
<a id="overview-of-refactoring-to-microservices"></a>
|
||||
|
||||
## 7.1、微服务重构概述
|
||||
单体应用程序转换为微服务的过程是[应用程序现代化](https://en.wikipedia.org/wiki/Software_modernization)的一种形式。这是几十年来开发人员一直在做的事情。因此,在将应用程序重构为微服务时,有一些想法是可以重用的。
|
||||
|
||||
一个不要使用的策略是“大爆炸”重写。就是您将所有的开发工作都集中在从头开始构建新的基于微服务的应用程序。虽然这听起来很吸引人,但非常危险,有可能会失败。[据 AsMartin Fowler 讲到](http://www.randyshoup.com/evolutionary-architecture):“大爆炸重写的唯一保证就是大爆炸!”("the only thing a Big Bang rewrite guarantees is a Big Bang!")。
|
||||
一个不要使用的策略是“大爆炸”重写。就是您将所有的开发工作都集中在从头开始构建新的基于微服务的应用程序。虽然这听起来很吸引人,但非常危险,有可能会失败。[据 Martin Fowler 讲到](http://www.randyshoup.com/evolutionary-architecture):“大爆炸重写的唯一保证就是大爆炸!”("the only thing a Big Bang rewrite guarantees is a Big Bang!")。
|
||||
|
||||
您应该逐步重构单体应用程序,而不是通过大爆炸重写。您可以逐渐添加新功能,并以微服务的形式创建现有功能的扩展 — 以互补的形式修改单体应用,并且一同运行微服务和修改后的单体。随着时间推移,单体应用程序实现的功能量会缩小,直到它完全消失或变成另一个微服务。这种策略类似于在 70公里/小时的高速公路上驾驶一辆汽车,很具挑战性,但比尝试大爆炸改写的风险要小得多。
|
||||
您应该逐步重构单体应用程序,而不是通过大爆炸重写。您可以逐渐添加新功能,并以微服务的形式创建现有功能的扩展 — 以互补的形式修改单体应用,并且一同运行微服务和修改后的单体应用。随着时间推移,单体应用程序实现的功能量会缩小,直到它完全消失或变成另一个微服务。这种策略类似于在 70公里/小时的高速公路上驾驶一辆汽车,很具挑战性,但比尝试大爆炸改写的风险要小得多。
|
||||
|
||||

|
||||
|
||||
@ -29,21 +29,21 @@ Martin Fowler 将这种应用现代化策略称为[杀手应用](http://www.mart
|
||||
|
||||

|
||||
|
||||
除了新服务和传统的单体,还有另外两个组件。第一个是请求路由,它处理传入的(HTTP)请求,类似于[第二章](2-using-an-api-gateway.md)中描述的 API 网关。路由向新服务发送与新功能相对应的请求。它将遗留的请求路由到单体。
|
||||
除了新服务和遗留的单体应用,还有另外两个组件。第一个是请求路由,它处理传入的(HTTP)请求,类似于[第二章](2-using-an-api-gateway.md)中描述的 API 网关。路由向新服务发送与新功能相对应的请求。它将遗留的请求路由到单体应用。
|
||||
|
||||
另一个组件是粘合代码,它将服务与单体集成。一个服务很少孤立存在,通常需要访问单体的数据。位于单体、服务或两者中的粘合代码负责数据集成。该服务使用粘合代码来读取和写入单体数据。
|
||||
另一个组件是粘合代码,它将微服务与单体应用集成。一个服务很少孤立存在,通常需要访问单体应用的数据。位于单体应用、微服务或两者中的粘合代码负责数据集成。该微服务使用粘合代码来读取和写入单体应用数据。
|
||||
|
||||
服务可以使用三种策略来访问单体数据:
|
||||
服务可以使用三种策略来访问单体应用的数据:
|
||||
|
||||
- 调用由单体提供的远程 API
|
||||
- 直接访问单体数据库
|
||||
- 维护自己的数据副本,与单体数据库同步
|
||||
- 调用由单体应用提供的远程 API
|
||||
- 直接访问单体应用的数据库
|
||||
- 维护自己的数据副本,与单体应用的数据库同步
|
||||
|
||||
粘合代码有时被称为防护层(anti-corruption layer)。这是因为粘合代码阻止了服务被遗留的单体领域模型的概念所污染,这些服务具有自己的原始领域模型。粘合代码在两种不同的模型之间转换。防护层一词首先出现于埃里克·埃文斯(Eric Evans)所著的必读图书[《领域驱动设计》](http://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/ref=sr_1_1?ie=UTF8&s=books&qid=1238687848&sr=8-1)(Domain Driven Design)中,并在[白皮书](http://docs.scala-lang.org/overviews/core/futures.html)中进行了改进。开发一个防护层并不是一件简单的事情。但是,如果您想要从单体地狱中走出来,这是必不可少的步骤。
|
||||
粘合代码有时被称为防护层(anti-corruption layer)。这是因为粘合代码阻止了服务被遗留的单体应用领域模型的概念所污染,这些服务拥有属于自己的新的领域模型。粘合代码在两种不同的模型之间转换。防护层一词首先出现于埃里克·埃文斯(Eric Evans)所著的必读图书[《领域驱动设计》](http://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/ref=sr_1_1?ie=UTF8&s=books&qid=1238687848&sr=8-1)(Domain Driven Design)中,并在[白皮书](http://docs.scala-lang.org/overviews/core/futures.html)中进行了改进。开发一个防护层并不是一件简单的事情。但是,如果您想要从单体应用地狱中走出来,这是必不可少的步骤。
|
||||
|
||||
使用轻量级服务来实现新功能有几个好处。它防止单体变得更加难以管理。该服务可以独立于单体开发、部署和扩展。可让您创建的每个新服务体验到微服务架构的优势。
|
||||
使用轻量级服务来实现新功能有几个好处。它防止单体变得更加难以管理。该服务可以独立于单体应用开发、部署和扩展。可让您创建的每个新服务体验到微服务架构的优势。
|
||||
|
||||
然而,这种方法没有解决单体问题。要解决这些问题,您需要分解单体。让我们来看看这样做的策略。
|
||||
然而,这种方法没有解决单体应用的问题。要解决这些问题,您需要分解单体应用。让我们来看看这样做的策略。
|
||||
|
||||
<a id="strategy-2-split-frontend-and-backend"></a>
|
||||
|
||||
@ -60,46 +60,46 @@ Martin Fowler 将这种应用现代化策略称为[杀手应用](http://www.mart
|
||||
|
||||
访问基础架构组件的组件,如数据库和消息代理。
|
||||
|
||||
一方的表现逻辑和另一方的业务和数据访问逻辑之间通常有一个完全的隔离。业务层具有由一个或多个门面组成的粗粒度 API,其封装了业务逻辑组件。这个 API 是一个天然的边界,您可以沿着该边界将单体拆分成两个较小的应用程序。一个应用程序包含表现层。另一个应用程序包含业务和数据访问逻辑。分割后,表现逻辑应用程序对业务逻辑应用程序进行远程调用。
|
||||
表现逻辑部分与业务和数据访问逻辑部分之间通常有一个清晰的界限。业务层具有由一个或多个门面组成的粗粒度 API,其封装了业务逻辑组件。这个 API 是一个天然的边界,您可以沿着该边界将单体应用拆分成两个较小的应用程序。一个应用程序包含表现层。另一个应用程序包含业务和数据访问逻辑。分割后,表现逻辑应用程序对业务逻辑应用程序进行远程调用。
|
||||
|
||||
重构之前和之后的架构如图 7-2 所示。
|
||||
|
||||

|
||||
|
||||
以这种方式拆分单体有两个主要优点。它使您能够独立于彼此开发、部署和扩展这两个应用。特别是它允许表现层开发人员在用户界面上快速迭代,并且可以轻松执行 A/B 测试。这种方法的另一个优点是它暴露了可以被您开发的微服务调用的远程 API。
|
||||
以这种方式拆分单体应用有两个主要优点。它使您能够独立于彼此开发、部署和扩展这两个应用。特别是它允许表现层开发人员在用户界面上快速迭代,并且可以轻松执行 A/B 测试。这种方法的另一个优点是它暴露了可以被微服务调用的远程 API。
|
||||
|
||||
然而,这一策略只是一个局部解决方案。两个应用程序中的一个或两个很可能是一个无法管理的单体。您需要使用第三种策略来消除剩余的整体或单体。
|
||||
然而,这一策略只是一个局部解决方案。两个应用程序中的一个或两个很可能在将来膨胀成一个无法管理的单体应用。您需要使用第三种策略来继续消减单体应用。
|
||||
|
||||
<a id="strategy-3-extract-services"></a>
|
||||
|
||||
## 7.4、策略三:提取服务
|
||||
第三个重构策略是将庞大的现有模块转变为独立的微服务。每次提取一个模块并将其转换成服务时,单体就会缩小。一旦您转换了足够的模块,单体将不再是一个问题。或者它完全消失,或者变得足够小,它就可以被当做一个服务看待。
|
||||
第三个重构策略是将庞大的现有模块转变为独立的微服务。每次提取一个模块并将其转换成微服务时,单体就会缩小。一旦您转换了足够的模块,单体应用将不再是一个问题。或者它完全消失,或者变得小到可以被当做一个服务看待。
|
||||
|
||||
<a id="prioritizing-which-modules-to-convert-into-services"></a>
|
||||
|
||||
### 7.4.1、优先将哪些模块转换为微服务
|
||||
一个庞大而复杂的单体应用由几十个或几百个模块组成,所有模块都是提取的候选项。弄清楚要先转换哪些模块往往存在一定的挑战。一个好的方法是从容易提取的几个模块开始。您将得到微服务的相关经验,特别是在提取过程方面。之后,您应该提取那些能给您最大利益的模块。
|
||||
一个庞大而复杂的单体应用由几十个或几百个模块组成,所有模块都是提取的候选项。弄清楚要先转换哪些模块往往存在一定的挑战。一个好的方法是从容易提取的几个模块开始。这将给你的微服务开发带来好的体验,特别是在提取过程方面。之后,您应该提取那些能给您最大收益的模块。
|
||||
|
||||
将模块转换为服务通常是耗时的。您想按照您将获得的利益对模块进行排列。提取频繁更改的模块通常是有益的。一旦将模块转换为服务,您就可以独立于单体开发和部署,这将加快开发工作。
|
||||
将模块转换为服务通常是耗时的。按照您将获得的收益对模块进行排列。提取频繁更改的模块通常是有益的。一旦将模块转换为服务,您就可以独立于单体应用开发和部署,这将加快开发工作。
|
||||
|
||||
提取这些与单体的其他模块有显著不同的模块也是有益的。例如,将有一个有内存数据库的模块转换为服务是很有用的,这样可以部署在具有大量内存的主机上,无论是裸机服务器、虚拟机还是云实例。同样,提取实现了计算昂贵算法的模块也是值得的,因为该服务可以部署在具有大量 CPU 的主机上。通过将具有特定资源需求的模块转换为服务,您可以使应用程序更加容易、廉价地扩展。
|
||||
提取单体应用中相对来说耗资源的模块也是有益的。例如,将有一个有内存数据库的模块转换为服务是很有用的,这样可以部署在具有大量内存的主机上,无论是裸机服务器、虚拟机还是云服务器。同样,提取实现了大量复杂算法的模块也是值得的,因为该服务可以部署在具有大量 CPU 的主机上。通过将具有特定资源需求的模块转换为服务,您可以使应用程序更加容易、廉价地扩展。
|
||||
|
||||
当找到要提取的模块时,寻找现有的粗粒度边界(又称为接缝)是有用的。它们使模块转成服务变得更容易和更连廉价。有关这种边界的一个例子是一个仅通过异步消息与应用程序的其他部分进行通信的模块。将该模块转变为微服务相对比较廉价和简单。
|
||||
当找到要提取的模块时,寻找现有的粗粒度边界(又称为接缝)是有用的。它们使模块转成服务更容易。有关这种边界的一个例子是一个仅通过异步消息与应用程序的其他部分进行通信的模块。将该模块转变为微服务相对比较廉价和简单。
|
||||
|
||||
<a id="how-to-extract-a-module"></a>
|
||||
|
||||
## 7.4.2、如何提取模块
|
||||
提取模块的第一步是在模块和单体之间定义一个粗粒度的接口。因为单体需要服务拥有的数据,它很可能是一个双向 API,反之亦然。由于模块和应用程序的其余之间存在着复杂的依赖关系和细粒度的交互模式,因此实现这样的 API 通常存在挑战。由于领域模型类之间的众多关联,使用[领域模型模式](http://martinfowler.com/eaaCatalog/domainModel.html)来实现的业务逻辑尤其具有挑战性。您通常需要进行重大的代码更改才能打破这些依赖。图 7-3 展示了重构。
|
||||
提取模块的第一步是在模块和单体应用之间定义一个粗粒度的接口。它很可能是一个双向 API,因为单体应用需要服务拥有的数据,反之亦然。由于该模块和应用程序的其余模块之间存在着复杂的依赖关系和细粒度的交互模式,因此实现这样的 API 通常存在挑战。由于领域模型类之间的众多关联,使用[领域模型模式](http://martinfowler.com/eaaCatalog/domainModel.html)实现的业务逻辑的重构尤其具有挑战性。您通常需要进行重大的代码更改才能打破这些依赖。图 7-3 展示了重构。
|
||||
|
||||

|
||||
|
||||
一旦实现了粗粒度的接口,您就可以将模块变成独立的服务。要做到这点,您必须编写代码以使单体和服务通过使用进程间通信(IPC)机制的 API 进行通信。图 7-3 显示了重构前、重构中和重构后的架构。
|
||||
一旦实现了粗粒度的接口,您就可以将模块变成独立的服务。要做到这点,您必须编写代码以使单体应用和服务通过使用进程间通信(IPC)机制的 API 进行通信。图 7-3 显示了重构前、重构中和重构后的架构。
|
||||
|
||||
在此例中,模块 Z 是要提取的候选模块。其组件由模块 X 使用,并且它使用了模块 Y。第一个重构步骤是定义一对粗粒度的 API。第一个接口是一个使用模块 X 来调用模块 Z 的入站接口。第二个接口是一个使用模块 Z 调用模块 Y 的出站接口。
|
||||
在此例中,模块 Z 是要提取的候选模块。它被模块 X 调用,并且它调用了模块 Y。第一个重构步骤是定义一对粗粒度的 API。第一个接口是一个使用模块 X 来调用模块 Z 的入站接口。第二个接口是一个使用模块 Z 调用模块 Y 的出站接口。
|
||||
|
||||
第二个重构步骤是将模块转换为一个独立服务。入站和出站接口使用 IPC 机制的代码来实现。您将很可能需要通过将 Module Z 与 [Microservice Chassis](http://microservices.io/patterns/microservice-chassis.html) 框架相结合来构建服务,该框架负责处理诸如服务发现之类的横切点。
|
||||
第二个重构步骤是将模块转换为一个独立服务。入站和出站接口使用 IPC 机制的代码来实现。您很可能需要把 Module Z 与 [Microservice Chassis](http://microservices.io/patterns/microservice-chassis.html) 框架相结合来构建服务,该框架负责处理诸如服务发现之类的横切点。
|
||||
|
||||
一旦您提取了一个模块,您就可以独立于单体和任何其他服务开发、部署和扩展其他服务。您甚至可以从头开始重写服务。在这种情况下,整合服务与单体的 API 代码成为在两个领域模型之间转换的防护层。每次提取服务时,您都会朝微服务方向迈近一步。随着时间的推移,单体将缩小,您将拥有越来越多的微服务。
|
||||
一旦您提取了一个模块,您就可以独立于单体应用和任何其他服务开发、部署和扩展服务。您甚至可以从头开始重写服务。在这种情况下,整合服务与单体应用的 API 代码成为在两个领域模型之间转换的防护层。每次提取服务时,您都会朝微服务方向迈近一步。随着时间的推移,单体将缩小,您将拥有越来越多的微服务。
|
||||
|
||||
<a id="summary"></a>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user