Jak skalować aplikacje Node.js?

Jan 25 2023
Pokrótce omówimy, jak skalować jedną z najpopularniejszych obecnie platform do tworzenia aplikacji. Zacznijmy więc od motywacji: jak stworzyć dobrze działające API i powielić tę logikę dla użytkowników X? Mamy trzy sposoby skalowania naszej aplikacji: Pionowy, Poziomy i mieszany (pionowy i poziomy).

Pokrótce omówimy, jak skalować jedną z najpopularniejszych obecnie platform do tworzenia aplikacji.

Zacznijmy więc od motywacji: jak stworzyć dobrze działające API i powielić tę logikę dla użytkowników X?

Mamy trzy sposoby skalowania naszej aplikacji: Pionowy , Poziomy i mieszany (pionowy i poziomy).

Skalowanie w poziomie

Skalowanie w poziomie oznacza, że ​​w większości przypadków duplikowane są serwery lub węzły. W przypadku tej repliki lub replik będziemy potrzebować modułu równoważenia obciążenia, aby wysłać do nich wszystkie żądania w równy sposób.

Musimy wziąć pod uwagę, że to rozwiązanie wiąże się ze złożonością podczas wdrażania aplikacji, ponieważ nowy moduł równoważenia obciążenia powinien być dobrze skonfigurowany, w przeciwnym razie cały ruch mógłby trafić na jeden serwer i stracilibyśmy pieniądze.

Skalowanie w pionie

Pionowe oznacza dodanie większej mocy obliczeniowej lub pamięci RAM/dysków.

To całkiem proste, jeśli twój komputer w chmurze działa na maszynie z 16 GB pamięci RAM i 16 rdzeniami/32 wątkami, podwojmy to i możesz dostarczyć podwójną liczbę użytkowników .

Ale w tym celu musimy wziąć pod uwagę, że w pewnym momencie będziemy mieli limit, a także ceny wzrosną wykładniczo w ostatnich dostępnych konfiguracjach.

A więc zacznijmy!

To będzie krótka teczka dotycząca skalowania w poziomie .

Łatwym początkiem byłoby użycie The Cluster Module .

Za pomocą tego modułu możemy tworzyć procesy potomne, które współdzielą wszystkie porty serwera. Dość słów, oto kod!

import cluster from 'node:cluster';
import http from 'node:http';
import { availableParallelism } from 'node:os';
import process from 'node:process';

const numCPUs = availableParallelism();

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} is running`);

  // Fork workers.
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`worker ${worker.process.pid} died`);
  });
} else {
  // Workers can share any TCP connection
  // In this case it is an HTTP server
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('hello world\n');
  }).listen(8000);

  console.log(`Worker ${process.pid} started`);
}

$ node server.js
Primary 3596 is running
Worker 4324 started
Worker 4520 started
Worker 6056 started
Worker 5644 started

Aby trochę lepiej zrozumieć ten proces roboczy , zróbmy przykład:

Przeczytaj duży plik json i dla każdej linii i dla każdego obiektu w nim zawartego przekonwertuj te informacje na XML.

Najpierw kod bez wątków roboczych =>

/* syncMode.js */

// Read all JSON file contents into an array
const contents = getContents()

/* Execution time start */
const start = hrtime.bigint()

// Convert each JSON file content to XML format
const result = contents.map((content) => {
  content = JSON.parse(content)
  return js2xmlparser.parse('user', content)
})

/* Execution time end */
const end = hrtime.bigint()
console.info(`Execution time: ${(end - start) / BigInt(10 ** 6)}ms`)

Załóżmy, że mamy katalog zawierający 1000 plików JSON i mamy za zadanie przekonwertować każdy plik JSON na plik XML.

Każdy plik JSON zawiera tablicę 1000 obiektów, z których każdy ma 6 par klucz-wartość, jak pokazano w poniższym przykładzie.

{
  "id": 1,
  "first_name": "abc",
  "last_name": "xyz",
  "email": "[email protected]",
  "gender": "Male",
  "ip_address": "163.213.59.129"
}

$ node syncMode.js
Execution time: 1036ms

Aby łatwiej zarządzać wątkami roboczymi, użyjemy biblioteki o nazwie Piscina , która jest idealna do tego zadania!

/* ThreadMode.js */

const Piscina = require('piscina')

// Read all JSON file contents into an array
const contents = getContents()

/* Execution time start */

// Divide the content into two chunks
const chunks = splitToChunks(contents, 2)

// Create a new thread pool
const pool = new Piscina()
const options = { filename: resolve(__dirname, 'worker-pool') }

// Run operation on the chunks parallely
const result = await Promise.all([pool.run(chunks[0], options), pool.run(chunks[1], options)])

/* Execution time end */

$node ThreadMode.js
Execution time: 687ms

Widzieliśmy niesamowity wzrost wydajności o ponad 66% !!! z zaledwie kilkoma liniami. Tutaj jest to bardzo specyficzny przypadek, ale zaczęliśmy widzieć, jak zwiększyć wydajność w Node.js za pomocą wbudowanych funkcjonalności.

W kolejnych krokach będziemy używać równoważenia obciążenia do rozdzielania obciążenia na kilka interfejsów API w węźle z dokerem, więc bądźcie czujni!!!