アルファサード株式会社 代表取締役 野田 純生のブログ


MTプラグイン、管理画面のパフォーマンス向上について。


公開日 : 2010-11-10 23:25:14


11月25日、新しくなったPower CMS for MTのお披露目(セミナー)をします。新機能の話はまた追ってご紹介しますが、このエントリーでは自分用のメモも兼ねてパフォーマンス向上をどう図ったかについて書きなぐってみます。

計測する

MT.pmの_init_plugins_coreにどれくらいかかっているかをTime::HiResを用いて計測とかMySQLのクエリをmy.cnfに log=/path/to/logs/mysql.log とか設定して確認とか計測プラグインとかベンチマークテスト(Apach benchとか自前のスクリプトとかで計測。今回は50リクエストを同時に送る前提でテストしました)。いくつかの数値データはセミナー時にご紹介したいと思います(現状で最大で250%近く速くなってます)。

PerformanceLogging関連の設定をmt-config.cgiに記述することでログで計測も出来ます。

PerformanceLogging 1
PerformanceLoggingPath /var/log/mt/
ProcessLoggingThreshold 0.5

ファイル数を減らす、ファイルを軽量化する

まずは数を減らすこと、容量を減らすことで軽量化を図りました。プラグイン50とか100設置すると_init_plugins_coreにしても0.5秒近くかかったりします(ログを吐いて計測)。

追記。プラグインを無効化するときは管理画面で「無効」ではなくプラグインファイル自体を退避するか削除した方が速度的には有利になります。pluginsディレクトリ以下のconfid.yaml、またはplファイルは全部一度読み込まれてからPluginSwitchの設定で有効無効を判断されるからです。また、プラグインの読み込み順は環境に依存しますが、addonsが先に読み込まれます。読み込み順が動作に影響するものの場合は、先に認識されていないとまずいものについてはaddonsとして作り、それを利用するものをpluginとして作成すると良いと思います。

plugins/myplugin/直下の config.yaml もしくは myplugin.pl のファイル容量を減らす。initとかpre_run以外の処理、テンプレートタグや特定のコールバック処理等はモジュール化します。つまり

 'CMSContext' => \&_hdlr_cms_context,

は、以下のようにしてlib/PowerCMS/Tags.pmに持っていきます

 'CMSContext' => '$powercms::PowerCMS::Tags::_hdlr_cms_context',

プラグインを可能な範囲で統合します。統合することでファイル数が減ることはもちろん、プラグインの設定をロードする際のSQLのクエリを減らせます。統合の方針としては、相互依存があって着脱可能でないものや比較的単機能のもの。複雑なものについては慎重に行います。

プラグインデータはプラグイン毎に1レコードなので(正確にはsystem/blog毎に保存されている)プラグインが一つなら複数の値を持っていてもクエリはプラグイン毎に1回だけ呼ばれます。こんな感じのSQL。

SELECT *
FROM mt_plugindata
WHERE (plugindata_plugin = 'powercms')
AND (plugindata_key = 'configuration')
LIMIT 1

あるいは

SELECT *
FROM mt_plugindata
WHERE (plugindata_plugin = 'powercms')
AND (plugindata_key = 'configuration:blog:1')
LIMIT 1

プラグインが複数だと、こんなのが呼び出し時にプラグインの数だけ呼ばれます。統合すればクエリは1回になります。設定が移ることになるのでアップグレード時に移行。当然こういうのはアップグレード時だけ必要なコードなのでコードは本体には書かずにモジュールのほうに移します。

upgrade_functions => {
    config_settings => {
        plugin => 'PowerCMS',
        code => '$powercms::PowerCMS::Upgrade::_upgrade_functions',
    },
    config_settings_nv => {
        plugin => 'PowerCMS',
        version_limit => $SCHEMA_VERSION,
        code => '$powercms::PowerCMS::Upgrade::_upgrade_functions',
    } },
}

# 以下は外部モジュールに

sub _upgrade_functions {
    my $app = MT->instance();
    my $plugin_powercms = MT->component( 'PowerCMS' );
    require MT::PluginData;
    my $data = MT::PluginData->load( { plugin => 'old_plugin',
                                       key    => 'configuration' } );
    if ( $data ) {
        if ( my $cfg = $data->data() ) {
            my $old_plugin_setting = $cfg->{ old_plugin_setting_key };
            $plugin_powercms->set_config_value( 'new_plugin_setting', $old_plugin_setting );
        }
        $data->remove or die $data->errstr;
    }
    # ...
}

