【问题标题】:Lambda function taking >3 seconds to run + 5-10 secs warmup each timeLambda 函数运行时间 > 3 秒 + 每次预热 5-10 秒
【发布时间】:2019-06-16 08:59:07
【问题描述】:

我有一个简单的 node.js 函数,其中包含 2 个 REST API 调用和一个托管在 AWS lambda 中的套接字连接输出。它需要 5-10 秒的预热时间和 >3+ 秒的执行时间。

当代码在本地运行时,它会执行两个请求、套接字连接并在大约 1300 毫秒内完成。为什么 AWS 的执行时间增加了一倍以上?我将超时设置为 120 秒,内存设置为 128mb(默认)。

我很欣赏代码不是很整洁;我正在清理它,但暂时需要一些东西。

当被 webhook 订阅调用时,该项目只需通过 API 从 ServiceM8 获取信息,然后将信息格式化为 ZPL 字符串并将它们转发到 tcp 服务器以通过热敏打印机打印。

我的问题是:

  1. 是我的代码瓶颈吗?
  2. 能否对其进行优化以更快地运行?
  3. 我是否只需要为我的功能使用一个预热插件来允许热启动?

我的功能:

'use strict';
//Require libraries
var request = require("request");
var net = require('net');

exports.handler = (event, context, callback) => {
    if (event.eventName != 'webhook_subscription') { 
        callback(null, {});
    }


    //Global Variables
    var strAssetUUID;
    var strAssetURL;
    var strFormUUID;
    var strTestDate;
    var strRetestDate;
    var appliancePass = true;
    var strAccessToken;
    var strResponseUUID;

    //Printer Access
    const tcpUrl = 'example.com';
    const tcpPort = 12345;
    var client = new net.Socket();

    //UUID of Appliance Test Form.
    const strTestFormUUID = 'UUID_of_form';

//Begin function

    /**
     * Inspect the `eventArgs.entry` argument to get details of the change that caused the webhook
     * to fire.
     */
    strResponseUUID = event.eventArgs.entry[0].uuid;
    strAccessToken = event.auth.accessToken;

    console.log('Response UUID: ' + strResponseUUID);
    console.log('Access Token: ' + strAccessToken);

    //URL Options for FormResponse UUID query
    const urlFormResponse = {  
        url: 'https://api.servicem8.com/api_1.0/formresponse.json?%24filter=uuid%20eq%20' + strResponseUUID,
        headers: {
            // Use the temporary Access Token that was issued for this event
            'Authorization': 'Bearer ' + strAccessToken
        }
    };

//Query form Response UUID to get information required.
request.get(urlFormResponse, function(err, res, body) {
    //Check response code from API query
    if (res.statusCode != 200) {
        // Unable to query form response records
        callback(null, {err: "Unable to query form response records, received HTTP " + res.statusCode + "\n\n" + body});
        return;
        }
    //If we do recieve a 200 status code, begin 
    var arrRecords = JSON.parse(body);

    //Store the UUID of the form used for the form response.
    strFormUUID = arrRecords[0].form_uuid;
    console.log('Form UUID: ' + strFormUUID);

    //Store the UUID of the asset the form response relates to.
    strAssetUUID = arrRecords[0].asset_uuid;
    console.log('Asset UUID: ' + strAssetUUID);

    if (strFormUUID == strTestFormUUID){
            //Get the edited date and parse it into a JSON date object.
            var strEditDate = new Date(arrRecords[0].edit_date);
            //Reassemble JSON date to dd-mm-yyyy.
            strTestDate = strEditDate.getDate() + '/' + (strEditDate.getMonth() + 1) + '/' + strEditDate.getFullYear();
            //Extract the response for retest period.
            var strRetestAnswer = JSON.parse(arrRecords[0].field_data);
            strRetestAnswer = strRetestAnswer[0].Response;
            //Appropriate function based on retest response.
            switch(strRetestAnswer) {
                case '3 Months':
                    //Add x months to current test date object
                    strEditDate.setMonth(strEditDate.getMonth() + 3);
                    strRetestDate = strEditDate.getDate() + '/' + (strEditDate.getMonth() + 1) + '/' + strEditDate.getFullYear();
                    break;
                case '6 Months':
                    strEditDate.setMonth(strEditDate.getMonth() + 6);
                    strRetestDate = strEditDate.getDate() + '/' + (strEditDate.getMonth() + 1) + '/' + strEditDate.getFullYear();
                    break;
                case '12 Months':
                    strEditDate.setMonth(strEditDate.getMonth() + 12);
                    strRetestDate = strEditDate.getDate() + '/' + (strEditDate.getMonth() + 1) + '/' + strEditDate.getFullYear();
                    break;
                case '2 Years':
                    strEditDate.setMonth(strEditDate.getMonth() + 24);
                    strRetestDate = strEditDate.getDate() + '/' + (strEditDate.getMonth() + 1) + '/' + strEditDate.getFullYear();
                    break;
                case '5 Years':
                    strEditDate.setMonth(strEditDate.getMonth() + 60);
                    strRetestDate = strEditDate.getDate() + '/' + (strEditDate.getMonth() + 1) + '/' + strEditDate.getFullYear();
                    break;
                default:
                    strRetestDate = "FAIL";
                    appliancePass = false;
            }

            console.log('Appliance Pass: ' + appliancePass);
            console.log('Test Date: ' + strTestDate);
            console.log('Retest Period: ' + strRetestAnswer);
            console.log('Retest Date: ' + strRetestDate);

            //URL Options for Asset UUID query
            const urlAssetResponse = {
            url: 'https://api.servicem8.com/api_1.0/asset/' + strAssetUUID + '.json',
            headers: {
                // Use the temporary Access Token that was issued for this event
                'Authorization': 'Bearer ' + strAccessToken
                }
            };

            //Query the api for the asset URL of the provided asset UUID.
            request.get(urlAssetResponse, function(err, res, body) {
                //Check response code from API query
                if (res.statusCode != 200) {
                    // Unable to query asset records
                    callback(null, {err: "Unable to query asset records, received HTTP " + res.statusCode + "\n\n" + body});
                    return;
                }
                //If we do recieve a 200 status code, begin 
                var strAssetResponse = JSON.parse(body);
                //Store the asset URL
                strAssetURL = 'https://sm8.io/' + strAssetResponse.asset_code;
                console.log('Asset URL: ' + strAssetURL);
            //generate tag and send to printer
            var strZPLPass = ('^XA....^XZ\n');
            var strZPLFail = ('^XA....^XZ\n');
                            //Now that we have our ZPL generated from our dates and URLs
                            //Send the correct ZPL to the printer.
                            client.connect(tcpPort, tcpUrl, function() {
                                console.log('Connected');
                                //Send Appropriate ZPL
                                if (appliancePass) {
                                    client.write(strZPLPass);
                                }else {
                                    client.write(strZPLFail);
                                }
                                console.log('Tag Successfully Printed!');
                                //As the tcp server receiving the string does not return any communication
                                //there is no way to know when the data has been succesfully received in full.
                                //So we simply timeout the connection after 750ms which is generally long enough
                                //to ensure complete transmission.
                                setTimeout(function () {
                                    console.log('Timeout, connection closing...');
                                    client.destroy();
                                    }, 750);
                                });
            });
    }
});
};

