如何優化你的 Node.js API
前提條件
想要充分了解本文內容,你必須了解以下概念:
- Node.js 的設置與安裝
- 如何使用 Node 創建 API
- 如何使用 Postman
- JavaScript 的 async/await 工作原理
- Redis 的基礎操作
API 優化到底指的是什么
優化包含了改善 API 的響應時間。響應時間越短,API 的速度越快。
我將在本文分享一些技巧,幫助你縮短響應時間、降低延遲、管理錯誤和吞吐量,并且最大限度地減少 CPU 和內存的使用。
如何優化 Node.js 的 API
1. 始終使用異步函數
異步函數就像 JavaScript 的心臟。因此,優化 CPU 使用率的最佳方法就是編寫異步函數來執行非阻塞 I/O 操作。
I/O 操作包括對數據的讀和寫。它可以在數據庫、云存儲或者任何本地磁盤上進行。
在大量使用 I/O 操作的應用使用異步函數可以提高效率。因為由于沒有阻塞 I/O,當一個請求在做輸入/輸出操作的時候,CPU 可以同時處理多個請求。
舉例如下:
var fs = require('fs');
// 執行阻塞I/O
var file = fs.readFileSync('/etc/passwd');
console.log(file);
// 執行非阻塞I/O
fs.readFile('/etc/passwd', function(err, file) {
if (err) return err;
console.log(file);
});
- 使用 Node 包fs來處理文件
- readFileSync()是同步函數,會在執行完成前阻塞線程
- readFile()是異步函數,會立刻返回并在后臺運行
2. 避免在 API 中使用 session 和 cookie,僅在 API 響應中發送數據
當我們使用 cookie 或者 session 來存儲臨時狀態的時候,會占用非常多的服務器內存。
現在通用無狀態 API,并且也有 JWT、OAuth 等驗證機制。驗證令牌保存在客戶端以便服務器管理狀態。
JWT 是基于 JSON 的用于 API 驗證的安全令牌。JWT 可以被看到,但一旦發送就無法修改。JWT 只是一個序列并沒有加密。OAuth 不是 API 或服務——相反,它是授權的開放標準。OAuth 是一組用于獲取令牌的標準步驟。
同時,也不要把時間浪費在使用 Node.js 來服務靜態文件。這方面 NGINX 和 Apache 做得更好。
使用 Node 搭建 API 的時候,不要在響應中發送完整的 HTML 頁面。當僅有數據通過 API 發送的時候,Node 服務得會更好。大部分 Node 應用都使用 JSON 數據。
3. 優化數據庫查詢
優化 Node API 的重要一環是優化查詢。特別是對于大型應用來說,我們需要多次查詢數據庫,所以一個糟糕的查詢會降低應用的整體性能。
索引是一種優化數據庫性能的方法,通過最小化處理查詢時所需的磁盤訪問次數來實現。它是一種數據結構技術,用于快速定位和訪問數據庫中的數據。索引是使用幾個數據庫列創建的。
假設我們有一個沒有索引的數據庫模式,并且數據庫包含 100 萬條記錄。與帶有索引的模式相比,使用沒有索引的模式做一個簡單的 find(查找)查詢將掃描更多的記錄來找到匹配的記錄。
- 沒有索引的查詢
> 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:
方法 | 掃描文件 |
沒有索引 | 1039 |
有索引 | 1 |
4. 使用 PM2 集群模式優化 API
PM2 是為 Node.js 應用程序設計的生產流程管理器。它內置了負載平衡器,允許應用程序在不修改代碼的情況下,作為多個進程運行。
使用 PM2 時的應用停機時間幾乎為零。總體來說,PM2 確實可以提升 API 性能和并發性。
在生產環境中部署代碼并運行以下命令以查看 PM2 集群如何在所有可用 CPU 上進行擴展:
pm2 start app.js -i 0
5. 減少 TTFB(第一字節時間)
第一字節時間是一種測量方式,用作表示 Web 服務器或者其他網絡資源的響應時間。TTFB 測量從用戶或客戶發出 HTTP 請求到客戶的瀏覽器收到頁面的第一個字節的時間。
所有用戶訪問瀏覽器的同一頁面加載速度不可能在 100 毫秒之內,這僅僅是因為服務器和用戶之間的物理距離。
我們可以通過使用 CDN 和全球本地數據中心緩存內容來減少第一個字節的時間。這有助于用戶以最小的延遲訪問內容。你可以從 Cloudflare 提供的 CDN 解決方案開始著手。
6. 使用帶日志的錯誤腳本
監視 API 是否正常工作最好的辦法是記錄行為,于是記錄日志就派上用場。
一個常見的辦法是將記錄打印在控制臺上(使用console.log())。
比console.log()更高效的方法是使用 Morgan、Buyan 和 Winston。我將在這里以 Winston 為例。
如何使用 Winston 記錄 – 功能
- 支持 4 個可以自由選擇的日志等級,如:info、error、verbose、debug、silly 和 warn
- 支持查詢日志
- 簡單的分析
- 可以使用相同的類型進行多個 transports 輸出
- 捕獲并記錄 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);
}
};
7. 使用 HTTP/2 而不是 HTTP
除了上述使用的這些技巧,我們還可以使用 HTTP/2 而不是 HTTP,因為它具備以下優勢:
- 多路復用
- 頭部壓縮
- 服務器推送
- 二進制格式
它專注提高性能,并解決 HTTP 的問題。它使網頁瀏覽更快、更容易,并且消耗更少的帶寬。
8. 并行任務
使用 async.js 來運行任務。并行任務對 API 的性能有很大改善,它減少了延遲并最大限度地減少了阻塞操作。
并行意味著同時運行多個任務。當你并行任務的時候,不需要控制程序的執行順序。
以下是一個數組異步并行的簡單例子:
const async = require("async");
// 使用對象而不是數組
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);
// 結果相當于: {task2: 2, task1: 1}
});
在以上例子中,我們使用了 async.js 以異步的形式執行了兩個任務。task 1 需要 200 毫秒完成,但是 task 2 不需要等待 task 1 完成后再執行 – 它在設定的 100 毫秒后執行。
并行任務對 API 的性能有很大的影響。它減少了延遲并最大限度地減少了阻塞操作。
9. 使用 Redis 緩存應用
Redis 是 Memcached 的高級版本。它通過在服務器的主內存中存儲和檢索數據來優化 API 響應時間。它提高了數據庫查詢的性能,也減少了訪問延遲。
在下面的代碼片段中,我們分別調用了不使用 Redis 和使用 Redis 的 API,并比較了響應時間。
響應時間差異巨大~ 899.37 毫秒:
方法 | 響應時間 |
不使用 Redis | 900ms |
使用 Redis | 0.621ms |
以下是不使用 Redis 的 Node:
'use strict';
//定義需要的所有依賴項
const express = require('express');
const responseTime = require('response-time')
const axios = require('axios');
//加載 Express 框架
var app = express();
//創建在響應頭中添加 X-Response-Time 的中間件
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 的 Node:
'use strict';
//定義需要的所有依賴項
const express = require('express');
const responseTime = require('response-time')
const axios = require('axios');
const redis = require('redis');
const client = redis.createClient();
//加載 Express 框架
var app = express();
//創建在響應頭中添加 X-Response-Time 的中間件
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;
//設置string-key:緩存中的 isbn。以及緩存的內容: title
// 設置緩存的過期時間為 1 個小時(60分鐘)
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;
//對照服務器的 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 的響應時間。
JavaScript 重度依賴函數,因此,使用異步函數可以使腳本運行得更快并且不阻塞。
除此之外,我們還可以使用緩存記憶(Redis)、數據庫索引、TTFB 和 PM2 集群來提高響應速度。
最后請記住,注意路由的安全性并盡可能優化路由也很重要。我們不能為了提高 API 響應速度而妥協掉安全性。因此,在 Node.js 中構建優化的 API 時,應該保留所有標準安全檢查。