如何优化Node.js API

175 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情

始终使用异步函数

异步函数就像JavaScript的核心。因此,为了优化CPU使用,我们能做的最好的事情就是编写异步函数来执行非阻塞I/O操作。

I/O操作包括执行读写数据操作的进程。它可以是数据库、云存储或任何执行I/O操作的本地存储磁盘。

在大量使用I/O操作的应用程序中使用异步函数将改善它。这是因为由于不阻塞I/O,CPU将能够同时处理多个请求,而其中一个请求正在进行输入/输出操作。

这里有一个例子:

var fs = require('fs');
// Performing a blocking I/O
var file = fs.readFileSync('/etc/passwd');
console.log(file);
// Performing a non-blocking I/O
fs.readFile('/etc/passwd', function(err, file) {
    if (err) return err;
    console.log(file);
});
  • 我们使用fs节点包来处理文件。
  • readFileSync() 是同步的,并阻止执行直到完成。
  • readFile() 是异步的,当东西在后台运行时,会立即返回。

避免API中的会话和Cookie,并在API响应中仅发送数据。

您使用cookie和会话在服务器中存储临时状态。它们对服务器来说要花很多钱。

现在,无状态API很常见,并提供JWT、OAuth和其他身份验证机制。这些身份验证令牌保存在客户端,并保护服务器以管理状态。

JWT是基于JSON的API身份验证安全令牌。可以看到JWT,但一旦发送,它们就无法修改。JWT只是序列化,而不是加密的。OAuth不是一个API或服务,而是授权的开放标准。OAuth是获取令牌的标准步骤集。

此外,不要浪费时间让Node.js服务器服务静态文件。改用NGINX和Apache,因为它们在这方面比Node工作得更好。

在节点中构建API时,不要在API响应中发送完整的HTML页面。当API仅发送数据时,节点服务器工作得更好。一般来说,这种应用程序适用于JSON数据。

优化数据库查询

查询优化是在节点中构建优化API的重要组成部分。特别是在较大的应用程序中,您需要多次查询数据库。因此,错误的查询可能会降低应用程序的整体性能。

索引是一种通过最小化处理查询时所需的磁盘访问次数来优化数据库性能的方法。它是一种数据结构技术,用于快速定位和访问数据库中的数据。使用几个数据库列创建索引。

假设我们有一个没有索引的DB模式,数据库包含100万条记录。与带索引的模式相比,一个简单的查找查询将经过更多的记录来找到匹配的记录。

  • 不带索引的查询:
> db.user.find({email: 'ofan@skyshi.com'}).explain("executionStats")
  • 带有索引的查询:
> db.getCollection("user").createIndex({ "email": 1 }, { "name": "email_1", "unique": true })
{
 "createdCollectionAutomatically" : false,
 "numIndexesBefore" : 1,
 "numIndexesAfter" : 2,
 "ok" : 1
}

扫描的文件数量差异很大~1038:

方法文件扫描
没有索引带有索引
10391

使用PM2集群优化API

PM2是专为Node.js应用程序设计的生产流程管理器。它有一个内置的负载均衡器,允许应用程序作为多个进程运行,而无需修改代码。

使用PM2,应用程序停机时间几乎为零。总体而言,PM2可以真正提高API的性能和并发性。

在生产中部署代码并运行以下命令,以查看PM2集群如何在所有可用CPU上扩展:

pm2 start  app.js -i 0

减少TTFB(时间到第一个字节)

第一个字节的时间是用于指示Web服务器或其他网络资源响应的测量。TTFB测量从用户或客户端发出HTTP请求到客户端浏览器接收页面的第一个字节的持续时间。

所有用户在网页浏览器上访问的页面不太可能在100毫秒内加载。这只是因为服务器和用户之间的物理距离。

在这里,我们可以通过使用CDN和在全球本地数据中心缓存内容来减少到第一个字节的时间。这有助于用户以最小的延迟访问内容。Cloudflare是您可以首先使用的CDN解决方案之一。

将错误脚本与日志记录一起使用

监控API正常运行的最佳方法是跟踪其活动。这就是记录数据发挥作用的地方。

日志记录的一个常见例子是将日志打印到控制台(使用console.log())。

与console.log相比,更高效的日志记录模块是Morgan、Buyan和Winston。在这里,我将以温斯顿为例。

