高速なWebシステムの為の非道なる非同期

#1．非同期とは

##　フロントエンドの非同期
　
　何らかの処理を行うイベントを発火しておいて、その終了を待たないことである。フロントエンド側の非同期というとJavaScriptでのAjaxによる通信処理を指すことが多い。その他にタイマー系の機能を使用して実行タイミングをずらしたり、ES6以降ではPromiseを使った物が挙げられる。

##　バックエンドの非同期

　バックエンド側は少々ややこしい。そのまま話を進めていくとシングルスレッドの非同期とマルチスレッドの話で混乱が生じるのだ。

　まずシングルスレッドの非同期とは何かを明確にしたい。何らかの処理を行うイベントを発火しておいて、その終了を待たないというのは共通する。ただし結果の受け取り方がOSや言語、処理方法によって異なってくる。非同期で出した処理要求に対するレスポンスにイベントを発生させるもの、関数などをコールバックするもの、排他系の機能を使う物、終わっているかどうか逐次自分で確認するもの等がある。

　マルチスレッドによる非同期は、非同期で処理するべき物をサブスレッド化して分離する。この間メインスレッドはブロックされず処理を続けられるのだ。処理を終えたサブスレッドは、結果をメインスレッドに通知する。


##　アプリケーションサーバの動作方法
　さて、ここでアプリケーションサーバの動きを整理してみよう

1.コネクションごとにプロセスが割り当てられる
2.コネクションごとにスレッドが割り当てられる
3.全てのコネクションをシングルプロセス、シングルスレッドで処理する

　Webシステムの構築でよく使われる1の方式だ。PHPを例に挙げると以下のような特徴を持つ。

・Apache+mod_cgiとphpプロセス
　毎回プロセスを起動するため、そのための負荷がかかる
　接続ごとにPHPのプログラムは再解析される
　実行権限は調整できるのでセキュリティの管理はしやすい
　大量のアクセスに不安

・Apache+mod_php
　Webサーバに組み込まれているため応答が早い
　接続ごとにPHPのプログラムは再解析される
　Webサーバと同一権限で動くためセキュリティ面での不安がある
　接続ごとにWebサーバを含んだプロセスが増えるのでメモリが多く使われる傾向がある
　初期設定がすごく簡単

・nginx(Apacheでも可) + PHP-FPM
　PHP-FPMが一定数のプロセスを立ち上げてPHPの処理に備えているため応答が早い
　接続ごとにPHPのプログラムは再解析される
　大量アクセスに対しては、そのぶんだけPHPのプロセスが立ち上がるのでメモリは必要となる

　メモリをそれなりに積んでnginxを窓口にすれば、マルチプロセスでも大量アクセスをさばくことは可能だ。

　2の方式はコネクションを受け付けたら、そこで必要となる処理を新たなスレッドで行うという手順だ。マルチスレッド対応の言語で扱うことが出来る。スレッドごとにスタック領域を作らねばならず、コネクションごとにメモリを消費していくことになる。しかしマルチプロセスよりは遙かに消費量は少ない。そして応答速度は速い。ただし同一プロセスという特性上、致命的なエラーを出すと、他のコネクションを巻き込んで死んでしまう。ネイティブ系の言語なら、かなりのパフォーマンスが期待できる。

　3の方式は2の方式とは違い、接続要求を受け入れてソケットが生成された後、新たなスレッドを作成しない。各ソケットとのやりとりを単一スレッドで行う。各コネクションに対して送られてきたデータを確認し、適切に処理を回していく。ただしAPIの制約上、一部機能の中でマルチスレッドが使用される場合もある。


##　実行処理中の非同期

　さて、ここまで挙げたのは、クライアント接続時の処理方法に関してである。同期、非同期はその後のプログラムの作り方でも分類される。ファイルIOやDBとの通信にも同期、非同期が存在するのだ。

　一般的な言語だとその辺りは同期処理がデフォルトとなっている。データを要求したら結果が返ってくるまで待ち状態となる。ただし待っている間はOSが適切にリソースを管理するので、CPUを食い潰したりはしない。応答があるまで先の処理がブロックされるだけだ。これがNode.jsだと非同期がデフォルトになっており、すぐに次の処理へ進むことになる。利点はどんどん先に処理を進めていけることだが、要求したデータが次の処理で必要となる場合、同期処理よりも記述が複雑化しやすくなる。

