Important Node.js concepts

Naresh ChoudharyNaresh Choudhary |
Cover image for Important Node.js concepts
  • Blocking and Non-Blocking code
  • Concurrency
  • Horizontal Scaling

GitHub Repo -> https://github.com/N-dcool/nodes-concepts

How Node.js event loop works ?

how-nodejs-works.png

  • Imagine you're at a food festival with multiple food stalls. You want to try different dishes, but you don't want to wait in line at one stall until you get your food. Instead, you decide to take a loop around all the stalls, and whenever a dish is ready at any stall, you grab it and move on. This way, you keep moving and trying different dishes without waiting at any single stall for too long.
  • In Node.js, the event loop works similarly. It's like a festival-goer moving between food stalls. The event loop continuously checks if there are any tasks that are ready to be executed (like fetching data from a database or reading a file). If a task is ready, it gets picked up and executed, and then the loop moves on to the next task. This allows Node.js to efficiently handle many tasks without waiting for each one to finish before moving to the next.
  • This non-blocking, asynchronous nature of the event loop is what makes Node.js so efficient in handling many connections or tasks simultaneously, just like efficiently navigating a food festival.

Blocking and Non-Blocking code

  1. Blocking Code : Imagine you're in a queue at a coffee shop. The cashier takes your order, prepares the coffee, and hands it to you. While this is happening, the queue is blocked; no one else can place an order until you get your coffee. In programming, a similar thing happens with blocking code. If a task takes time to complete, it blocks the execution of the rest of the code until it's done.

    example:

    blocking(): string {
        const now = new Date();
        while (new Date().getTime() < now.getTime() + 10000) {
          // ordering coffee in line
        }
        return 'Your coffee is ready (now next customer can be served)!';
    }
    
  2. Non-blocking Code: Now, picture a different scenario where you place your coffee order and get a token. While your coffee is being prepared, you're free to do other things like read a book or check your phone. Others can also place their orders in the meantime. In programming, non-blocking code allows the program to continue with other tasks while waiting for an asynchronous operation to complete.

    example:

     async nonBlocking(): Promise<string> {
        return new Promise(async (resolve) => {
          // Ordering coffee and waiting to get :)
          setTimeout(() => {
            return resolve('Your coffee will get ready its promise (simultaneously multiple client are waiting to get coffee) !');
          }, 10000);
        });
     }
    

Concurrency

Let's take a look at the code snippet that will be our guide through the realm of concurrency:

async promises(): Promise<string[]> {
    const result = [];
    for (let i = 0; i < 10; i++) {
      result.push(await this.sleep());
    }
    return result;
  }

  async promisesParallel(): Promise<string[]> {
    const result = [];
    for (let i = 0; i < 10; i++) {
      result.push(this.sleep());
    }
    return Promise.all(result);
  }

  private async sleep() {
    return new Promise((resolve) => {
      this.logger.log('Start Sleeping');
      setTimeout(() => {
        this.logger.log('End Sleeping');
        resolve('Prosessing done');
      }, 1000);
    });
  }
  1. Understanding promises() The promises() function demonstrates a sequential approach to promises. It awaits the resolution of each asynchronous task before proceeding to the next iteration in the loop. As a result, the "Start Sleeping" and "End Sleeping" logs alternate, reflecting a synchronous execution of tasks. This ensures that the code is non-blocking but may lead to longer overall execution times. concurrency-1.png

  2. Understaing promisesParallel() In contrast, the promisesParallel() function takes a parallel approach. It pushes all promises into an array without waiting for their resolution. Instead, it utilizes Promise.all() to concurrently wait for all promises to resolve. This leads to a faster overall execution time, as evidenced by the logs displaying 10 consecutive "Start Sleeping" followed by 10 consecutive "End Sleeping" . concurrency-parallel.png

    [Important ⚠️] The developer also notes the importance of being careful when executing an unbound array of promises in the application, as they can cause memory overhead and performance issues.

Horizontal scaling

Horizontal scaling involves adding more instances of your application to distribute the load evenly. This contrasts with vertical scaling, where you increase the resources (CPU, RAM) of a single server. Horizontal scaling, particularly in a containerized environment like Docker and orchestrated by Kubernetes, offers flexibility and efficiency.

Here's a simplified guide, how I horizontally scaled:

  1. Docker Setup

    • Create a Dockerfile for your Node.js app.
    • Build a Docker image and push it to a container registry like Docker Hub. pushing-image-to-docker.png
  2. Kubernetes Setup

    • Activate Kubernetes in Docker Desktop.
    • Use Helm to create a Kubernetes chart for your app. deploy-using-helm.png
  3. Deployment and Service Configuration

    • Establish a Kubernetes directory and use Helm to generate YAML files.
    • Create a deployment specifying image, port, and name.
    • Configure a service for external access and load balancing. running-multiple-pod-replicas.png
  4. Testing and Scaling

    • Use kubectl to create local Kubernetes pods for testing.
    • Initiate horizontal scaling with 'kubectl scale,' deploying multiple replicas.
    • Employed autocannon to generate 1000 request at a time to k8s cluster.
    • Monitor load distribution among pods using docker stats for efficient CPU utilization. Load-distribuuted-across-pods.png