如何使用Winston登录-功能

  • 提供4个自定义级别,我们可以使用,如信息、错误、冗长、调试、愚蠢和警告。
  • 支持查询日志
  • 简单的剖析
  • 您可以使用同一类型的多个传输
  • Catches and logs uncaughtException

您可以使用以下命令设置Winston:

npm install winston --save

以下是Winston用于日志记录的基本配置

const winston = require('winston');

let logger = new winston.Logger({
  transports: [
    new winston.transports.File({
      level: 'verbose',
      timestamp: new Date(),
      filename: 'filelog-verbose.log',
      json: false,
    }),
    new winston.transports.File({
      level: 'error',
      timestamp: new Date(),
      filename: 'filelog-error.log',
      json: false,
    })
  ]
});

logger.stream = {
  write: function(message, encoding) {
    logger.info(message);
  }
};

使用HTTP/2而不是HTTP

除了这些技术外,我们还可以应用一些其他技术,例如通过HTTP使用HTTP/2,因为它具有以下优点:

  • 多路复用
  • 标题压缩
  • 服务器推送
  • 二进制格式

它专注于以前版本的HTTP的性能和问题。它使网页浏览更快、更容易,并消耗更少的带宽。

并行运行任务

使用async.js帮助您运行任务。并行任务对API的性能有很大影响。它减少了延迟,并最大限度地减少了阻塞操作。

并行意味着同时运行多个东西。但是,当您并行运行东西时,您不需要控制程序的执行顺序。

以下是使用与数组并行异步的简单示例:

const async = require("async");
// an example using an object instead of an array
async.parallel({
  task1: function(callback) {
    setTimeout(function() {
      console.log('Task One');
      callback(null, 1);
    }, 200);
  },
  task2: function(callback) {
    setTimeout(function() {
      console.log('Task Two');
      callback(null, 2);
    }, 100);
    }
}, function(err, results) {
  console.log(results);
  // results now equals to: {task2: 2, task1: 1}
});

在本例中,我们使用async.js在异步模式下执行两项任务。任务1需要200毫秒才能完成,但任务2不会等待完成——它以指定的100毫秒延迟执行。

并行化任务对API的性能有很大影响。它减少了延迟,并最大限度地减少了阻塞操作。

使用Redis缓存应用程序

Redis是Memcached的高级版本。它通过存储和检索服务器主内存中的数据来优化API响应时间。它提高了数据库查询的性能,也减少了访问延迟。

在以下代码片段中,我们分别调用了没有Redis和与Redis一起的API,并比较了响应时间。

响应时间存在巨大差异~899.37ms:

方法响应时间
不使用Redis与Redis
900米0.621

这是没有Redis的节点:

'use strict';

//Define all dependencies needed
const express = require('express');
const responseTime = require('response-time')
const axios = require('axios');

//Load Express Framework
var app = express();

//Create a middleware that adds a X-Response-Time header to responses.
app.use(responseTime());

const getBook = (req, res) => {
  let isbn = req.query.isbn;
  let url = `https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}`;
  axios.get(url)
    .then(response => {
      let book = response.data.items
      res.send(book);
    })
    .catch(err => {
      res.send('The book you are looking for is not found !!!');
    });
};

app.get('/book', getBook);

app.listen(3000, function() {
  console.log('Your node is running on port 3000 !!!')
});

这是Redis的节点:

'use strict';

//Define all dependencies needed
const express = require('express');
const responseTime = require('response-time')
const axios = require('axios');
const redis = require('redis');
const client = redis.createClient();

//Load Express Framework
var app = express();

//Create a middleware that adds a X-Response-Time header to responses.
app.use(responseTime());

const getBook = (req, res) => {
  let isbn = req.query.isbn;
  let url = `https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}`;
  return axios.get(url)
    .then(response => {
      let book = response.data.items;
      // Set the string-key:isbn in our cache. With the contents of the cache : title
      // Set cache expiration to 1 hour (60 minutes)
      client.setex(isbn, 3600, JSON.stringify(book));

      res.send(book);
    })
    .catch(err => {
      res.send('The book you are looking for is not found !!!');
    });
};

const getCache = (req, res) => {
  let isbn = req.query.isbn;
  //Check the cache data from the server redis
  client.get(isbn, (err, result) => {
    if (result) {
      res.send(result);
    } else {
      getBook(req, res);
    }
  });
}
app.get('/book', getCache);

app.listen(3000, function() {
  console.log('Your node is running on port 3000 !!!')
)};

结论

在本指南中,我们学习了如何优化Node.js API的响应时间。