头图

Best practices for using Node.js Express

JerryWang_汪子熙
中文

Production best practices: performance and reliability

This article discusses performance and reliability best practices for Express applications deployed to production.

This topic obviously belongs to the "devops" world, covering traditional development and operations. Therefore, the information is divided into two parts:

Things to do in your code (development part)

  • Use gzip compression
  • Don't use synchronous functions
  • Record correctly
  • Handle exceptions correctly

Things to do in your environment/settings (operation part)

  • Set NODE_ENV to "production"
  • Make sure your app restarts automatically
  • Run your application in a cluster
  • Cache request results
  • Use a load balancer
  • Use reverse proxy

Use gzip compression

Gzip compression can greatly reduce the size of the response body, thereby increasing the speed of Web applications. Use compression for gzip compression in your Express application. E.g:

var compression = require('compression')
var express = require('express')
var app = express()
app.use(compression())

For high-traffic sites in production, the best way to implement compression is to implement it at the reverse proxy level. In this case, you do not need to use compression middleware. For details on enabling gzip compression in Nginx, see the module ngx_http_gzip_module in the Nginx documentation.

Don’t use synchronous functions

Synchronous functions and methods block the executing process before they return. A single call to a synchronous function may return within a few microseconds or milliseconds, but in high-traffic websites, these calls can add up and reduce the performance of the application. Avoid using them in production.

Although Node and many modules provide synchronous and asynchronous versions of their functions, asynchronous versions are always used in production. The only time that the synchronization function can be justified is at the initial startup.

If you are using Node.js 4.0+ or io.js 2.1.0+, you can use the --trace-sync-io command line flag to print warnings and stack traces when your application uses the synchronization API. Of course, you don't want to use it in production, but make sure that your code is ready for production.

Do logging correctly

Generally, there are two reasons for logging from your application: for debugging and recording application activity (essentially everything else). Using console.log() or console.error() to print log messages to the terminal is a common practice in development. But when the target is a terminal or a file, these functions are synchronous, so they are not suitable for production unless you pipe the output to another program.

If you are logging for debugging purposes, then do not use console.log(), but use special debugging modules like debug. This module enables you to use the DEBUG environment variable to control which debug messages are sent to console.error() (if any). In order to keep your application completely asynchronous, you still want to pipe console.error() to another program.

If you want to log application activity (for example, to track traffic or API calls), don't use console.log(), but use log libraries like Winston or Bunyan. For a detailed comparison of these two libraries, see the StrongLoop blog post Comparing Winston and Bunyan Node.js Logging .

Handle exceptions properly

The Node application crashes when it encounters an uncaught exception. Failure to handle the exception and take appropriate measures will cause your Express application to crash and go offline. If you follow the suggestions below to ensure that your application restarts automatically, then your application will recover from the crash. Fortunately, the startup time of Express applications is usually very short. Nevertheless, you must first avoid crashes, and for this, you need to handle exceptions properly.

To ensure that you handle all exceptions, use the following techniques:

  • try-catch
  • promises

Before delving into these topics, you should have a basic understanding of Node/Express error handling: using error-first callbacks, and propagating errors in middleware. Node uses the "error-first callback" convention to return errors from asynchronous functions, where the first parameter of the callback function is the error object, followed by the result data in the parameters. To indicate that there are no errors, pass null as the first parameter. The callback function must follow the error-first callback convention accordingly to handle errors meaningfully. In Express, the best practice is to use the next() function to propagate errors through the middleware chain.

What not to do

One thing you shouldn't do is listen to the uncaughtException event, which is emitted when the exception bubbling all the way back to the event loop. Adding an event listener for uncaughtException will change the default behavior of the process that encounters the exception; the process will continue to run despite the exception. This sounds like a good way to prevent your application from crashing, but continuing to run the application after an uncaught exception is a dangerous practice and it is not recommended because the state of the process becomes unreliable and unpredictable.

In addition, the use of uncaughtException is officially considered crude. So listening for uncaughtException is just a bad idea. This is why we recommend things like multiple processes and supervisors: crashes and restarts are usually the most reliable way to recover from errors.

We also do not recommend using domains. It usually does not solve the problem and is a deprecated module.

Use try-catch

Try-catch is a JavaScript language structure that can be used to catch exceptions in synchronous code. For example, use try-catch to handle JSON parsing errors, as shown below.

Use tools such as JSHint or JSLint to help you find implicit exceptions, such as reference errors on undefined variables.

The following is an example of using try-catch to handle potential process crash exceptions. This middleware function accepts a query field parameter named "params", which is a JSON object.

