簡體   English   中英

在 Amazon SQS 中將消息移出 DLQ 的最佳方式?

[英]Best way to move messages off DLQ in Amazon SQS?

將消息從死信隊列移回 Amazon SQS 中的原始隊列的最佳做法是什么?

可不可能是

  1. 從 DLQ 獲取消息
  2. 將消息寫入隊列
  3. 從 DLQ 中刪除消息

或者有更簡單的方法嗎?

此外,AWS 最終會在控制台中提供一個工具來將消息從 DLQ 中移出嗎?

這是一個快速的技巧。 這絕對不是最好或推薦的選擇。

  1. 將主 SQS 隊列設置為實際 DLQ 的 DLQ,最大接收數為 1。
  2. 查看 DLQ 中的內容(這會將消息移動到主隊列,因為這是實際 DLQ 的 DLQ)
  3. 刪除設置,使主隊列不再是實際 DLQ 的 DLQ

有一些腳本可以為您執行此操作:

  • 基於 npm/nodejs: http ://github.com/garryyao/replay-aws-dlq
# install
npm install replay-aws-dlq;

# use
npx replay-aws-dlq [source_queue_url] [dest_queue_url]
# compile: https://github.com/mercury2269/sqsmover#compiling-from-source

# use
sqsmover -s [source_queue_url] -d [dest_queue_url] 

不需要移動消息,因為它會帶來許多其他挑戰,如重復消息、恢復場景、丟失消息、重復數據刪除檢查等。

這是我們實施的解決方案 -

通常,我們將 DLQ 用於暫時性錯誤,而不是永久性錯誤。 所以采取了以下方法 -

  1. 像普通隊列一樣從 DLQ 讀取消息

    好處
    • 避免重復消息處理
    • 更好地控制 DLQ - 就像我檢查一樣,只有在完全處理完常規隊列時才進行處理。
    • 根據 DLQ 上的消息放大流程
  2. 然后遵循常規隊列所遵循的相同代碼。

  3. 在中止作業或進程在處理時終止的情況下更可靠(例如實例被終止或進程終止)

    好處
    • 代碼可重用性
    • 錯誤處理
    • 恢復和消息重放
  4. 擴展消息可見性,以便沒有其他線程處理它們。

    益處
    • 避免多個線程處理相同的記錄。
  5. 僅當出現永久性錯誤或成功時才刪除消息。

    益處
    • 繼續處理,直到我們收到暫時性錯誤。

我使用 boto3 lib 編寫了一個小的 python 腳本來執行此操作:

conf = {
  "sqs-access-key": "",
  "sqs-secret-key": "",
  "reader-sqs-queue": "",
  "writer-sqs-queue": "",
  "message-group-id": ""
}

import boto3
client = boto3.client(
    'sqs',
        aws_access_key_id       = conf.get('sqs-access-key'),
        aws_secret_access_key   = conf.get('sqs-secret-key')
)

while True:
    messages = client.receive_message(QueueUrl=conf['reader-sqs-queue'], MaxNumberOfMessages=10, WaitTimeSeconds=10)

    if 'Messages' in messages:
        for m in messages['Messages']:
            print(m['Body'])
            ret = client.send_message( QueueUrl=conf['writer-sqs-queue'], MessageBody=m['Body'], MessageGroupId=conf['message-group-id'])
            print(ret)
            client.delete_message(QueueUrl=conf['reader-sqs-queue'], ReceiptHandle=m['ReceiptHandle'])
    else:
        print('Queue is currently empty or messages are invisible')
        break

您可以在此鏈接中獲取此腳本

這個腳本基本上可以在任意隊列之間移動消息。 它支持先進先出隊列以及您可以提供message_group_id字段。

這看起來是你最好的選擇。 您的流程有可能在第 2 步之后失敗。在這種情況下,您最終將復制消息兩次,但您的應用程序應該處理消息的重新傳遞(或不關心)。

這里:

import boto3
import sys
import Queue
import threading

work_queue = Queue.Queue()

sqs = boto3.resource('sqs')

from_q_name = sys.argv[1]
to_q_name = sys.argv[2]
print("From: " + from_q_name + " To: " + to_q_name)

from_q = sqs.get_queue_by_name(QueueName=from_q_name)
to_q = sqs.get_queue_by_name(QueueName=to_q_name)

def process_queue():
    while True:
        messages = work_queue.get()

        bodies = list()
        for i in range(0, len(messages)):
            bodies.append({'Id': str(i+1), 'MessageBody': messages[i].body})

        to_q.send_messages(Entries=bodies)

        for message in messages:
            print("Coppied " + str(message.body))
            message.delete()

for i in range(10):
     t = threading.Thread(target=process_queue)
     t.daemon = True
     t.start()

while True:
    messages = list()
    for message in from_q.receive_messages(
            MaxNumberOfMessages=10,
            VisibilityTimeout=123,
            WaitTimeSeconds=20):
        messages.append(message)
    work_queue.put(messages)

work_queue.join()

