Amazon SQS 觸發 AWS Lambda 及重試/DLQ
Amazon 在 2018 年 6 月份宣佈可以設定用 SQS 來觸發 Lambda,SQS 不再是單純用於 ECS 服務中,或用於伸縮控制的。這兒就來親自嘗試一下用 SQS 驅動的 Lambda,以及要注意的要素。
首先使用 Java 編寫 Lambda 的話,AWS 在 com.amazonaws:aws-lambda-java-events:2.20 版本開始加入了 com.amazonaws.services.lambda.runtime.events.SQSEvent 類,可是這個版本的 aws-lambda-java-events 是有所限的,因為 SQSEvent.SQSMessage
類是私有的,這就造成不能獲取到 SQSEvent 中的記錄資料。
//下面的操作程式碼無法編譯,因為 SQSEvent.SQSMessage 是私有的,不可訪問
SQSEvent.SQSMessage sqs = sqsEvent.getRecords().get(0);
sqsEvent.getRecords().get(0).getBody();
Java 使用 SQS 來驅動 Lambda 的話,至少需要 com.amazonaws:aws-lambda-java-events:2.2.1 版本,從此 SQSEvent.SQSMessage 變成 public 了。該版本是於 2018 年 6 月傳到 Maven 官方中央倉庫的,這就是那時才能真正用來寫 Java 的 SQS 觸發的 Lambda.
同時此篇也是作為上文 ofollow,noindex" target="_blank">AWS Lambda 重試與死信佇列(DLQ) 的一個很重要的補充。在此也會驗證 SQS 觸發的 Lambda 的重試機制以及 DLQ 相的內容。
建立兩個 SQS 佇列
test-sqs-queue
用於觸發 Lambda
test-sqs-dlq-queue
不能被 Lambda 觸發的訊息希望達到最大重試次數後轉到該死信佇列中去
Lambda 程式碼
public class Handler implements RequestHandler<SQSEvent, Object> { @Override public Object handleRequest(SQSEvent sqsEvent, Context context) { try { System.out.println("received: " + ObjectMapperSingleton.getObjectMapper().writeValueAsString(sqsEvent)); process(sqsEvent); } catch (Exception ex) { throw ex; } return null; } private void process(SQSEvent sqsEvent) { SQSEvent.SQSMessage sqs = sqsEvent.getRecords().get(0); if(sqs.getBody().contains("dlq")) { throw new RuntimeException("dlq"); } } }
想法是希望在處理 SQS 中的訊息時,如果訊息體中含有 dlq
字串丟擲異常,重試若干次後轉入到 test-sqs-dlq-queue
中去。
部署 Lambda
打成可部署的 Lambda jar 包自是不必說,主要強調的是關於執行 Lambda role 和 SQS 的超時設定
該 Lambda 如果設定由 test-sqs-queue
來觸發(可以設定 batch size 大小),那麼執行該 Lambda 的 role 必須要有針對 SQS 主題 test-sqs-queue
的以下三個許可權
sqs:ReceiveMessage //Lambda 由 SNS 觸發是不需要特別的 SNS 相關許可權
sqs:DeleteMessage //Lambda 執行成功後會刪除處理過的訊息
sqs:GetQueueAttributes
並且該 SQS 主題 test-sqs-queue
的超時 Default Visibility Timeout 不得小於該 Lambda 的 Timeout 設定。
同時設定該 Lambda 的 DLQ 為 test-sqs-dlq-queue
。下面的測試實際可看到由 SQS 觸發的 Lambda,再設定 DLQ 是沒有意義的。
測試用 SQS 驅動 Lambda
我們往 test-sqs-queue
中放入一條訊息,內容為 hello dlq exception
, 使得 Lambda 丟擲異常。我們立馬可以看到 Lambda 被 SQS 觸發,並且每次執行都丟擲異常,至少該訊息始終無法從佇列 test-sqs-queue
中清除掉。
連續觀察一段時間,看到該 Lambda 每隔一分鐘(由於 test-sqs-queue 設定的 Default Visibility Timeout 是 60 秒)重試一次,永不停歇。因此即例設定了該 Lambda 的 DLQ, 該訊息都沒有機會送入到 DLQ 中去。
每一分鐘重試一次是因為 test-sqs-queue
設定的 Default Visibility Timeout 是 60 秒,所以 Lambda 取出訊息(Message In Flight),處理出現異常,不能刪除該訊息,一分鐘後該訊息又變為 Available。再取出訊息處理,異常,訊息回佇列,周而復始,每次在訊息在外面呆一分鐘後又回隊。
如果設定 test-sqs-queue
的 Default Visibility Timeout 為 2 分鐘,那麼就是每 2 分鐘重試一次,一直持續下去。
我們也可以測試一個能正常處理的邏輯,把 test-sqs-quque
清空掉,併發一條內容為 hello
的訊息,我們會發現 Lambda 處理完馬上該訊息從佇列中消失了。
SQS 驅動的 Lambda 的重試與 DLQ 設定
用 SQS 驅動的 Lambda 仍然可以為 Lambda 設定 DLQ,但是這個 DLQ 設定是沒用的了,並且是一個陷阱,如果沒有為源 SQS 佇列設定 DLQ 的情況下,該 Lambda 的 DLQ 設定會造成無限重試,直到訊息失效為止, 預設為 4 天,這期間如果是按預設的 Default Visibility Time 30 秒重試一次,那麼 Lambda 被呼叫的費用也相當可觀。
所以一定要注意:使用 SQS 來驅動 Lambda 的話,千萬不要設定 Lambda 的 DLQ,而應當設定源 SQS 佇列的 DLQ。
如下圖
針對源 SQS 佇列 test-sqs-queue
, 設定重試 3 次後訊息轉移到 DLQ(Dead Letter Queue) 佇列 test-sqs-dlq-queue
, 從此該訊息不再觸發 Lambda 了,後續自行額外處理。
SQS 觸發器的 Batch Size
SQS 的觸發器也有像 Kinesis 觸發器那樣的 Batch Size, 它們是類似的,也是決定了 sqsEvent.getRecords() 中最大的記錄數。至於併發的 Lambda 例項數目尚不清楚。
現在我們可進步測試 Lambda 取到 SQS 多條記錄時,丟擲異常時怎麼處理訊息的。為此我們把上面的 process 方法改動一下
private void process(SQSEvent sqsEvent) { if(sqsEvent.getRecords().stream().anyMatch(sqs->sqs.getBody().contains("dlq"))) { throw new RuntimeException("dlq"); } }
Batch 中只要含有一條訊息有 dlq
字樣的就丟擲異常,然後設定 SQS 觸發器的 Batch Size 為 10, 再往佇列 test-sqs-queue
放入以下 16 條訊息
a, dlq, b, dlq, c, dlq, d, dlq, e, dlq, f, dlq, f, dlq, g, dlq
SQS 觸發的 Lambda 處理 SQS 訊息的機制是,如果 Lambda 能正常處理所有取到的訊息後,就把它們從佇列中全部刪除,如果有任意的異常發生,那麼它們全部重新歸隊。其中某個訊息達到最大的重試次數就進到所設定 SQS DLQ 中去,這很容易棒殺正常的訊息。
比如按照上面的 test-sqs-queue
的 DLQ 設定,並且 SQS 觸發器的 Batch Size 為 2 的情況下
test-sqs-queue test-sqs-queue test-sqs-queue
上面最冤枉的莫過於訊息 b
了,即便是良民,因為每次沒跟對人,最後的下場與壞人一般,被送到了 DLQ test-sqs-dlq-queue
去了。
關於 SQS 觸發器,Batch Size 是一個值得當心的地方,Batch Size 為 1 自然是最安全的,但效率會是個問題。也許必要時我們可以考慮在處理完 Batch 中的某一條訊息後手工從源 SQS 佇列中把該條訊息刪除掉,由 SQSMessage 是能夠獲得 receiptHandle 的。