2010年11月アーカイブ

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();
}

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

MTDDC Meetup FUKUSHIMA行ってきました。やっぱり大阪まで戻るのは飛行機にするべきだった...

こんな感じのレポートにしてしまうと結局のところプラグインの話になってるように取られちゃうかもしれませんが、技術ネタは前半の少しだけであとはビジネスの話と思い出話(?)でした。とはいえビジネスの話は会場限定なので前半の話のおさらいを少し。ひとことで言えば、MTはもっとPHP使いにアピールしようよ or PHPerはMTのフレームワーク使ってマークアップとの分離、協業(分業)スタイルとか「デジタルコピーの使い回し」ができるような方法を考えようね、ってことです。

一応Perlのコードも動くようにしましたが、これはね...PHPプログラマに見て欲しいんだ(で、みんなもっとダイナミック使おう)。そう、MTはPerlプロラグマのもんじゃない。そしてMTMLはもっと自由なんだよ。

さて、前準備として、DynamicMTMLをインストールするかダイナミックパブリッシングを設定しておいてください。DynamicMTMLをインストールした方が絶対面白いんだけどね(こいつも公開した当時からいくつか進歩しているのでまたそのうち近々新しい版を公開します)。

ファンクションタグ : MTHelloWorld

最初のプラグインとして、Hello World! だけ出力するもの。会場でデモしたやつです。モディファイアの渡し方のサンプル的に worldモディファイアを追加しています。つまり、

<MTHelloWorld world="Movable Type">
Hello Movable Type!

を出力します。

モディファイア : addstring

<MTHelloWorld world="Movable Type" addstring="でいいじゃん!">
Hello Movable Type!でいいじゃん!

を出力します。つなげてるだけですけどね...

ブロックタグ:MTQueryLoop

ブロックタグはちょっとややこしいので、DBにアクセスするサンプルでなく簡単なサンプルを。

example.html?param[]=foo&param[]=bar&param[]=buz

のようにアクセスした時に、

<MTQueryLoop key="param" glue=",">
<mt:var name="param" escape="html">
</MTQueryLoop>
foo,bar,buz

と出力されます。まぁこの「glue」ってのがWordpress的なので、MTっぽく書くことも出来るようにしてあります(ユーザーが指定可能な値を動的に出力するときはescape="html"を忘れずに!)。

<MTQueryLoop key="param">
<mt:if name="__first__"><ul></mt:if>
<li class="<mt:if name="__odd__">odd<mt:else>even</mt:else></mt:if>"><mt:var name="param" escape="html"></li>
<mt:if name="__last__"></ul></mt:if>
</MTQueryLoop>

ファンクションタグ:MTQuery

MTQueryLoopのパラメタ1つバージョン

<MTQuery key="param" escape="html">
example.html?param=foo

のようにアクセスすると foo を出力します。

ファンクションタグ:MTRand

<MTRand min="1" max="10">

で、1から10までの乱数を返します。

会場でやったデモでは以下のようなテンプレートで最近のブログ記事10件から1件ランダムに出力するもでしたね。

<MTRand min="1" max="10" setvar="rand">
<MTEntries limit="1" offset="$rand">
<MTEntryTitle escape="html">
</MTEntries>

* そういえば、昨日で会社7周年。懇親会で「来週くらい」とか言ってて「覚えてないのかよ!」と思わず自分でツッコミ入れてしまいました。写真はウチの子が小遣いで買って来てくれたクラシックラガー(ありがとー)。

クラシックラガーの缶!!

いよいよ週末にMTDDC Meetup FUKUSHIMA 2010です。

当日枠あるみたいです。お時間ある方は是非。

私のセッションのアジェンダを以下に(多分全部入んないから今から削るけどww)。尚、スライド等は非公開にします。その方が絶対面白い話できるもん!!

あなたの知らない Movable Type

〜プラグイン/テーマ/テンプレートだけじゃないMovable Typeの本当の実力と可能性を知ろう〜

What's MT

  • サーバーインストール型CMS/ブログ
  • プラグインによる拡張性
  • 覚えやすく強力なテンプレート言語
  • Perl/PHPフレームワーク
  • データベースドライバとMT::Object
  • アプリケーション開発フレームワーク

最近つくったものをいくつか...(デモ)

  • Xtalk(Twitterクローン)
  • Viewerアプリケーション
  • ePublisher(電子書籍編集/出版)
  • DynamicMTML(静的ファイルにMTML)
  • カスタムオブジェクト
  • MTObjectBuilder(GUIでプラグイン)
  • PowerCMS::Util

Movable Type on Buisiness

〜地方+小規模+Web系会社にとってのプラットフォームビジネスを考える〜

  • About Me
  • About Us(アルファサードについて)
  • アルファサードのProduct
  • Background - 1(野田純生のバックグラウンドについて1995年から2002年頃まで)
  • Ju's iEdit(1997-2002)
  • Background - 2(アルファサード設立から2005年-2006年くらいまで)
  • Shift_the Movable Type.
  • 2007 - Movable Type 4
  • Power CMS for MT

Movable Type on Buisiness

  • どんなビジネスの種類がある?
  • どんな特徴のビジネスモデル?
  • アルファサードでは?
  • デメリットはないの?
  • プラグインが書けなくてもビジネスできる?

Why Movable Type?(Why not Wordpress?)

  • 再利用可能なデジタルコピーが利益につながるモデルをつくる
  • Inside Movable Type(拡張MVCと分業スタイル)

Movable Type on Buisiness, -地方の小規模組織にとってのビジネスチャンス-

  • theme-1 地方案件は予算がないのか?
  • theme-2 東京の仕事とどう付き合うか?
  • theme-3 アライアンスは有効な手段か?
  • theme-4 シフトチェンジはどう行うか?
  • theme-5 どう考えて、どう行動するか?

みなさんへ、いくつかの提案


それでは当日、お会いしましょう。

Facebook

Twitter

このアーカイブについて

このページには、2010年11月に書かれたブログ記事が新しい順に公開されています。

前のアーカイブは2010年10月です。

次のアーカイブは2010年12月です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

Powered by Movable Type 6.2.6