還有另一種方法可以在不編寫單行代碼的情況下實現這一點。 考慮您的實際隊列名稱是 SQS_Queue,而它的 DLQ 是 SQS_DLQ。 現在按照以下步驟操作:

  1. 將 SQS_Queue 設置為 SQS_DLQ 的 dlq。 由於 SQS_DLQ 已經是 SQS_Queue 的一個 dlq。 現在,兩者都充當另一個的 dlq。
  2. 將 SQS_DLQ 的最大接收計數設置為 1。
  3. 現在從 SQS_DLQ 控制台讀取消息。 由於消息接收計數為 1,它會將所有消息發送到它自己的 dlq,這是您實際的 SQS_Queue 隊列。

我們使用以下腳本將消息從 src 隊列重新驅動到 tgt 隊列:

文件名: redrive.py

用法: python redrive.py -s {source queue name} -t {target queue name}

'''
This script is used to redrive message in (src) queue to (tgt) queue

The solution is to set the Target Queue as the Source Queue's Dead Letter Queue.
Also set Source Queue's redrive policy, Maximum Receives to 1. 
Also set Source Queue's VisibilityTimeout to 5 seconds (a small period)
Then read data from the Source Queue.

Source Queue's Redrive Policy will copy the message to the Target Queue.
'''
import argparse
import json
import boto3
sqs = boto3.client('sqs')


def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument('-s', '--src', required=True,
                        help='Name of source SQS')
    parser.add_argument('-t', '--tgt', required=True,
                        help='Name of targeted SQS')

    args = parser.parse_args()
    return args


def verify_queue(queue_name):
    queue_url = sqs.get_queue_url(QueueName=queue_name)
    return True if queue_url.get('QueueUrl') else False


def get_queue_attribute(queue_url):
    queue_attributes = sqs.get_queue_attributes(
        QueueUrl=queue_url,
        AttributeNames=['All'])['Attributes']
    print(queue_attributes)

    return queue_attributes


def main():
    args = parse_args()
    for q in [args.src, args.tgt]:
        if not verify_queue(q):
            print(f"Cannot find {q} in AWS SQS")

    src_queue_url = sqs.get_queue_url(QueueName=args.src)['QueueUrl']

    target_queue_url = sqs.get_queue_url(QueueName=args.tgt)['QueueUrl']
    target_queue_attributes = get_queue_attribute(target_queue_url)

    # Set the Source Queue's Redrive policy
    redrive_policy = {
        'deadLetterTargetArn': target_queue_attributes['QueueArn'],
        'maxReceiveCount': '1'
    }
    sqs.set_queue_attributes(
        QueueUrl=src_queue_url,
        Attributes={
            'VisibilityTimeout': '5',
            'RedrivePolicy': json.dumps(redrive_policy)
        }
    )
    get_queue_attribute(src_queue_url)

    # read all messages
    num_received = 0
    while True:
        try:
            resp = sqs.receive_message(
                QueueUrl=src_queue_url,
                MaxNumberOfMessages=10,
                AttributeNames=['All'],
                WaitTimeSeconds=5)

            num_message = len(resp.get('Messages', []))
            if not num_message:
                break

            num_received += num_message
        except Exception:
            break
    print(f"Redrive {num_received} messages")

    # Reset the Source Queue's Redrive policy
    sqs.set_queue_attributes(
        QueueUrl=src_queue_url,
        Attributes={
            'VisibilityTimeout': '30',
            'RedrivePolicy': ''
        }
    )
    get_queue_attribute(src_queue_url)


if __name__ == "__main__":
    main()

DLQ 只在原消費者多次嘗試消費消息失敗時才起作用。 我們不想刪除該消息,因為我們相信我們仍然可以用它做一些事情(也許嘗試再次處理或記錄它或收集一些統計信息)並且我們不想一次又一次地遇到此消息並停止能力處理此消息背后的其他消息。

DLQ 只不過是另一個隊列。 這意味着我們需要為 DLQ 編寫一個消費者,它在理想情況下運行頻率較低(與原始隊列相比),它將從 DLQ 消費並將消息生成回原始隊列並從 DLQ 中刪除它 - 如果這是預期的行為,我們認為原始消費者現在准備再次處理它。 如果這個循環持續一段時間應該沒問題,因為我們現在也有機會手動檢查並進行必要的更改並部署另一個版本的原始使用者而不會丟失消息(當然在消息保留期限內 - 這是 4 天)默認)。

如果 AWS 提供這種開箱即用的功能會很好,但我還沒有看到它 - 他們將它留給最終用戶以他們認為合適的方式使用它。

這也是將消息從一個 AWS 隊列移動到另一個 AWS 隊列的腳本(用Typescript編寫)。 也許它對某人有用。


import {
    SQSClient,
    ReceiveMessageCommand,
    DeleteMessageBatchCommand,
    SendMessageBatchCommand,
} from '@aws-sdk/client-sqs'

const AWS_REGION = 'eu-west-1'
const AWS_ACCOUNT = '12345678901'

