使用 TypeScript 和 Node.js開發 Azure Functions 時,你需要了解函式的效能,以及這些效能如何影響函式應用程式的擴展性。 這種理解在設計高效能應用程式時更為重要。 設計、撰寫及配置函式應用程式時,主要需要考慮的因素包括水平擴展、吞吐量效能配置,以及 Node.js專屬的優化。
水平擴充
預設情況下,Azure Functions 會自動監控你應用程式的負載,並根據需要建立更多主機實例 Node.js。 Azure Functions 會針對不同的觸發程序類型,使用內建的臨界值來決定何時新增執行個體,例如 QueueTrigger 的訊息存在時間和佇列大小。 你無法設定這些閾值。 如需詳細資訊,請參閱 Azure Functions 中的事件驅動調整。
為了提升 Node.js 函式應用程式的擴展性,請依序遵循以下方法:
- 從預設設定開始,先用非同步輸入輸出模式優化程式碼。
- 在擴大背景工作處理序計數之前,請先橫向擴增 (增加更多執行個體)。
- 只有在負載測試顯示每個實例上的 CPU 已達飽和後,才增加
FUNCTIONS_WORKER_PROCESS_COUNT。 - 對於 I/O 受限的工作負載,請保守工作者數量,並專注於減少阻塞通話與相依延遲。
- 每次變更後重新測試,監控吞吐量、延遲與錯誤率以驗證改進。
提升吞吐量效能
預設配置適用於大多數 Azure Functions 應用程式。 不過,你可以根據工作負載配置來提升應用程式的吞吐量效能。 第一步是了解你正在執行的工作量類型。
| 工作負載類型 | 功能應用程式特性 | Examples |
|---|---|---|
| I/O 系結 | • 應用程式需要處理多個同時呼叫。 • 應用程式處理大量 I/O 事件,如網路通話及磁碟讀寫。 |
• 網頁 API • 資料庫作業 • 檔案處理 |
| CPU 受限型 | • 應用程式執行長時間運算,例如影像處理。 • 應用程式可進行資料轉換或複雜計算。 |
• 資料處理 • 內容轉換 • 演算法執行 |
由於實際功能工作負載通常是 I/O 和 CPU 限制的混合,請在真實的生產負載下對應用程式進行分析。 以下章節將介紹具體的優化:
非同步/await 模式
Node.js 建立在事件驅動、非阻塞的 I/O 模型上,使其在 I/O 綁定操作中輕量且高效。 對於處理大量 I/O 事件或受 I/O 限制的應用程式,透過適當的非同步/等待模式,可以大幅提升效能:
-
用於
Promise.all()平行運算:當你有多個獨立的非同步操作時,請將它們平行執行,而非順序執行。 - 避免阻塞事件迴圈:不要在非同步函式中使用同步操作。
- 使用串流處理大量資料:使用串流處理大型資料集,而不是將所有資料載入記憶體。
- 實作適當的錯誤處理:使用 try/catch 區塊,並處理遭拒的 Promise。
以下範例示範如何使用 Promise.all() 進行平行非同步作業:
const { app } = require('@azure/functions');
app.http('optimizedHttpTrigger', {
methods: ['GET', 'POST'],
handler: async (request, context) => {
// Good: Using async/await for HTTP calls
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// Good: Parallel async operations
const [user, orders] = await Promise.all([
fetchUser(data.userId),
fetchOrders(data.userId)
]);
return {
jsonBody: { user, orders }
};
}
});
async function fetchUser(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
return await response.json();
}
async function fetchOrders(userId) {
const response = await fetch(`https://api.example.com/orders?userId=${userId}`);
return await response.json();
}
module.exports = async function (context, req) {
// Good: Using async/await for HTTP calls
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// Good: Parallel async operations
const [user, orders] = await Promise.all([
fetchUser(data.userId),
fetchOrders(data.userId)
]);
context.res = {
body: { user, orders }
};
};
async function fetchUser(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
return await response.json();
}
async function fetchOrders(userId) {
const response = await fetch(`https://api.example.com/orders?userId=${userId}`);
return await response.json();
}
使用多語言工作進程
預設情況下,每個 Functions 主機實例都有一個語言工作程序。 你可以透過應用程式 FUNCTIONS_WORKER_PROCESS_COUNT 設定增加每台主機的工作程序數量(最多 10 個)。 Azure Functions 接著嘗試將同時的函式調用平均分配給這些工作者。
Warning
請小心使用 FUNCTIONS_WORKER_PROCESS_COUNT 設定。 在相同執行個體中執行的多個程序可能會導致無法預測的行為,並延長函式載入時間。 如果你用這個設定, 從套件檔執行 可以抵銷這些缺點。
對於受限於 CPU 的應用程式,將語言工作者數量設為等於或高於每個函式應用程式可用的核心數。 欲了解更多,請參閱可用實例 SKU。
受 I/O 限制的應用程式,也可能因將工作處理程序的數量增加到超過可用核心數而受益。 請記住,將工作者數量設定過高可能會影響整體效能,因為需要切換上下文的次數增加。
事件迴圈優化
Node.js 使用單執行緒事件迴圈來處理 I/O 操作。 優化程式碼與事件迴圈的互動對效能至關重要。
保持事件迴圈暢通
執行大量計算的同步迴圈會阻擋事件迴圈,阻止其他調用被處理。 相反地,將大型資料集拆分成批次,並在批次間進行產出控制,使用:setImmediate
async function processDataNonBlocking(data: any[]) {
const batchSize = 100;
for (let i = 0; i < data.length; i += batchSize) {
const batch = data.slice(i, i + batchSize);
await Promise.all(
batch.map(item => processItem(item))
);
// Allow event loop to process other tasks
await new Promise(resolve => setImmediate(resolve));
}
}
監控事件迴圈延遲
請使用以下模式來衡量任務在事件迴圈佇列中等待的時間:
import { performance } from 'perf_hooks';
function measureEventLoopDelay(context: any) {
const start = performance.now();
setImmediate(() => {
const delay = performance.now() - start;
context.log(`Event loop delay: ${delay}ms`);
});
}
記憶體管理
有效的記憶體使用對 Azure Functions 的效能至關重要,尤其是在冷啟動與擴展方面。 優化記憶體使用時請遵循以下一般指引:
- 最小化全域變數:全域範圍中的重物體會影響冷啟動時間。
- 使用連線池:重用資料庫連線和 HTTP 用戶端。
- 清理資源:妥善關閉連接並清理計時器。
import { app, InvocationContext, Timer } from '@azure/functions';
// Good: Connection pooling
class DatabaseManager {
private static instance: DatabaseManager;
private connectionPool: any;
private constructor() {
this.connectionPool = createConnectionPool({
max: 10,
min: 2,
idleTimeoutMillis: 30000
});
}
public static getInstance(): DatabaseManager {
if (!DatabaseManager.instance) {
DatabaseManager.instance = new DatabaseManager();
}
return DatabaseManager.instance;
}
public async query(sql: string): Promise<any> {
const client = await this.connectionPool.connect();
try {
return await client.query(sql);
} finally {
client.release();
}
}
}
async function timerTrigger(myTimer: Timer, context: InvocationContext): Promise<void> {
const db = DatabaseManager.getInstance();
const results = await db.query('SELECT * FROM users');
context.log(`Processed ${results.length} users`);
}
app.timer('timerTrigger', {
schedule: '0 */5 * * * *',
handler: timerTrigger
});
代碼最佳化
你還可以透過打包應用程式、採用懶散載入策略,以及優化資料結構來進一步優化程式碼。
將您的相依性打包
使用像 esbuild、webpack 或 rollup 這類打包工具,將函式應用程式合併成更少的檔案。 打包可以減少冷啟動時執行時載入的檔案數量,這直接提升啟動時間,尤其是對於依賴樹較大的應用程式。 更多資訊請參閱 部署前的套件。
最小化模組載入時間
在全域範圍載入重模組會增加冷啟動時間,因為每次匯入都會在第一次調用前執行。 使用動態匯入來延遲載入,直到模組真正需要時再進行:
async function processData(data: any) {
const { heavyFunction } = await import('heavy-library');
return heavyFunction(data);
}
使用高效的資料結構
當你需要頻繁查找物品時,請用 a Map 代替 Array.find()。 A Map 提供 O(1) 查詢,相較於陣列掃描的 O(n):
const userMap = new Map();
const users = await fetchUsers();
users.forEach(user => userMap.set(user.id, user));
const user = userMap.get(userId); // O(1) lookup
監控與剖析
用 context.log() 來追蹤函式處理器中的執行時序。 若要自訂指標與相依性追蹤,請直接使用 Application Insights Node.js SDK 使用。 欲了解更多資訊,請參閱 追蹤自訂資料。
以下範例記錄執行時間與記憶體使用情況:
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
async function trackedFunction(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
const startTime = Date.now();
const memoryBefore = process.memoryUsage().heapUsed;
try {
const result = await processData(request);
const duration = Date.now() - startTime;
const memoryDelta = (process.memoryUsage().heapUsed - memoryBefore) / 1024 / 1024;
context.log(`Execution time: ${duration}ms, Memory delta: ${memoryDelta.toFixed(2)}MB`);
return { jsonBody: result };
} catch (error) {
context.error(`Function failed after ${Date.now() - startTime}ms`, error);
throw error;
}
}
app.http('trackedFunction', {
methods: ['GET', 'POST'],
handler: trackedFunction
});
遵循這些建議,您可以大幅提升 Node.js Azure Functions 應用程式的效能與可擴展性。