LoginSignup
1
1

More than 5 years have passed since last update.

安い!うまい!速い!効果抜群 凄いぞrubyでのキャッシュ化による速度向上

Last updated at Posted at 2018-04-29

ここ1年で一番感動した、キャッシュの効果凄いな!という話です。
今回はrubyで実現してるのでrubyとつけてますが、色々な言語でも適用可能だと思います。

とある問題。 rubyで作ったcgiのレスポンスが遅すぎる

とあるシステムで、フォルダを検索して各フォルダの設定値一覧をUIを作りました。
サーバー側はcgi(コマンド)として処理を実行し、結果をJSON形式で返すという形。

実現方法としてはrubyを使い、

各フォルダのJSONフォーマット設定値を読み込み
⇒レスポンス用のHashを成形
⇒JSONフォーマットに変換して送信

という流れだったんですがこれが遅い遅い。
フォルダ数が膨大な数になると、応答返すまで数分レベルになってしまいました。
概算ではいけそうだったんだけど甘かった(-_-;)

原因と対処の検討

ruby上での1フォルダ1フォルダの設定値open/close時間を計測すると、1open/closeが極めて遅いわけではないけど、フォルダ数が万単位になったらこうなる。
純粋に必要な情報を集めるのに時間がかかる

しかもこのフォルダ数は設定値の組み合わせ的に十分あり得る数。
つまり、今の仕組みでは限界。なので何か手を入れることに

速度は限界なので、何かしらの方法でレスポンスのキャッシュするしかない。
実現方法はどうしよう

レスポンスキャッシュ方法

対処について最初に出した草案は2つ。

  1. キャッシュサーバーを作る
  2. レスポンス用のキャッシュファイルを作る。

草案考えてた時点では、2はキャッシュファイル管理が煩雑になるな~と思っており、
やるなら1かなと思っていました。

作る規模はちょっとかかるけど、システム設計上も問題なく作れそう。
じゃあ実際に対処を考えますか。許される期間は?
1日。…

はい、無理なので案2で考えます。。果たして綺麗に行くのだろうか

キャッシュファイルについて整理

冷静にシステムの状況を考えてみる。

  1. このHTTPサーバーはシングルスレッド
  2. レスポンス用のデータはHash構造
  3. 時間がかかるのは最初のキャッシュ
  4. レスポンス状態が変更されるケースがある

1⇒cgiアクセスによる排他制御は考えなくていい。
  ってことはキャッシュファイルはコマンド名やquery毎に作ればいいのか。あれ?わかりやすいぞ

2.⇒rubyにはJSONファイル⇔Hash/Array構造を変換する処理が標準である。
  ってことはレスポンス用データ=そのままキャッシュするファイルでいいじゃん。

3.⇒ってことは、起動時にキャッシュを作ってやればいいな。
  getでキャッシュが作れるようにしておけば、起動時に1発getしとけばいいぞ!

4.⇒これは更新の仕組みがいるな。でもキャッシュ情報ベースだからそんなに大変ではなさそう。
   最悪キャッシュファイルを消してgetすればいいか。

…あれ?簡単だぞキャッシュ化

対処 キャッシュファイル化

修正前:元々の実装はこんな感じでした。

CgiActer.rb
class CgiActer
    def getResponse(query)
        #queryパース
        responseData = createRespnse(input1, input2)
        return responseData
    end
end

修正後:CacheManagerクラスを追加し、こんな感じに変更。

CgiActer.rb
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クラスはこんなんです。

CacheManager.rb
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にコピーしてコマンド化。

こんな感じの元ソースから

original_file.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
    }
}

こんなコマンドを作りました。

new_command.c
//既存関数
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で取得します。

こんな感じ

original_file.c
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でしたが、その言語の得意なファイルパース処理を用いて、
簡単にキャッシュ化、高速化が実現できるはず!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1