為什麼 NodeJS 是構建微服務的最佳選擇?

語言: CN / TW / HK

什麼是微服務微服務是一種應用架構,它將每個應用功能都放在自己的服務中,與其他服務隔離。這些服務是鬆散耦合的,可獨立部署。

這種架構的出現是為了解決舊的 Web 應用開發的單體方法。在單體軟體中,所有的東西都是作為一個單元構建的,所有的業務邏輯都被歸入一個廣泛的應用。

這種方法使更新程式碼庫的過程變得複雜化,因為它影響到整個系統,即使是最小的程式碼改動也需要構建和部署整個軟體的新版本。此外,哪怕你只想擴充套件應用的某個特定功能,卻需要擴充套件整個應用來實現它。

微服務解決了單體系統所面臨的這些挑戰,它將應用從一個整體分割成幾個小部分。

什麼時候應該使用微服務?從本質上講,微服務架構解決了龐大、複雜應用的快速開發問題。

對於“哪個更好?”這一問題,目前還沒有通用的答案。答案取決於各種情況,因為每一種情況都有其好處和缺點。

下面是一些微服務架構的優點和缺點,你可能對此已經有所瞭解:

優點語言不可知性:微服務並不限於特定的程式語言,每個微服務都可以用不同的語言來編寫,以支援選定的通訊協議。

可擴充套件性:由於微服務和它的職責可以由開發者共同承擔,所以如果有一個大的團隊參與到這個專案中,應用就會變得更加易於維護。

無限迭代:由於開發者不會被其他元件所束縛,所以在微服務上迭代會變得更加簡單。

單元測試:由於微服務是獨立的應用,它的重點是特定的功能,因此,開發者可以很輕鬆地編寫測試指令碼,以驗證該特定功能。

缺點要作為一個整體來管理是很困難的:凱撒大帝有一句名言“分而治之”(divide et impera,拉丁語),即使在這裡也可以大規模應用,但是要謹慎,因為過多的活動部分會變得難以管理。

難以追蹤:如果架構變得過於複雜,微服務之間的通訊渠道會非常多,出現錯誤後會很難追溯並確定故障點。

需要大量的專業知識:構建和部署微服務要求非常高的計劃和協調方面的軟技能。

具有挑戰性的測試:測試是一把雙刃劍,因為微服務作為一個整體更難測試。整合和端到端的測試同樣會有挑戰。

審計日誌:可能更難獲得和調查。

在架構方面,SaaS 微服務非常適合,因為微服務是 SaaS 應用的一個不錯的選擇。由於這類應用想要使用者付錢買單,那麼它就需要提供高可用的服務,因此將軟體分成小塊可以加快恢復速度。同時,SaaS 應用的發展主要是由其社群推動,所以,它也會受到很多變化的影響,而通過微服務和解耦,開發者可以獲得了靈活性,這是單體架構無法提供的。

單體應用程式可能難以水平擴充套件,因為你必須複製整個應用程式,如果它依賴於單個數據庫,這個過程將變得更加困難。另一邊,微服務卻可以根據單個服務進行擴充套件、複製或負載平衡。比如,如果你需要傳送更多的電子郵件,你只需要擴充套件負責電子郵件功能的微服務。今天你有 10 個使用者,明天你有 1000 個;SaaS 應用可以在短時間內維持大規模的增長,這就是為什麼他們的架構必須要以最經濟的方式進行輕鬆擴充套件的原因。

這樣還可以減少資源的消耗,因此可以減少賬單。所以,可以肯定地說,微服務是 SaaS 企業架構的下一個階段。

弄清你是否需要微服務的最好方法是問自己:我有關於單體應用的問題嗎?如果有的話,或許你應該考慮轉向微服務。如果沒有,那就堅持下去——沒有必要把時間花在一個根本不存在的問題上。

微服務通訊是如何工作的?由於服務之間彼此獨立,所以與微服務的通訊需要好好選擇。通訊協議的使用不當會造成應用的效能下降,大家必須根據自己應用的具體需求來選擇通訊協議。

有兩種通訊方式可以選擇:同步通訊和非同步通訊,這是請求 - 響應和基於事件的模式的基礎。

