├── .gitignore ├── logServer.js ├── package.json ├── bin └── index.js ├── README.md └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | combined.log 3 | error.log -------------------------------------------------------------------------------- /logServer.js: -------------------------------------------------------------------------------- 1 | import winston from "winston"; 2 | 3 | const logger = winston.createLogger({ 4 | level: "info", 5 | format: winston.format.json(), 6 | defaultMeta: { service: "user-service" }, 7 | transports: [ 8 | // 9 | // - Write all logs with importance level of `error` or less to `error.log` 10 | // - Write all logs with importance level of `info` or less to `combined.log` 11 | // 12 | new winston.transports.File({ filename: "error.log", level: "error" }), 13 | new winston.transports.File({ filename: "combined.log"}), 14 | ], 15 | }); 16 | 17 | export default logger; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loadbalancer-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node server.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "bin": { 15 | "start-lb": "./bin/index.js" 16 | }, 17 | "dependencies": { 18 | "axios": "^1.6.2", 19 | "chalk": "^5.3.0", 20 | "express": "^4.18.2", 21 | "inquirer": "^9.2.12", 22 | "nanospinner": "^1.1.0", 23 | "node-cron": "^3.0.3", 24 | "winston": "^3.11.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import chalk from "chalk"; 4 | import inquirer from "inquirer"; 5 | import startServer from "../server.js"; 6 | 7 | console.log(chalk.green("Welcome to the load balancer CLI")); 8 | 9 | let serverCount = 0; 10 | global.servers = new Map(); 11 | global.healthCheckPeriod = 10; 12 | global.healthCheckEndopint = "/"; 13 | 14 | const getNumberOfServers = async()=>{ 15 | const server = await inquirer.prompt({ 16 | name: "serverCount", 17 | type: "input", 18 | message: "Enter number of servers:", 19 | validate: (input)=> /^\d+$/.test(input) || 'Please enter a valid numeric value.' 20 | }); 21 | serverCount = server.serverCount; 22 | } 23 | 24 | const validateUrl = (url)=>{ 25 | const urlPattern = 26 | /^(https?:\/\/)?([a-zA-Z0-9.-]+(:[0-9]+)?)(\/[^\s?#]*)?(\?[^#\s]*)?(#.*)?$/; 27 | 28 | return urlPattern.test(url); 29 | } 30 | 31 | const getServersDetails = async()=>{ 32 | 33 | for(let i=1;i<=serverCount;i++){ 34 | const server = await inquirer.prompt({ 35 | name: "serverUrl", 36 | type: "input", 37 | message: `Enter server url ${i}`, 38 | 39 | }); 40 | 41 | servers.set(i, server.serverUrl) 42 | } 43 | } 44 | 45 | const getHealthCheckEndpoint = async()=>{ 46 | const server = await inquirer.prompt({ 47 | name: "endpoint", 48 | type: "input", 49 | message: "Enter the health check endpoint (e.g., /health). By default, it is set to '/'.", 50 | }); 51 | 52 | healthCheckEndopint = server.endpoint; 53 | } 54 | 55 | const getHealthCheckPeriod = async()=>{ 56 | const server = await inquirer.prompt({ 57 | name: "seconds", 58 | type: "number", 59 | message: `Enter the time period (in seconds) for checking the server health.`, 60 | validate: (input)=> /^\d+$/.test(input) || 'Please enter a valid numeric value.' 61 | }); 62 | 63 | healthCheckPeriod = server.seconds; 64 | } 65 | 66 | const confirmStartServer = async()=>{ 67 | const server = await inquirer.prompt({ 68 | name: "startLB", 69 | type: "confirm", 70 | message: `Start LoadBalancer server ?`, 71 | }); 72 | if(server.startLB){ 73 | startServer(); 74 | } 75 | else{ 76 | console.log("Closing"); 77 | } 78 | } 79 | 80 | const startIt = async()=>{ 81 | await getNumberOfServers(); 82 | await getServersDetails(); 83 | await getHealthCheckEndpoint(); 84 | await getHealthCheckPeriod(); 85 | await confirmStartServer(); 86 | } 87 | 88 | startIt(); 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Load Balancer 2 | 3 | A simple load balancer with cli implemented in Node.js and Express. 4 | 5 | Demo: https://youtu.be/yqBR1M1jlPA?si=EcGiI7i7gxPPoxkp 6 | 7 | ## Description 8 | 9 | This project is a basic implementation of a load balancer using NodeJS and ExpressJS. The load balancer distributes incoming requests across multiple servers to ensure efficient utilization of resources and improved system reliability. 10 | 11 | ![Screenshot 2023-12-23 214943](https://github.com/harshilsharmaa/Load-Balancer/assets/71216106/c91bdfca-7327-45a7-a5a4-d39f0330f81a) 12 | 13 | 14 | ## Features 15 | 16 | - Distributes incoming requests across multiple servers. 17 | - Health checks to monitor server status. 18 | - Simple CLI configuration for setting up server details. 19 | - Logging of requests and errors for monitoring. 20 | 21 | ## Installation 22 | 23 | 1. **Clone the repository:** 24 | 25 | ```bash 26 | git clone https://github.com/your-username/load-balancer.git 27 | 28 | 29 | 2. **Install dependencies:** 30 | 31 | ```bash 32 | cd load-balancer 33 | npm install 34 | 35 | 36 | ## Usage 37 | 38 | 1. **Configure the load balancer using the CLI:** 39 | 40 | ```bash 41 | start-lb 42 | 43 | The CLI tool prompts the user for essential details, including: 44 | 45 | - Number of servers 46 | - Server URLs 47 | - Health check endpoint 48 | - Health check period 49 | - This configuration ensures the load balancer has accurate information about available servers and their health. 50 | 51 | 52 | 2. **Send requests to the load balancer and observe the distributed load among servers.** 53 | 54 | ## Technical Details 55 | 56 | ### Round-Robin Load Balancing 57 | The load balancer utilizes a round-robin algorithm to evenly distribute incoming requests among the available servers. This ensures each server gets an equal share of the load, preventing any single server from becoming a bottleneck. 58 | 59 | ### Health Checks 60 | Periodic health checks are performed on each server to verify its availability. If a server fails a health check, it is temporarily removed from the rotation, preventing the load balancer from directing requests to an unhealthy server. 61 | 62 | ### Logging 63 | The load balancer logs every incoming request and any encountered errors. This logging mechanism aids in monitoring and troubleshooting the system. 64 | 65 | ## Contributing 66 | Contributions are welcome! Feel free to open issues or submit pull requests. 67 | 68 | ## License 69 | This project is licensed under the MIT. License. 70 | Feel free to customize this further based on your specific implementation details and preferences. 71 | 72 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | const app = express(); 3 | import axios from "axios"; 4 | import logger from "./logServer.js"; 5 | import cron from "node-cron"; 6 | import chalk from "chalk"; 7 | import Table from 'cli-table3' 8 | var table = new Table({ head: [ chalk.white("Time"), chalk.blue("Total Servers"), chalk.green("Healthy Servers"), "Dead Servers"] }); 9 | 10 | let healthyServers = []; 11 | 12 | let current = -1; 13 | 14 | const changeServer = () => { 15 | if (healthyServers.length <= 0) { 16 | return null; // Means all servers are dead 17 | } 18 | current = (current + 1) % healthyServers.length; 19 | return current; 20 | }; 21 | 22 | 23 | const makeRequestToServer = async (req, res) => { 24 | try { 25 | const { data } = await axios({ 26 | method: req.method, 27 | url: `${healthyServers[current]}${req.originalUrl}`, 28 | }); 29 | return res.status(200).json({ 30 | success: true, 31 | data, 32 | }); 33 | } catch (error) { 34 | res.status(500).json({ 35 | success: false, 36 | error: error.message, 37 | }); 38 | } 39 | }; 40 | 41 | const handleRequest = async (req, res) => { 42 | try { 43 | logger.info("Handling request"); 44 | logger.info( 45 | `Received request from ${req.ip}\nHost: ${ 46 | req.hostname 47 | }\nUser-Agent: ${req.get("User-Agent")}` 48 | ); 49 | 50 | const currentServer = changeServer(); 51 | 52 | if (currentServer===null) { 53 | return res.status(500).json({ 54 | success: false, 55 | error: "All Servers are dead !!!", 56 | message: 57 | "If you are a developer, ensure that you have provided the correct URLs in the load balancer configuration.", 58 | }); 59 | } 60 | return makeRequestToServer(req, res); 61 | } catch (error) { 62 | res.status(500).json({ 63 | success: false, 64 | error: error.message, 65 | }); 66 | } 67 | }; 68 | 69 | const healthCheck = async () => { 70 | try { 71 | console.log(chalk.blue(`----- Health check run at every ${healthCheckPeriod} seconds -----`)); 72 | for (let i = 1; i <= servers.size; i++) { 73 | const curr = servers.get(i); 74 | try { 75 | const res = await axios.get(`${curr}${healthCheckEndopint}`); 76 | 77 | const index = healthyServers.indexOf(curr); 78 | if (index < 0) healthyServers.push(curr); 79 | } catch (error) { 80 | // console.log(error); 81 | const index = healthyServers.indexOf(curr) 82 | index > -1 && healthyServers.splice(index, 1); 83 | logger.error( 84 | `healthCheckError - > serverNumber -> ${current} , errorMessage: ${error.message}` 85 | ); 86 | } 87 | } 88 | 89 | const healthyServersCount = healthyServers.length ; 90 | const deadServersCount = servers.size - healthyServers.length; 91 | 92 | table.splice(0, table.length); 93 | table.push( 94 | [new Date().toTimeString(), servers.size, healthyServersCount, deadServersCount] 95 | ); 96 | 97 | console.log(table.toString()); 98 | } catch (error) { 99 | console.log(error); 100 | } 101 | }; 102 | 103 | 104 | app.get("/favicon.ico", (req, res) => { 105 | res.status(204).end(); 106 | }); 107 | 108 | app.all("*", (req, res) => { 109 | handleRequest(req, res); 110 | }); 111 | 112 | 113 | 114 | const startServer = () => { 115 | const PORT = 4000; 116 | app.listen(PORT, () => { 117 | for (const [key, value] of servers) { 118 | healthyServers.push(value); 119 | } 120 | 121 | console.log(`Load Balancer is running on port: ${PORT}`); 122 | const helthCheckCronJob = cron.schedule(`*/${healthCheckPeriod} * * * * *`, () => { 123 | healthCheck(); 124 | }); 125 | }); 126 | }; 127 | 128 | export default startServer; 129 | --------------------------------------------------------------------------------