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依赖


    
        com.amazonaws
        aws-java-sdk-sqs
    
    
        com.fasterxml.jackson.core
        jackson-databind
    



    
        
            com.amazonaws
            aws-java-sdk-bom
            1.11.431
            pom
            import
        
        
            com.fasterxml.jackson.core
            jackson-databind
            2.9.7
        
    

示例代码

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 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 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 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 messages = sqs.receiveMessage(receiveMessageRequest).getMessages();  
        for (Message message : messages) {  
            System.out.println("Message: " + message.getBody());  
            for (Map.Entry 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 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 parse(String content, Class valueType) throws IOException {
        return mapper.readValue(content, valueType);
    }
}

SNS

Amazon Simple Notification Service (Amazon SNS)是AWS消息通知服务,发布者通过创建消息并将消息发送至主题与订阅者进行异步交流。
AWS学习笔记(七)--Lambda集成SQS和SNS_第1张图片
订阅者可以为Web服务器、电子邮件地址、SQS队列、Lambda函数。一个主题可以有多个或多种订阅者。

POM依赖


    com.amazonaws
    aws-java-sdk-sns

示例代码

下面列出了常用的方法:创建、删除主题,创建、删除订阅,确认订阅,发布消息。

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 Subscribe 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", "[email protected]");

创建订阅后,会向订阅者发送确认邮件,有效期为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依赖


    
        com.amazonaws
        aws-java-sdk-sqs
    
    
        com.amazonaws
        aws-lambda-java-core
    
    
        com.amazonaws
        aws-lambda-java-events
    
    
        com.amazonaws
        aws-lambda-java-log4j2
    
    
        com.fasterxml.jackson.core
        jackson-databind
    



    
        
            com.amazonaws
            aws-java-sdk-bom
            1.11.431
            pom
            import
        
        
            com.amazonaws
            aws-lambda-java-core
            1.2.0
        
        
            com.amazonaws
            aws-lambda-java-events
            2.2.2
        
        
            com.amazonaws
            aws-lambda-java-log4j2
            1.1.0
        
        
            com.fasterxml.jackson.core
            jackson-databind
            2.9.7
        
    

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为自定义的类型  
    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 {  
    @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包,:

  
      
          
            org.apache.maven.plugins  
            maven-shade-plugin  
            3.2.0
              
                  
                    package  
                      
                        shade  
                    
                    
                         false
                     
                  
              
          
      

说明:参数createDependencyReducedPom默认值为true,会生成一个简化版的POM文件,名为dependency-reduced-pom.xml,其中不包含已被shade打进jar包的依赖。

创建Lambda Function

下面通过Web Console创建Lambda Function
AWS学习笔记(七)--Lambda集成SQS和SNS_第2张图片
注意:role要有lambda、Cloudwatch Logs、SQS权限。

然后上传jar包,配置Handler
AWS学习笔记(七)--Lambda集成SQS和SNS_第3张图片
调整内存和超时参数:
AWS学习笔记(七)--Lambda集成SQS和SNS_第4张图片
设定并发数,保存。
AWS学习笔记(七)--Lambda集成SQS和SNS_第5张图片
下面配置测试参数,测试一下:
AWS学习笔记(七)--Lambda集成SQS和SNS_第6张图片
执行成功输出:
AWS学习笔记(七)--Lambda集成SQS和SNS_第7张图片

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
AWS学习笔记(七)--Lambda集成SQS和SNS_第8张图片
测试成功后,配置CloudWatch Events触发器,Rule Type选择Schedule expression:
AWS学习笔记(七)--Lambda集成SQS和SNS_第9张图片
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,如下:



    
        
            
                %d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1}:%L - %m%n
            
        
    
    
        
            
        
    

如使用maven-shade-plugin,还需增加如下配置,否则将会报ERROR StatusLogger Unrecognized format specifier [d]:

plugin>
    org.apache.maven.plugins
    maven-shade-plugin
    3.2.0
    
        
            package
            
                shade
            
            
                false
                
                    
                    
                
            
        
    
    
        
            com.github.edwgiz
            maven-shade-plugin.log4j2-cachefile-transformer
            2.8.1
        
    

重试与错误处理

以下任意原因可能会导致Lambda 函数失败:

  • 函数在尝试访问终端节点时超时
  • 函数无法成功解析输入数据
  • 函数遇到资源限制,例如内存不足错误或者其他超时

如果出现任何这些错误,函数会引发异常。

Amazon SQS 队列配置为事件源时,AWS Lambda 将轮询队列中的一批记录并调用Lambda 函数。如果调用成功,消息将从队列中删除;如果失败,Lambda 将继续处理批处理中的其他消息。同时,Lambda 将继续重试失败的消息,直至消息调用成功或消息保留期到期:在这种情况下,消息将被删除,或者如果您已配置 Amazon SQS 死信队列,失败信息将被定向,以供分析。

管理并发

出于成本、调节处理批量事件所花费的时间或与下游资源匹配(比如函数中建立了数据库连接)等原因,需要控制并发数。Lambda 提供了账户级别和函数级别的并发执行数限制控制。

  • 账户级别

默认,AWS Lambda 将给定区域中所有函数的总并发执行数限制为 1000。为提高并发执行数限制,可到AWS 支持中心创建Case:
AWS学习笔记(七)--Lambda集成SQS和SNS_第10张图片

  • 函数级别

默认,系统会对所有函数的并发执行总数进行限制,这种共享并发执行池称为非预留并发分配。如尚未设置任何函数级别的并发限制,则非预留并发限制与账户级别并发限制相同。如您为某个函数设置并发执行数限制,则将从非预留并发池中扣除该值。例如,如账户的并发执行数限制为 1000,共有 10 个函数,则可为一个函数指定 200 的限制,为另一个函数指定 100 的限制,剩余的 700 将由其他 8 个函数共用。

注意:

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

AWS学习笔记(七)--Lambda集成SQS和SNS_第11张图片

参考文档

Integrate SQS and Lambda: serverless architecture for asynchronous workloads
Amazon Simple Queue Service Developer Guide
AWS Lambda Developer Guide
Programming Model for Authoring Lambda Functions in Java
AWS SDK for Java Developer Guide
Schedule Expressions Using Rate or Cron
日志记录
AWS 视频中心
AWS微服务和无服务器架构入门
快速理解AWS Lambda,轻松构建Serverless后台
用无服务器应用模型构建AWS Lambda应用
如何通过运行无服务器来满足企业需求
无服务器架构设计模式和最佳实践