在第一種情況下,即同步方式,客戶端傳送請求並等待響應。這種方法有一個缺陷,那就是它是一個阻塞模式。但是,如果你有一個讀操作非常多的應用時,那就不一定了,因為你的應用更傾向從外部讀取和接受資訊。在這種情況下,使用同步方式可能是一個很好的選擇,特別是當它涉及實時資料時。

我們的另一個選擇是非同步通訊,這是一個非阻塞模式。如果你想要一種有彈性的微服務,那麼,與同步通訊相比,非同步通訊是一種更好的選擇。在這種情況下,客戶端會發送一個請求,收到請求的確認,並將其遺忘。這種方法最適用於大量寫操作、無法承受資料記錄丟失的應用。

下面是一些涉及微服務通訊的解決方案,你可以從中選擇:

基於 HTTP 的 REST

基於 HTTP/2 的 REST

WebSocket

TCP 套接字

UDP 資料包

好好考慮最適合自身需求的通訊協議,因為這將使應用響應更快、效率更高。

為什麼 NodeJS 用於微服務?在構建微服務時,有很多頂級程式語言可供選擇。NodeJS 就是其中之一。那麼,為什麼 NodeJS 是最佳選擇呢?

單執行緒 & 非同步:NodeJS 使用事件迴圈來執行程式碼,允許非同步程式碼被執行,從而使伺服器能夠使用非阻塞機制來響應。

事件驅動:NodeJS 使用事件驅動架構,該架構建立在軟體開發的常見模式上,被稱為釋出 - 訂閱或觀察者模式,能夠構建強大的應用,尤其是實時應用。

快速和高度的可擴充套件性:執行環境建立在最強大的 JavaScript 引擎之一 V8 JavaScript Engine 之上,因此程式碼執行速度快,使得伺服器能夠同時處理多達 10000 個併發請求。

易於開發:建立多個微服務會導致重複的程式碼。Node.js 的微服務框架很容易建立,因為它抽象了大部分的底層系統。所以用這種程式語言建立一個微服務可以像寫幾行程式碼一樣簡單。

實施微服務架構我們從建立用於使用者管理的微服務開始,它將使用 TCP 資料包進行通訊,並負責對使用者進行 CRUD 操作。我們將使用 PacketSender 對其進行測試,PacketSender 是一個免費的工具,用於傳送支援 TCP 的網路資料包。

微服務的架構和作用域被進一步界定。因此,從演示的角度來看,通過 HTTP 實現一個微服務與實現 NodeJS API 沒有什麼不同。

同時,通過 HTTP 來使用 REST 也很容易,但如果從這個協議切換到其他協議時,會出現一些問題。這也是本文中我們將會使用 TCP 包的非同步模式來與微服務通訊的原因。

我們將使用 NestJS 作為應用的框架。它並非 NodeJS 微服務框架,而是一個用於構建伺服器端應用的框架。但是,由於其內建了多個微服務特性,使得工作變得更加容易。

步驟一:微服務設定用 Node.js 構建微服務相當容易,尤其是用 NestJS 框架。開始時,可以使用 CLI 建立一個新的 NestJS 應用,使用如下命令:

npx @nestjs/cli new user-microservice

該命令會建立並初始化一個新專案。要開始構建一個微服務,你需要安裝以下軟體包:

npm i --save @nestjs/microservices

最後,為了讓微服務啟動和執行,我們需要用以下內容更新 main.ts 檔案:

import { INestMicroservice } from '@nestjs/common';import { NestFactory } from '@nestjs/core';import { Transport } from '@nestjs/microservices';import { AppModule } from './app.module';async function bootstrap() { const microservicesOptions: any = { transport: Transport.TCP, options: { host: '127.0.0.1', port: 8875, }, }; const app: INestMicroservice = await NestFactory.createMicroservice( AppModule, microservicesOptions, ); app.listen(() => console.log('Microservice is listening'));}bootstrap();

NestJS 支援幾個內建的傳輸層實現,稱為傳輸器。上面的程式碼將建立一個微服務,通過 TCP 傳輸層繫結到本地機器的 8875 埠進行通訊。

步驟 2:微服務監聽訊息我們可以使用訊息模式或事件模式來與微服務通訊。

訊息模式的作用就像一個請求 - 響應方法,它適用於在服務之間交換訊息,而當你只想釋出事件而不等待響應時,就可以使用事件模式。

在我們的案例中,我們只實現根據給定的輸入建立一個使用者的功能,並且將獲得建立的使用者。因此,我們將在 app.controller.ts 檔案中註冊一個名為 create_user 的訊息模式。

@Controller()export class AppController { constructor(private readonly appService: AppService) {} @MessagePattern('create_user') async createUser(@Payload() payload: CreateUserDto) { const user = await this.appService.createUser(payload); return user; }}

我們抽象出建立新使用者的邏輯,因為它可以根據需求和使用的資料庫以各種方式實現,我們將只關注與微服務相關的主題。

我們用來建立一個新使用者的有效負載有以下格式:

import { IsString, IsEmail } from 'class-validator';export class CreateUserDto { @IsEmail() email: string; @IsString() password: string;}

一個帶有 email 和 password 的簡單物件

步驟 3:測試微服務為了測試這個微服務,我們將使用 PacketSender 嚮應用傳送一個 TCP 包。為此,將地址和埠設定為 127.0.0.1:8875,並從右側的下拉選單中選擇 TCP。要對我們的資訊進行編碼,請使用 ASCII 欄位,並用以下值來完成:

122#{"pattern":"create_user","data":{"email":"[email protected]","password":"12345678"},"id":"ce51ebd3-32b1-4ae6-b7ef-e018126c4cc4"}

pattern:是我們正在尋找的資訊,create_user。

data:是我們要傳送的 JSON 物件,一個帶有 email 和 password 的物件。

值 122 代表我們的訊息的長度,從第一個大括號開始到最後一個大括號(包括兩個)。

資料包傳送器配置

如果我們點選 Send 按鈕,我們會看到如下日誌:

日誌活動

第二個是我們傳送給微服務的內容,第一個是我們收到的內容。裡面的響應是由我們的微服務返回的物件,即被建立的使用者。

步驟 4:API 閘道器現在我們有了微服務,並進行了快速測試,看它是否能接收請求並返回響應,現在是時候建立一個 API 閘道器並將其連線到微服務上了。

為此,我們將使用上面描述的相同步驟建立一個新的 NestJS 應用,然後用以下內容更新 app.module.ts 檔案。

import { Module } from '@nestjs/common';import { AppController } from './app.controller';import { AppService } from './app.service';import { ConfigService } from "./config/config.service";@Module({ imports: [], controllers: [AppController], providers: [ { provide: 'USER_MICROSERVICE', useFactory: (configService: ConfigService) => { const options = { transport: Transport.TCP, options: { host: configService.get('USERS_MICROSERVICE_HOST'), port: Number(configService.get('USERS_MICROSERVICE_PORT')), }, }; return ClientProxyFactory.create(options as ClientOptions); }, inject: [ConfigService], }, AppService, ],})export class AppModule {}

我們將使用 .env 檔案,我們將在其中儲存任何與配置有關的值。這些檔案將在一個配置服務的幫助下被讀取。該微服務可以在 host 127.0.0.1:8875 處找到,其中 port 為 8875。

通過上面的程式碼,我們使用 ClientProxy 注入一個新的物件,代表與我們的使用者 - 微服務的連線。這個 NestJS 類提供了幾個內建的工具來與遠端微服務交換資訊。

為了使用這個連結物件,我們可以在 AppController 或 AppService 中注入它,如下所示:

@Controller()export class AppController { constructor( @Inject('USER_MICROSERVICE') private readonly client: ClientProxy, private readonly appService: AppService ) {} @Post('create-user') async createUser(@Body() payload: CreateUserDto) { return this.client.send('create_user', payload).toPromise(); }}

現在,每次 API 在路由 create-user 處受到 POST 請求時,API 閘道器將把請求和有效載荷一起轉發給微服務,然後從微服務返回響應給使用者。