This article was originally published on the public account CoyPan as expected

Writing in the front

Before I know it, I’ve been writing Node.js for a year. Different from the beginning of demo, local tools, etc., in this year, all online businesses are written with Node.js. From the beginning of node.js isomorphism straight out, to the recent Node access layer, it is also a gateway to Node development. Currently, I maintain most of the Node services that have been handed down within the group, including internal systems and online services. The new backend services are also developed using Node as much as possible. This article is some of my recent summary and thinking.

This article won’t go into the specifics of Node.js itself, its architecture, etc. I haven’t written Node extensions or libraries, and I don’t know enough about Node.js.

Why use the Node

For me, and for the team, the reason Node works is simple: it’s fast to develop. Familiar with JS front-end students can quickly get started, cost savings. Choose an HTTP server library to build a server, choose the appropriate middleware, match the request routing, according to the situation of reasonable use of ORM library to link the database, add, delete, change and check.

Application scenarios of Node

Node.js uses an event-driven, non-blocking I/O model, making it lightweight and efficient. This model allows Node.js to avoid waiting for input or output (database, file system, Web server…). CPU time lost due to the response. Therefore, Node.js is suitable for high concurrency, I/O intensive, small business logic scenarios.

Corresponding to the usual specific business, if it is the internal system, most of the only need to add, delete, change and check a database, so the Server is a direct node.js shuttle.

For online services, if the traffic is small and the service logic is simple, the Server can use Node.js completely. For projects with huge traffic and high complexity, Node.js is generally used as the access layer, and the backend students are responsible for realizing the service. The diagram below:

What’s the difference between Node.js development and page development

Developing pages on the browser side is about dealing with and interacting with users, and the browser also provides a variety of Web apis for us to use. Node.js is data-oriented and returns specific data upon receipt of a request. This is the difference in the business path between the two. The real difference is in the business model (a word I made up myself). Let me just draw it.

Every user has a copy of the JS code on their browser when they develop the page. If the code breaks under certain circumstances, it only affects the current user, not other users, and can be restored with a refresh. In Node.js, without enabling multiple processes, all user requests go into the same JS code, and only one thread executes the JS code. If a user’s request causes an error and the Node.js process hangs, the server hangs. Although there may be process daemons and suspended processes will be restarted, errors will be frequently triggered in the case of a large number of user requests. As a result, the server may continue to hang up and restart, affecting user experience.

This is probably the biggest difference between Node.js development and front-end JS development.

Considerations for node.js development

When users access node.js services, if a request is stuck, the service fails to return results, or the service is suspended due to logic errors, it will cause large-scale experience problems. The goal on the server side is to return data quickly and reliably.

The cache

Since Node.js is not good at handling complex logic (JavaScript itself is inefficient), complex logic should be avoided if you use Node.js as an access layer. To process and return data quickly, one crucial point is to use caching.

For example, using Node to do React isomorphism and renderToString is a bit of heavy logic. If the complexity of the page is high, renderToString is executed in its entirety on each request, taking up threads to execute the code for a long time, increasing response time and reducing throughput of the service. This is where caching becomes very important.

The main way to achieve cache: memory cache. You can use Map, WeakMap, WeakRef, etc. Consider the following simple example code:

const cache = new Map(a); router.get('/getContent'.async (req, res) => {
  const id = req.query.id;
  
  // Hit cache
  if(cache.get(id)) {
    return res.send(cache.get(id));
  }
  
  // Request data
  const rsp = await rpc.get(id);
 	// After a complex operation, processing data
  const content = process(rsp);
  // Set the cache
  cache.set(id, content);
  
  return res.send(content);
});
Copy the code

When working with caches, one important question is how the memory cache is updated. The easiest way to do this is to set a timer that periodically deletes the cache and then resets the cache when the next request comes in. Add the following code to the above code:

setTimeout(function() {
  cache.clear();
}, 1000 * 60); // Delete the cache once a minute
Copy the code

If the server is completely implemented by Node, the Node side needs to be directly connected to the database. Under the condition that the data timeliness requirement is not too high and the traffic is not too large, the similar model mentioned above can be used, as shown in the figure below. This reduces database stress and speeds up Node responsiveness.

Also, be aware of the size of the memory cache. If you keep writing new data into the cache, the memory will get bigger and bigger and eventually burst. Consider using the LRU (Least Recently Used) algorithm for caching. Set aside an area of memory for caching. When the cache size reaches the maximum, the oldest unused cache is eliminated.

The memory cache is completely invalidated when the process restarts.

When the background services are complex, the traffic at the access layer is large, and the data volume is large, you can use the following architecture to use an independent memory cache service. The Node access layer directly obtains data from the cache service, and the background service directly updates the cache service.

Of course, the architecture in the figure above is the simplest case, but in reality there are distributed caches and cache consistency issues to consider. That’s a topic for another day.

Error handling

Due to the nature of the Node.js language, Node services are error-prone. If something goes wrong, the effect is that the service is not available. Therefore, error handling is very important.

The most common way to handle errors is to try catch. However, try catch cannot catch asynchronous errors. Asynchronous operations are very common in Node.js, which expose errors in callback functions. Look at an example:

const readFile = function(path) {
	return new Promise((resolve,reject) = > {
		fs.readFile(path, (err, data) = > {
			if(err) { 
				throw err; // Catch cannot catch an error. This is related to Node eventloop.
        // reject(err); // Catch can catch
      }
      resolve(data);
		});
	});
}

router.get('/xxx'.async function(req, res) {
  try {
    const res = await readFile('xxx'); . }catch (e){
    // Catch error handling. res.send(500); }});Copy the code

In the code above, errors thrown from readFile cannot be caught by a catch. If we replace throw err with promise. reject(err), we can catch an error in a catch.

We can Promise all asynchronous operations and use async, try, and catch to handle errors.

But there will always be something left out. At this point, you can use Process to catch global errors and prevent the process from exiting directly, causing subsequent requests to hang. Sample code:

process.on('uncaughtException'.(err) = > {
  console.error(`${err.message}\n${err.stack}`);
});

process.on('unhandledRejection'.(reason, p) = > {
  console.error(`Unhandled Rejection at: Promise ${p} reason: `, reason);
});
Copy the code

For error catching in Node.js, you can also use the Domain module. This module is no longer recommended, and I haven’t used it in projects, so I won’t expand it here. Node.js’s async_hooks module, introduced in recent years, is also experimental and not recommended for use directly in online environments. To improve the efficiency and stability of Node service, you need to do a good job in process daemon, enable multi-process, timely repair error alarms, develop good coding standards and use appropriate frameworks.

Write in the back

This paper summarizes the practice of Node.js development over a year. The development of Node.js is different from the development of front-end web pages, and the focus is different. I haven’t been officially developing Node.js for too long, and I don’t have a deep understanding of some of the points, so this article is just a snapshot of my experience. Welcome to exchange.