AWS Lambda 允许设置 Debugging and error handling
, 在 Lambda 出现异常,达到最大的重试次数后,把以下信息放到选择的 SNS 或 SQS 主题作为死信队列(DLQ - Dead Letter Queue),包括
- 原始 Lambda 接收到的消息(基于 SNS 和 SQS 消息的总大小,可能会被截取,本人猜测,尤其是 Kinesis 的消息会比较大)
- 原始 Lambda 的 RequestId
- ErrorCode(三位数字的 HTTP 错误码)
- ErrorMessage, 即原 Lambda 抛出 Exception 的 getMessage() 信息,截取 1 KB 字符串
并且 Lambda 要使用 DLQ 的话还必须设置当前 Lambda 的 IAM role 有对于 SNS/SQS 主题相应的 sns:Publish
和 sqs:SendMessage
权限。
AWS Lambda 基本重试规则:对于 Kinesis 消息会无限重试直至消息过期,对于 SNS 或 SQS 的消息出现异常后会再重试两次。参考:AWS Lambda Retry Behavior。
而在重试次数用完后仍然失败,并且设置了 DLQ 的话就会发送消息到 DLQ 中去。
最感观的理解就是来做一个测试,创建一个 Lambda test-dlq-lambda
, 该 Lambda 由 SNS topic sns-test-topic
触发,并且设置 Lambda 的 DLQ 为另一个 SNS topic sns-test-dlq-topic
。为方便测试,我们用同样的 Lambda 来接收 sns-test-dlq-topic
中的消息验证 DLQ 中的内容。
Lambda 的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class Handler implements RequestHandler<SNSEvent, Object> { @Override public Object handleRequest(SNSEvent snsEvent, Context context) { try { System.out.println("received: " + ObjectMapperSingleton.getObjectMapper().writeValueAsString(snsEvent)); } catch (JsonProcessingException e) { e.printStackTrace(); } process(snsEvent); return null; } private void process(SNSEvent snsEvent) { SNSEvent.SNS sns = snsEvent.getRecords().get(0).getSNS(); //如果消息包含 dlq, 但不是从 dlq 主题中来的消息抛出异常 if(sns.getMessage().contains("dlq") && !sns.getTopicArn().contains("dlq")) { throw new RuntimeException("test dlq"); } } } |
以上代码打包成一个可部署的 Lambda jar 包,并且部署为两个 Lambda
- test-dlq-lambda: 监听 SNS topic
sns-test-topic
, 并且设置该 Lambda 的 DLQ 为 SNS topicsns-test-dlq-topic
- test-dlq-receiver-lambda: 监听 SNS topic
sns-test-dlq-topic
发送消息给 test-dlq-lambda
发送消息的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public static void main(String[] args) { AmazonSNS amazonSNS = AmazonSNSClientBuilder.defaultClient(); MessageAttributeValue messageAttributeValue = new MessageAttributeValue() .withDataType("String").withStringValue("1234"); HashMap<String, MessageAttributeValue> attributeHashMap = new HashMap<>(); attributeHashMap.put("id", messageAttributeValue); PublishRequest publishRequest = new PublishRequest().withMessage("dlq") .withTopicArn("<topic-arn-of-sns-test-topic") .withMessageAttributes(attributeHashMap); PublishResult publishResult = amazonSNS.publish(publishRequest); System.out.println(publishResult.getMessageId()); } |
消息中含有 dql
,所以 test-dlq-lambda
将会触发 RuntimeException
异常,异常消息是 test dlq
。
Lambda test-dlq-lambda
的日志
从以上日志解读到的是
- 三次消费同一消息的时间点为 03:52:38, 03:53:35, 03:55:26,大概是 Lambda 第一次失败后, 一分钟后重试一次,第二次失败两分钟后再重试一次
- 三次 Lambda 执行都是相同的 RequestId:
7029fce0-c52d-11e8-a386-0dc76ede9b27
。 - 最多执行三次(针对 SNS 消息),最后把错误相关的信息送到所设定的 DLQ 中去
- 因为只发送了一条 SNS 消息,所以上面三次执行都是同一个 Lambda 实例,并发高的时候不确定重试是否也是由同一个 Lambda 实例
这里 Lambda 接收到的 SNS 消息是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
received: { "records": [ { "sns": { "messageAttributes": { "id": { "type": "String", "value": "1234" } }, "signingCertUrl": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-xxxxxxxx.pem", "messageId": "35f269ce-7e84-5fe1-8fb0-3de5e2c22231", "message": "dlq", "subject": null, "unsubscribeUrl": "https://sns.us-ea......:sns-test-topic:459bb1e5-3282-4d20-8b9a-24e36b4a3228", "type": "Notification", "signatureVersion": "1", "signature": "fndUv/cWXZQRhrCIKBQxfH+WKg0t4/2jgNO2A6/oktogD2ptI8bLAk9PelAglWarQaLbFi.......Q==", "timestamp": { "year": 2018, "dayOfMonth": 1, //.... 省略 timstamp 的细节 }, "topicArn": "arn:aws:sns:us-east-1:<account-id>:sns-test-topic" }, "eventVersion": "1.0", "eventSource": "aws:sns", "eventSubscriptionArn": "arn:aws:sns:........:sns-test-topic:459bb1e5-3282-4d20-8b9a-24e36b4a3228" } ] } |
DLQ sns-test-dlq-topic
中的消息
我们通过 test-dlq-receiver-lambda
的日志来了解前面 Lambda 三次重试后送到 DLQ 中的内容。它所收到的消息是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
received: { "records": [ { "sns": { "messageAttributes": { "RequestID": { "type": "String", "value": "7029fce0-c52d-11e8-a386-0dc76ede9b27" }, "ErrorCode": { "type": "String", "value": "200" }, "ErrorMessage": { "type": "String", "value": "test dlq" } }, "signingCertUrl": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-xxxxxx.pem", "messageId": "390612c4-ba66-535c-b00a-46ef2688b4b6", "message": "{\"Records\":[{\"EventSource\":\"aws:sns\",\"EventVersion\":\"1.0\",\"Event.....", "subject": null, "unsubscribeUrl": "https://sns.us...:sns-test-dlq-topic:332608d0-0092-408a-9d96-edba86e55685", "type": "Notification", "signatureVersion": "1", "signature": "fRKsL23VkfoJd8sdy0THFkL8tuhWFGC7IPvS1HsV6UVtDp+RSnTMfxd0hlQYb/BWhwm......Mw==", "timestamp": { "year": 2018, "dayOfMonth": 1, //....省略 timestamp 的细节 }, "topicArn": "arn:aws:sns:us-east-1:<account-id>:sns-test-dlq-topic" }, "eventVersion": "1.0", "eventSource": "aws:sns", "eventSubscriptionArn": "arn:aws:。。。。:sns-test-dlq-topic:332608d0-0092-408a-9d96-edba86e55685" } ] } |
sns-test-dlq-topic
中的 SNS 消息条目包含以下关键内容(前面说过,在此重复一遍)
- 原始 Lambda 接收到的完整
sns
记录内容作为新 SNS 消息的message
字段的字符串内容 - 原始 Lambda 的 RequestId,放在新 SNS 消息的
messageAttributes
中,键为RequestID
- ErrorCode(三位数字的 HTTP 错误码),放在新 SNS 消息的
messageAttributes
中,键为ErrorCode
。不知如何设置不同的 ErrorCode。 - ErrorMessage, 即原 Lambda 抛出 Exception 的 getMessage() 信息,截取 1 KB 字符串,也是放在新 SNS 消息的
messageAttributes
中,键为ErrorMessage
从这里发现从 DLQ 中的消息一个能用于追踪原 Lambda 的信息是 RequestId,还有就是 ErrorMessage,不过它只是取到原 Lambda 的异常的 getMessage() 消息,过于简单。但我们可以用 ErrorMessage
携带更有用的错误信息,看接下来
利用 DLQ 的 ErrorMessage
传递异常栈信息
从前面了解到 DLQ 中信息的 ErrorMessage
字段只是取了原 Lambda 异常的 getMessage() 消息,可能并不太助于定位错误点,所以我们也许希望用它来展示完整的异常栈信息。这需要在原始 Lambda 捕获异常后作些文章
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class Handler implements RequestHandler<SNSEvent, Object> { @Override public Object handleRequest(SNSEvent snsEvent, Context context) { try { System.out.println("received: " + ObjectMapperSingleton.getObjectMapper().writeValueAsString(snsEvent)); process(snsEvent); } catch (Exception ex) { StringWriter stringWriter = new StringWriter(); ex.printStackTrace(new PrintWriter(stringWriter)); throw new RuntimeException("Function: " + context.getFunctionName() + "\n" + stringWriter.toString()); } return null; } private void process(SNSEvent snsEvent) { SNSEvent.SNS sns = snsEvent.getRecords().get(0).getSNS(); if(sns.getMessage().contains("dlq") && !sns.getTopicArn().contains("dlq")) { throw new RuntimeException("dlq"); } } } |
把异常栈的信息转换为一个字符串作为新 RuntimeException
的 message, 现在来看 DLQ 中的消息的 ErrorMessage
部分的内容就是
"ErrorMessage": {
"type": "String",
"value": "Function: yanbin-test-dlq\njava.lang.RuntimeException: dlq\n\tat com.serverless.Handler.process(Handler.java:41)\n\tat com.serverless.Handler.handleRequest(Handler.java:29)\n\tat com.serverless.Handler.handleRequest(Handler.java:18)\n\tat lambdainternal.EventHandlerLoader$PojoHandlerAsStreamHandler.handleRequest(EventHandlerLoader.java:178)\n\tat lambdainternal.EventHandlerLoader$2.call(EventHandlerLoader.java:888)\n\tat lambdainternal.AWSLambda.startRuntime(AWSLambda.java:286)\n\tat lambdainternal.AWSLambda.<clinit>(AWSLambda.java:64)\n\tat java.lang.Class.forName0(Native Method)\n\tat java.lang.Class.forName(Class.java:348)\n\tat lambdainternal.LambdaRTEntry.main(LambdaRTEntry.java:94)\n"
}
原 Lambda 函数名也有了,并且带上了完整的异常信息。唯有不足的地方就是原始 Lambda 在输出该异常信息日志时重复了一遍异常栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
Function: yanbin-test-dlq java.lang.RuntimeException: dlq at com.serverless.Handler.process(Handler.java:41) at com.serverless.Handler.handleRequest(Handler.java:29) at com.serverless.Handler.handleRequest(Handler.java:18) at lambdainternal.EventHandlerLoader$PojoHandlerAsStreamHandler.handleRequest(EventHandlerLoader.java:178) at lambdainternal.EventHandlerLoader$2.call(EventHandlerLoader.java:888) at lambdainternal.AWSLambda.startRuntime(AWSLambda.java:286) at lambdainternal.AWSLambda.<clinit>(AWSLambda.java:64) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:348) at lambdainternal.LambdaRTEntry.main(LambdaRTEntry.java:94) : java.lang.RuntimeException java.lang.RuntimeException: Function: yanbin-test-dlq java.lang.RuntimeException: dlq at com.serverless.Handler.process(Handler.java:41) at com.serverless.Handler.handleRequest(Handler.java:29) at com.serverless.Handler.handleRequest(Handler.java:18) at lambdainternal.EventHandlerLoader$PojoHandlerAsStreamHandler.handleRequest(EventHandlerLoader.java:178) at lambdainternal.EventHandlerLoader$2.call(EventHandlerLoader.java:888) at lambdainternal.AWSLambda.startRuntime(AWSLambda.java:286) at lambdainternal.AWSLambda.<clinit>(AWSLambda.java:64) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:348) at lambdainternal.LambdaRTEntry.main(LambdaRTEntry.java:94) at com.serverless.Handler.handleRequest(Handler.java:33) at com.serverless.Handler.handleRequest(Handler.java:18) |
不过呢,应该不太碍事,每次 Lambda 失败只有一次重复出现。
上面测试的是被 SNS 触发的 Lambda,DLQ 也是设置的 SNS。可以对下面几种组合进行测试
- Trigger: Kinesis -> DLQ: SNS
- Trigger: Kinesis -> DLS: SQS
- Trigger: SNS -> DLQ: SQS
- Trigger: SQS -> DLQ: SNS
- Trigger: SQS -> DLQ: SQS
Kinesis 也不容易测试,因为 Kinesis 消息的重试是直至消息过期,可简单看下 DLQ 是 SQS 的话消息是如何包装的(直接从 AWS 的 SQS 控制台)
Message Body 内容:
1 |
{"Records":[{"EventSource":"aws:sns","EventVersion":"1.0","EventSubscriptionArn":"arn:aws:sns:us-east-1:...原始消息内容} |
就是原始的 SNS 消息内容
同样包含以上列举的三个字段。
对本文有必要小结一下:
- SNS 触发的 Lambda 重试总共为三次,间隔时间分别为 1 分钟后,两分钟后
- Lambda 对 SNS 消息的多次重试所记录的 RequestId 是一样的
- DLQ 中的消息含有原 Lambda 执行时的 RequestId
- DLQ 中消息的 message 是原始 Lambda 接收到的消息内容
- DLQ 中消息的 ErrorMessage 字段是原始 Lambda 抛出异常的 getMessage() 内容,需要更丰富的信息自行包裹,再长不过 1KB
本文链接 https://yanbin.blog/aws-lambda-work-with-dlq/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
大佬真棒 留个qq, 347774786
一般用邮件 yabqiu@gmail.com
[…] 同时此篇也是作为上文 AWS Lambda 重试与死信队列(DLQ) 的一个很重要的补充。在此也会验证 SQS 触发的 Lambda 的重试机制以及 DLQ 相的内容。 […]