TPDN Blog
2022-05-12

もし本当に「1000万ユーザー想定する必要がある」場合、何を考えるのか

多数のユーザーを抱える製品を支えるエンジニアとして、私個人は何を考えているか

KEY POINTS
  • 「机上の空論において1000万ユーザに耐えるサーバーである」と宣言するなら、裏付けが必要
  • 1000万ユーザーに耐えられるかどうかはサーバーだけでは決まらない
  • ユーザーの多い製品の運用開発は総合格闘技みたいなもの

最近話題になった記事にこのようなものがあった。

1000万ユーザに耐えるサーバを作ってみた

https://zenn.dev/higashimura/articles/74c6e6bf63a133

「1000万ユーザに耐えるサーバ」とのことで、ソースコードは綺麗そうだし実装は真っ当だとおもう。

ところで自称1000万ユーザに耐えるサーバーとは何なのだろうか?

ハテブとかTwitterにみんな好き勝手書いてるが、何が言いたいのかよくわからなかった。(タイトルが大げさすぎるというのは共通していた)

なので自分が開発でどんなことを考えているかをざっくりと書き出してみた。

前提

  • あくまで考え方についてざっくりと書いている
    • 何をやるかについてはあまり書いていない
    • 「DynamoDB使えばOK」みたいなことは一切書いていない
  • なるべく何かに依存しない汎用的な考えを書いているつもりだが、どうしても偏りがある
    • 私の経験はウェブ・ゲーム・スマートフォンアプリに偏っている
      • もっというなら、B2Cの自社サービスに偏っている
    • 負荷に対する対応は、スパイクをかなり意識している
  • セクションを厳密に分けている企業(分けられるほどのリソースがある企業)では、ここで書いていることの大半は役に立たないかもしれない

製品の性質・負荷の想定

製品やユーザーの性質

そもそも1000万ユーザーといっても色々ある。というより総ユーザー数だけでは状態を表現できない。 総ユーザーが1000万だから常に全ユーザー常時ログインしているとは限らないし、全ユーザーが常にRead系の負荷を生んでいるわけでもないし、全ユーザーがガチ勢というのも現実では想定しがたい。

なのでまず製品の性質・ユーザーの性質を考えたい。いわゆるユーザーペルソナ設定みたいなものと思えばよい。 1000万ユーザーを想定するほど成長した製品であれば、豊富なログやレポートが必ずある。なのでそこまで難しく考えることではない。

  • ユーザーの性質はどうなのか?
    • ユーザーが大量に居るがほとんどログイン勢で、アクティブなユーザー数は少ない
    • 一部ユーザーはフレンド数が異常に多い
    • リセマラ勢がユーザーを作っては捨てまくる
  • 考えられる行動は?
    • お昼休みの時間帯に特有の行動を行う
    • お正月の00:00:00に一斉投稿
    • LINEトークやテレビ放送などをきっかけに多数のユーザーが同一の行動を行う

性質がわかればベンチマークテストのテストケースを作成したり、シミュレーションを行ったり、過去の実績と比較することでの負荷予測ができる。 ここで初めて「机上の空論において1000万ユーザに耐えるサーバーである」と宣言できる。

情報不足で本当に何もかも全くわからない(だからテストケースが作成できない)と感じるなら、1000万ユーザー対応以前にやることがあるのではと思う。

負荷の想定

製品の性質がわかれば、おそらくどんな種類の負荷がかかるのかが推定できるようになる。

  • どんなクエリーが発行され、どんな実行計画で処理されるのか
    • 本当に想定通り(テストと同様)の動きをする?
    • 大量取得した時に予想外に費用や時間がかかったりしないか
    • どのようなロックがかかるのか
  • 古いデータを参照する頻度はどれくらいあるか
    • 新しいデータだけ読めればよいなら思い切ったことができるかも
  • どのようなコンポーネントがどれくらいの頻度で呼ばれるか、偏りが生じるか
    • 熱心なユーザーが特定のshardに集中しているとかは起こり得る?
  • 回線にどれくらい負荷がかかるのか
    • 実装が荒いとデータベースやオブジェクトストレージやキャッシュサーバー呼び出しも馬鹿にならない
  • キャッシュは効く状態なのか
    • キャッシュサーバーのみを指しているわけではない(どこにでもキャッシュはある)
    • 書き込みが集中すると、実質的にキャッシュが効かないかもしれない
    • たまにしか使わないユーザーが多い場合だと、キャッシュが温まってないかもしれない

厳密さが要求される優先度の高い処理・厳密さが要求されない優先度の低い処理の分離

サーバー上では多様な処理が行われている。だがそれらの多様な処理は必ず全部まとめて行う必要があるわけではない。非同期に処理できるよう、分離できるなら分離しておきたい。

厳密さが要求される処理

例えば課金や購入が絡む処理は見るからに厳密そうである。トランザクション処理のような高負荷になりやすそうな処理を即時に行う必要がある。

厳密さが要求されない処理

ユーザーによるコンテンツの投稿は即時反映する必要がないかもしれない。 同様に特定の事象を起こすと報酬がもらえるようなものの判定(いわゆるミッション)はそれほど厳密さは要求されないだろう。

課金に関連する処理であっても、課金により貯まるポイントに関連する処理(いわゆるVIP制度)や、不届きなユーザーに対する処理(返金リセマラのようなもの)の優先度は高くない。 そういった即時性が要求されないものは裏で非同期にゆっくり処理することを検討できる。

あえてあいまいに

処理や判定をあえて曖昧にして負荷を低減するという手段もある。 境界値の判定を曖昧に行うとか、ユーザー間の完全な同期を保証しないとかという感じである。

これは「ゲーム的な発想」なのでどこでも使えるわけではないが、負荷軽減と仕組みの隠蔽とユーザー体験の向上を一度に行える可能性がある。

そのミドルウェアやライブラリは大丈夫か

一般的によく使用されるミドルウェアやライブラリやクラウドサービスを普通に使っている限り、おおむね変なことは起こらない。

しかしながら特有のワークロードを突如大量にかけると異常が発生するケースがたまにある。 「予期せぬロックが広範囲にかかる」というのは典型的な例。

大抵は良くない実装だったり使い方を間違っていたりというのが原因であるが、ミドルウェア側にバグが潜んでいる可能性もある。 ミドルウェアのチケットやIssueが開示されているのであれば目を通しておきたい。何らかの有償サポートに頼れるなら頼っても良い。

基本的には実装を改良して回避することになる。 一定以下の負荷であれば問題が発生しないことが明らかなら、水平分散を行うのも1つの手かもしれない。

技術的に規定された上限

具体的には以下のようなものを指している。あくまでもこれは一例。

  • int max
  • ポート数
  • 同時接続数

余裕を持っておけば超えないように思うが意外に超えてしまう場合がある。 危険な状態になっていないか監視しておきたい。

またこの上限がスケールアウトの上限になってしまうこともある。それを認識せずに上限に達すると詰んでしまう。

他社APIが耐えられるか

現代においては他社APIと連携するのは普通に行うことだろう。たとえばスマホアプリの課金機能は(この2社には限らないが)GoogleやAppleと連携してはじめて成り立つものである。

「自社サービスは突然の高負荷に耐えられる、GoogleやAppleも大量のリクエストに耐えられる、だから他社も同様に耐えられる」と思ってはいけない。 1000万ユーザー級まで育った製品はまともな規模ではない。

他社と自社のシステムの規模に圧倒的な差がある場合

この場合は注意が必要。 もしアクティブなユーザーが1割程度だとしても、そのユーザーの突撃を予告なく瞬時にぶっ放せば相手のサービスを簡単に落とせるくらいに思ってよい。

可能な限り他社さんに優しい実装を行い、可能であれば事前に相談するなどしておきたい。

あまり積極的に推奨できないが、コストがかかるのを承知したうえでそれ相応の契約を結んで以下をやるという手もある。

  • 他社のアプリを自社の強力なサーバーで動かす
  • 下位互換性を持つものを自作して製品に組み込み、一部の処理だけ自前でやる

高負荷に耐えられるが「クオータ」の概念がある場合

大負荷に耐えられる場合であっても、他社APIをタコ殴りにしてよいというわけではない。 クオータによって単位時間あたりの呼び出しに制限があるケースもあるので要注意である。

万一の際に備えた、意図的なサービス水準の低下

高負荷により丸ごとすべてをダウンさせるのはなるべく避けたい。なのでサービス水準を下げることで動作を継続できる可能性を探りたい。 ユーザーの視点から見れば「エラーは出ないけど重い」「たまにエラーが出るけど何回かやれば動く」という感じを意図的に作り出すということである。

丸ごとすべてをダウンさせるよりは人間(ユーザー・運用者)の負荷を軽減させられる可能性がある。

もちろん合意なく行うのは良くないし、深刻なダメージを発生させるならやるべきではない。 死守すべきものを犠牲にするくらいなら全部落とした方が良い。

以下にいくつか例を載せる。

リクエストのキューイング

レスポンスに時間がかかっても許されるならリバースプロキシか何かでキューイングすることが検討できる。

当然ながらユーザーを待たせることになるが、それと引き換えに全体のダウンを防ぐことができるかもしれない。

非同期レプリケーションのリードレプリカ

データベースの負荷が高い場合に、非同期レプリケーションをリードレプリカに使う手荒な技が使えることもある。

そんなことをしたら不整合によるエラーが出る(あるいは明示的にエラーを出す)場面があるかもしれない。でもエラー以外の問題が発生しないなら検討できる。

各サービス単位でのメンテメード

負荷が高いサービスが明らかであるなら、サービス単位で落としてしまうなんてのもありだと思う。

ゲームで言うなら「ガチャのみメンテメード、それ以外は使用可能」のようなものである。 もちろんそれを想定した実装が必要なのは言うまでもない。

ランダム強制エラー

大胆なやり方だが、一定の確率で無条件エラーにするなんて方法も検討できる。 ただ個人的にはユーザーへの説明に困りそうなのであまりやりたいとは思わない。

予告なき高負荷の予防

高負荷対応の難易度は、高負荷の起こり得るイベントの存在が事前にわかっているかわかっていないかで変わってくる。

今はクラウドの高性能なツールがたくさんあり自動でスケールアウトしてくれる。しかしウォーミングアップなしでいきなり高負荷を与えると多少のエラーが出たり、予想外のコストがかかったりする場合がある。

なのでどんな予定があるかを事前に把握しておけるなら把握しておきたい。 予定がわかっていれば事前にスケールアップしておいたり、リソースの配分を変更したりなどの対策ができる。

この辺は一緒に業務を行う関係者とコミュニケーションを地道に行うしかない。具体的には以下のようなものである。

  • 直近で特殊なイベントはあるか?イベントの注目度やユーザーの熱心さはどれくらいか?目玉になるコンテンツは何か?
  • テレビ放送で特集される予定はあるか?何時ごろに放送されるか?
  • イベントが15:00開始だが15:00ちょうどにLINEトークを送信するとユーザーが集中する。なので業務に大きな影響がないなら15:15に送信できないか?
  • システムの大幅な改善をメンテなしに行いたい。リスクは高くないが30分間ほどサーバーの力が半分になる。なのでちょうどよい日時があれば教えてほしい。
  • 普段動かしていない特殊な機能を使うか?もし使うのなら人員を配置する必要があるので教えてほしい。

特定クラウドへのロックイン対策

高負荷に伴う問題を特定クラウドの専用機能に完全に乗っかり札束で解決するという手がある。 しかしこれに頼り切ると「どうやっても他社サービスに移行できない上客」になってしまう。こうなると交渉力が低下する。

なので特定機能に頼る場合でも以下を頭の片隅においておきたい。実際にそれを行うかとは別の話である。

  • マイグレーション手順はあるか?
  • 複数の事業者の機能を併用できるか?

以下の要件を満たしていると非常に理想的で、強い交渉力を得ることができる。そうなると良い条件を得やすくなると思う。

  • 特定クラウドをいっぱい使っている上客である
  • しかしながら他のクラウドも併用している
  • 技術力とリソースがあるので移行しようと思えば他へ移行できる
  • オンプレミスも扱える

特殊なケース

あえて(表向きは)ロックインされる決断をするケースもある。 スタートアップが特定クラウドの特定サービスに依存して採用例となることでサーバー代が実質無料になるなんてのは時々ある話。

「交渉力が問題になるのはおそらくEXITした後なので、依存したところで問題ない」というのも一つの考え方ではある。

最後に

ユーザーの多い製品の運用開発は総合格闘技みたいなもの。1000万ユーザに耐えられない原因がサーバー以外というのもありうるので広い視点を持ちたい。