NestJS Microservice 的微服務架構初探

微服務簡介

微服務架構(Microservice Architecture)是一種架構概念,旨在經過將功能分解到各個離散的服務中以實現對解決方案的解耦。html

回到微服務的概念中,它不是具體指某一技術,而是關於某種架構風格的集合,所以微服務自己是沒有明肯定義的,但咱們知道它是有不僅一個的獨立服務組成的一個總體架構。redis

做爲 ThoughtWorks 的諮詢師,我十分樂意推薦你閱讀關於 Martin Fowler 的這篇關於 Microservices 的文章:docker

martinfowler.com/articles/mi…數據庫

最開始的單體應用

咱們以相似於 Uber 的打車服務做爲一個業務案例,因爲初期團隊規模小,業務量也很少,整個系統是以下的一個單體應用:npm

如上圖所示,乘客和司機經過 REST API 進行交互,全部服務都請求的是一個數據庫,而且全部服務,例如支付、訂單、我的中心等都存在於一個框架服務之中,在早期的時候,這樣的開發架構,對於一個創業型產品是十分常見的,集中管理、開發效率高,但是隨着業務的不斷擴展與量級的增大,慢慢的這個單體應用就變爲了一個巨石應用,那咱們再對其進行代碼維護時,就很容易遇到如下的問題:json

  • 當咱們對其中一個服務進行代碼更新,那麼整個系統所涉及到的測試、集成於部署都會從新執行,整個流程十分緩慢。
  • 當某個服務出現問題的時候,整個服務器會變得不可用,因爲在一個系統倉庫中,修復 Bug 定位也十分困難。
  • 擴展服務與引入新的特性會變得十分困難,期間可能會涉及到整個系統的重構,牽一髮而動全身。
  • ...

改頭換面的微服務架構

爲了解決當前的業務痛點,這家打車公司參考了 Amazon、Netflix 等巨頭公司的應用架構,最終將其巨石應用按照微服務的架構進行從新設計:bootstrap

在這個服務地圖中,咱們看到每一個核心業務模塊都單獨拆分出來做爲一個獨立的服務,針對於用戶還引入了 API 網關的概念用來導航到內部的服務。如今來看,這套微服務架構解決了一些曾經單體應用下存在的缺陷:設計模式

  • 全部功能模塊都是獨立的,去除了服務間代碼的相互依賴,加強了應用的擴展性
  • 每一個模塊能夠單獨部署,修改起來縮短了應用的部署時間,也能更快的對錯誤進行定位

固然微服務也會帶來更多的問題與挑戰,這個咱們就不在此展開討論了。從這個例子能夠看出,從一個單體應用遷移到微服務架構實際上是一個服務演進的過程,任何架構不可能憑空出現,最佳的架構取決於有多麼適合當前的業務模式。服務器

微服務設計準則

微服務自己相較於傳統架構,會帶來許多優勢,但同時又會增長額外的複雜度與管理成本,因此我一直比較信奉一句話:不要爲了微服務而微服務。所以在架構初期,我傾向於按照單體應用的方式進行組織代碼,經過清晰的拆包邏輯,將業務進行隔離,下降模塊間的複雜度,而後到項目後期若在業務與具體架構上能與微服務設計理念契合,那時候咱們再將模塊拆分出去。網絡

有一篇文章對於微服務的設計總結的很到位,在這裏就不贅述了,直接推薦給你們吧:

medium.com/@WSO2/guide…

使用 Nest 開發微服務程序

相信你們對 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 模塊內,咱們簡單的進行歸類:

  • 直接傳輸:TCP
  • 消息中轉:REDIS、NATS、MQTT、RMQ、KAFKA
  • 遠程過程調度:GRPC

咱們必須選取一種通信協議來做爲彼此微服務間的通信機制,對於 Nest 框架來講切換傳輸協議是十分快捷的十分方便,所以咱們須要根據自身項目的特性來決定。在接下來的文章中,我會先直接選用 TCP 做爲傳輸方式,而後再將其改成 Redis,最後講一講如何使用 gRPC 來完成調度,並對接入其餘語言的服務進行實踐。

服務間通信模式

在 Nest microservice 中,通信模式有兩種:

  • Request-response 模式,當咱們須要在內部服務間交互訊息時使用,異步的 response 函數也是支持的,咱們的返回結果甚至能夠是一個 Observable 對象。
  • Event-based 模式,當服務間是基於事件的時候—咱們僅僅想發佈事件,而不是訂閱事件時,就不須要等待 response 函數的響應,此時就是 Event-based 模式就是最好的選擇。

爲了在微服務間進行準確的傳輸數據和事件,咱們須要用到一個稱做模式(pattern)的值,pattern 是由咱們進行自定的一個普通的對象值,或者是字符串,模式至關於微服務之間交流的語言,當進行通信時,它會被自動序列化並經過網絡請求找到與之匹配的服務模塊。

構建一個簡單的 WordCount 服務

如今,我會帶你們來一塊兒實現一個微服務架的簡單示例,假設須要爲整個系統添加一個數據處理的模塊,名爲 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
複製代碼

使用 Redis 做爲消息代理

在以前的章節中,咱們構建了一個簡單的微服務架構,微服務間使用 TCP 進行直接的傳輸通信,那麼在這一節中,咱們準備將消息傳輸機制改成使用 Redis 做爲消息代理進行轉發,以此來使得咱們的微服務更加健壯。

什麼是消息代理

消息代理(Message broker)是一箇中間程序模塊,在計算機網絡中用於交換消息,它是面向消息的中間件的建造模塊,所以它的職責並不包括負責遠程過程調度(RPC)。

消息代理也是一種架構模式,用於消息驗證、變換、路由。調節應用程序的通訊,極小化互相感知(依賴),有效實現解耦合。例如,消息代理能夠管理一個工做負荷隊列或消息隊列,用於多個接收者,提供可靠存儲、保證消息分發、以及事務管理。

爲何選用 Redis

上部分講解了 Nest 框架對於消息協議的實現支持,目前支持如下:REDIS、NATS、MQTT、RMQ、KAFKA,在這些消息服務中切換自己就是十分方便的,而我選擇 Redis 的緣由主要有如下幾點:

  • Redis 自己足夠輕量級與高效,使用率很是高,也比較受歡迎
  • 在我本身的工程代碼中,個人服務列表裏自己就有 Redis 服務,我並不但願選擇其餘的服務單獨做爲消息代理,增長了服務依賴與管理成本。
  • 我對 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 進行相應的驗證,咱們依然能獲得正確的輸出。

相關文章
相關標籤/搜索