使用 Azure NodeJS Functions 下载 100,000 个平均 50kB 文件的最快方法是什么?
What's the fastest way to download 100,000 files of 50kB average using Azure NodeJS Functions?
我正在编写一个备份程序,它应该复制所有客户在白天上传的所有文件并将它们存储在 Azure Blob 存储中。
我正在使用函数链接模式来支持一个又一个客户。我这样做是为了将每个客户的日志分组,但我想我也可以使用扇出扇入模式。
这是我的代码结构:
- 触发函数获取所有客户的列表并将其发送到协调器函数
- 对于每个帐户,编排器调用一个子编排器函数,该函数
- 获取要备份的文件 ID 列表
- 将这些文件以 250 个为一组进行分组,然后调用 DownloadFiles 函数将它们下载并上传到 blob 存储。 (我将文件分组以避免达到 5 分钟的限制,但对于一批,我仍然可以重复使用相同的 HTTP 代理)
这是我的 DownloadFiles 函数:
const request = require('request');
const { getContainerURL, uploadStreamToBlob } = require('../blob-storage');
const log = require('../log');
function downloadFile(options, containerURL, id) {
return new Promise((resolve, reject) => {
request
.get(options)
.on('error', err => reject(err))
.on('response', async (res) => {
if (res.statusCode === 404) reject('HTTP 404');
else if (res.statusCode !== 200) {
reject(new Error(`got response code ${res.statusCode} for file ID ${id}\n${res.statusMessage}`));
} else {
try {
await uploadStreamToBlob(containerURL, `photos/${id}`, res);
} catch (err) {
reject(new Error(`Cannot upload fileID to blob: ${id}`));
}
resolve();
}
});
});
}
async function downloadFileWithRetry(options, containerURL, id, retry = false) {
try {
await downloadFile(options, containerURL, id);
return null;
} catch (error) {
if (retry) return downloadFileWithRetry(options, containerURL, id, false);
return error;
}
}
module.exports = async function (context) {
const { containerName, token, slice } = context.bindings.name;
const containerURL = getContainerURL(containerName);
const finalResult = {
nbErrors: 0,
nbFiles: 0,
};
const sliceSize = 8;
for (let i = 0; i < slice.length; i += sliceSize) {
const promises = slice.slice(i, i + sliceSize).map(async (file) => {
const options = {
url: `https://${process.env.HOSTNAME}/download/`,
forever: true, // use the forever agent
qs: { id: file.id, auth: token },
};
return {
file,
err: await downloadFileWithRetry(options, containerURL, file.id, true),
};
});
const results = await Promise.all(promises);
for (let j = 0; j < results.length; j++) {
const res = results[j];
if (!res.err) {
finalResult.nbFiles++;
} else {
log(`Could not download file ID ${res.file.id}`);
log(res.err);
finalResult.nbErrors++;
}
}
}
return finalResult;
};
DownloadFiles 函数下载 250 个文件平均需要 15.7 秒。我可能可以通过将文件数量增加到 500 或 1,000 来提高吞吐量,但由于某些实例需要 250 秒,我恐怕会达到函数的最长持续时间 5 分钟。
有没有更好的方法来解决这个问题?
编辑 我还使用了 https.globalAgent 并将 keepAlive 设置为 true,它比使用永远的请求代理要快得多。
Azure 函数确实有 5 分钟的超时限制,首先它是消费计划的默认值,对于 App Service 计划 v2 默认值为 30 分钟。
根据您的情况,直接方法设置为 functionTimeout
属性 in host.json
。
这里是 timeout duration:
下面是host.json样本:
{
"functionTimeout": "00:05:00"
}
有关 functionTimeout 的更多信息,您可以参考此文档:functionTimeout。
我建议您在函数应用程序中创建两个函数。一种将客户姓名放入队列的功能。另一个由该队列触发的函数。然后每个负载不受5分钟的限制,可以并行化。如果您喜欢 ping 我的方法,我会为您创建一个演示。
我建议您尝试一下,将您的 http 客户端放在函数范围之外,以便在一个函数主机内共享客户端。您可以看到示例 here. Maybe 答案将激励您进一步改进。
但是如果我在队列中插入 1,000 个文件的速度比下载文件的速度还快怎么办?
你的问题需要注意两点。
1) 您 can control 队列的并发执行数。
2) 每封邮件可以有多个文件名。
我将在 // 中有 600 个 DownloadFile 函数实例 运行 并且第 601 个实例将等到其中一个实例完成,这样说是否正确?
您应该区分函数宿主和函数执行。如果您共享 http 客户端,您的负载应该少于 100。参见 here。
我正在编写一个备份程序,它应该复制所有客户在白天上传的所有文件并将它们存储在 Azure Blob 存储中。
我正在使用函数链接模式来支持一个又一个客户。我这样做是为了将每个客户的日志分组,但我想我也可以使用扇出扇入模式。
这是我的代码结构:
- 触发函数获取所有客户的列表并将其发送到协调器函数
- 对于每个帐户,编排器调用一个子编排器函数,该函数
- 获取要备份的文件 ID 列表
- 将这些文件以 250 个为一组进行分组,然后调用 DownloadFiles 函数将它们下载并上传到 blob 存储。 (我将文件分组以避免达到 5 分钟的限制,但对于一批,我仍然可以重复使用相同的 HTTP 代理)
这是我的 DownloadFiles 函数:
const request = require('request');
const { getContainerURL, uploadStreamToBlob } = require('../blob-storage');
const log = require('../log');
function downloadFile(options, containerURL, id) {
return new Promise((resolve, reject) => {
request
.get(options)
.on('error', err => reject(err))
.on('response', async (res) => {
if (res.statusCode === 404) reject('HTTP 404');
else if (res.statusCode !== 200) {
reject(new Error(`got response code ${res.statusCode} for file ID ${id}\n${res.statusMessage}`));
} else {
try {
await uploadStreamToBlob(containerURL, `photos/${id}`, res);
} catch (err) {
reject(new Error(`Cannot upload fileID to blob: ${id}`));
}
resolve();
}
});
});
}
async function downloadFileWithRetry(options, containerURL, id, retry = false) {
try {
await downloadFile(options, containerURL, id);
return null;
} catch (error) {
if (retry) return downloadFileWithRetry(options, containerURL, id, false);
return error;
}
}
module.exports = async function (context) {
const { containerName, token, slice } = context.bindings.name;
const containerURL = getContainerURL(containerName);
const finalResult = {
nbErrors: 0,
nbFiles: 0,
};
const sliceSize = 8;
for (let i = 0; i < slice.length; i += sliceSize) {
const promises = slice.slice(i, i + sliceSize).map(async (file) => {
const options = {
url: `https://${process.env.HOSTNAME}/download/`,
forever: true, // use the forever agent
qs: { id: file.id, auth: token },
};
return {
file,
err: await downloadFileWithRetry(options, containerURL, file.id, true),
};
});
const results = await Promise.all(promises);
for (let j = 0; j < results.length; j++) {
const res = results[j];
if (!res.err) {
finalResult.nbFiles++;
} else {
log(`Could not download file ID ${res.file.id}`);
log(res.err);
finalResult.nbErrors++;
}
}
}
return finalResult;
};
DownloadFiles 函数下载 250 个文件平均需要 15.7 秒。我可能可以通过将文件数量增加到 500 或 1,000 来提高吞吐量,但由于某些实例需要 250 秒,我恐怕会达到函数的最长持续时间 5 分钟。
有没有更好的方法来解决这个问题?
编辑 我还使用了 https.globalAgent 并将 keepAlive 设置为 true,它比使用永远的请求代理要快得多。
Azure 函数确实有 5 分钟的超时限制,首先它是消费计划的默认值,对于 App Service 计划 v2 默认值为 30 分钟。
根据您的情况,直接方法设置为 functionTimeout
属性 in host.json
。
这里是 timeout duration:
下面是host.json样本:
{
"functionTimeout": "00:05:00"
}
有关 functionTimeout 的更多信息,您可以参考此文档:functionTimeout。
我建议您在函数应用程序中创建两个函数。一种将客户姓名放入队列的功能。另一个由该队列触发的函数。然后每个负载不受5分钟的限制,可以并行化。如果您喜欢 ping 我的方法,我会为您创建一个演示。
我建议您尝试一下,将您的 http 客户端放在函数范围之外,以便在一个函数主机内共享客户端。您可以看到示例 here. Maybe
但是如果我在队列中插入 1,000 个文件的速度比下载文件的速度还快怎么办?
你的问题需要注意两点。
1) 您 can control 队列的并发执行数。
2) 每封邮件可以有多个文件名。
我将在 // 中有 600 个 DownloadFile 函数实例 运行 并且第 601 个实例将等到其中一个实例完成,这样说是否正确?
您应该区分函数宿主和函数执行。如果您共享 http 客户端,您的负载应该少于 100。参见 here。