AWS SQS 消息中间件的好与坏

4,560 阅读7分钟

Aws SQS实如其名(Amazon Simple Queue Service),SQS实现的是一个简单的消息队列,并不是说SQS的实现简单,而是消息队列设计简单,没有分区那些概念,非常容易上手。

SQS提供两种消息队列类型,分别是标准队列和FIFO先进先出队列。标准队列提供无限吞吐量,FIFO队列则是确保按照消息的发送顺序消费。使用批量操作,FIFO队列每秒支持最多3000条消息(TPS),非批量操作则每秒最多支持300条消息,当然,这是默认情况下,如果有需要,可申请获取更高的TPS支持。

按请求量收费

每月可免费获得100万个 Amazon SQS 请求,超过这个数量之后,每100w个请求0.4USD。这只是API的收费,还有网络数据流出费用,少于10TBGB价格为0.09USD。如果SQS能满足业务需求,还是非常推荐使用SQS的,费用相比自己购买EC2实例部署mq集群更低,以及免去维护,无需自己确保集群的可靠性。

消息发送

SQS使用内网发送一条消息平均耗时在4~9毫秒,跟mysql的存储速度有得一拼。虽然官方提供批量写消息的支持,但批量消息发送需要自己实现消息队列的缓存,加大内存的使用。

批量写消息的速度依然很耗时,但如果是异步写消息的话,批量操作可以更高效的使用线程和连接提交吞吐量。缺点是你还必须自己去维护一个消息缓存队列,等消息凑够了再批量发送,而且限制一次最大只能发送十条记录。因为SQS是按请求量计费的,因此批量操作的优势是降低使用成本。

之前打算用go来实现消息消费的,所以我就直接用go来写测试用例了,不想用java重复写一次。不懂go的朋友直接看测试结果就好了。

package mq

import (
	"fmt"
	"time"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/sqs"
)

type SqsSender interface {
	Send(message string) (string, error)
	SendBatch(msgArray []string) ([]string, error)
}

type MessageSender struct {
	QueueUrl string
}

// 单消息发送
func (sender *MessageSender) Send(message string) (string, error) {
	sendParams := &sqs.SendMessageInput{
		MessageBody:  aws.String(message),         // Required
		QueueUrl:     aws.String(sender.QueueUrl), // Required
		DelaySeconds: aws.Int64(3),                // (optional) 傳進去的 message 延遲 n 秒才會被取出, 0 ~ 900s (15 minutes)
	}
	sendResp, err := SqsSession.SendMessage(sendParams)
	if err != nil {
		return "", err
	}
	return *sendResp.MessageId, nil
}

// 批量发送
func (sender *MessageSender) SendBatch(msgArray []string) ([]string, error) {
	msgSize := len(msgArray)
	var messageArrays []*sqs.SendMessageBatchRequestEntry = make([]*sqs.SendMessageBatchRequestEntry, msgSize)
	for index, msg := range msgArray {
	    // 随便生成一个id
		id := fmt.Sprintf("%d", time.Now().Unix()+int64(index))
		messageArrays[index] = &sqs.SendMessageBatchRequestEntry{
			MessageBody: aws.String(msg),
			Id:          aws.String(id),
		}
	}
	batchMessageParams := &sqs.SendMessageBatchInput{
		QueueUrl: aws.String(sender.QueueUrl),
		Entries:  messageArrays,
	}
	sendResp, err := SqsSession.SendMessageBatch(batchMessageParams)
	if err != nil {
		return nil, err
	}
	var mids []string = make([]string, msgSize)
	for index, result := range sendResp.Successful {
		mids[index] = *result.Id
	}
	return mids, nil
}

1、单消息发送单元测试

package sqs_test

import (
	"asynctrack/log"
	"asynctrack/model"
	"asynctrack/mq"
	"encoding/json"
	"fmt"
	"testing"
	"time"
)

const (
	TestQueueUrl = ""
)

func TestSendMsg(test *testing.T) {
	var sender mq.SqsSender = &mq.MessageSender{QueueUrl: TestQueueUrl}
	var trackMsg = &model.TrackMessage{
		AffiliateId:   10,
		TrackLink: "https://www.ichestnuts.com",
	}
	if trackMsgJsonStr, err := json.Marshal(trackMsg); err == nil {
		for i := 0; i < 5; i++ {
			startTime := time.Now()
			msgId, _ := sender.Send(string(trackMsgJsonStr))
			fmt.Printf("const time: %v \n", time.Since(startTime))
			log.DefaultErrorLog.Info(msgId)
		}
	}
}

测试结果

const time: 4.729507ms 
const time: 5.198433ms 
const time: 4.747722ms 
const time: 4.343568ms 
const time: 4.604975ms 
const time: 4.631468ms 
const time: 3.695921ms 
const time: 3.386071ms 
const time: 4.143131ms 
const time: 4.429111ms 
const time: 4.763628ms 
const time: 4.802968ms 

2、批量(10条记录)发送单元测试

package sqs_test

