SQS
Amazon Simple Queue Service (SQS) 是一项可靠、可扩展的消息队列服务。Amazon SQS支持标准队列和 FIFO 队列,中国区目前仅支持标准队列。
Amazon SQS 的主要优势:
- 安全性 – 可以控制谁能向Amazon SQS队列发送消息以及谁能从队列接收消息。利用服务端加密 (SSE),可以通过使用在 AWS Key Management Service (AWS KMS) 中管理的密钥保护队列中消息的内容来传输敏感数据。
- 持久性 – 为确保消息的安全,Amazon SQS 将消息存储在多个服务器上。
- 可用性 – Amazon SQS 使用冗余基础设施来支持生成和消费消息的高并发访问和高可用。
- 可扩展性 – Amazon SQS 可独立处理各个缓冲的请求,并可透明扩展以处理任何负载增加或峰值,无需任何预配置指令。
- 可靠性 – Amazon SQS 在处理期间锁定消息,以便多个生成者可同时发送消息,多个消费者可同时接收消息。
- 自定义 – 队列不必完全相同,例如,您可以设置队列的默认延迟。可以使用 Amazon Simple Storage Service (Amazon S3) 或 Amazon DynamoDB 存储大于 256 KB 的消息内容,Amazon SQS 保留指向 Amazon S3 对象的指针,您也可以将一个大消息拆分为几个小消息。
下面以标准Queue为例,演示Java创建Queue、配置Dead Letter Queue、发送Message、接收Message、删除Message、删除Queue的方法。
配置AWS账户
在{HOME}/.aws目录下配置AWS账户信息,用户要有SQS权限:
[default]
aws_access_key_id = AAAAAAAAAAAAAA
aws_secret_access_key = MXXXXXXXXXXXXXXXXXXXXXX9
POM依赖
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sqs</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-bom</artifactId>
<version>1.11.431</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.7</version>
</dependency>
</dependencies>
</dependencyManagement>
示例代码
import com.amazonaws.regions.Regions;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
import com.amazonaws.services.sqs.model.*;
import java.util.*;
public class SqsUtil {
private static final String ARN_ATTRIBUTE_NAME = "QueueArn";
private static AmazonSQS sqs;
static {
sqs = AmazonSQSClientBuilder.standard().withRegion(Regions.CN_NORTH_1).build();
}
private SqsUtil() {
}
// 根据Queue Name查询Url
public static String getQueueUrl(String queueName) {
return sqs.getQueueUrl(queueName).getQueueUrl();
}
// 创建Queue
public static String createQueue(String queueName) {
System.out.println("Creating a new SQS queue called " + queueName);
CreateQueueRequest createQueueRequest = new CreateQueueRequest(queueName);
Map<String, String> attributes = new HashMap<>();
// 接收消息等待时间
attributes.put("ReceiveMessageWaitTimeSeconds", "30");
createQueueRequest.withAttributes(attributes);
return sqs.createQueue(createQueueRequest).getQueueUrl();
}
// 创建死信Queue
public static String createDeadLetterQueue(String queueName) {
String queueUrl = createQueue(queueName);
// 配置Dead Letter Queue时使用ARN
return getQueueArn(queueUrl);
}
// 配置死信Queue
public static void configDeadLetterQueue(String queueUrl, String deadLetterQueueArn) {
System.out.println("Config dead letter queue for " + queueUrl);
Map<String, String> attributes = new HashMap<>();
// 最大接收次数设为5,当接收次数超过5后,消息未被处理和删除将被转到死信队列
attributes.put("RedrivePolicy", "{\"maxReceiveCount\":\"5\", \"deadLetterTargetArn\":\"" + deadLetterQueueArn + "\"}");
sqs.setQueueAttributes(queueUrl, attributes);
}
// 发送消息
public static void sendMessage(String queueUrl, String message) {
System.out.println("Sending a message to " + queueUrl);
SendMessageRequest request = new SendMessageRequest();
request.withQueueUrl(queueUrl);
request.withMessageBody(message);
Map<String, MessageAttributeValue> messageAttributes = new HashMap<>();
// 添加消息属性,注意必须要有DataType和Value
messageAttributes.put("Hello", new MessageAttributeValue().withDataType("String").withStringValue("COCO"));
request.withMessageAttributes(messageAttributes);
sqs.sendMessage(request);
}
// 接收消息
public static void receiveMessages(String queueUrl) {
System.out.println("Receiving messages from " + queueUrl);
ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest(queueUrl);
receiveMessageRequest.setMaxNumberOfMessages(5);
receiveMessageRequest.withWaitTimeSeconds(10);
// 要添加MessageAttributeNames,否则不能接收
receiveMessageRequest.setMessageAttributeNames(Arrays.asList("Hello"));
List<Message> messages = sqs.receiveMessage(receiveMessageRequest).getMessages();
for (Message message : messages) {
System.out.println("Message: " + message.getBody());
for (Map.Entry<String, MessageAttributeValue> entry : message.getMessageAttributes().entrySet()) {
System.out.println(" Attribute");
System.out.println(" Name: " + entry.getKey());
System.out.println(" Value: " + entry.getValue().getStringValue());
}
// 删除消息
System.out.println("Deleting a message.");
sqs.deleteMessage(queueUrl, message.getReceiptHandle());
}
}
// 删除Queue
public static void deleteQueue(String queueUrl) {
System.out.println("Deleting the queue " + queueUrl);
sqs.deleteQueue(queueUrl);
}
// 查询Queue Arn
public static String getQueueArn(String queueUrl) {
List<String> attributes = new ArrayList<>();
attributes.add(ARN_ATTRIBUTE_NAME);
GetQueueAttributesResult queueAttributes = sqs.getQueueAttributes(queueUrl, attributes);
return queueAttributes.getAttributes().get(ARN_ATTRIBUTE_NAME);
}
}
测试一下:
// 创建Dead Letter Queue
String deadLetterQueueArn = createDeadLetterQueue("DeadLetterQueue");
// 创建Task Queue
String queueUrl = createQueue("TaskQueue");
// 配置Dead Letter Queue
configDeadLetterQueue(queueUrl, deadLetterQueueArn);
// 发送Message
for (int i = 0; i < 6; i++) {
sendMessage(queueUrl, "Hello COCO " + i);
}
// 接收Message
receiveMessages(queueUrl);
// 删除Queue
deleteQueue(queueUrl);
构造与解析消息
SQS消息体是字符串,可以使用jackson-databind进行对象与JSON字符串转换,来发送、接收消息。 JsonUtil
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
public final class JsonUtil {
private static ObjectMapper mapper = new ObjectMapper();
private JsonUtil() {
}
public static String generate(Object object) throws JsonProcessingException {
return mapper.writeValueAsString(object);
}
public static <T> T parse(String content, Class<T> valueType) throws IOException {
return mapper.readValue(content, valueType);
}
}
SNS
Amazon Simple Notification Service (Amazon SNS)是AWS消息通知服务,发布者通过创建消息并将消息发送至主题与订阅者进行异步交流。 订阅者可以为Web服务器、电子邮件地址、SQS队列、Lambda函数。一个主题可以有多个或多种订阅者。
POM依赖
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sns</artifactId>
</dependency>
示例代码
下面列出了常用的方法:创建、删除主题,创建、删除订阅,确认订阅,发布消息。
import com.amazonaws.regions.Regions;
import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.AmazonSNSClientBuilder;
import com.amazonaws.services.sns.model.*;
public class SnsUtil {
private static AmazonSNS sns;
static {
sns = AmazonSNSClientBuilder.standard().withRegion(Regions.CN_NORTH_1).build();
}
private SnsUtil() {
}
/**
* Creates a topic to which notifications can be published
*/
public static CreateTopicResult createTopic(String name) {
return sns.createTopic(name);
}
/**
* Deletes a topic and all its subscriptions
*/
public static DeleteTopicResult deleteTopic(String topicArn) {
return sns.deleteTopic(topicArn);
}
/**
* Prepares to subscribe an endpoint by sending the endpoint a confirmation message
*/
public static SubscribeResult subscribe(String topicArn, String protocol, String endpoint) {
return sns.subscribe(topicArn, protocol, endpoint);
}
/**
* Deletes a subscription
*/
public static UnsubscribeResult unsubscribe(String subscriptionArn) {
return sns.unsubscribe(subscriptionArn);
}
/**
* Verifies an endpoint owner's intent to receive messages by validating the token sent to the endpoint by an earlier <code>Subscribe</code> action
*/
public static ConfirmSubscriptionResult confirmSubscription(String topicArn, String token) {
return sns.confirmSubscription(topicArn, token);
}
/**
* Sends a message to an Amazon SNS topic
*/
public static PublishResult publish(String topicArn, String message, String subject) {
return sns.publish(topicArn, message, subject);
}
}
以Email为例,创建主题、订阅方法如下:
CreateTopicResult topic = SnsUtil.createTopic("test-topic");
SubscribeResult subscribe = SnsUtil.subscribe(topic.getTopicArn(), "email", "jason@163.com");
创建订阅后,会向订阅者发送确认邮件,有效期为3天,订阅者确认后订阅才生效。未确认前,订阅状态(Subscription Arn)为PendingConfirmation,不可删除,如一直未确认,过期后会自动删除。
确认订阅:
SnsUtil.confirmSubscription("arn:aws-cn:sns:cn-north-1:891245299999:test-topic", "...token...");
发布消息:
SnsUtil.publish("arn:aws-cn:sns:cn-north-1:891245299999:test-topic", "Hello COCO", "Hello COCO");
删除订阅、主题:
SnsUtil.unsubscribe("arn:aws-cn:sns:cn-north-1:891245299999:test-topic:bcd65f82-ae54-4604-a763-30b7ff877e8a");
SnsUtil.deleteTopic("arn:aws-cn:sns:cn-north-1:891245299999:test-topic");
Lambda
AWS Lambda 是一项计算服务,无需预配置或管理服务器即可运行代码。AWS Lambda只在需要时执行代码并根据请求频率自动扩展。支持Node.js、Java、C#、Go 和 Python语言。在使用 AWS Lambda 时,只需负责自己的代码,AWS Lambda管理内存、CPU、网络和其他资源均衡的计算机群。可以通过事件驱动Lambda函数,可以使用Amazon API Gateway 运行代码以响应 HTTP 请求,也可以通过 AWS SDK/AWS Lambda CLI按需调用。Lambda会自动跟踪请求数、每个请求的延迟、产生错误的请求数,并发布相关的 CloudWatch 指标,可以借助这些指标设置 CloudWatch 自定义警报。可以在代码中插入日志来帮助验证代码是否按预期运行,Lambda 自动与 Amazon CloudWatch Logs 集成。
Lambda 限制
如果超过任何限制,函数调用都会失败并引发exceeded limits异常。这些限制是固定的,目前无法更改。
函数限制
资源 | 限制 |
---|---|
内存 | 128MB至3008MB (增量为64MB) |
临时磁盘容量(“/tmp”空间) | 512MB |
文件描述符数 | 1024 |
过程和线程数(合并总数量) | 1024 |
每个请求的最大执行时长 | 900 秒(15分钟) |
Invoke 请求正文负载大小(RequestResponse/同步调用) | 6MB |
Invoke 请求正文负载大小 (Event/异步调用) | 128KB |
账户限制
资源 | 限制 |
---|---|
并发执行数 | 1000 |
部署限制
资源 | 限制 |
---|---|
最大部署包大小 (压缩的 .zip/.jar 文件) | 50MB |
控制台代码编辑器支持的最大部署包大小 | 3MB |
每个区域可以上传的所有部署包的总大小 | 75GB |
可压缩到部署程序包中的代码/依赖项的大小 (未压缩的 .zip/.jar 大小) | 250MB |
环境变量集的总大小 | 4 KB |
说明:中国区目前不支持环境变量
POM依赖
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sqs</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-log4j2</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-bom</artifactId>
<version>1.11.431</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-log4j2</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.7</version>
</dependency>
</dependencies>
</dependencyManagement>
Function Code
Lambda函数定义支持两种方式 :
- 实现预定义接口RequestStreamHandler 或 RequestHandler
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.Context;
public class Hello implements RequestHandler<Request, Response> {
// Request,Response为自定义的类型
public Response handleRequest(Request request, Context context) {
String greetingString = String.format("Hello %s %s.", request.firstName, request.lastName);
return new Response(greetingString);
}
}
- 不实现任何接口,直接定义处理程序方法
outputType handler-name(inputType input, Context context) {
...
}
inputType和outputType 可为以下类型之一:
- Java 标准类型(如 String 或 int)。
- aws-lambda-java-events 库中的预定义 AWS 事件类型。 如S3Event、ScheduledEvent、SNSEvent、SQSEvent等。
- 自定义POJO 类,AWS Lambda 会根据该 POJO 类型自动序列化和反序列化输入、输出 JSON。
同步方式调用时,outputType可以为任何支持的类型;异步方式调用时(event方式),outputType应为void。
如不需要,可以省略处理程序方法签名中的 Context 对象。
先编写一个简单的测试用例接收SQS消息,输入参数input为Queue URL:
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import com.amazonaws.services.lambda.runtime.RequestHandler;
public class Hello implements RequestHandler<String, String> {
@Override
public String handleRequest(String input, Context context) {
LambdaLogger logger = context.getLogger();
logger.log("received : " + input);
SqsUtil.receiveMessages(input);
return "success";
}
}
程序编写完了,如何部署到AWS Lambda中呢?POM中增加shade插件,将代码及依赖打成jar包,:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
说明:参数createDependencyReducedPom默认值为true,会生成一个简化版的POM文件,名为dependency-reduced-pom.xml,其中不包含已被shade打进jar包的依赖。
创建Lambda Function
下面通过Web Console创建Lambda Function 注意:role要有lambda、Cloudwatch Logs、SQS权限。
然后上传jar包,配置Handler 调整内存和超时参数: 设定并发数,保存。 下面配置测试参数,测试一下: 执行成功输出:
Lambda触发器
CloudWatch Events 使用CloudWatch Events触发器可以定时调用Lambda,下面修改一下代码,将输入参数类型改为ScheduledEvent:
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.ScheduledEvent;
public class Hello {
public void handleRequest(ScheduledEvent input, Context context) {
LambdaLogger logger = context.getLogger();
logger.log("received : " + input.toString());
SqsUtil.receiveMessages("https://sqs.cn-north-1.amazonaws.com.cn/891245999999/TaskQueue");
}
}
上传后,同样先手工测试一下,这次选择模板Scheduled Event 测试成功后,配置CloudWatch Events触发器,Rule Type选择Schedule expression: SQS aws-lambda-java-events 2.2后支持SQS触发器,方便了SQS与lambda的集成,SQS收到消息后自动触发lambda,lambda可从SQSEvent中读取消息内容,执行成功后自动删除SQS消息。
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import com.amazonaws.services.lambda.runtime.events.SQSEvent;
import org.itrunner.aws.sqs.JsonUtil;
import org.itrunner.aws.sqs.MessageBody;
public class Hello {
public void handleRequest(SQSEvent event, Context context) {
LambdaLogger logger = context.getLogger();
logger.log("received : " + event.toString());
try {
MessageBody message = JsonUtil.parse(event.getRecords().get(0).getBody(), MessageBody.class);
// do something
} catch (Exception e) {
logger.log(e.getMessage());
SnsUtil.publish("arn:aws-cn:sns:cn-north-1:891245299999:test-topic", e.getMessage(), "Lambda Error");
}
}
}
日志
在程序中可以使用System.out 和 System.err输出日志,日志保存在CloudWatch Logs中,但Lambda 将 System.out 和 System.err 返回的每一行都视为独立的事件,如下,将记录两条日志:
System.out.println("Hello \n world");
因此最好使用log4j:
private static final Logger logger = LogManager.getLogger(Hello.class);
使用log4j,需要增加日志配置文件log4j2.xml,如下:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration packages="com.amazonaws.services.lambda.runtime.log4j2.LambdaAppender">
<Appenders>
<Lambda name="Lambda">
<PatternLayout>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1}:%L - %m%n</pattern>
</PatternLayout>
</Lambda>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Lambda"/>
</Root>
</Loggers>
</Configuration>
如使用maven-shade-plugin,还需增加如下配置,否则将会报ERROR StatusLogger Unrecognized format specifier [d]:
plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="com.github.edwgiz.mavenShadePlugin.log4j2CacheTransformer.PluginsCacheFileTransformer">
</transformer>
</transformers>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.github.edwgiz</groupId>
<artifactId>maven-shade-plugin.log4j2-cachefile-transformer</artifactId>
<version>2.8.1</version>
</dependency>
</dependencies>
</plugin>
重试与错误处理
以下任意原因可能会导致Lambda 函数失败:
- 函数在尝试访问终端节点时超时
- 函数无法成功解析输入数据
- 函数遇到资源限制,例如内存不足错误或者其他超时
如果出现任何这些错误,函数会引发异常。
Amazon SQS 队列配置为事件源时,AWS Lambda 将轮询队列中的一批记录并调用Lambda 函数。如果调用成功,消息将从队列中删除;如果失败,Lambda 将继续处理批处理中的其他消息。同时,Lambda 将继续重试失败的消息,直至消息调用成功或消息保留期到期:在这种情况下,消息将被删除,或者如果您已配置 Amazon SQS 死信队列,失败信息将被定向,以供分析。
管理并发
出于成本、调节处理批量事件所花费的时间或与下游资源匹配(比如函数中建立了数据库连接)等原因,需要控制并发数。Lambda 提供了账户级别和函数级别的并发执行数限制控制。
- 账户级别
默认,AWS Lambda 将给定区域中所有函数的总并发执行数限制为 1000。为提高并发执行数限制,可到AWS 支持中心创建Case:
- 函数级别
默认,系统会对所有函数的并发执行总数进行限制,这种共享并发执行池称为非预留并发分配。如尚未设置任何函数级别的并发限制,则非预留并发限制与账户级别并发限制相同。如您为某个函数设置并发执行数限制,则将从非预留并发池中扣除该值。例如,如账户的并发执行数限制为 1000,共有 10 个函数,则可为一个函数指定 200 的限制,为另一个函数指定 100 的限制,剩余的 700 将由其他 8 个函数共用。
注意:
- AWS Lambda 至少会为非预留并发池保留 100 的并发执行数,以便未设置限制的函数仍可处理请求。因此,在实践中,如账户限制为 1000,则最多能为单个函数分配 900。
- 如要函数停止处理任何调用,则可将并发设置为 0 。
参考文档
Integrate SQS and Lambda: serverless architecture for asynchronous workloads Amazon Simple Queue Service Developer Guide AWS Lambda Developer Guide 使用Java构建Lambda函数 适用于Java的AWS SDK开发人员指南 CloudWatch Rate和Cron表达式 Java中的AWS Lambda函数日志记录