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 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Handler implements RequestHandler<SQSEvent, Object> { @Override public Object handleRequest(SQSEvent sqsEvent, Context context) { try { System.out.println("received: " + ObjectMapperSingleton.getObjectMapper().writeValueAsString(sqsEvent)); process(sqsEvent); } catch (Exception ex) { throw ex; } return null; } private void process(SQSEvent sqsEvent) { SQSEvent.SQSMessage sqs = sqsEvent.getRecords().get(0); if(sqs.getBody().contains("dlq")) { throw new RuntimeException("dlq"); } } } |
想法是希望在处理 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 2 3 4 5 |
private void process(SQSEvent sqsEvent) { if(sqsEvent.getRecords().stream().anyMatch(sqs->sqs.getBody().contains("dlq"))) { throw new RuntimeException("dlq"); } } |
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 的情况下
- 第一轮:(a, dlq), (b, dlq), (c), (dlq, d), e : 搭上 dlq 的第 1, 2, 4 组消息会失败,所以它们又回到
test-sqs-queue
队列 - 第二轮:(a), (dlq, b), (dlq, dlq): : 含有 dlq 的第 2, 3 组消息会失败,它们又回
test-sqs-queue
队列中去 - 第三轮: (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, 以下几点强调一下
- 执行 Lambda 的 role 需要用对于源 SQS 的 ReceiveMessage, DeleteMessage 和 GetQueueAttributes 权限
- 源 SQS 的 Default Visibility Timeout 设置不能小于 Lambda 的超时设置
- Lambda 会依照源 SQS 的 Default Visibility Timeout 的间隔时间进行重试,直到消息被移除源队列
- 不能再使用 Lambda 的 DLQ 设置了,会造成无限重试,应该对源 SQS 队列直接设置 DLQ
- SQS 触发器的 Batch Size 需多留意,可能会因为 Batch 中的某条消息产生异常而影响正常的消息处理
本文链接 https://yanbin.blog/amazon-sqs-trigger-aws-lambda/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。