【问题讨论】:

  • 您是否尝试过使用更多预置内存运行它?在这种情况下,性能有何变化?

标签: node.js json rest sockets aws-lambda


【解决方案1】:

首先,我建议你停止使用request 模块并切换到本机。如今,一切都可以在没有大量线路的情况下完成。 request 是一个有 48 个依赖的模块;如果你算一算,一个简单的 GET 请求需要数千行。

您应该始终尽量减少依赖项的复杂性。我使用 Lambda 来检查我的站点的运行状况,获取整个请求并检查 完全不同的服务器上的 HTML。 VPS 位于爱尔兰的 AWS 法兰克福。我的 ms/request 介于 100~150 ms 之间。

这是我正在使用的一个简单的承诺请求:

function request(obj, timeout) {
    return new Promise(function(res, rej) {
        if (typeof obj !== "object") {
            rej("Argument must be a valid http request options object")
        }
        obj.timeout = timeout;
        obj.rejectUnauthorized = false;
        let request = http.get(obj, (response) => {
            if (response.statusCode !== 200) {
                rej("Connection error");
            }
            var body = '';
            response.on('data', (chunk) => {
                body += chunk;
            });
            response.on('end', () => {
                res(body);
            });
            response.on('error', (error) => {
                rej(error);
            });
        });

        request.setTimeout(timeout);
        request.on('error', (error) => {
            rej(error);
        })
        request.on('timeout', () => {
            request.abort();
            rej("Timeout!")
        })
    });
}

