微服务架构(Microservice Architecture)是一种架构概念,旨在经过将功能分解到各个离散的服务中以实现对解决方案的解耦。html
回到微服务的概念中,它不是具体指某一技术,而是关于某种架构风格的集合,所以微服务自己是没有明肯定义的,但咱们知道它是有不仅一个的独立服务组成的一个总体架构。redis
做为 ThoughtWorks 的咨询师,我十分乐意推荐你阅读关于 Martin Fowler 的这篇关于 Microservices 的文章:docker
咱们以相似于 Uber 的打车服务做为一个业务案例,因为初期团队规模小,业务量也很少,整个系统是以下的一个单体应用:npm
如上图所示,乘客和司机经过 REST API 进行交互,全部服务都请求的是一个数据库,而且全部服务,例如支付、订单、我的中心等都存在于一个框架服务之中,在早期的时候,这样的开发架构,对于一个创业型产品是十分常见的,集中管理、开发效率高,但是随着业务的不断扩展与量级的增大,慢慢的这个单体应用就变为了一个巨石应用,那咱们再对其进行代码维护时,就很容易遇到如下的问题:json
为了解决当前的业务痛点,这家打车公司参考了 Amazon、Netflix 等巨头公司的应用架构,最终将其巨石应用按照微服务的架构进行从新设计:bootstrap
在这个服务地图中,咱们看到每一个核心业务模块都单独拆分出来做为一个独立的服务,针对于用户还引入了 API 网关的概念用来导航到内部的服务。如今来看,这套微服务架构解决了一些曾经单体应用下存在的缺陷:设计模式
固然微服务也会带来更多的问题与挑战,这个咱们就不在此展开讨论了。从这个例子能够看出,从一个单体应用迁移到微服务架构实际上是一个服务演进的过程,任何架构不可能凭空出现,最佳的架构取决于有多么适合当前的业务模式。服务器
微服务自己相较于传统架构,会带来许多优势,但同时又会增长额外的复杂度与管理成本,因此我一直比较信奉一句话:不要为了微服务而微服务。所以在架构初期,我倾向于按照单体应用的方式进行组织代码,经过清晰的拆包逻辑,将业务进行隔离,下降模块间的复杂度,而后到项目后期若在业务与具体架构上能与微服务设计理念契合,那时候咱们再将模块拆分出去。网络
有一篇文章对于微服务的设计总结的很到位,在这里就不赘述了,直接推荐给你们吧:
相信你们对 NestJS 或多或少有一些了解,简单来归纳的话,NestJS 是由 TypeScript 编写的一款 Node.js 服务端框架,底层的 HTTP Server 由 Express 提供支持,与 Koa、Express 不一样的是,它更加注重架构设计,让本来松散的 JS 服务端工程开箱具有各类个样的设计模式与规范,并借鉴了来自 Angular 和 Spring Boot 等框架的各类设计模式,好比 DI、AOP、Filter、Intercept、Observable 等。
Nest 是一个渐进式的框架,它还内置了微服务的支持,咱们彻底可使用它来尝试构建复杂的 Web 应用,接下来我会与你们一步步地来探索下如何从零开始搭建一个 Nest 微服务应用。
Nest 内置了几种不一样的微服务传输层实现,它们定义在 @nestjs/microservices
包的 Transport
模块内,咱们简单的进行归类:
咱们必须选取一种通信协议来做为彼此微服务间的通信机制,对于 Nest 框架来讲切换传输协议是十分快捷的十分方便,所以咱们须要根据自身项目的特性来决定。在接下来的文章中,我会先直接选用 TCP 做为传输方式,而后再将其改成 Redis,最后讲一讲如何使用 gRPC 来完成调度,并对接入其余语言的服务进行实践。
在 Nest microservice 中,通信模式有两种:
为了在微服务间进行准确的传输数据和事件,咱们须要用到一个称做模式(pattern)的值,pattern 是由咱们进行自定的一个普通的对象值,或者是字符串,模式至关于微服务之间交流的语言,当进行通信时,它会被自动序列化并经过网络请求找到与之匹配的服务模块。
如今,我会带你们来一块儿实现一个微服务架的简单示例,假设须要为整个系统添加一个数据处理的模块,名为 math,其中有一个服务主要功能为 WordCount(词频统计),让咱们来看看如何在 Nest 中进行构建。
咱们首先在单体架构上进行功能实现:
其中,ms-app 经过对外暴露一个 REST API,curl 请求示例以下:
curl --location --request POST 'http://localhost:3000/math/wordcount' \
--header 'Content-Type: application/json' \
--data-raw '{
"text": "a b c c"
}'
复制代码
了解了大概须要作的事情,如今让咱们从零开始进行项目的搭建,执行如下脚本进行项目的初始化工做:
npm i -g @nestjs/cli
nest new ms-app
cd ms-app && nest g service math
复制代码
此时,Nest 会自动帮你生成 math 模块,而且将 MathService 做为 Provider 在 app.module.ts
进行引用。咱们服务的核心功能为 WordCount,在编写函数以前咱们不妨先给 service 加一条测试用例,用于定义咱们预期的请求参数与返回格式:
// math.service.spec.ts
it('should be return correct number', () => {
expect(service.calculateWordCount('a b c c')).toEqual({ a: 1, b: 1, c: 2 });
expect(service.calculateWordCount('c c c d')).toEqual({ c: 3, d: 1 });
});
复制代码
测试驱动开发的好处在此就不一一进行列举了,咱们如今准备在 math.service.ts
中编写一个简单的 WordCount 方法,咱们须要作的事情就是以空格分割文本中的每个单词,而后进行单词:
import { Injectable } from '@nestjs/common';
@Injectable()
export class MathService {
calculateWordCount(str: string) {
const words = str.trim().split(/\s+/);
return words.reduce((a, c) => ((a[c] = (a[c] || 0) + 1), a), {});
}
}
复制代码
这时候,就能够在 app.controller.ts
中进行路由的设计了,咱们首先在构造器中申明 mathService 服务,Nest 会经过依赖注入的方式进行实例初始化,而后进行路由的编写,代码以下:
import { Controller, Post, Body } from '@nestjs/common';
import { MathService } from './math/math.service';
@Controller()
export class AppController {
constructor(private readonly mathService: MathService) {}
@Post('/math/wordcount')
wordCount(@Body() { text }: { text: string }): { [key: string]: number } {
return this.mathService.calculateWordCount(text);
}
}
复制代码
经过上述 curl 命令进行终端测试,返回结果以下:
{"a":1,"b":1,"c":2}
复制代码
由于种种缘由,咱们可能会面临微服务的拆分,也就是将 WordCount 做为微服务的方式进行交互:
咱们在这里使用 Nest 微服务默认的通信协议为 TCP,此时的架构图为:
咱们经过 nest new ms-math
建立一个新的服务,首先安装内置的微服务模块:
yarn add @nestjs/microservices
复制代码
而后咱们改造 src/main.ts
,将之前建立普通实例的方法改成使用微服务的方式进行建立:
import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
},
);
app.listen(() => console.log('Microservice is listening'));
}
bootstrap();
复制代码
此时,咱们能够将 math.service 中的 calculateWordCount
函数拷贝到 app.service.ts
,而后再对 app.controller.ts
进行改造。在控制器中,咱们再也不使用 @Get
或是 @Post
进行暴露接口,而是经过 @MessagePattern
进行设置模式(pattern),供微服务间识别身份,来看一看咱们优雅的代码:
import { Controller } from '@nestjs/common';
import { AppService } from './app.service';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@MessagePattern('math:wordcount')
wordCount(text: string): { [key: string]: number } {
return this.appService.calculateWordCount(text);
}
}
复制代码
此时微服务已经建立好了,咱们来启动它,若你足够细心,应该能收到命令行的输出:Microservice is listening
。
此时,咱们还须要改造原先的 ms-app 服务,咱们将 math 目录先直接删除,由于咱们已经不须要在这里调用 mathService了,同时删除 app.module.ts 与 app.controller.ts 中全部相关代码,而后安装微服务的依赖:
yarn add @nestjs/microservices
复制代码
改造第一步,咱们先在程序中注册一个用于对微服务进行数据传输的客户端,在这里咱们使用 ClientsModule
提供的 register()
方法进行 mathService 的注册:
import { ClientsModule, Transport } from '@nestjs/microservices';
@Module({
imports: [
ClientsModule.register([
{ name: 'MATH_SERVICE', transport: Transport.TCP },
]),
]
...
})
复制代码
模块注册成功后,咱们就能在 app.controller 中使用依赖注入的方式进行引用:
constructor(@Inject('MATH_SERVICE') private client: ClientProxy) {}
复制代码
ClientProxy 对象有两个核心方法:
send()
,请求响应模式下的消息发送方法,该方法会调用微服务并返回一个 Observable 对象的响应体,所以能很简单的去订阅该微服务返回的数据,须要注意的是,只有你对该对象进行订阅后,相应的消息体才会被发送。emit()
,基于事件的消息发送方法,不管你是否订阅数据,该消息都会被当即发送。了解了一些基本的概念,如今咱们来对 app.controller 中的路由进行一些改造:
@Post('/math/wordcount')
wordCount(
@Body() { text }: { text: string },
): Observable<{ [key: string]: number }> {
return this.client.send('math:wordcount', text);
}
复制代码
此时启动 ms-app 服务,让咱们再在终端经过相同的 curl 命令进行测试,预期会获得相同的结果。
以上工程虽然已经知足了咱们的微服务改造需求,为了学习使用,能够在这里再添加一个经过事件响应模型进行触发的数据,事件名称定为:math:wordcount_log
,首先在 ms-app 中的原 /math/wordcount
路由方法里添加一行事件触发代码:
this.client.emit('math:wordcount_log', text)
复制代码
而后打开 ms-math 服务,在 app.controller 中注册相应的订阅器:
@EventPattern('math:wordcount_log')
wordCountLog(text: string): void {
console.log(text);
}
复制代码
以上就是全部须要作的工做,如今执行 curl 命令,咱们能够在 ms-math 服务的终端看到如下打印:
receive: a b c c
复制代码
在以前的章节中,咱们构建了一个简单的微服务架构,微服务间使用 TCP 进行直接的传输通信,那么在这一节中,咱们准备将消息传输机制改成使用 Redis 做为消息代理进行转发,以此来使得咱们的微服务更加健壮。
消息代理(Message broker)是一个中间程序模块,在计算机网络中用于交换消息,它是面向消息的中间件的建造模块,所以它的职责并不包括负责远程过程调度(RPC)。
消息代理也是一种架构模式,用于消息验证、变换、路由。调节应用程序的通讯,极小化互相感知(依赖),有效实现解耦合。例如,消息代理能够管理一个工做负荷队列或消息队列,用于多个接收者,提供可靠存储、保证消息分发、以及事务管理。
上部分讲解了 Nest 框架对于消息协议的实现支持,目前支持如下:REDIS、NATS、MQTT、RMQ、KAFKA,在这些消息服务中切换自己就是十分方便的,而我选择 Redis 的缘由主要有如下几点:
若是你对为何使用消息代理有疑问的话,那么我来给你画一张示意图:
咱们首先在项目相关文件夹下建立一个 docker-compose.yml
,用于管理 Redis 服务:
version: '3.7'
services:
redis:
image: redis:latest
container_name: service-redis
command: redis-server --requirepass rootroot
ports:
- "16379:6379"
volumes:
- ./data:/data
复制代码
经过 docker-compose up -d
后,执行 docker ps
查看服务状态:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6faba303e0ae redis:latest "docker-entrypoint.s…" 9 minutes ago Up 9 minutes 0.0.0.0:16379->6379/tcp service-redis
复制代码
首先咱们在 ms-app 与 ms-math 中安装 Redis 依赖:
yarn add redis
复制代码
而后咱们须要改的地方其实不多,首先就是在 ms-math 中的 bootstrap函数内,咱们将 Transport 替换为 REDIS,并附上服务地址:
// before
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
},
);
// after
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.REDIS,
options: {
url: "redis://:rootroot@localhost:16379",
}
},
);
复制代码
这就是全部 ms-math 须要作的工做,而后打开 ms-app,在注册客户端的地方,咱们也进行相应的替换:
// before
ClientsModule.register([
{ name: 'MATH_SERVICE', transport: Transport.TCP },
]),
// after
ClientsModule.register([
{
name: 'MATH_SERVICE',
transport: Transport.REDIS,
options: {
url: 'redis://:rootroot@localhost:16379',
},
},
]),
],
复制代码
大功告成,经过 curl 进行相应的验证,咱们依然能获得正确的输出。