統合する際に、各プラグインで同じコールバックに対する処理を書いていたときは配列で指定します(config.yamlではなくfoo.plの場合)。

    'MT::App::CMS::template_param.preview_strip' => [
        { handler => '$powercms::OldPlugin1::Plugin::_preview_param', priority => 2, },
        { handler => '$powercms::OldPlugin2::Plugin::_preview_param', priority => 3, },
        { handler => '$powercms::OldPlugin3::Plugin::_preview_param', priority => 4,}
    ],

*もちろんコードを統合してもいいけれど。

* 統合とモジュール化する際の注意

プラグイン内にできるだけコードを書かずにモジュール化した関係で各モジュールの中で必要に応じてrequire Foo;するように。移植の際にMT::Fooのロードの時にそのあたりでハマらないためにできるだけ

MT::Entry->load( $terms );

ではなく、

MT->model( 'entry' )->load( $terms );

にする。プラグインのロード順は環境依存するので、モジュールが読み込めずに死んでしまう場合の対策にもなる。モジュールの各ルーチンの中でプラグインデータを取得する際には以下のように明示的に指定します。

my $plugin = MT->component( 'PowerCMS' );

システム全体の設定で変更するケースがほぼないものをmt-config.cgiへ

細かな話なのですが、プラグイン設定のデータに保存したものを呼び出すためにはSQLが発行されますので、システム全体で導入時に設定すれば変更しない一部のものをmt-config.cgiに記述する仕様に変更します。

クエリの実行結果をキャッシュする

同じmt.cgiへのリクエストに対する処理の中で同じSQLを再発行せずに使いまわします。

require MT::Request;
my $obj;
my $key = 'cache_key:' . $class . ':' . $obj_id;
$obj  = MT::Request->instance->cache( $key );
if (! $obj ) {
    $obj = MT-model( $class )->load( $obj_id );
    MT::Request->instance->cache( $key, $obj );
}
$obj;

MTが内部的にすでにキャッシュしているものはそっちを使うようにします。例えば、

my $blogs = MT::Blog->load( { parent_id => $website->id,
                              class => 'blog' } );

みたいなのは

my $blogs = $website->blogs;

を使う。MTの内部ですでにこれが呼び出されていたらキャッシュが使われるし、以後MTがこれを利用する際にはキャッシュが使われます。

追記。MT::OnjectのサブクラスをIDでロードする時はMT::Foo->load( { id => $id } ); ではなく MT::Foo->load( $id ); とします。そうすることで、->lookupが使われてSQLが複数発行されなくなります。そういう意味では上記のMT::Requestの例の効果は限定的です。

さらに追記。MT::Foo->cache_property( $key, sub{ [ some code ] } ); とすることで実行結果をキャッシュしつつキャッシュが無ければ実行結果を返すことが出来てこっちの方がコードはシンプルになります。以下、$website->blogs のコード。

# lib/MT/Website.pm
sub blogs {
    my $class = shift;
    my ($terms, $args) = @_;

    my $blog_class = MT->model('blog');
    if ($terms || $args) {
        $terms ||= {};
        $terms->{class} = 'blog';
        $terms->{parent_id} = $class->id;
        return [ $blog_class->load( $terms, $args ) ];
    } else {
        $class->cache_property('blogs', sub {
            [ $blog_class->load({
                parent_id => $class->id,
                class     => 'blog'
            }) ];
        });
    }
}

リクエストをまたがるキャッシュはMemcache、DBキャッシュ、ファイルキャッシュ等

require MT::Memcached;
if ( MT::Memcached->is_available ) {
    my $cache = MT::Memcached->instance;
    my $data = $cache->get( $key );
    return $data if $data;
    $data = 'foo';
    $cache->set( $key => $data );
    return $data;
} else {
    # ...
}

もしくはプラグインデータとか、MT::Sessionとかに一時的に保存してしかるべき時にクリアするとか。僕らは専用のテーブルを作っていてMTタグでキャッシュの定義が出来るようにしてあります。ついでに環境変数の設定によってDBの変わりにファイルキャッシュが使えるようにしています。MT::Cache::Negotiateを使うことでMemcachedが使えない時にMT::Sessionを使ってDBにキャッシュさせることが出来ます。こちらのコードはMemcachedが有効かどうかを気にする必要はありません。

require MT::Cache::Negotiate;
$cache_driver = MT::Cache::Negotiate->new( ttl => $ttl );
my $cache_value = $cache_driver->get( $cache_key );
$cache_value = utf8_on( $cache_value );
$cache_driver->replace( $cache_key,$cache_value, $ttl );

特定のウィジェットが表示されているかどうかで処理をスキップするなど