import (
	"asynctrack/log"
	"asynctrack/model"
	"asynctrack/mq"
	"encoding/json"
	"fmt"
	"testing"
	"time"
)

func TestSendMsgBatch(test *testing.T) {
	var sender mq.SqsSender = &mq.MessageSender{QueueUrl: TestQueueUrl}
	var trackMsg = &model.TrackMessage{
		AffiliateId:   10,
		TrackLink: "https://www.ichestnuts.com",
	}
	if trackMsgJsonStr, err := json.Marshal(trackMsg); err == nil {
		for i := 0; i < 10; i++ {
			var str = string(trackMsgJsonStr)
			var msgArray []string = []string{str, str, str, str, str, str, str, str, str, str}
			startTime := time.Now()
			msgIds, _ := sender.SendBatch(msgArray)
			fmt.Printf("batch send const time: %v \n", time.Since(startTime))
			log.DefaultErrorLog.Info(msgIds)
		}
	}
}

测试结果

batch send const time: 8.499662ms 
batch send const time: 8.238962ms 
batch send const time: 18.728901ms 
batch send const time: 8.693126ms 
batch send const time: 8.571933ms 
batch send const time: 12.441125ms 
batch send const time: 9.132969ms 
batch send const time: 8.348875ms 
batch send const time: 11.538344ms 

批量消息发送确实能提高吞吐量,10条消息使用批量发送耗时大概是两条消息单独发送的耗时总和,节省了另外8条消息的耗时。注意,消息body不能超过256KB

消息消费

消费者消费消息一次最多只能拉取10条,之前项目中使用的JavaAPI是需要自己定时去拉取的。定多少个线程拉取,频率设置多少合适会是个很头疼的问题。一旦设置的线程数多或者拉取频率小,如果消息少的情况下,无疑是增加了费用。但如果设置的频率小,线程数少,可能会导致高峰时段消息积压,大量消息得不到实时消费。

为此,我还写了一个简单的自适应消费的算法。当拉取不到消息时,按2倍步长增加等待时间,最大为30s,如果拉取到消息,则会按2被缩小频率,最小为消息的一次消费耗时。其次是能够感知消费者的消费能力,按消费者的消费能力设置最小拉取时间间隔。

最近想使用go语言实现某块业务的消费时,才发现go语言的api是提供长轮询的,然后我再去看文档才发现文档写了支持长轮询,可能之前还没有,最近更新的,也可能之前我看文档没看仔细?java的我没注意去看文档,但肯定也是支持的。使用长轮询,在队列为空时,请求会为下一条消息等待至多20秒。使用长轮询的好处是不需要自己去控制消息拉取频率,从而实现最低成本。

    // Receive message
	receiveParams := &sqs.ReceiveMessageInput{
		QueueUrl:            aws.String(revice.QueueUrl),
		MaxNumberOfMessages: aws.Int64(10), // 一次最多取幾個 message
		VisibilityTimeout:   aws.Int64(30), // 如果這個 message 沒刪除,下次再被取出來的時間
		WaitTimeSeconds:     aws.Int64(10), // long polling 方式取,會建立一條長連線並且等在那邊,直到 SQS 收到新 message 回傳給這條連線才中斷
	}

	receiveResp, err := SqsSession.ReceiveMessage(receiveParams)

消息可见性超时

消息的可见性超时可在aws控制台设置。也可在拉取到消息时调用api设置,但不建议这么用,一个是会影响消息消费速度,二是增加使用成本。可见性超时如果设置得短,可能会出现消费者消费未完成,没有将消息删除的情况下,又被其它消费者拉取到,导致重复消费。而如果设置的时间较长,失败后的消息将要等待很长时间才能再次被消费者拉取到。

如果是每次拉取多条记录,建议消费完一条消息就立即删除,否则就需要将消息的可见性超时设置为n条记录的消费时长,不然可能会出现消息重复消费现象。严格要求一条消息只能被消费一次的,除了在消息可见性超时上控制外,还需要在代码中控制消息的幂等性消费,而且是支持分布式集群的幂等性消费。

不支持广播

SQS不支持广播功能。一个消息队列可以有多个消费者消费,但一条消息只能被一个消费者消费。假设·消息A被某个消费者拉取到,在可见性超时之前,其它消费者都拿不到这条数据。如果消费之后不删除,同一个消费者在该消息的可见性超时之后,还可能会再次拿到这个消息。

通过设置可见性超时为0实现广播方式行不通。一是你不知道该由谁来删除这条消息,另一个则是,同一个消息可能会被同一个消费者消费多次,而也有可能有消费者一次都能不到这条消息。可通过往同一队列发送n条一样的消息,n对应n个消费者,通过给消息设置属性,指定让某个消费者拿到,但不推荐这样做。其次,还可以通过FIFO队列实现,FIFO支持分组,可为不同消费者设置不同分组,每个消费者只订阅一个分组,但需要发送者往每个分组都发送一条通内容的消息。

其它确实没啥可说的,简单就是Amazon Simple Queue Service的特点!