ここ1年で一番感動した、キャッシュの効果凄いな!という話です。
今回はrubyで実現してるのでrubyとつけてますが、色々な言語でも適用可能だと思います。
とある問題。 rubyで作ったcgiのレスポンスが遅すぎる
とあるシステムで、フォルダを検索して各フォルダの設定値一覧をUIを作りました。
サーバー側はcgi(コマンド)として処理を実行し、結果をJSON形式で返すという形。
実現方法としてはrubyを使い、
各フォルダのJSONフォーマット設定値を読み込み
⇒レスポンス用のHashを成形
⇒JSONフォーマットに変換して送信
という流れだったんですがこれが遅い遅い。
フォルダ数が膨大な数になると、応答返すまで数分レベルになってしまいました。
概算ではいけそうだったんだけど甘かった(-_-;)
原因と対処の検討
ruby上での1フォルダ1フォルダの設定値open/close時間を計測すると、1open/closeが極めて遅いわけではないけど、フォルダ数が万単位になったらこうなる。
純粋に必要な情報を集めるのに時間がかかる
しかもこのフォルダ数は設定値の組み合わせ的に十分あり得る数。
つまり、今の仕組みでは限界。なので何か手を入れることに
速度は限界なので、何かしらの方法でレスポンスのキャッシュするしかない。
実現方法はどうしよう
レスポンスキャッシュ方法
対処について最初に出した草案は2つ。
- キャッシュサーバーを作る
- レスポンス用のキャッシュファイルを作る。
草案考えてた時点では、2はキャッシュファイル管理が煩雑になるな~と思っており、
やるなら1かなと思っていました。
作る規模はちょっとかかるけど、システム設計上も問題なく作れそう。
じゃあ実際に対処を考えますか。許される期間は?
1日。…
はい、無理なので案2で考えます。。果たして綺麗に行くのだろうか
キャッシュファイルについて整理
冷静にシステムの状況を考えてみる。
- このHTTPサーバーはシングルスレッド
- レスポンス用のデータはHash構造
- 時間がかかるのは最初のキャッシュ
- レスポンス状態が変更されるケースがある
1⇒cgiアクセスによる排他制御は考えなくていい。
ってことはキャッシュファイルはコマンド名やquery毎に作ればいいのか。あれ?わかりやすいぞ
2.⇒rubyにはJSONファイル⇔Hash/Array構造を変換する処理が標準である。
ってことはレスポンス用データ=そのままキャッシュするファイルでいいじゃん。
3.⇒ってことは、起動時にキャッシュを作ってやればいいな。
getでキャッシュが作れるようにしておけば、起動時に1発getしとけばいいぞ!
4.⇒これは更新の仕組みがいるな。でもキャッシュ情報ベースだからそんなに大変ではなさそう。
最悪キャッシュファイルを消してgetすればいいか。
…あれ?簡単だぞキャッシュ化
対処 キャッシュファイル化
修正前:元々の実装はこんな感じでした。
class CgiActer
def getResponse(query)
#queryパース
responseData = createRespnse(input1, input2)
return responseData
end
end
修正後:CacheManager
クラスを追加し、こんな感じに変更。
class CgiActer
def initialize ()
@cache_mng = CacheManager.new()
end
def getResponse(query)
#queryパース
responseData={}
#キャッシュがあるかを確認。あるならキャッシュを取得。なければ既存処理 && キャッシュ保存
if @cache_mng.isCached(input1, input2) then
responseData = @cache_mng.getCache(input1, input2)
else
responseData = createRespnse(input1, input2)
@cache_mng.writeCache(input1, input2, responseData)
end
return responseData
end
end
CacheManager
クラスはこんなんです。
require 'json'
class CacheManager
CACHEFILE_NAME = "cachefile.json"
#ファイル名取得
def getName(input1, input2)
return ".#{input1}_#{input2}_#{CACHEFILE_NAME}"
end
#ファイルチェック
def isCached(input1, input2)
#ファイルがあるか見るだけ
return File.exist?(getName(input1, input2)))
end
def getCache(input1, input2)
#JSON.parseしてHash化するだけ
responseData ={}
File.open(getName(input1, input2), "r") {|f|
responseData = JSON.parse(f.read())
}
return responseData
end
def writeCache(input1, input2, resdata)
#JSON.形式にしてwriteするだけ
File.open(getName(input1, input2), "w") {|f|
f.write(JSON.pretty_generate(resdata))
}
end
end
シンプルなCacheManager
クラスを追加するだけ。サクッと実装出来ました。
構成次第ではcreateRespnse
ごとCacheManager
に移動すれば、CgiActer
は常にcreateRespnse
を呼ぶだけで実現可能ですね。
(Flyweightパターンだな)
実装後のレスポンス速度も1cgi requestに数秒⇒300msecの高速化に成功。
簡単なのに凄い!
もう一つ問題。ファイルを増やすと起動が遅すぎて動かない!
さっきの対処でどんなデータでもcgiの応答は早くなりました!
ということでやっとシステム試験に入ることに。
今回の件もあるので大きなデータも交えた構成にして試験。
…したところ、サーバーの起動が遅すぎて動かない。なぜだ!
原因と対処の検討
似たような理由がCで作ったサーバー側にもありました。
ファイルサイズをfopen/サイズ読み込み、サイズとファイルの種類を取得/fcloseの繰り返しで測ってるんですが、対象ファイル量が多すぎて起動が終わらない…
10分弱かかってやっと起動。ほかのプロセスが待ちきれずにabort。
またか。でもこちらにはキャッシュ化があるぜ!
キャッシュ化 Cを交えた場合
時間がないので、ファイルパースが苦手なCさんではなく、rubyさんを利用してキャッシュ化。
まずfopen/サイズ読み込み/fclose部分をまるっと新しいmain.cにコピーしてコマンド化。
こんな感じの元ソースから
//既存関数
void get_filesize(char *file, int *size, int * type) {
//ファイル読み込み処理
}
void read_allfile () {
while(file 全てのファイルを) {
int size, type;
get_filesize(ファイル名, &size, &type);
//size, typeをstore
}
}
こんなコマンドを作りました。
//既存関数
void get_filesize(char *file, int *size, int * type) {
//ファイル読み込み処理
}
int main(int argc, char *argv) {
char *fname = argv[1];
int size, type;
get_filesize(fname, &size, &type);
printf("[%d,%d]", size, type);
}
ファイル名を指定されたらrubyのArray形式で結果を表示するようにします。
後はrubyさんが全ファイルに対してファイル情報を取得。得意のJSON形式でファイル情報をキャッシュします。
ファイル追加時もrubyさんがキャッシュ更新。
rubyはCacheManager
みたいな感じなので省略。
起動時はC側からrubyを実行。
rubyさんに得意のJSONファイルパースをしてもらい、変換したArrayを出力してもらいます。
自然と
[1,1]
[2,2]
[3,3]
みたいな[]のリストに成形してくれるので、C側は何も考えずにfgets/sscanfで取得します。
こんな感じ
void read_allfile () {
int size, type;
char result[64]={0};
FILE *fp = popen("rubyのコマンド", "r");
while(fgets(result, 64, fp)) {
int size, type;
sscanf(result, "[%d,%d]", &size, &type);
//size, typeをstore
}
pclose(fp);
}
簡単修正でサクッと対処後、実行。
10分弱⇒1秒かからずの超高速化に成功しました!
凄いぞキャッシュ!
まとめ 安い!うまい!速い!凄いぞキャッシュ化
キャッシュ化、こんなに簡単で効果のあるものだとは思っていませんでした!
感想として素晴らしさを3点で
安い!
⇒工数的な意味で。今はファイルパーサーが用意されてる言語が沢山あるので、
うまくはまれば使いたいデータのファイル化は簡単!
うまい!
⇒基本getの処理へのテコ入れと、新規のキャッシュ処理追加。
影響範囲が少なく、導入の為にああだこうだシステム設計を変えず、すっきり導入可能!
速い
⇒ケースに寄るでしょうが、今回は適用後の速度改善が本当に凄い!
今回の例はruby×JSONでしたが、その言語の得意なファイルパース処理を用いて、
簡単にキャッシュ化、高速化が実現できるはず!