Amazon SQS 触发 AWS Lambda 及重试/DLQ

Amazon 在 2018 年 6 月份宣布可以设置用 SQS 来触发 Lambda,SQS 不再是单纯用于 ECS 服务中,或用于伸缩控制的。这儿就来亲自尝试一下用 SQS 驱动的 Lambda,以及要注意的要素。

首先使用 Java 编写 Lambda 的话,AWS 在 com.amazonaws:aws-lambda-java-events:2.20 版本开始加入了 com.amazonaws.services.lambda.runtime.events.SQSEvent 类,可是这个版本的 aws-lambda-java-events 是有所限的,因为 SQSEvent.SQSMessage 类是私有的,这就造成不能获取到 SQSEvent 中的记录数据。

//下面的操作代码无法编译,因为 SQSEvent.SQSMessage 是私有的,不可访问
SQSEvent.SQSMessage sqs = sqsEvent.getRecords().get(0); 
sqsEvent.getRecords().get(0).getBody();

Java 使用 SQS 来驱动 Lambda 的话,至少需要 com.amazonaws:aws-lambda-java-events:2.2.1 版本,从此 SQSEvent.SQSMessage 变成 public 了。该版本是于  2018 年 6 月传到 Maven 官方中央仓库的,这就是那时才能真正用来写 Java 的 SQS 触发的 Lambda.

同时此篇也是作为上文 AWS Lambda 重试与死信队列(DLQ) 的一个很重要的补充。在此也会验证 SQS 触发的 Lambda 的重试机制以及 DLQ 相关的内容。

创建两个  SQS 队列

test-sqs-queue 用于触发 Lambda

test-sqs-dlq-queue 不能被 Lambda  触发的消息希望达到最大重试次数后转到该死信队列中去

Lambda 代码

 1public class Handler implements RequestHandler<SQSEvent, Object> {
 2
 3    @Override
 4    public Object handleRequest(SQSEvent sqsEvent, Context context) {
 5        try {
 6            System.out.println("received: " + ObjectMapperSingleton.getObjectMapper().writeValueAsString(sqsEvent));
 7
 8            process(sqsEvent);
 9        } catch (Exception ex) {
10            throw ex;
11        }
12        return null;
13    }
14
15    private void process(SQSEvent sqsEvent) {
16        SQSEvent.SQSMessage sqs = sqsEvent.getRecords().get(0);
17        if(sqs.getBody().contains("dlq")) {
18            throw new RuntimeException("dlq");
19        }
20    }
21}

想法是希望在处理 SQS 中的消息时,如果消息体中含有 dlq 字符串就抛出异常,重试若干次后转入到 test-sqs-dlq-queue 中去。

部署 Lambda

打成可部署的 Lambda jar 包自是不必说,主要强调的是关于执行 Lambda role 和 SQS 的超时设置

该 Lambda 如果设置由 test-sqs-queue 来触发(可以设置 batch size 大小),那么执行该 Lambda 的 role 必须要有针对 SQS 主题 test-sqs-queue 的以下三个权限

sqs:ReceiveMessage     //Lambda 由 SNS 触发是不需要特别的 SNS 相关权限
sqs:DeleteMessage       //Lambda 执行成功后会删除处理过的消息
sqs:GetQueueAttributes

并且该 SQS 主题 test-sqs-queue 的超时 Default Visibility Timeout 不得小于该 Lambda 的 Timeout 设置。因为不希望 Lambda 正在处理消息,还没有 Timeout 之前消息早已回到了源 SQS 队列去了,会造成一条消息被成功处理多次。

同时设置该 Lambda 的 DLQ 为 test-sqs-dlq-queue。下面的测试实际可看到由  SQS 触发的 Lambda,设置 DLQ 是没有意义的。

测试用 SQS 驱动 Lambda

我们往 test-sqs-queue 中放入一条消息,内容为 hello dlq exception, 使得 Lambda 抛出异常。我们立马可以看到 Lambda 被 SQS 触发,并且每次执行都抛出异常,而且该消息始终无法从队列 test-sqs-queue 中清除掉。

连续观察一段时间,看到该 Lambda 每隔一分钟(由于 test-sqs-queue 设置的 Default Visibility Timeout 是 60 秒)重试一次,永不停歇。因此即例设置了该 Lambda 的 DLQ, 该消息都没有机会送入到 Lambda 的 DLQ 中去。

每分钟重试一次是因为 test-sqs-queue 设置的 Default Visibility Timeout 是 60 秒,所以 Lambda 取出消息(Message In Flight),处理出现异常,不能删除该消息,一分钟后该消息又变为  Available。再取出消息处理,异常,消息回队列,周而复始,每次在消息在外面呆一分钟后又回队。