　そしてこの非同期、高速化に寄与するかというとあんまり効果が出ない。頑張って非同期化を行って、できる限りリソースをまんべんなく使うようにプログラムを組んだとする。結果、単一のコネクションに対する応答が早くなる。しかし普通に組んだプログラムでも、複数のコネクションから同じタイミングで処理要求が来たら、処理は並列で進むことになる。そしてリソースはフルに使われることになるのだ。そうなると総合的な速度は変わらない。

　Webアプリケーションのボトルネックは大抵の場合DB周りに収束する。DBに無駄なクエリーを発行していればシステムは遅くなるし、効率化させればシステム全体の速度は向上する。DBの応答が遅れたらそこまでだ。そういった部分は非同期によって解決できるものでは無い。

　さらに非同期ならではの問題もある。非同期はどうやって非同期たり得るか。流れとしては、その場で実行が保留になった処理をキューに積んでいく形だ。さて、クライアントから1万件ほど接続要求が来た場合を考えてみよう。

　マルチプロセス系なら設定した最大プロセス数を超えることになる。そうなると受付の段階で待ち状態となるかエラー応答することになる。非同期系ならどうなるかというと、全て受け付けることが可能だ。ところが受け付けたところでDBの処理が間に合わない。応答待ちのキューが溜まりメモリーを圧迫する。さらにDBに対して同時に大量の処理要求を行うことになり、DBが限界を迎える可能性が出てくる。並行処理の限界数を自分でカウントして対処しないと、えらいことになってしまうのだ。

　非同期によって実質的に得られるものは、少ない処理要求に対する小さな高速化と、適度なアクセスに対しての省メモリ化だ。

　では高速なWebシステムはどうやって作れば良いのか。答えはすでに書いている。DBに対するクエリーを見直すことだ。テーブル設計を見直し、無駄な条件を削り、そもそも処理そのものが省略できないか考える。非同期とか小手先の処理で何とかしようとか考えるだけ無駄なのだ。

「え？身も蓋もないじゃん」

　ここまで読んでそう思ったかもしれない。ならば一つ追加で提案したい。抜本的にDBの処理を減らす方法、その鍵はフロントエンド側にある。

#2．SPA(SinglePageApplication)で実現する、バックエンドの処理軽減

　フロントエンドを非同期にする。これだけでDBが処理するべき仕事が激減するのだ。SNSのシステムを例に挙げてみる。フレンドの投稿、フレンドのリスト、所属するグループのリスト、イベント通知、まだまだあるかもしれないが、そんな沢山のデータをDBに問い合わせるこ必要がある。そこからフレンドの投稿の詳細を確認しようとリンクをクリックする。非同期を使わない場合はページ遷移要求とともに、表示内容全てに対するDBアクセスが発生する。

　これを非同期のSPAにした場合を考えよう。SPAの場合、必要な部分のみ更新することが可能だ。フレンド投稿の詳細が表示したいのなら、サーバ側にはその要求だけ送れば良い。DBは投稿の詳細を返すだけで済む。正確には権限の確認にプラスアルファで仕事をすることはある。そしてクライアント側にデータを渡し、必要な部分のみを書き換えて作業終了だ。やり方を変えただけで、とんでもなくDBの作業は減った。リストラを心配しなければならないほど仕事が激減したのだ。

　結局のところ、やっていることはDBアクセスの見直だ。システム構築の上では完全なる王道なのだ。

　実はフロントエンドを非同期にすると、バックエンドの非同期が効果を持ち始める。フロントエンドの非同期化は、細かい処理要求という傾向を持つ。この細かい処理要求を得意としているのは、非同期のバックエンドなのだ。フロントエンドを非同期にすることによって、ようやくバックエンドの非同期が日の目を見るのだ。

#3．まとめ

　Webシステムを高速化させたいのならバックエンドを非同期にするより、まずはフロントエンドを非同期にした方が圧倒的に効果が高い効果が得られる。バックエンドはその後で十分なのだ。