Mayank C · Follow
Published in · 5 min read · Oct 21, 2023
--
Node.js, as its default behavior, operates on a single thread and does not make full use of available CPU cores, even when multiple cores are at its disposal. However, this single-threaded approach doesn’t preclude you from harnessing the power of multiple cores within your environment.
In contrast, Node.js includes a built-in cluster module. This module facilitates the creation of child processes (referred to as workers) that can operate concurrently while sharing the same server port. Each child process functions independently with its own event loop, memory allocation, and V8 engine instance.
These child processes employ interprocess communication to establish seamless communication with the primary parent Node.js process. This capability ensures enhanced performance and resource utilization in Node.js applications running on multi-core machines.
In this article, we will analyze the performance of a URL shortener application under two different configurations: first, when it operates over Fastify in its standard mode, and second, when it runs in cluster mode with a size of 4.
Environment
All tests are executed on MacBook Pro M1 with 16G of RAM. The Node.js version is v21.0.0 (latest at the time of writing). Load testing was performed using a tailored version of the Bombardier test tool. This customized tool allowed for the sending of random URLs in each request.
Database
In the context of our URL shortener series, we have opted for the widely used PostgreSQL database. To facilitate this, we have established a straightforward table structure designed to store the short ID and corresponding original URL. The table is defined as follows:
urlshortener=> \d shortenedurls;
Table "public.shortenedurls"
Column | Type | Collation | Nullable | Default
--------------+-----------------------------+-----------+----------+---------
id | character(10) | | not null |
srcurl | character(250) | | not null |
created | timestamp without time zone | | |
lastaccessed | timestamp without time zone | | |
Indexes:
"firstkey" PRIMARY KEY, btree (id)
Before each test, the table is truncated.
Code
The URL shortener service is designed using the Model-View-Controller (MVC) architectural pattern. The controller, service, and database part is common between regular and clustered application. The differentiator is the main file where fastify application is created.
First, let’s look at the main file for both cases:
fastifyMain.mjs
import Fastify from "fastify";
import { handleRequest } from "./fastifyController.mjs";const app = Fastify({
logger: false,
});
app.post("/shorten", handleRequest);
app.listen({ port: 3000 });
fastifyClusterMain.mjs
import cluster from "node:cluster";
import Fastify from "fastify";
import { handleRequest } from "./fastifyController.mjs";const numClusterWorkers = parseInt(process.argv[2] || 1);
if (cluster.isPrimary) {
for (let i = 0; i < numClusterWorkers; i++) {
cluster.fork();
}
cluster.on(
"exit",
(worker, code, signal) => console.log(`worker ${worker.process.pid} died`),
);
} else {
const app = Fastify({
logger: false,
});
app.post("/shorten", handleRequest);
app.listen({ port: 3000 });
}
Next, let’s look at the common parts:
fastifyController.mjs
import { shorten } from "./service.mjs";export async function handleRequest(req, rep) {
if (!(req.body && req.body.srcUrl)) {
return rep.code(400).send({ errMsg: "Parameter 'srcUrl' is missing" });
}
const srcUrl = req.body.srcUrl;
if (srcUrl.length > 250) {
return rep.code(400).send({
errMsg: "Parameter 'srcUrl' must not be more than 250 characters",
});
}
if (!(srcUrl.startsWith("http://") || srcUrl.startsWith("https://"))) {
return rep.code(400).send({
errMsg: "Parameter 'srcUrl' must start with http:// or https://",
});
}
const shortenedUrl = await shorten(srcUrl);
if (!shortenedUrl) {
return rep.code(500).send({ errMsg: "Failed to shorten" });
}
rep.send({ srcUrl, shortenedUrl });
}
service.mjs
import { nanoid } from "nanoid";
import { save } from "./db.mjs";
const baseUrl = "http://test.short/";export async function shorten(srcUrl) {
if (!srcUrl) {
return;
}
const urlId = nanoid(10);
const shortenedUrl = `${baseUrl}${urlId}`;
const dbStatus = await save(urlId, srcUrl);
return dbStatus ? shortenedUrl : undefined;
}
db.mjs
import { DataTypes, Sequelize } from "sequelize";const dbUser = process.env.dbUser;
const dbUserPass = process.env.dbUserPass;
const dbName = process.env.dbName;
const sequelize = new Sequelize(
`postgres://${dbUser}:${dbUserPass}@localhost:5432/${dbName}`,
{
logging: false,
pool: {
max: 10,
min: 10,
},
},
);
await sequelize.authenticate();
const ShortenedUrl = sequelize.define("shortenedurl", {
id: {
type: DataTypes.STRING,
primaryKey: true,
},
srcurl: DataTypes.STRING,
created: DataTypes.DATE,
lastaccessed: DataTypes.DATE,
}, {
timestamps: false,
});
export async function save(id, srcUrl) {
await ShortenedUrl.create({
id,
srcurl: srcUrl,
created: new Date(),
lastaccessed: new Date(),
});
return true;
}
The performance benchmarking involved conducting tests with 1 million requests, evaluated under varying concurrent connections of 10, 50, and 100. To conduct these load tests, a tailored version of the Bombardier load test tool was employed, as previously mentioned. Additionally, a warm-up phase consisting of 1,000 initial requests preceded each test run for accurate assessment.
A cluster size of 4 has been used for benchmarking.
Charts
If you prefer charts, the results in chart form are as follows:
Tables
If you prefer tables, here are the same results in tabular form:
It’s no revelation that clustered Fastify applications deliver superior performance. A thorough analysis indicates that Fastify application operating within a cluster of size 4 offers nearly 60% more performance over its regular counterpart. However, this enhancement comes with a significant trade-off, necessitating approximately four times the CPU and memory usage. If computational resources are not a constraint, opting for a clustered configuration is the ideal choice!
Note: Should you encounter any discrepancies in the comparison and anticipate updates or corrections to the article, kindly provide the revised and comprehensive application code along with evidence of improved results using the Bombardier test tool. Speculations will not be considered for article revisions.
For a comprehensive list of real-world benchmarking, you can visit the main article here.