如果设置 test-sqs-queue 的 Default Visibility Timeout 为 2 分钟,那么就是每 2 分钟重试一次,一直持续下去。

我们也可以测试一个能正常处理的逻辑,把 test-sqs-quque 清空掉,并发一条内容为  hello 的消息,我们会发现 Lambda  处理完马上该消息从队列中消失了。

SQS 驱动的 Lambda 的重试与 DLQ 设置

用 SQS 驱动的 Lambda 仍然可以为 Lambda 设置 DLQ,但是这个 DLQ 设置是无效的了,并且是一个陷阱,如果没有为源 SQS 队列设置 DLQ 的情况下,该 Lambda 的 DLQ 设置会造成无限重试,直到消息失效为止, 默认为 4  天,这期间如果是按默认的 Default Visibility Time 30 秒重试一次,那么 Lambda 被调用的费用也相当可观。

所以一定要注意:使用 SQS 来驱动 Lambda 的话,千万不要设置 Lambda 的 DLQ,而应当设置源 SQS 队列的 DLQ。

如下图

针对源 SQS 队列 test-sqs-queue, 设置重试 3 次后消息转移到 DLQ(Dead Letter Queue) 队列 test-sqs-dlq-queue, 从此该消息不再触发 Lambda 了,后续自行额外处理。

SQS 触发器的 Batch Size

SQS 的触发器也有像 Kinesis 触发器那样的 Batch Size, 它们的含义是类似的,也是决定了 sqsEvent.getRecords() 中最大的记录数。至于并发的 Lambda 实例数目尚不清楚。

现在我们可进步测试 Lambda 取到 SQS 多条记录时,抛出异常时怎么处理消息的。为此我们把上面的 process 方法改动一下

1    private void process(SQSEvent sqsEvent) {
2        if(sqsEvent.getRecords().stream().anyMatch(sqs->sqs.getBody().contains("dlq"))) {
3            throw new RuntimeException("dlq");
4        }
5    }

Batch 中只要含有一条消息有 dlq 字样的就抛出异常,然后设置 SQS 触发器的  Batch Size 为 10, 再往队列 test-sqs-queue 放入以下 16 条消息

a, dlq, b, dlq, c, dlq, d, dlq, e, dlq, f, dlq, f, dlq, g, dlq

SQS 触发的 Lambda 处理 SQS 消息的机制是,如果 Lambda 能正常处理所有取到的消息后,就把它们从队列中全部删除,如果有任意的异常发生,那么它们全部重新归队。其中某个消息达到最大的重试次数就进到所设置 SQS DLQ 中去,这很容易棒杀正常的消息。

比如按照上面的 test-sqs-queue 的 DLQ 设置,并且 SQS 触发器的 Batch Size 为 2 的情况下

  1. 第一轮:(a, dlq), (b, dlq), (c), (dlq, d), e        :  搭上 dlq 的第 1, 2, 4 组消息会失败,所以它们又回到 test-sqs-queue 队列
  2. 第二轮:(a), (dlq, b), (dlq, dlq):                      : 含有 dlq 的第  2, 3 组消息会失败,它们又回 test-sqs-queue 队列中去
  3. 第三轮:  (dlq, b), (dlq, dlq):                              : 所有消息都要回到 test-sqs-queue, 因为它们都含有 dlq 字符串

上面最冤枉的莫过于消息 b了,即便是良民,因为每次没跟对人,最后的下场与坏人没有分别,被送到了 DLQ test-sqs-dlq-queue 去了。要是在第一轮中 b 被正常处理过一次或许也无妨,最终 b 跑到了 test-sqs-dlq-queue 也不打紧。

关于 SQS 触发器,Batch Size 是一个值得当心的地方,Batch Size 为 1 自然是最安全的,但效率会是个问题。也许必要时我们可以考虑在处理完 Batch 中的某一条消息后手工从源 SQS 队列中把该条消息删除掉,由 SQSMessage 是能够获得 receiptHandle 的。

习惯性小结一下

对于使用 SQS 来触发 Lambda, 以下几点强调一下

  1. 执行 Lambda 的 role 需要用对于源 SQS 的 ReceiveMessage, DeleteMessage 和  GetQueueAttributes 权限
  2. 源  SQS 的 Default Visibility Timeout 设置不能小于 Lambda 的超时设置
  3. Lambda 会依照源 SQS 的 Default Visibility Timeout 的间隔时间进行重试,直到消息被移除源队列
  4. 不能再使用 Lambda 的 DLQ 设置了,会造成无限重试,应该对源 SQS 队列直接设置 DLQ
  5. SQS 触发器的 Batch Size 需多留意,可能会因为 Batch 中的某条消息产生异常而影响正常的消息处理
永久链接 https://yanbin.blog/amazon-sqs-trigger-aws-lambda/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。