关于AWS SQS 和 Lamb function的实现
得力于最近项目上的需求,需要了解一下关于AWS SQS的使用方法。不多说,先上overview design:
使用场景:
这次的study主要是为了解决之前项目上的一些pain point. 当Batch jobs 被触发时,API 到 IBM Sterling之间会做很多处理来将documents 生成出来并且分发给不同的客户。 在这个模型里面,当Sterling 和 Batch在处理请求时,客户会反应说document没有收到, Ops team只有通过查看日志的方式在troubleshoot, 然而该系统下并没有日志系统做支撑,ops team 只能手动通过关键词去服务器查询日志找出问题。引入AWS SQS 的目的就是为了解决当我们的程序出现异常时,SQS 可以帮助我们log error, 并且提供retry mechanism.Agenda:
- AWS SQS 介绍
- 如何使用AWS SQS
- 如何使用lambda function
- 如何查看日志
- 设置Dead loop queue 和 retry machanism
- AWS SQS 的一些经验分享
- 总结
AWS SQS 介绍
“Amazon Simple Queue Service (SQS)是一个完全托管的消息队列服务,它使您能够解耦和扩展微服务、分布式系统和无服务器应用程序。” 简单来说,SQS 就是一个云上的消息队列服务, 它可以帮助我们将请求存储在消息队列里然后触发我们的lambda function 来处理这些message, 这样做的目的就是为了解耦程序之间的关联,这样一来,我们的每个请求就不用等待后续的process跑完才return给用户,用户在这个过程中不需要等待就可以拿到一个success的结果,在后续的lambda function 会去执行剩下的代码。lambda function 是AWS 的另一个服务,我们可以将代码上传给lambda function, 让它帮我们执行我们需要跑的代码。
为了直观,这篇文章就不用代码演示如何创建Queue, 具体的代码可以参考官方github.
可以看到通过AWS Console,我们就可以完成创建队列,发送消息,删除消息等操作
Default Visibility Timeout: 当我们的队列里的message被处理时,Messages in flight 会记录有一条record正在被处理,当这个message被处理之后出现了异常, 那么这个message 会等待这个时间(Default Visibility Timeout) 之后,才会出现在Messages available里。设置这个时间是为了让我这条fail的message不会在我的lambda function没跑完之前被抓起来重跑。
Message Retention Period: message将会被delete如果过了这个期限message依然queuing.
Dead Letter Queue Settings
Dead Letter Queue: 需要创建一个新的queue 作为DLQ
Maximum Receives: retry的次数
当创建完Queue之后,我们可以试着发送一个Message
Message Body: message 的内容
Message Attribute: 这里可以加入一些attr来区别不同的message,这里的attr主要是用在lambda function 里面。可以通过使用attr对不同的message 做不同的分类和处理。
当我创建一个Message之后,这个message 就很快进入了in flight,
这时候我们可以在CloudWatch 里面看到这条message被执行的log 如下,
这里我们的message 会被处理,是因为我们关联一个lambda function去处理这条message, 从Log里面可以看出,这条message 是fail的,因为1除以0是会报错的,这里让它出现异常,是为了体现retry之后,这条message 会进入Dead loop queue.
你会看到这条Message queue在了Dead loop queue 里, 等待被其他程序处理。
同学肯定会好奇我的lambda function代码是什么样,如下:
public class LambdaFunctionHandler implements RequestHandler<Object, String> {
@Override
public String handleRequest(Object input, Context context) {
context.getLogger().log("Input: " + input);
String output = "Hello, " + input + "!";
String indicator = input.toString();
if(indicator.contains("fail")) {
System.out.println(1/0);
}
System.out.print(output);
return output;
}
}
这里的代码只是为了让程序出现异常,如果字段里面包含了"fail" 就去执行1/0 触发异常。
如何创建lambda function
这里的role 需要去配置,因为AWS有自己的一套方法去管理权限,基本上就是点鼠标,让后把SQS, LAMBDA的权限都分配好就可以使用了。
创建一个S3 Bucket
这里的S3 Bucket是为了帮助我们把代码寄存在bucket里面,当lambda function被触发时,它会去bucket里面获取代码并执行代码。
当ACCESS KEY 和 S3 Bucket 创建完成之后, 剩下的就是将代码上传到S3 Bucket,然后让代码和我们的lambda function 关联起来,这里我们使用Eclipse做demo:
-
创建一个 AWS lambda Java Project
-
找到LambdaFunctionHandler.java file, 然后把代码复制进去, save。
public class LambdaFunctionHandler implements RequestHandler<Object, String> {
@Override
public String handleRequest(Object input, Context context) {
context.getLogger().log("Input: " + input);
String output = "Hello, " + input + "!";
String indicator = input.toString();
if(indicator.contains("fail")) {
System.out.println(1/0);
}
System.out.print(output);
return output;
}
}
- 上传到S3 Bucket 并且测试代码:
right click ->Upload function to AWS Lambda
这里我们要选好S3 Bucket是哪个然后完成
完成后会看到我们最新upload的代码了:
测试function:
right click ->run function on AWS Lambda, 我们可以加入我们要测试的text 然后选择invoke
这样我们就完成了一次测试,并且成功将Eclipse里的代码上传到S3 Bucket里。
设置Dead loop queue 和 retry machanism
当我们的message 在SQS里面被process之后出现了异常,通常我们需要为lambda function 设置dead loop queue,让那些有问题的message在超过retry之后被send 去另外一个queue里面等待处理, 如上图所示的DeadLoopQueue
通过在lambda function里面配置debugging and error handling来设置DLQ
这样以来,那些failed 的record 在超过retry之后就会进入DLQ:
进入到DLQ 的message 通常有2种方式来处理:
a) 通过给DLQ配置lambda function, 当DLQ收到message之后立马开始执行lambda function, 具体做法和以上一致,通过eclipse上传代码到S3 Bucket
b) 通过配置WatchCloud Event 来对message 实现定时批处理
首先配置一个event source
然后关联这个event source 和 lambda function
为了处理queue里面的message, 我们需要让lambda function 去access 我们的DLQ, 以下是代码示例:
public class LambdaFunctionHandler implements RequestHandler<Object, String> {
@Override
public String handleRequest(Object input, Context context) {
context.getLogger().log("Input: " + input);
String output = "Hello, " + input + "-";
String indicator = input.toString();
if(indicator.contains("fail")) {
System.out.println(1/0);
}
System.out.print(output);
return output;
}
}
package com.amazonaws.lambda.demo;
import com.amazonaws.client.builder.AdvancedConfig.Key;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
import com.amazonaws.services.sqs.model.GetQueueAttributesRequest;
import com.amazonaws.services.sqs.model.Message;
import com.amazonaws.services.sqs.model.MessageAttributeValue;
import com.amazonaws.services.sqs.model.ReceiveMessageRequest;
import com.amazonaws.services.sqs.model.SendMessageRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DLQHandlerPullQueue {
private static final String QUEUE_NAME = "NTPDLQTest";
private static final AmazonSQS sqs = AmazonSQSClientBuilder.defaultClient();
private static final String queueUrl = sqs.getQueueUrl(QUEUE_NAME).getQueueUrl();
//To get attributes of queue
public void ProcessSQSMessage() {
List<String> attributeNames = new ArrayList<String>();
attributeNames.add("All");
GetQueueAttributesRequest gqar = new GetQueueAttributesRequest(queueUrl);
gqar.setAttributeNames(attributeNames);
Map<String, String> attrMap = new HashMap<String,String>(sqs.getQueueAttributes(gqar).getAttributes());
int messagesSize = Integer.parseInt(attrMap.get("ApproximateNumberOfMessages"));
//ApproximateNumberOfMessages is number of available message in Queue
System.out.println("Messages in the queue: " + messagesSize);
//Print all attrs of queue
for (Map.Entry<String, String> entry : attrMap.entrySet()) {
System.out.println(entry.getKey() + "/" + entry.getValue());
}
// for (int i = 0; i < messagesSize; i++) {
// ReceiveAllMessage();
// }
List<String> attrList = new ArrayList<String>();
attrList.add("failA");
attrList.add("failB");
//attrList.add("failC");
for (int i = 0; i < messagesSize; i++) {
GetMessageByAttr(attrList);
}
}
//To receive all message in Queue
public void ReceiveAllMessage() {
List<Message> messages = sqs.receiveMessage(queueUrl).getMessages();
System.out.println("test - ReceiveMessage");
for (Message m : messages) {
System.out.println(m.getBody());
}
}
public void GetMessageByAttr(List<String> attrnameList) {
final List<Message> messages = sqs.receiveMessage(
new ReceiveMessageRequest(queueUrl)
.withMessageAttributeNames(attrnameList)
.withMaxNumberOfMessages(Integer.valueOf("1"))
).getMessages();
System.out.println("Size = " + messages.size());
for (Message m : messages) {
//Get message attrs
System.out.println(m.getMessageAttributes().toString());
System.out.println(m.getBody());
}
}
public void SendMessageToqueueWithAttr() {
final Map<String, MessageAttributeValue> messageAttributes = new HashMap<>();
messageAttributes.put("failcase", new MessageAttributeValue()
.withDataType("String")
.withStringValue("Jane"));
final SendMessageRequest sendMessageRequest = new SendMessageRequest();
sendMessageRequest.withMessageBody("fail: To test QueueAttr");
sendMessageRequest.withQueueUrl(queueUrl);
sendMessageRequest.withMessageAttributes(messageAttributes);
sqs.sendMessage(sendMessageRequest);
}
}
为了实现类似batch的效果,我们首先到去queue里面拿到所有的message的count, 然后使用for loop来完成对所有message的处理,因为ReceiveAllMessage() 这个每次去queue里面拿record的时候都是不一致的,例如Queue里面有A,B,C,D,E,for loop 5 之后拿出来的message可能是E-D-A-B-C, 每次的顺序都会不同。所以当我们的message在处理的时候fail, 我们需要设置Default Visibility Timeout时间,来让我们的message不会被瞬间设置会available, 否则的话,这条fail的message会重新被lambda function pick up 如此一来,就会有一条message被miss掉。 那么Default Visibility Timeout要怎么设置才可以避免这种情况发生?
Default Visibility Timeout = 12 hours
If lambda function triggered at 7:00 am,
If there are too much records to process, the batch will take 12 hours(max for 1 day)
7:00 PM, job stops and if the final message processing fail at 6:59pm, it will be shown in DLQ as available at 6:59am.
Default Visibility Timeout = 6 hours
If lambda function triggered at 7:00 am,
13:00pm, job completes. if the final message processing fail at 12:59pm. it will be shown in DLQ as available at 18:59am.
通过延长Default Visibility Timeout, 就可以避免message被重复pick up 然后process了。
总结:
以下摘自极客时间 胡忠想老师的课程:从零开始学习微服务
基于 RPC 通信的微服务架构,其特点是一个服务依赖于其他服务返回的结果,只有依赖服务执行成功并返回后,这个服务才算调用成功。这种架构适用于用户请求是读请求的情况,就像下图所描述的那样,比如微博用户的一次 Feed API 请求,会调用 Feed RPC 获取关注人微博,调用 Card RPC 获取微博中的视频、文章等多媒体卡片信息,还会调用 User RPC 获取关注人的昵称和粉丝数等个人详细信息,只有在这些信息都获取成功后,这次用户的 Feed API 请求才算调用成功。
而基于 MQ 消息队列通信的架构,其特点是服务之间的交互是通过消息发布与订阅的方式来完成的,一个服务往 MQ 消息队列发布消息,其他服务从 MQ 消息队列订阅消息并处理,发布消息的服务并不等待订阅消息服务处理的结果,而是直接返回调用成功。这种架构适用于用户请求是写请求的情况,就像下图所描述的那样,比如用户的写请求,无论是发博、评论还是赞都会首先调用 Feed API,然后 Feed API 将用户的写请求消息发布到 MQ 中,然后就返回给用户请求成功。如果是发博请求,发博服务就会从 MQ 中订阅到这条消息,然后更新用户发博列表的缓存和数据库;如果是评论请求,评论服务就会从 MQ 中订阅到这条消息,然后更新用户发出评论的缓存和数据库,以及评论对象收到评论的缓存和数据库;如果是赞请求,赞服务就会从 MQ 中订阅到这条消息,然后更新用户发出赞的缓存和数据库,以及赞对象收到的赞的缓存和数据库。这样设计的话,就把写请求的返回与具体执行请求的服务进行解耦,给用户的体验是写请求已经执行成功,不需要等待具体业务逻辑执行完成。
总结一下就是,基于 RPC 通信和基于 MQ 消息队列通信的方式都可以实现微服务的拆分,两者的使用场景不同,RPC 主要用于用户读请求的情况,MQ 主要用于用户写请求的情况。对于大部分互联网业务来说,读请求要远远大于写请求,所以针对读请求的基于 RPC 通信的微服务架构的讨论也更多一些,但并不代表基于 MQ 消息队列不能实现,而是要区分开它们不同的应用场景。
以上摘自极客时间 胡忠想老师的课程:从零开始学习微服务
我们所使用的SQS 也是为了解决应用之间解耦的问题,AWS SQS 的确是一个很好用的中间件,配合eclipse和lambda function 可以很容易的实现给应用的解耦,而且AWS 提供了clockwatch log 来帮忙我们track那些有问题的message。
Reference:
AWS Check source code for lambda:
https://s3.console.aws.amazon.com/s3/buckets/ntpsqstest/?region=ap-southeast-1&tab=overview#
AWS Check message log for lambda:
https://ap-southeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-southeast-1
AWS Configuration for lambda:
https://ap-southeast-1.console.aws.amazon.com/lambda/home?region=ap-southeast-1#/functions/NTPDLQHandler?tab=graph
Sample Code Github:
https://github.com/awsdocs/aws-doc-sdk-examples/blob/master/java/example_code/sqs/src/main/java/aws/example/sqs/UsingQueues.java
AWS Lambda Adds Amazon Simple Queue Service to Supported Event Sources
https://aws.amazon.com/blogs/aws/aws-lambda-adds-amazon-simple-queue-service-to-supported-event-sources/
Tutorial: How to Create, Upload, and Invoke an AWS Lambda Function
https://docs.aws.amazon.com/toolkit-for-eclipse/v1/user-guide/lambda-tutorial.html
https://supsystic.com/documentation/id-secret-access-key-amazon-s3/
coding sample:
https://www.programcreek.com/java-api-examples/index.php?api=com.amazonaws.services.sqs.model.GetQueueAttributesRequest
https://www.javatips.net/api/com.amazonaws.services.sqs.model.receivemessagerequest