完全な終了を待たなくてはならないから(のはず。中の実装がどうなってるかすべて熟知しているわけではないから言い切れないけど)。
この「完全な終了を待たなければならない」はまずい(美味しくない)。
なぜか?
a->b->c->dとメッセージが詰められるとする。
こんな感じで処理してくれる。
メッセージを詰めるスレッドは処理が終わるのを待たないでクライアントに返事を返せる。
メッセージを処理するワーカーが1台で、1度に1メッセージしか処理できない場合、普通に一つずつメッセージを処理していくだけだから何の問題もない。
何かあって再処理したい場合も、単にretryするようにPublisherにメッセージを返せばいい。
処理量が増えて、ワーカー1台、同時処理1メッセージでは捌ききれなくなったら?
そんな時のためにMessageQueueを使って分散処理できるようにしてあったのだ!
って感じでワーカーを1台増やしてみよう。
たぶん、こうなる。
……あれ?
お分りいただけただろうか?
思い描いていたイメージでは、cの処理がbの処理を追い越しているでしょ?
これを防ぐのが「順番の保証」だから、下の方のように確実に終了したのを待って次のメッセージが飛ぶ(はず。依存関係を持たせたメッセージを飛ばす仕組みはなかったはずだから、こうするしかないはず)。
ワーカーを増やす意味がないことがお分りいただけるだろうか?
ワーカーを増やす意味がないことがお分りいただけるだろうか?
ワーカーを増やす意味がないことがお分りいただけるだろうか?
意味ないんですよ。
このパターンでは。
分散処理にはMessageQueueを使うが、MessageQueueを使いさえすれば(イメージしているような)分散処理になるわけではない。
いや、上の例でも複数のワーカーに処理が分散されてはいるから一応分散処理系ではあるんだよ?(……たぶん)
イメージしている通りのものであるかは別として。
これはワーカーのインスタンス数だけの問題ではない。
ワーカーのインスタンスサイズをあげて複数スレッド実行できるようにしたって無駄だ。
だって、並列実行されないから。
なぜそんな無意味なオプションがあるんだよっ!
って腹をたてるのはお門違い。
これでも意味があるから用意されているオプションなのだから。
想定される目的が異なるだけ。
ネットワーク異常やサービス異常などで処理が異常終了してしまった時、処理落ちしたものを拾い集めて、サーバに接続してコマンドを直接打ち込む必要なく、ほぼ自動で再処理を開始できるからだ。
物理サーバをお守りしていた頃に比べればどれほど楽か……。
SQSに配信順保証のオプションが追加されたのは、そういったリクエストがあったのではなかろうかと思っている(中の人ではないから真相はわからないが)。
まぁ、通常処理がいっぱいいっぱいの場合、停止時間と調査時間分の遅れを取り戻すのは大変だから、やめたほうがいいと思うけど……。
「配信順を保証する」という制約を課すということはこういう処理能力が逼迫してから発生する遅延発火型の副作用があるということだ。
だから、「とりあえずonにしておけば?」はやめてね。
ちゃんと設計してるんだから。
「遅延は大したことないよ」って処理量が少ないからいえることだからね。
あと、「スレッド増やしてもワーカー増やしても処理が早くならないからMessageQueueは意味がない」って吹聴して回るの、やめてね。
MessageQueueを使う場合、他のメッセージと依存関係がある設計にしてはいけない。
それでも依存関係のある処理がある場合はどうするか?
1メッセージで一連の依存関係がある処理がまとめて実行できるように処理を切るか、別キューを用意して前段の処理が終わったら次段のMessageQueueにメッセージを詰めてからackを返すようにすればいい。
簡単じゃろ?
ちなみに、「最低一回は配信を保証する」ってのは「二回以上同じメッセージが配信される可能性がある」ということだが、これもそれほど難しい事情ではない。
処理時間がackを返さなければならない制限を超えてしまったために、見切り発車で再配信される場合がどうしても排除できないから(のはず。これも中の実装がどうなってるか知らないから言い切れないけど)。
その場合に、同じメッセージが見切り発車で二回以上送信されるのだ。
純粋に複数のMessageQueueインスタンスがうっかり二箇所に同じメッセージを送ってしまうことがちょくちょくある、なんて無様な実装しているのはないはずだ。
設定できるなら対応方法はある。
ackを返さなければならない制限時間を無制限にすればいい。
アプリ側の不具合によってackもretryも返せない状態に陥ったらそのメッセージで処理されるはずのデータは電脳の彼方に消えちゃうけどね。
そんなMessageQueueを使うなら、大事なのは「順番が変わろうが複数回処理されようが問題ないように設計しておく」と言うことだ。
「冪等性」って言葉、聞いたことあるでしょ?
それ、これのことよ。
そのためには、処理インスタンス/スレッド特有のパラメータや処理開始時間、再現性のない乱数を使ってはいけないし、データソースに変更を加えてはいけない(readonlyで利用する)。
いつ、どこで、誰が処理しても、同じ入力から同じ結果を出力できなくてはいけないのだ。
ん?
どこかで聞いたことない?
そう、関数型プログラミングってやつだ。
関数型プログラミングが注目されているのは、こういった分散処理のための制約が導入されているからだ。
別にモナドとか圏論とか言いたいだけの話じゃない。
中二じゃないんだから。
単一インスタンスで閉じる処理なら、手続き型にオブジェクト指向を突っ込んだ普通のJavaとかで普通に組めば十分な場合がほとんどだ。
シンプルだし、たぶん早いだろうし、DBでTransactionを有効に使える。
MessageQueueを使った分散系を作ったのに、「冪等性は確保されてますか?」って聞かれて「当然でしょ」と答えたのに、
「処理の順番が入れ替わったら何が起こるかわからない。ボタン一つで設定できるし課金も変わらないから」
と台無しにされた悲しい過去を持つ技術者の呟き(にしては長ぇな……)。