特定のダッシュボード用のCSSをhtml_head部で組み立てる時とか。ウィジェットが非表示なのに処理を行っているのは明らかに無駄なので判別するようにしました(判別するための処理に時間がかからない前提ならば)。以下の例はBlogTreeダッシュボードが無効な時にCSSを組み立てるための権限チェック等をスキップするための判定用のコードです。

sub __is_blogtree {
    my $app = shift;
    my $r = MT::Request->instance;
    my $is_blogtree = $r->cache( 'powercms_is_blogtree' );
    if ( $is_blogtree eq 'true' ) {
        return 1;
    } elsif ( $is_blogtree eq 'false' ) {
        return 0;
    }
    my $mode = $app->mode;
    my $view = $app->view;
    my $user = $app->user || return 0;
    my $bt;
    if ( $mode ne 'blogtree' ) {
        if ( ( $view eq 'user' ) || ( $mode eq 'default' ) || ( $mode eq 'dashboard' ) ) {
            my $widget_store = $user->widgets;
            if ( my $blog = $app->blog ) {
                my $blog_id = $blog->id;
                my $widgets = $widget_store->{ "dashboard:blog:$blog_id" } if $widget_store;
                $bt = $widgets->{ blog_tree } if $widgets;
            } else {
                my $user_id = $user->id;
                my $widgets = $widget_store->{ "dashboard:$view:$user_id" } if $widget_store;
                $bt = $widgets->{ blog_tree } if $widgets;
            }
        }
    } else {
        $bt = 1;
    }
    if ( $bt ) {
        $r->cache( 'powercms_is_blogtree', 'true' );
    } else {
        $r->cache( 'powercms_is_blogtree', 'false' );
    }
    return $bt;
}

ダイナミックパブリッシングのキャッシュの設定を適切にする

これは公開側の話。ウェブサイト/ブログの設定で「キャッシュする」、「条件付き取得を有効にする」を選択した上で、キャッシュさせたくないケースのみmtview.phpの中で分岐させる。例えばケータイ向けのページを除外するなど。

    $request_uri  = $_SERVER[ 'REQUEST_URI' ];
    if (! preg_match( "/\/m\//", $request_uri ) ) {
        $mt->conditional( true );
        $mt->caching( true );
    }
    $mt->view();

ダイナミックパブリッシングで条件付きGETに対応させる

MT標準の場合は既に対応しているので、DynamicMTMLの方を対応させました(スマートフォン、携帯等で分岐したりログインユーザー毎に違うものを出力するケースを除く)。

if ( $conditional && (! $author_id ) ) {
    $last_ts = $blog->blog_children_modified_on;
    if ( $if_modified && ( $if_modified >= filemtime( $existing_file ) ) ) {
        if ( ( strtotime( $last_ts ) <= $if_modified ) ) {
            header( 'HTTP/1.1 304 Not Modified' );
            exit();
        }
    }
    $orig_mtime = strtotime( $last_ts );
}
//...

管理画面のJavaScript/CSSを圧縮する

minifyを利用。プラグインで.htaccess (mod_rewriteの設定)を自動生成して導入を簡単にする仕組みを作成しました。バックエンドだけじゃなくCSS/JavaScriptの量も半端ないからです。

データベースの最適化、軽量化

./tools/optimize-mysql というスクリプトを作って定期的に最適化をcronジョブに登録できるようにしました。./tools/remove_old_sessions てのはMTに標準であるのであとはシステムログのローテーション。

処理に負担のかかるものを代替手段提供で選択できるように

アクセス解析がどうしてもDBの肥大化と処理のボトルネックになりがちだったので、処理はGoogleAnalytics に任せてMTと連携させ、レポートの表示やアクセスランキングのテンプレート出力が出来るようにしました。

FastCGIに対応させる

普通に拡張すればそのまま対応の筈なんですが、registryを動的に変更させたり等の処理をプラグインで行っている場合など、変更されたものが正しく初期化されない場合があります。ということで、take_downコールバックで設定関係を初期化することでおかしな挙動を回避可能になります。

sub _take_down {
    my ( $cb, $app ) = @_;
    return unless $ENV{FAST_CGI};
    my $cfg = $app->config; my $c = $app->find_config;
    $cfg->read_config( $c ); $cfg->read_config_db();
}

* こういうのやってると何か脳内にアドレナリンが出ます。いや、本当。



このブログを書いている人
野田純生の写真
野田 純生 (のだ すみお)

大阪府出身。ウェブアクセシビリティエバンジェリスト。 アルファサード株式会社の代表取締役社長であり、現役のプログラマ。経営理念は「テクノロジーによって顧客とパートナーに寄り添い、ウェブを良くする」。 プロフィール詳細へ