連打機能を提供するシステムを構築する必要が出てくることがあります.
例えばあるコンテンツについてボタンを連打することで「良いね」を表明するようなシステムです.
こうしたシステムは素朴に実装してもある程度のトラフィックまでは耐えられるかもしれませんが,ある規模を超えると安定して機能提供する事は難しくなってくるかもしれません.
ここでは,サーバサイドの話題を中心として,快適な連打機能を提供するシステムをどうすれば提供できるかを考えていきます (あくまで一例です).
想定としては,
- あるコンテンツについてボタンが付いていて,そのボタンは連打が出来る
- あるコンテンツについてボタンが何回押されたかを取得できる
というシステムを仮定します.
なんとなく結論が分かる雑な図
本題
サーバを分離する
API を通じてやり取りするアプリケーションを考えた時に,通常の API を提供するサーバと連打可能な API を提供するサーバを分けるという方法が考えられます.
これらのコンポーネントが同居していると,連打機能の影響でシステムの負荷が上昇した時に通常の API まで巻き込んでしまうので,分離させておくことでそうしたリスクを排除することが可能となります.
また,それぞれのコンポーネントについてインスタンスを適宜追加投入することでスケールアウトが可能となるので,負荷が高まった時に柔軟な対応を取ることができるようになります.
リクエスト自体を少なくする
連打可能なボタンを設置した時に,1回ボタンが押されるごとに API が呼ばれては大変です.
クライアント側でそのイベントをバッファしておき,あるタイミングでそれらをまとめて1つのリクエストとして扱うといった方法をとることで,総リクエスト数を減らすということが出来ます.リクエストを減らすというのは,負荷に対する根本的な解決の1つと言えるでしょう.
こういう風にバッファリングすることでリクエスト数を減らすという方法はクライアント側の実装にある程度の負担を強いることとなりますが,そこは開発のコストのバランス感という感じでしょう.
また,不正行為への対策も必要となる場合があります.例えば,「あるタイムウィンドウ内に何回押されたか」を束ねて1リクエストにする方法を考えた時に,「何回押されたか」をユーザの手によって詐称されて “LONG_MAX” なんかを送られてきた日には目も当てられません.こうした異常な入力に対してはサーバ側で何らかの対策を取る必要が出てくるでしょう.
高速なストレージを使う
memcached や Redis というような高速なストレージを利用するという話題です.
ある程度の規模感までは RDBMS でも耐えられるかもしれませんが,連打可能なサービスは往々にして I/O が増えがちとなるのでメモリストレージを利用できれば考えることが劇的に減り楽ができるようになります.
また,メモリストレージを使うのであればバックアップについて考えておく必要が出てくると思います.メモリストレージに全ての信頼を寄せるのであれば不要かもしれませんが,僕は心が弱いので特定のタイミングで Redis に乗っているデータを MySQL に掃き出すということをやっています.
データ構造をシンプルにする
上記のメモリストレージを使うという話題にも共通するトピックです.
データ構造が複雑であれば複雑であるほど,メモリストレージに乗せにくくなり,またカウントをシュッと取得することが難しくなります.
いかに格納を高速にするか,いかに参照を高速にするかはデータ構造のシンプルさにかかっていると言うことができます.
例えばこの記事で想定しているシステムだと,シンプルに「ボタンが何回押されたか」というカウント情報のみをストレージに保持するなどといった構造を考えることが出来るでしょう.
まとめ
連打できる要素が用意されると人間は連打をしがちです.基本的に連打という営みは楽しいからです.
快適な連打機能を提供するためには色々な工夫をする必要があり,本記事では以下のようにその一例について述べました.
- サーバを分離する
- リクエスト自体を少なくする
- 高速なストレージを使う
- データ構造をシンプルにする
- 解析用のストレージは別に用意する (そして非同期で格納する)
とは言え「誰が,いつ,何回押したか」をリアルタイムかつランキング形式で出したい!!! などといったクレイジーな要件が出てくるとこの記事で書いたようなシンプルな方法では難しくなるので,更に考える必要が出てきます.世の中は難しい.地獄じゃ.
また,本記事では非同期プログラミングや HTTP/2 に関する話題については触れませんでしたが,こうした技術は快適な連打を支える一助になるかもしれません.
快適な連打を支えるにはここで書いた他にももっと効果的な方法があることと思います.何か知っている方がいたら語らいましょう.
*1:同期的にやるとその I/O 処理に引きずられてしまう