例子

const reqOpts = {
    hostname: 'www.example.com',
    port: 443,
    path: '/hello',
    method: 'GET',
    headers: {
        handshake: "eXTNxFMxQL4pRrj6JfzQycn3obHL",
        remoteIpAddress: event.sourceIp || "lambda"
    }
}
try {
    httpTestCall = await request(reqOpts, 250);
}
catch (e) {
    console.error(e);
}

现在基于该更改,使用exports.handler = async(event, context, callback) => {} 将您的处理程序切换为异步,并使用控制台测量每个请求的执行时间,使用console.time()console.timeEnd() 处理您的请求或任何内容。从那里您可以看到使用 Cloudwatch 日志降低您的代码的原因。这是基于您的代码的另一个示例:

let reqOpts = {
  hostname: 'api.servicem8.com',
  port: 443,
  path: '/api_1.0/formresponse.json?%24filter=uuid%20eq%20' + strResponseUUID,
  method: 'GET',
  headers: {
    // Use the temporary Access Token that was issued for this event
    'Authorization': 'Bearer ' + strAccessToken
}
}

console.time("=========MEASURE_servicem8=========")
let error = null;
await request(reqOpts, 5555).catch((e)=>{
  error = e;
})
console.timerEnd("=========MEASURE_servicem8=========")
if (error){
  callback(null, {err: "Unable to query form response records, received HTTP" + error}); /* or anything similar */ 
}

参考
https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html
https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html

【讨论】:

  • 首先,非常感谢如此详细的回复。谢谢你。其次,我很高兴地不知道请求包含的依赖项的数量。哇。我将使用您的示例重建我的代码,并在测试后报告。我唯一担心的是,更改为异步处理程序是否有任何好处,因为此脚本一次只调用一次,相隔几分钟,并且脚本中的每个请求都依赖于从前一个请求中提取的值,所以我可以不要同时运行它们。
  • @Jasonson 你是什么意思?如果你执行一个 Lambda 函数 10 次,AWS 将生成 10 个不同的容器,所以基本上你不能在你的应用程序或任何东西的任何地方存储值。现在,如果你不想切换到异步,你可以像下面的例子一样“链接”你的承诺:request(args_here).then((res,rej)=>{ code_here request(args_here).then((res,rej)=>{ code_here }) })
  • 我一定是错误地实现了你的代码,因为现在我的函数什么都不做。更改为您的代码并更改为异步后,我什至无法登录到控制台,servicem8发送到功能的数据event.eventArgs.entry[0].uuid
  • @Jasonson 好奇怪,你用的是最新的 Node.js 版本吗?
【解决方案2】:

aws lambda 本质上并不快(在撰写此答案时)。启动时间无法保证,而且已知会很高。

如果您需要性能 - 您将无法通过这种方式获得它。

【讨论】:

  • 这太糟糕了。该功能与 servicem8 托管在同一台服务器 us-west-1 上,因此我认为请求将以不错的速度运行。我能找到的所有信息都表明,如果我经常点击 lambda,它会保持热状态并避免冷启动时间。但是,我的实际用例每 3-4 分钟就会触发一次。我可能会研究一个加热插件来保持我的功能热。
  • 或者使这个函数成为你服务器的标准 API。您还将获得调试由您的...“函数”运行的代码的能力。
  • 我目前没有服务器。这就是我在 aws 上托管该功能的原因。我正在研究在树莓派上使用此功能构建一个基本节点服务器。
  • 你不会从中得到好的表现。如果你正在学习,好的。但除此之外 - 使用真正的服务器是个好主意。 DigitalOcean 相当便宜,您可能想看看它们。
猜你喜欢
  • 2018-05-17
  • 1970-01-01
  • 1970-01-01
  • 2021-05-10
  • 1970-01-01
  • 2012-01-09
  • 2022-01-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多