app.get('/search', (req, res) => {
  // Simulating async operation
  setImmediate(() => {
    var jsonStr = req.query.params
    try {
      var jsonObj = JSON.parse(jsonStr)
      res.send('Success')
    } catch (e) {
      res.status(400).send('Invalid JSON string')
    }
  })
})

However, try-catch only applies to synchronous code. Because the Node platform is mainly asynchronous (especially in a production environment), try-catch will not catch many exceptions.

Use promises

Promise will handle any exceptions (explicit and implicit) in asynchronous code blocks that use then(). Just add .catch(next) to the end of the promise chain. E.g:

app.get('/', (req, res, next) => {
  // do some sync stuff
  queryDb()
    .then((data) => makeCsv(data)) // handle data
    .then((csv) => { /* handle csv */ })
    .catch(next)
})

app.use((err, req, res, next) => {
  // handle error
})

Now all asynchronous and synchronous errors will be propagated to the error middleware.

However, there are two caveats:

  • All asynchronous code must return a promise (except the emitter). If a particular library does not return a promise, use helper functions such as Bluebird.promisifyAll() to convert the base object.
  • Event emitters (such as streams) will still cause uncaught exceptions. Therefore, please ensure that error events are handled correctly; for example:
const wrap = fn => (...args) => fn(...args).catch(args[2])

app.get('/', wrap(async (req, res, next) => {
  const company = await getCompanyById(req.query.id)
  const stream = getLogoStreamById(company.id)
  stream.on('error', next).pipe(res)
}))

The wrap() function is a wrapper that captures the rejected promise and calls next() with the error as the first parameter.

For more details, please refer to this blog: Asynchronous Error Handling in Express with Promises, Generators and ES7 .

Set NODE_ENV to “production”

The NODE_ENV environment variable specifies the environment in which the application runs (usually a development environment or a production environment). To improve performance, one of the simplest things you can do is to set NODE_ENV to "production".

Setting NODE_ENV to "production" makes Express:

  • Cache view template.
  • Cache CSS files generated from CSS extensions.
  • Generates less detailed error messages.

If you need to write environment-specific code, you can use process.env.NODE_ENV to check the value of NODE_ENV. Please note that checking the value of any environment variable will cause performance degradation, so proceed with caution.

In development, you usually set environment variables in an interactive shell, such as using export or .bash_profile files. But generally speaking, you should not do this on a production server; instead, use your operating system's initialization system (systemd or Upstart). The next section provides more detailed information on using the init system, but setting NODE_ENV is very important for performance (and easy to operate), so it is highlighted here.

With Upstart, use the env keyword in your job file. E.g:

# /etc/init/env.conf
 env NODE_ENV=production

Using systemd, use the Environment directive in the unit file. E.g:

# /etc/systemd/system/myservice.service
Environment=NODE_ENV=production

Ensure your app automatically restarts

In production, you never want your application to be offline. This means you need to make sure that it restarts when the application crashes and the server itself crashes. Although you hope that neither of these situations will happen, you actually have to explain both of these situations in the following ways:

  • Use the process manager to restart the application (and node) in the event of a crash.
  • Use the init system provided by the operating system to restart the process manager when the operating system crashes. You can also use the init system without a process manager.

If an uncaught exception is encountered, the node application will crash. The most important thing you need to do is to ensure that your application is well tested and handles all exceptions.

But as a fail-safe measure, a mechanism should be adopted to ensure that when your application crashes, it will automatically restart.

Use a process manager

In development, you only need to use node server.js or something similar to start your application from the command line. But doing so in production will lead to disaster. If the app crashes, it will be offline until you restart it. To ensure that your application restarts when it crashes, use the process manager. The process manager is the "container" of the application that facilitates deployment, provides high availability, and enables you to manage the application at runtime.

In addition to restarting the application when it crashes, the process manager also allows you to:

  • In-depth understanding of runtime performance and resource consumption.
  • Modify settings dynamically to improve performance.
  • Control the cluster (StrongLoop PM and pm2).

Here are three more popular process managers:

  • StrongLoop Process Manager
  • PM2
  • Forever

For a feature-by-feature comparison of the three process managers, please refer to http://strong-pm.io/compare/.

Using any of these process managers is enough to keep your application running, even if it crashes from time to time.

Use an init system

The next level of reliability is to ensure that your application restarts when the server restarts. Due to various reasons, the system may still malfunction. To ensure that your application restarts when the server crashes, use the init system built into the operating system. The two main initialization systems currently in use are systemd and Upstart.

