Overview
Cloud Pub/Sub でトピックを立てて、メッセージの送受信を行うケースにおいて、メッセージの処理プロセスの中でエラーが発生した場合、そのメッセージのハンドリングが必要になります。
Cloud Pub/Sub にはこのエラー処理の仕組みとして Dead Letter Topic というメッセージのエラー処理の機構が実装されているので、この Dead Leatter Topic を用いて、メッセージの処理プロセスでエラーが発生した場合の復旧方法について検討します。
公式のドキュメントとしては以下にかかれている内容になります。
Dead Letter Topic とは?
Cloud Pub/Sub が実装している Dead Letter Queue の仕組みの名称です。
AWS SQS にも Dead Letter Queue という仕組みが用意されており、その Pub/Sub 版と考えるのがわかりやすいかもしれません。
この仕組みがないとどうなるか?というとキューイングされたあるメッセージにはそのキューに残り続けていられる TTL が存在します。
そのため、メッセージを取り出すことなく TTL を過ぎるとそのメッセージは消失して二度と処理に回すことはできません。
通常はこの TTL 以内にメッセージを取り出し、そのメッセージの情報を使って特定の処理を進めます。(ex SQS で UserID を送信して、AWS Lambda を hook し Lambda 上の処理経由でメールを送信する等)
ただし、この処理がいつも正常に完了するとは限りません。
プロセスが動いているランタイム上で異常により処理を進めなくなるかもしれません。このときメッセージングサービス上では何もしないとエラーが発生したメッセージはキューの中に戻ります。明示的に処理を正常系で完了させたり等、メッセージを削除するマーカーを付けなければメッセージは上記の TTL まで生き残っています。ただ、再度処理をしなければ TTL を過ぎたら消えてしまいます。また同じメッセージをもう一度取り出しても同様のエラーが発生してキューに戻る、という操作を繰り返す可能性もあります。
もしこのメッセージの情報が欠損してはいけない情報(決済の情報であったり CRM の配信情報であったり等)の場合、上記のような杜撰なメッセージ管理をするわけには行きません。必ず、あるメッセージが処理を正常に完了できたのか、できなかったのか?といったことを監視し、状態を把握しておく必要があります。
この状態把握の一つの方法として、規定回数以上の試行に失敗したら「失敗をためておく専用の箱」に移動しておき、この箱の中身を監視しておくことで、メッセージの処理の失敗に素早く気付けるようになります。この箱とのことを DLQ (Pub/Sub では DLT) と呼称しています。
Pub/Sub における DLT の設定方法
Terraform では以下の Example のような設定を追記します。
このとき必要になる GCP のリソースが以下です。
- 通常のTopic (この Topic が DLT の対象になる。)
- 通常の Topic に紐づく Subscription
- Dead Letter Topic
- Dead Letter Topic に紐づく Subscription
※ Dead Letter Topic の識別子は Subscription に紐づける。こうすることで Subscription 経由でメッセージを受信し、そのメッセージに紐づく処理を進めていて異常が発生しても、Retry 上限以上に失敗したら DLT に入ります。
DLT に入ったメッセージを最初に回す方法
これが少しややこしいです。
Cloud Pub/Sub には SQS のような DLQ 再送ボタンと呼ばれるものがありません。AWS SQS の場合、例え DLQ に落ちたとしても AWS のコンソールから再処理のワークフローにメッセージを遷移させることは可能ですが、Pub/Sub にはないのでこのメッセージの再処理フローを時前で用意しないといけません。
大きく再処理フローを実装するパターンとしてあるのは2パターンがあると思います。
- DLT に紐づく Subscription を用意し、DLT にメッセージが入ったらその Subscription 経由で GCP 上のランタイムを hook して起動させる。(ex. CloudRun)
- DLT に紐づく Subscription を用意し、定期的(毎分ごとなど)に Message を Pull して処理を進めていく。
1つ目のパターンの場合
シーケンスはざっくり以下のようになるかなと思います。
sequenceDiagram participant p1 as publisher participant p2 as pub/sub(DLT) participant p3 as subscription participant p4 as cloudRun(その他のruntime) p1->>p2: send dead letter message p2->>p3: send message p3->>p4: execute runtime (message) p4->>p4: handling dead letter message
何かしらのメッセージの処理が失敗し DLT にメッセージが入った場合にその最初利用の Runtime (ここでは CloudRun など)内でメッセージの再処理のハンドリングをしたり、このランタイムをプロキシとして他のサービス内でメッセージを処理させます。
GCP 内で完結させる場合、Pub/Sub と CloudRun の連携が簡単にできるので、DLQ に入ったメッセージをハンドリングするランタイムには CloudRun 等 GCP のリソースを選ぶことが多いと思います。(Cloud Function 他)
2つ目のパターンの場合
シーケンスは以下のようになるかなと思います。
sequenceDiagram participant p1 as publisher participant p2 as pub/sub(DLT) participant p3 as subscription participant p4 as runtime p1->>p2: send dead letter message p2->>p3: send message p4-)p4: execute runtime p4->>p3: pull request p3-->>p4: message p4->>p4: handle dead letter message
この場合はパターン1とは異なり、Dead Letter Queue に入ったメッセージの再処理をハンドリングするランタイム側で Pub/Sub の Subscription に pull request を送信し、DLQ に入ったメッセージを取得して再処理をするかどうかのハンドリングをします。
このケースでは、pull request を送信する側は GCP の認証を通してリクエストを送信することができればいいので、GCP のリソースに限らずともランタイムの選択肢が何でも良いのはメリットです。
まとめ
Pub/Sub における Dead Letter Queue の処理方法について調べたのでまとめてみました。AWS SQS と違って再処理ボタンがないことで個人的にはめんどくさい実装を挟まないとメッセージのエラー処理フローを構築できない、というのは若干デメリットに感じました。