Redis
 Computer >> コンピューター >  >> プログラミング >> Redis

巨大メッセージをさばく技術:プロセス分割とRedisカウンターで実現するスケーラブルなシステム設計

はじめに

著者:Pramono Winata

1つのプロセスが大きすぎたり重すぎたりして、うまく扱えずに困ったことはありませんか?もしそうなら、この記事がその悩みを解決するヒントになるはずです。

本記事では、単一のプロセスでは処理しきれないほど大きなメッセージを、どのように管理しているのかを紹介します。メッセージを複数のチャンクに分割し、それぞれを独立したプロセスとして処理するようにしました。

技術的な詳細よりも、アーキテクチャの設計プロセスに焦点を当てます。キャッシュやPub/Subについても少し触れますが、実装の細部には立ち入りません。あくまで「パターン」そのものを解説します。

課題:なぜプロセスを分割する必要があるのか

まず頭に浮かぶ疑問は、「なぜ1つのプロセスを複数の並行プロセスに分割する必要があるのか」ということでしょう。

理由はいくつか考えられますが、私の場合はシンプルに「メッセージが大きすぎた」ことが原因でした。

状況をイメージしてもらうために、簡単な構成で説明します。サービスAとサービスBという2つの別々のサービスがあり、その間にPub/Subサービスが存在する構成です。

Pub/Subサービスが何かわからない方のために補足すると、あるサービスからのメッセージを別のサービスへ届ける仲介役(ブローカー)だと考えてください。

サービスAがメッセージをパブリッシュすると、それがPub/Sub経由でサービスBに渡され、処理されます。処理が完了したら、そのメッセージが処理済みであることを示す後続の処理を行います。

シンプルですよね?

ところが、メッセージが大きすぎる場合、Pub/Subサービスの制限によってパブリッシュに失敗することがあります。

これで、私が直面した問題の全体像をつかめたと思います。では、この問題をどう解決したのでしょうか?次のセクションで解決策を紹介します。

最初のアプローチ

最初に思いついたのは、Pub/Subサービスが扱えるサイズの上限を引き上げることでした。これは設定を1つ変更するだけで実現できます。

しかし、そんなに簡単に解決できたら話は早すぎますよね。そもそも、メッセージがさらに大きくなり続けたらどうなるでしょうか?そのたびにPub/Subの上限を増やし続けるのでしょうか?

実際、このやり方は多くのスケーラビリティ上の問題を引き起こします。長期的な解決策としては好ましくありません。

そこで、別の解決策を思いつきました。メッセージを複数の小さなメッセージに分割し、それぞれを個別に処理するというものです。

図を見るとわかるように、メッセージは複数の小さなメッセージに分割されます。どのように分割するか、メッセージのどの部分を分割対象にするかは、ケースやフローによって異なります。

私の場合、メッセージの中身がアイテムのリストだったため、アイテムごとに分割できました。

例えば10個のアイテムがあるとします。以前は10個すべてを1つのメッセージとしてパブリッシュしていましたが、分割後はそのメッセージが10個のメッセージになります。

結果として、1つのプロセスが複数のプロセスに変わります。1回のパブリッシュが10回のパブリッシュになり、1つのプロセスが10個のプロセスになるのです。

一見すると理想的ではないように見えるかもしれませんが、これが私の出した最善の解決策であり、実際にうまく機能しています。

では、分割すればそれで終わりなのでしょうか?

実はそうではありません。「処理完了のマークをつける」という最後の部分を覚えていますか?新しい構成図からその部分が消えていることに気づいたかもしれません。

ご安心ください。忘れたわけではなく、あえて次のパートのために残しておいたのです。

問題は、メッセージを分割して複数のプロセスに分けると、システム全体として「すべての処理が完了したかどうか」を把握できなくなることです。これも対処すべき大きな課題ですが、幸いこちらにも解決策を見つけられました。

処理の完了を検知する仕組み

では、複数のプロセスが同時に走っている状態で、全体の処理が完了したことをどうやって知ればいいのでしょうか?

私が考案したのは、「完了すべきプロセスの総数を保存しておき、1つのプロセスが終わるごとにカウントを減らしていく」という方法です。こうすれば、最後のプロセスが終わったタイミングを正確に把握できます。

信頼できるデータの保存場所さえあれば、非常にシンプルな仕組みです。そして、その選択肢は実はたくさんあります。そのひとつがRedisで、私はこの問題に対してRedisを採用しました。

Redisをご存じない方のために説明すると、Redisは主にキャッシュとして利用される高速なインメモリデータストアです。

処理の流れ自体は以前とまったく同じですが、途中にRedisが加わります。重要なのは、有効な初期カウント値を設定することです。

私の場合、リストをパブリッシュしているので、リストの長さをそのまま初期カウンターにできます。そして、各プロセスが完了するたびにカウンターを1ずつ減らしていきます。Redisのカウンターを参照すれば、すべてのプロセスが完了したかどうかがひと目でわかります。カウンターが0になれば、全プロセスの完了を安全にマークできるというわけです。

まとめ

まとめると、私は大きなメッセージを複数のメッセージに分割し、それぞれを並行して動く複数のプロセスで処理するようにしました。そして、その処理状況の管理にはRedisのキャッシュを活用しています。

ただし、ここで紹介した解決策が、巨大なメッセージ処理の問題に対する万能薬(シルバーブレット)というわけではありません。メッセージをストリーミングで扱うなど、他のアプローチもありますが、それはまた別の機会にお話ししたいと思います。

最後までお読みいただきありがとうございました!この記事を楽しんでいただけたこと、そして何よりお役に立てたことを心から願っています。

  1. SQLパフォーマンスを劇的に向上:Drizzle ORMクエリのためのUpstash Redisキャッシュ活用法

    先日、私たちはDrizzle ORMとのコラボレーションを実現する機会に恵まれました。 TypeScript ORMとしてコミュニティから絶大な支持を集めるDrizzle ORM。そのため、「はい 😳」と答えるのは簡単な決断でした: 本記事では、Upstash Redis × Drizzleのキャッシング統合がどのようにSQLパフォーマンスを向上させるのか、そしてLuaスクリプトとハッシュデータ構造を活用してこの統合を最適化した手法について詳しく解説します。 課題:モダンアプリケーションにおけるSQLパフォーマンス 従来のSQLデータベースは一貫性や複雑なリレーションのモデル化に優れていま

  2. Redisでリストの最後の要素を削除して取得する方法 – RPOP / BRPOPコマンド徹底解説

    このチュートリアルでは、Redisデータストアに保存されたリスト型の値から、最後の要素を削除しつつ取り出す(ポップする)方法を学びます。この操作には、RedisのRPOPコマンドとBRPOPコマンドを使用します。 RPOPコマンドとは RPOPは、指定したキーに格納されているリストの末尾(最後)の要素を削除し、その値を返すコマンドです。 キーが存在しない場合は (nil) が返されます。 キーは存在するが、格納されている値がリスト型ではない場合はエラーが返されます。 基本構文 redis host:post> RPOP <keyname> 戻り値 - (string) リ