一、定義
微服務的定義即為將相同模塊或相關業務的操作,封裝在一個服務中,達到獨立運行、獨立部署的效果。目的是為了功能的解耦,並且做到互不影響。
此時的服務可以采用不用的語言、不一樣的架構實現,便於適合不同的開發人員根據自身的技術情況進行靈活選擇。
設計微服務的時候,最主要的是根據業務邏輯、安全、穩定、高效等方面進行服務功能的分離,但在此之外,還要設計服務之間相互通訊的方式。
最普遍的是通過服務間HTTP接口進行功能的調用,該方式相對來說最易於實現,整個調用流程也較為清晰易懂。但采用HTTP請求進行微服務間通訊也有些缺點,如不必要的請求頭信息,導致發送的數據包過大,再比如很難實現任務隊列的功能等,所以微服務之間還可以選擇RPC、mq、MQTT等方式進行信息流轉。
微服務還有很多注意事項,如服務管理、負載均衡、服務監控等問題。與本篇文章無太大關聯,在此不會進行闡述
二、NESTJS微服務
nestjs是一種類似angular與spring boot的nodejs后端架構,其架構思想包含DI(依賴注入)、OOP(面向對象編程)、AOP(切面編程)等特點,使得原本較為松散的后端js工程代碼能夠有較為清晰的管理方式。詳情請戳nestjs官網。
nestjs本身除了可設計普通的API服務外,還可以以Microservice的方式設計微服務,在其官方文檔中可以看到相關的描述與定義:地址。通過左側的導航欄可以看到,該框架支持多種介質的服務實現方式,包括redis、MQTT、rabbitmq、gRPC等:
本文將會按照官方文檔,選取幾種方式進行簡單demo實現。
三、TCP方式
nestjs實現微服務主要依賴包@nestjs/microservices
,所以在開始之前需要提前在項目中安裝該依賴:$ npm i --save @nestjs/microservices
。
首先准備兩個nestjs項目,使用官方提供的腳手架工具進行項目構建:
$ npm i -g @nestjs/cli
$ nest new project-name
項目名可以自定義,本文中暫時采用[項目一]與[項目二]進行描述。項目一為API與微服務混合模式,項目二為單微服務模式,前者為調用方,后者為服務功能提供方。
在兩個項目中安裝@nestjs/microservices
依賴后,即可開始代碼編寫。
3.1 微服務模式(服務提供方)
先改造[項目二],將src/main.ts
文件改寫為如下:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
async function bootstrap() {
// const app = await NestFactory.create(AppModule);
// await app.listen(3000);
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
},
);
app.listen();
}
bootstrap();
對比改動前后的代碼,可以發現app
的生成方式發生的變化,更改前是通過NestFactory.create
方法生成,更改后則是通過NestFactory.createMicroservice
生成,並且多了相關的參數。
前者是nestjs生成普通應用程序對象(INestApplication),該對象只啟用了HTTP監聽器,所以只能處理HTTP接口消息,而后者生成INestMicroservice對象,該對象可監聽TCP消息,該對象即是框架實現TCP消息流轉的入口。
上述man文件只是提供了基礎的能力支持,如果想體驗具體功能,還需要編寫相應的處理代碼。
打開文件src/app.controller.ts
,這是工程默認生成的一個控制器(控制器主要服務接受請求與返回響應),可以看到已經使用@Get
注解定義了一個接口。
此時如果使用命令npm run start
啟動項目的話,使用請求發送工具(linux的curl命令或者postman等工具),發送get請求:http://localhost:3000,該請求是無法得到響應的。原因是我們在main.ts中實現的是微服務對象,該對象不支持HTTP監聽。
這里感覺框架的處理方式不太合理,如果是不支持的話,可以返回一些錯誤信息,或者在控制台打印日志,而不是一直讓請求pending直到超時
將app.controller.ts
中的@Get
注解及其下方的函數實現先注釋掉(不注釋也可,沒有影響)。
實現一個微服務接收函數,在nestjs中,不管是TCP方式還是其他,都是通過使用注解@MessagePattern
定義所有的微服務接口。將app.controller.ts
文件改寫為如下形式:
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
// @Get()
// getHello(): string {
// return this.appService.getHello();
// }
@MessagePattern('accumulate')
accumulate(data: number[]): number {
return (data || []).reduce((a, b) => a + b);
}
}
在這里,我們定義了一個接口叫accumulate
,接口作用是接收一個數字型數組,把所有元素相加后,將結果返回。
至此,我們實現了一個很基礎的微服務,這個微服務有個接口,接口功能是計算數組之和。但我們要怎么調用這個功能呢?前面在啟動項目后,無法通過HTTP接口進行調用,這時候就需要使用另一個項目充當客戶端的角色,對該功能進行調用使用了。
也可以在這個項目里實現HTTP、TCP混合的應用程序,同時實現HTTP接口與TCP接口,進行相互調用。但這樣就沒有缺乏了微服務服務之間的味道了,所以不以這種例子作為說明
3.2 HTTP服務(調用方)
前面我們實現了一個微服務,接下來我們定義一個新的服務(項目一),對該微服務的功能進行調用。
還是先關注入口文件main.ts
,我們將監聽的端口號換一下,例如改為10086
。
不使用3000的原因是在上一個項目中已經占用了。雖然上一個項目中沒有明確定義,但nestjs會給設置一個默認的端口號為3000
端口號基本由開發人員自己定義,但不要與linux系統端口號或常見端口重復,如80、3679等,基本超過5位數的端口號使用的情況比較少
接下來我們在程序中引入一個客戶端,在app.module.ts
中,改造為如下形式:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Transport, ClientsModule } from '@nestjs/microservices';
@Module({
imports: [
ClientsModule.register([
{ name: 'MATH_SERVICE', transport: Transport.TCP },
]),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
對比改造前后,會發現我們在imports
中引入了一個客戶端,該客戶端通過ClientsModule
進行定義,名字叫做MATH_SERVICE
,介質為Transport.TCP
。
客戶端名稱可自定義,其目的也是在調用方有多個微服務時區分使用
此時沒有定義地址與端口號,采用默認的localhost與3000。如果服務不在同一環境,可添加options參數定義host與port
接下來,還是改造app.controller.ts文件,改寫為以下形式:
import { Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';
import { ClientProxy } from '@nestjs/microservices';
import { Observable } from 'rxjs';
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
@Inject('MATH_SERVICE') private readonly client: ClientProxy,
) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('testMicroservice')
testMicroservice(): Observable<number> {
return this.client.send('accumulate', [1, 2, 3, 4, 5]);
}
}
對比更改前后,有兩點地方值得注意:
- 使用
@Inject
注解引入名為MATH_SERVICE
的客戶端 - 實現一個get接口,名為
testMicroservice
上述引入的客戶端即為app.module.ts
中定義的客戶端,而testMicroservice
接口中,操作該客戶端發送數據。
發送數據的函數中,第一個參數為接收方的接口名(即為上一個項目中所定義的名稱),第二個參數為發送的數據(即為上個項目中的接收參數)。
Observable為觀察者對象,在本文中並不重要,讀者可自行了解
運行項目:npm run start
,可以看到控制台存在以下提示:
該提示也表示在app.module.ts
中的客戶端已啟用並被初始化。
使用http請求工具,如curl http://localhost:10086/testMicroservice
,可以得到請求響應,結果為15,即為傳入數組的總和,說明我們服務之間調用成功。
被調用方也要啟動,要不然會請求不到,在控制台報錯:connect ECONNREFUSED 127.0.0.1:3000
至此,我們實現了一個微服務,並且定義了一個客戶端,對該服務進行調用。
四、與HTTP接口對比
服務與服務之間相互調用,HTTP接口相對來說最為簡單、調試最為方便,但是TCP在高並發情境下還是有一定的使用場景,高並發下,請求數量陡升,攜帶的信息差之毫厘,也會影響資源的使用,進而影響服務的速度,下面是分別使用HTTP方式與TCP方式,用wireshark工具進行抓包分析時,所展示的數據包大小:
(前者為HTTP,后者為TCP。攜帶的數據都是{ data: [1,2,3,4,5] }
)
從上面可以看到,當攜帶的信息較少時,TCP方式明顯比HTTP方式所發送的數據包要小,HTTP數據包大小占比主要集中在請求頭headers
當請求中所發送的數據比較大,在數據包中的占比較大時,兩種方式基本沒區別。所以要看情況選擇使用
五、不同框架間通訊
不同語言,不同框架之間數據接收、數據解析的方式可能不同,可以使用相對獨立的介質,如gRPC、RabbitMQ等方式進行信息傳遞,通過單獨的配置文件或者數據流轉進行通訊。