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 个函数共用。

注意:

  1. AWS Lambda 至少会为非预留并发池保留 100 的并发执行数,以便未设置限制的函数仍可处理请求。因此,在实践中,如账户限制为 1000,则最多能为单个函数分配 900。
  2. 如要函数停止处理任何调用,则可将并发设置为 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函数日志记录