const DLQ = `https://sqs.${AWS_REGION}.amazonaws.com/${AWS_ACCOUNT}/dead-letter-queue`
const QUEUE = `https://sqs.${AWS_REGION}.amazonaws.com/${AWS_ACCOUNT}/queue`

const loadMessagesFromDLQ = async () => {
    const client = new SQSClient({region: AWS_REGION})
    const command = new ReceiveMessageCommand({
        QueueUrl: DLQ,
        MaxNumberOfMessages: 10,
        VisibilityTimeout: 60,
    })
    const response = await client.send(command)

    console.log('---------LOAD MESSAGES----------')
    console.log(`Loaded: ${response.Messages?.length}`)
    console.log(JSON.stringify(response, null, 4))
    return response
}

const sendMessagesToQueue = async (entries: Array<{Id: string, MessageBody: string}>) => {
    const client = new SQSClient({region: AWS_REGION})
    const command = new SendMessageBatchCommand({
        QueueUrl: QUEUE,
        Entries: entries.map(entry => ({...entry, DelaySeconds: 10})),
        // [
        // {
        //     Id: '',
        //     MessageBody: '',
        //     DelaySeconds: 10
        // }
        // ]
    })
    const response = await client.send(command)
    console.log('---------SEND MESSAGES----------')
    console.log(`Send: Successful - ${response.Successful?.length}, Failed: ${response.Failed?.length}`)
    console.log(JSON.stringify(response, null, 4))
}

const deleteMessagesFromQueue = async (entries: Array<{Id: string, ReceiptHandle: string}>) => {
    const client = new SQSClient({region: AWS_REGION})
    const command = new DeleteMessageBatchCommand({
        QueueUrl: DLQ,
        Entries: entries,
        // [
        //     {
        //         "Id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        //         "ReceiptHandle": "someReceiptHandle"
        //     }
        // ]
    })
    const response = await client.send(command)
    console.log('---------DELETE MESSAGES----------')
    console.log(`Delete: Successful - ${response.Successful?.length}, Failed: ${response.Failed?.length}`)
    console.log(JSON.stringify(response, null, 4))
}

const run = async () => {
    const dlqMessageList = await loadMessagesFromDLQ()

    if (!dlqMessageList || !dlqMessageList.Messages) {
        console.log('There is no messages in DLQ')
        return
    }

    const sendMsgList: any = dlqMessageList.Messages.map(msg => ({ Id: msg.MessageId, MessageBody: msg.Body}))
    const deleteMsgList: any = dlqMessageList.Messages.map(msg => ({ Id: msg.MessageId, ReceiptHandle: msg.ReceiptHandle}))

    await sendMessagesToQueue(sendMsgList)
    await deleteMessagesFromQueue(deleteMsgList)
}

run()


PS 該腳本有改進的余地,但無論如何可能會有用。

AWS Lambda 解決方案對我們來說效果很好 -

詳細說明: https : //serverlessrepo.aws.amazon.com/applications/arn : aws : serverlessrepo : us-east-1 : 303769779339 : applications~aws-sqs-dlq-redriver

Github: https : //github.com/honglu/aws-sqs-dlq-redriver

一鍵部署,再單擊即可開始重新驅動!

AWS 終於發布了將消息從 DLQ 重新驅動回源隊列的功能。 剛剛自己測試了它,它的工作原理完全符合您的預期,重新驅動非常快速和簡單。

對於想要快速修復的人:

  1. 導航到 DLQ
  2. 單擊開始 DLQ 重新驅動
  3. 確保選擇“重新驅動到源隊列”
  4. 單擊 DLQ 重新驅動

你可以在這里讀更多關於它的內容:

https://aws.amazon.com/blogs/compute/introducing-amazon-simple-queue-service-dead-letter-queue-redrive-to-source-queues/

希望有人覺得這有幫助!

這是一個簡單的 python 腳本,您可以從 cli 使用它來執行相同的操作,僅取決於 boto3

用法

python redrive_messages __from_queue_name__ __to_queue_name__

代碼

import sys
import boto3

from src.utils.get_config.get_config import get_config
from src.utils.get_logger import get_logger

sqs = boto3.resource('sqs')

config = get_config()
log = get_logger()

def redrive_messages(from_queue_name:str, to_queue_name:str):
  # initialize the queues
  from_queue = sqs.get_queue_by_name(QueueName=from_queue_name)
  to_queue = sqs.get_queue_by_name(QueueName=to_queue_name)

  # begin querying for messages
  should_check_for_more = True
  messages_processed = []
  while (should_check_for_more):
    # grab the next message
    messages = from_queue.receive_messages(MaxNumberOfMessages=1);
    if (len(messages) == 0):
      should_check_for_more = False;
      break;
    message = messages[0]

    # requeue it
    to_queue.send_message(MessageBody=message.body, DelaySeconds=0)

    # let the queue know that the message was processed successfully
    messages_processed.append(message)
    message.delete()
  print(f'requeued {len(messages_processed)} messages')

if __name__ == '__main__':
  from_queue_name = sys.argv[1]
  to_queue_name = sys.argv[2]
  redrive_messages(from_queue_name, to_queue_name)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM