Webサービスとボトルネック

2019-03-08 21:50:25

Webサービスを開発する上での、ユーザの体験を阻害するボトルネックについて考えていきたい

1.DBに起因するボトルネック

2019-03-08 21:52:03

 Webサービスを開発するに当たって、最もボトルネックになる可能性が高いのはDBの部分である。ユーザの数に比例してDBの負荷はどんどん上昇する。ユーザ数が少ないうちは大丈夫だと思っていても、同時アクセスが100を超えたあたりから突然症状が顕著化することが多い。その境目はCPU使用率が100%に到達するかどうかだ。そこに張り付いた時点で、そのWebサービスは終焉を迎える。負荷が負荷を呼び、もう戻ってこない。下手をするとユーザも戻って来ない。


 対処法は以下の通りだ

・DBに対して発しているQueryを見直し最適化する
・Indexを付けるべき場所を間違えていないか確認する
・そもそも本当にそのQueryが必要なのか考える
・アプリケーションサーバ側でデータをキャッシュする
・サーバのCPUやMemoryの増強
・レプリケーションとクラスタリング

 Queryの見直しは基本中の基本だ。場合によってはとんでもない効果を発揮する場合がある。何度も利用される上に変更が少ないデータをキャッシュするのも効果的だ。しかもDBに確認をとる必要の無いデータなら、DB側の負荷を0に抑えることが出来る。

 そのあたりの対処をとり、これ以上絞り上げてももやはこの雑巾からは一滴の水も出ない状態になったら、もやはサーバのスペックを挙げるしかない。単純にCPUのコア数を増やせば、それだけ終焉を先送りすることが出来るのだ。

 もちろんコストが跳ね上がるので、ユーザ数の増加が収益に見合わないのなら、残念ながらそのサービスは打ち止めにした方が良いという判断となる。逆にコストがペイするなら、次はDBを複数に分散させ運用することを考えることになる。ところが分散と口で言うのは容易いが、DBの読み書きで異なるサーバを利用したり、分散したサーバの同期の問題が付きまとう。システム開発当初からそのあたりを織り込んでおけば良いのだが、後からの突然の変更はかなり危ない橋を渡ることになるだろう。

 読み込みに関する部分がボトルネックというのなら、前述の通り単純なレプリケーションでDBサーバを増やしていけば良い。しかし書き込みに関する部分が問題である場合、そしてもはや小細工が通用しないほどユーザが増加した場合、根本的な設計を見直す必要が出てくる。根本的なところではリアルタイム同期は諦めざるをえない。クリティカルな部分と遅延が許される部分を切り分けて、データの分散と統合をおこなっていくことになる。ただ、このレベルのシステムを組むことになるのは、本当に一握りのエンジニアだけだと思われる。タブン。


2.アプリケーションサーバによるボトルネック

2019-03-08 22:32:40

 アプリケーションサーバはPHP、Ruby、Go、Python、Node.jsなどの言語で書いたプログラムの部分だ。大量アクセスによってCPUが100%に張り付くか、メモリが足りなくなってスワップが発生することによって終焉を迎える。


 まずメモリが足りなくなる症状だが、サーバに割り当てているメモリと同時起動を許可しているプロセス数が釣り合っていないときに起こりやすい。CPUが上限に達していないのにメモリが限界を超えているのなら、プロセス数を制限するかサーバのメモリを増やす必要がある。ちなみにメモリの限界を超えたことによって発生するスワップは、そこへ到達した時点で実質試合終了である。そうなる前に対処しなければならない。

 CPUが100%に張り付いてしまった場合、根本的にプログラムの中に無駄なコードが入っていないかよく確認するべきだ。Webアプリケーションのボトルネックは、ほとんどがDBの負荷によるものなのだ。もしアプリケーションサーバがCPUを食い潰しているのなら、なにか別の問題を抱えていることが多い。それを見直した上でどうにもならないのなら、サーバのCPUの増強やクラスタリングで対処することになる。DBに比べると、アプリケーションサーバのクラスタリングの難易度はだいぶマシである。

 ちなみに大量の計算を必要とするような特殊なサービスを提供する場合は、その部分のみを分離してオートスケール化したインスタンスに投げることを考えた方が良い。逆にそれ以外もののをオートスケール化しても、面倒くさいだけであまり良いことはない。

3.フロントエンドのボトルネック

2019-03-08 22:54:12

 ブラウザの表示部分による問題だ。最も起こりやすいのは複雑なDOMによって、最終結果の計算が決定されないことだ。


 ブラウザはできるだけユーザに対して素早く表示結果を見せようと頑張るわけだが、無いデータは表示できない。画像のサイズやブロックの幅や折り返し位置など、データの読み込みが進むに従って少しずつ判明するような構造になっていると何度も再計算が発生し、ページ表示が荒ぶることになる。

 このあたりの対処法に関しては、比較的ネット上に情報が多い。

・CSSやJavaScriptのファイルを一つのファイルにまとめる(HTTP2のサーバプッシュを使えばまとめる必要はない)
・CSSやJavaScriptのファイルを最適化して文字数を減らす(そもそも普通はzip圧縮をかけて転送するので効果は薄いし、文字数を減らしたところでブラウザの解析時間の差は測定不可能なレベル)
・テーブルタグでレイアウトを作らない(サイズを明確に指定すれば問題を回避することは可能だが、使わない方が良いのは確か)
・floatのかわりにflexを使う(計算量が減るのでflex推奨)
・画像の圧縮(小さい方が良いのは確か)

 その他、JavaScriptに起因する問題もある。そもそもJavaScriptの規模が大きすぎて、その実行完了までの待ち時間が大きい場合だ。大手でもGoogleやMicrosoftのサービスでは、それが顕著に表れている。ぶっちゃけ、書かれているコードが悪いとしか言い様がない。たとえばGoogleDriveを利用するWebシステムを自分で組んでみたが、応答速度は全く遅くなかった。公式のGoogleDriveがなんであんなに遅いのか意味不明なレベルだ。

 JavaScriptの場合、Ajaxの応答によるボトルネックも発生しやすい。一つデータのやりとりを解決しては、次のやりとりに移行するような組み方をしている場合だ。Ajax自体は非同期なのに、それを完全に殺す組み方だ。対処法はAjaxで送受信すべき命令を複数パックして同時処理するような構造を作ることだ。もちろんアプリケーションサーバ側にも、それに対応した仕組みを用意する必要がある。

4.ネットワーク距離の積み重ねによるボトルネック

2019-03-08 23:22:38

 遅延も積もれば山となる。


 そもそもネットワークの距離とは何か。一般的にはルータを跨ぐことによる遅延や、物理的な距離による電気信号到達までの遅延を総合したものだ。

 前者のルータを跨ぐ数、ホップ数と呼称されるものだが、この数が多いほど遅延が発生しやすくなる。インターネットの過渡期、ルータ機器の性能が低かったころは、この数による影響はかなり大きかった。しかし最近は相互接続網の強化やルーティングプロトコルの改善、機器の処理速度の向上によって、それほど神経質になる必要は無くなった。

 後者の物理的な遅延はどうにもならない。電気信号は光の速さで送受信することが出来るが、逆に言うと一秒間で地球を7.5周しか出来ない。日本国内への送受信よりも、日本とアメリカ間のやりとりの方が時間がかかるのは自明の理である。人類が光の速度を超える通信手段を持たない以上、物理法則を超えることは出来ないのだ。対処法はユーザの近くにサーバを配置することである。ワールドワイドなサービスならば、各国のリージョンにサーバを置いていけば良い。ただしやっぱりデータの同期問題が発生するので、国ごとにどういうデータの扱いをするのか、きちんと設計しておく必要がある。

 さて、Webシステムを組むに当たって何を考慮しなければならないのかを説明しよう。当然物理法則は超えられないが、気をつけるべき点を気をつければ、遅延は最小限に抑えられる。

 データのやりとりは、極力ゼロ距離に抑えることを前提とし、アプリケーションサーバとDBサーバを一つのサーバインスタンス上に配置する。実はこれが結構重要なのだ。アプリケーションサーバが発行するQueryを同期的に行った場合、ネットワーク距離に応じた遅延がその度に発生する。この距離を無くし、プロセス間通信を前提にすれば、その遅延を限りなくゼロに近づけることが出来るのだ。プロセス間通信においてはTCPの使用をせず、UNIXドメインソケットを使うのも重要である。

 どうしてもアプリケーションサーバとDBサーバを同じサーバインスタンスに配置できないのであれば、そのやりとりを非同期化することを考えるべきだろう。前回発行したQueryを待たない構造であれば、遅延の影響は受けにくい。

 Webサーバとアプリケーションサーバもネットワーク距離に関してはそれほどシビアでは無い。元々並列アクセスで処理されることが前提なので、遅延が積み重なる可能性が低い。前述したAjaxによる非同期殺しをしていない限り、問題は起こりにくいのだ。