微服務架構(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 進行相應的驗證,咱們依然能獲得正確的輸出。