【问题标题】:Lambda function that returns before its background tasks complete在后台任务完成之前返回的 Lambda 函数
【发布时间】:2020-05-28 06:51:48
【问题描述】:

我正在尝试创建一个 Lambda 函数,该函数将在任意数量的 EC2 上重新启动 Apache。每次重新启动操作之间应该有 15 秒的延迟。此 Lambda 将通过 ELB 和 HTTP 请求触发。为避免请求超时,我希望代码在我获得要重新启动 Apache 的实例 ID 后立即返回响应,但到目前为止,它只做一个。这可能吗?

我的代码在下面

'use strict'

const AWS = require('aws-sdk')
const ssm = new AWS.SSM()
const ec2 = new AWS.EC2()
const documentName = 'reloadApache'
// Amount of time to wait in between restarting Apache instances
const waitTime = process.env.WAIT_TIME

// Bind prefix to log levels
console.debug = console.log.bind(null, '[DEBUG]')
console.log = console.log.bind(null, '[LOG]')
console.info = console.info.bind(null, '[INFO]')
console.warn = console.warn.bind(null, '[WARN]')
console.error = console.error.bind(null, '[ERROR]')

/*
 * This is intended to communicate with an Amazon ELB. Request / response from
 * https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html
 *
 * It expects the "event" object to have a member, "path", containing the URL path the function
 * was called from. This path is expected to match either "/apachereload/all" or
 * "/apachereload/<IP>", where <IP> is an IPv4 address in dotted quad (e.g. 10.154.1.1) notation
 */
module.exports.apachereload = async event => {
  const [service, ip] = event.path.split('/').slice(1)

  const ok = {
    isBase64Encoded: false,
    statusCode: 200,
    statusDescription: '200 OK',
    headers: {
      'Set-cookie': 'cookies',
      'Content-Type': 'application/json'
    },
    body: ''
  }

  const badRequest = {
    isBase64Encoded: false,
    statusCode: 400,
    statusDescription: '400 Bad Request',
    headers: {
      'Set-cookie': 'cookies',
      'Content-Type': 'text/html; charset=utf-8'
    },
    body: '<html><head><title>Bad Request</title></head>' +
      '<body><p>Your browser sent a request this server could not understand.</p></body></html>'
  }

  const internalServerError = {
    isBase64Encoded: false,
    statusCode: 500,
    statusDescription: '500 Internal Server Error',
    headers: {
      'Set-cookie': 'cookies',
      'Content-Type': 'text/html; charset=utf-8'
    },
    body: '<html><head><title>It\'s not you, it\'s us</title></head>' +
      '<body><p>Well, this is embarrassing. Looks like there\'s a problem on ' +
      'our end. We\'ll get it sorted ASAP.</p></body></html>'
  }

  const notFound = {
    isBase64Encoded: false,
    statusCode: 404,
    statusDescription: '404 Not Found',
    headers: {
      'Set-cookie': 'cookies',
      'Content-Type': 'text/html; charset=utf-8'
    },
    body: '<html><head><title>Not Found</title></head>' +
      '<body><p>No resources were found matching your query</p></body></html>'
  }

  let response
  console.info('Request ' + event.path)
  // Send 400 back on an unexpected service
  if (service !== 'apachereload') {
    console.info('Rejecting request; invalid service "' + service + '"')
    response = badRequest
  } else if (ip !== 'all' && !ip.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)) {
    console.info('Rejecting request; expected IP address or "all", got "' + ip + '"')
    response = badRequest
  }

  console.log('Event: ' + JSON.stringify(event, null, 2) + '\n')
  console.log('IP: ' + ip + '\n')

  const instanceIds = await getMatchingInstances(ip)

  if (instanceIds.length === 0) {
    response = notFound
    return response
  }

  response = ok
  response.body = JSON.stringify({ instanceIds: instanceIds })
  return Promise.resolve(response).then(reloadInstances(instanceIds))
}

/*
 * Reload Apache on the EC2 instance with the given IP.
 *
 * params:
 * * ip - IP address of the EC2 to reload Apache on. Pass "all" to restart
 *        all EC2s marked as Apache boxen (i.e. with tag "apache" having value "true")
 *
 * returns Promise chaining the AWS EC2 "describe-instances" request and the SSM
 *         "send-command" request
 */
const getMatchingInstances = (ip) => {
  const params = { Filters: [] }
  if (ip === 'all') {
    params.Filters.push({ Name: 'tag:okToRestart', Values: ['true'] })
  } else {
    params.Filters.push({ Name: 'private-ip-address', Values: [ip] })
  }
  console.log('describeInstances params: ' + JSON.stringify(params, null, 2) + '\n')

  /*
   * Retrieve a list of EC2 instance IDs matching the parameters above
   */
  return ec2.describeInstances(params).promise()
    // build a list of instance IDs
    .then((awsResponse) => {
      console.log('describeInstances response: ' + JSON.stringify(awsResponse, null, 2) + '\n')

      const instanceIds = []
      awsResponse.Reservations.forEach((reservation) => {
        reservation.Instances.forEach((instance) => {
          const instanceId = instance.InstanceId
          console.log('Found instance ID ' + instanceId + '\n')
          instanceIds.push(instanceId)
        })
      })
      return instanceIds
    })
}

