AWS Lambda函数对AWS SQS执行5000多个承诺是非常不可靠的

时间:2018-02-06 20:48:53

标签: node.js amazon-web-services async-await aws-lambda

我正在编写一个节点AWS Lambda函数,该函数从我的数据库中查询大约5,000个项目,并通过消息将它们发送到AWS SQS队列。

我的本​​地环境涉及我使用AWS SAM本地运行我的lambda,并使用GoAWS模拟AWS SQS。

我的Lambda的示例骨架是:

async run() {
  try {
    const accounts = await this.getAccountsFromDB();
    const results = await this.writeAccountsIntoQueue(accounts);
    return 'I\'ve written: ' + results + ' messages into SQS';
  } catch (e) {
    console.log('Caught error running job: ');
    console.log(e);
    return e;
  }
}

我的getAccountsFromDB()功能没有任何性能问题,它几乎立即运行,为我提供了5,000个帐户。

我的writeAccountsIntoQueue功能如下:

async writeAccountsIntoQueue(accounts) {
  // Extract the sqsClient and queueUrl from the class 
  const { sqsClient, queueUrl } = this;
  try {
    // Create array of functions to concurrenctly call later
    let promises = accounts.map(acc => async () => await sqsClient.sendMessage({
        QueueUrl: queueUrl,
        MessageBody: JSON.stringify(acc),
        DelaySeconds: 10,
      })
    );

    // Invoke the functions concurrently, using helper function `eachLimit`
    let writtenMessages = await eachLimit(promises, 3);
    return writtenMessages;
  } catch (e) {
    console.log('Error writing accounts into queue');
    console.log(e);
    return e;
  }
}

我的助手,eachLimit看起来像:

async function eachLimit (funcs, limit) {
  let rest = funcs.slice(limit);
  await Promise.all(
    funcs.slice(0, limit).map(
      async (func) => {
        await func();
        while (rest.length) {
          await rest.shift()();
        }
      }
    )
  );
}

据我所知,它应该将并发执行限制为limit

此外,我已经将AWS SDK SQS客户端打包,以返回具有sendMessage功能的对象,该对象如下所示:

sendMessage(params) {
  const { client } = this;
  return new Promise((resolve, reject) => {
    client.sendMessage(params, (err, data) => {
      if (err) {
        console.log('Error sending message');
        console.log(err);
        return reject(err);
      }
      return resolve(data);
    });
  });
}

所以没什么好看的,只是宣传回调。

我的lambda在300秒后设置为超时,并且lambda总是超时,如果它没有突然结束并且错过了一些应该继续的最终记录,这使我它甚至可能在某个地方出错,默默地。当我检查SQS队列时,我丢失了大约1,000个条目。

1 个答案:

答案 0 :(得分:6)

我可以在您的代码中看到一些问题,

首先:

    let promises = accounts.map(acc => async () => await sqsClient.sendMessage({
        QueueUrl: queueUrl,
        MessageBody: JSON.stringify(acc),
        DelaySeconds: 10,
      })
    );

您正在滥用async / await。始终牢记await将等到您的承诺得到解决后再继续下一个承诺,在这种情况下,每当您映射数组promises并调用每个函数项时,它将等待由此包含的承诺继续之前的功能,这很糟糕。由于您只对获得承诺感兴趣,所以您可以这样做:

const promises = accounts.map(acc => () => sqsClient.sendMessage({
       QueueUrl: queueUrl,
       MessageBody: JSON.stringify(acc),
       DelaySeconds: 10,
    })
);

现在,对于第二部分,您的eachLimit实现看起来错误且非常冗长,我在es6-promise-pool的帮助下重构了它以处理并发限制:

const PromisePool = require('es6-promise-pool')

function eachLimit(promiseFuncs, limit) {    
    const promiseProducer = function () {
        while(promiseFuncs.length) {
            const promiseFunc = promiseFuncs.shift();
            return promiseFunc();
        }

        return null;
    }

    const pool = new PromisePool(promiseProducer, limit)
    const poolPromise = pool.start();
    return poolPromise;
}

最后,但非常重要的是,看看SQS Limits,SQS FIFO最高可达300次/秒。由于您正在处理5k项目,您可能会将并发限制提高到5k /(300 + 50),大约为15. 50可以是任何正数,只是为了远离限制。 此外,考虑使用SendMessageBatch,您可以获得更多吞吐量并达到3k发送/秒。

<强> 修改

正如我上面建议的那样,使用sendMessageBatch吞吐量要好得多,因此我重构了代码,将您的承诺映射到支持sendMessageBatch

function chunkArray(myArray, chunk_size){
    var index = 0;
    var arrayLength = myArray.length;
    var tempArray = [];

    for (index = 0; index < arrayLength; index += chunk_size) {
        myChunk = myArray.slice(index, index+chunk_size);
        tempArray.push(myChunk);
    }

    return tempArray;
}

const groupedAccounts = chunkArray(accounts, 10);

const promiseFuncs = groupedAccounts.map(accountsGroup => {
    const messages = accountsGroup.map((acc,i) => {
        return {
            Id: `pos_${i}`,
            MessageBody: JSON.stringify(acc),
            DelaySeconds: 10
        }
    });

    return () => sqsClient.sendMessageBatch({
        Entries: messages,
        QueueUrl: queueUrl
     })
});

然后你可以像往常一样打电话给eachLimit

const result = await eachLimit(promiseFuncs, 3);

现在的差别是每个处理过的承诺都会发送一批大小为n的消息(上例中为10)。