There are two ways to use the init system in Express applications:

  • Run your application in the process manager and install the process manager as a service using the init system. When the application crashes, the process manager will restart your application, and when the operating system restarts, the init system will restart the process manager. This is the recommended method.
  • Run your application (and Node) directly using the init system. It's a bit simple, but you won't get the extra advantage of using the process manager.

Systemd

Systemd is a Linux system and service manager. Most major Linux distributions use systemd as their default initialization system.

The systemd service configuration file is called a unit file, and the file name ends with .service. This is a sample unit file for directly managing Node applications. Replace the value in angle brackets for your system and application:

[Unit]
Description=<Awesome Express App>

[Service]
Type=simple
ExecStart=/usr/local/bin/node </projects/myapp/index.js>
WorkingDirectory=</projects/myapp>

User=nobody
Group=nogroup

# Environment variables:
Environment=NODE_ENV=production

# Allow many incoming connections
LimitNOFILE=infinity

# Allow core dumps for debugging
LimitCORE=infinity

StandardInput=null
StandardOutput=syslog
StandardError=syslog
Restart=always

[Install]
WantedBy=multi-user.target

Run your app in a cluster

In a multi-core system, you can increase the performance of a Node application many times by starting a set of processes. A cluster runs multiple instances of the application, and ideally there is one instance on each CPU core to distribute load and tasks among the instances.

[picture]

Important note: Since application instances run as separate processes, they do not share the same memory space. That is, the object is local to each instance of the application. Therefore, you cannot maintain state in the application code. However, you can use an in-memory data store like Redis to store session-related data and state. This warning basically applies to all forms of horizontal expansion, whether it is a multi-process cluster or a multi-physical server.

In a cluster application, the worker process can crash alone without affecting the rest of the process. In addition to performance advantages, fault isolation is another reason for running application process clusters. Whenever a worker process crashes, always make sure to log the event and use cluster.fork () to spawn a new process.

Using PM2

If you use PM2 to deploy the application, you do not need to modify the application code to take advantage of the cluster. You should first ensure that your application is stateless, which means that no local data is stored in the process (such as sessions, websocket connections, etc.).

When using PM2 to run an application, you can enable cluster mode to run it in a cluster with multiple instances of your choice, such as matching the number of available CPUs on the machine. You can use the pm2 command-line tool to manually change the number of processes in the cluster without stopping the application.

To enable cluster mode, start your application like this:

# Start 4 worker processes
$ pm2 start npm --name my-app -i 4 -- start
# Auto-detect number of available CPUs and start that many worker processes
$ pm2 start npm --name my-app -i max -- start

This can also be configured in the PM2 process file (ecosystem.config.js or similar) by setting exec_mode to cluster and setting the instance to the number of workers to be started.

After running, the application can be scaled like this:

# Add 3 more workers
$ pm2 scale my-app +3
# Scale to a specific number of workers
$ pm2 scale my-app 2

Cache request results

Another strategy for improving performance in production is to cache the results of requests so that your application does not repeat operations to process the same requests repeatedly.

Using a caching server such as Varnish or Nginx (see also Nginx caching) can greatly improve the speed and performance of the application.

Use a load balancer

No matter how optimized the application is, a single instance can only handle limited load and traffic. One way to scale an application is to run multiple instances of it and distribute traffic through a load balancer. Setting up a load balancer can improve the performance and speed of your application and enable it to scale more than a single instance.

The load balancer is usually a reverse proxy , which is used to coordinate traffic in and out of multiple application instances and servers. You can use Nginx or HAProxy to easily set up a load balancer for your application.

With load balancing, you may have to ensure that requests associated with a specific session ID connect to the process that initiated them. This is called an affinity session or sticky session and can be solved by the suggestions above, using a data store such as Redis to store the session data (depending on your application).

Use a reverse proxy

The reverse proxy sits in front of the web application, and in addition to directing the request to the application, it also performs support operations on the request. It can handle error pages, compression, caching, file provisioning, load balancing, etc.

Handing over tasks that do not require knowledge of application state to the reverse proxy frees up Express to perform specialized application tasks. For this reason, it is recommended to use a reverse proxy (such as Nginx or HAProxy) to run Express in production.

阅读 483

Jerry Wang的SAP技术专栏
SAP成都研究院开发专家,SAP社区导师,SAP中国技术大使

Jerry 2007年从电子科技大学计算机专业硕士毕业后进入SAP成都研究院工作至今, SAP社区导师,SAP中国技术...

781 声望
1k 粉丝
0 条评论

Jerry 2007年从电子科技大学计算机专业硕士毕业后进入SAP成都研究院工作至今, SAP社区导师,SAP中国技术...

781 声望
1k 粉丝
文章目录
宣传栏