/*
 * Build a promise chain for each EC2 instance Apache is to be restarted on.
 * For each instance in the list, we will wait process.env.WAIT_TIME ms before going
 * on to the next instance.
 */
const reloadInstances = async instanceIds => {
  return instanceIds.reduce(async (promiseChain, nextInstanceId, i) => {
    await promiseChain
    const waitTime = i < (instanceIds.length - 1) ? process.env.WAIT_TIME : 0
    return restartApacheOnInstance(nextInstanceId)
      .delay(waitTime)
  }, Promise.resolve('INIITAL'))
}

/*
 * restart Apache on one or more EC2 instances. Helper method for apacheReload()
 *
 * params:
 * * instanceIds - array of EC2 instance IDs to send the "restart Apache" command to
 *
 * returns Promise containing AWS SSM "send-command" request for the given instance IDs
 *
 */
const restartApacheOnInstance = async instanceId => {
  console.info('Restart Apache on instance(s) ' + instanceId + '\n')
  const params = {
    DocumentName: documentName,
    InstanceIds: [instanceId],
    TimeoutSeconds: process.env.SSM_SEND_COMMAND_TIMEOUT
  }

  return ssm.sendCommand(params).promise().then(result => {
    console.debug('SSM sendCommand result: ' + JSON.stringify(result, null, 2) + '\n')
    return result
  })
}

/*
 * Returns a promise that waits a given amount of time before resolving
 *
 * Args:
 * * time:  Amount of time, in ms, to wait
 * * value: Optional value that the returned promise will resolve with
 */
// https://stackoverflow.com/questions/39538473/using-settimeout-on-promise-chain
function delay (time, value) {
  console.debug('delay args: ' + JSON.stringify({ time: time, value: value }))
  return new Promise(resolve => {
    console.debug('Wait ' + time + ' ms')
    setTimeout(resolve.bind(null, value), time)
  })
}

/*
 * Attach delay() to all Promises
 */
Promise.prototype.delay = function (time) {
  return this.then(function (value) {
    return delay(time, value)
  })
}

【问题讨论】:

  • 您能解释一下为什么代码“应该在我获得实例 ID 后立即返回响应”(大概是在您重新启动这些 Apache 服务器之前)?
  • 感觉像是很好的做法?它的目的是通过 REST 调用触发,所以我认为快速返回并说“好的,收到请求,我会马上处理它!”

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


【解决方案1】:

这是一个很常见的问题。解决方案是要么在整个过程中使用async/await(无回调),要么根本不使用 async/await。使用 async 时,一旦到达代码末尾,Lambda 就会退出并关闭进程,但如果没有 async,它将等待回调完成。

AWS Lambda Function Handler in Node.js

【讨论】:

  • 没错。如果您需要向调用者返回一个值,您可以使用传入的callback
  • 您不必使用 Lambda 函数的回调参数将结果返回给 Lambda 服务(因此也返回给调用者)。处理程序可以返回一个值或一个承诺(Lambda 将等待)。 aws.amazon.com/blogs/compute/… 的示例。大多数现代 Lambda 函数甚至不会将回调声明为处理程序的参数。
  • @jarmod 是正确的,使用 promises/async 时不需要使用回调。我指的是你不在的时候。
  • @kitpeters,我不确定我是否完全遵循了这一点。您是说要在 next 调用 lambda 时启动一个线程吗?
  • 一种方法是让初始 Lambda 枚举相关实例 ID,将它们排入 SQS,然后立即将实例 ID 列表返回给调用者。随后,SQS 将触发具有一个或多个实例 ID(取决于配置的批量大小)的第二个 Lambda 的一个或多个调用,并且这些 Lambda 调用将为给定的实例 ID 启动 Apache 重启。
【解决方案2】:

要实现这一点,您有两种选择(据我所知)

1- 您需要将重启 EC2 的任务安排到另一个 lambda 函数。因此,REST 调用的 lambda 将收集实例的 id 并触发另一个 lambda 以重新启动 EC2(使用直接异步调用、SNS、SQS、Step Functions 等),然后再响应 REST 调用者。

2-(非 REST)在 API Gateway 中使用 WebSocket API,您可以通过写入 URL 向调用 lambda 的调用者报告进度。

希望对您有所帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-08-25
    • 2017-05-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-25
    相关资源
    最近更新 更多