morolicious開発日記

MojoliciousとBootstrapで作られているレスポンシブなエロサイト「morolicious」の開発記録を綴りっています

サムネイル画像をlocalにキャッシュするようにしました

動画のサムネイルをキャッシュして表示スピードを上げる

一部の動画サイトでは画像の読み込みが遅く、表示に時間がかかるためサムネイルをlocalに保存する仕様にしました。また、スマートフォンで表示する場合に無駄に大きいサイズの画像を取得しているのも解消されます。

具体的には対象の画像をとってきて最適なサイズ(と言っても大きめですが)のサムネイルを作成して保存するだけです。

画像の取得

Fetch::Image - meta::cpan

イメージを取得するだけならLWP::UserAgentで出来るのですが、今回はFetch::Imageというモジュールを使ってみました。

このモジュールの特徴としては画像ファイルかどうかを判定するバリデーションしてくれると言う事と、取得した画像を一時ファイルとして保存してくれるところ。

テンポリファイルの保存場所はOSに応じて変わります。

cpanm でインストールするとtestでコケるので -fでインストールしてしまってます(testの中で該当のURLが無いっぽいのが原因みたいなので)

404の時のエラーを判定したかったので Fetch::Image を継承して _head メッソドをオーバーライドして使っています。

package Morolicious::Fetch::Image;
use strict;
use warnings;

use base "Fetch::Image";
use Data::Validate::URI qw/is_web_uri/;

# returns a HTTP::Response for a HTTP HEAD request
sub _head{
    my ( $self, $ua, $url ) = @_;

    my $head = $ua->head( $url );

    $head->is_error(404) && Exception::Simple->throw("not found");

    $head->is_error && Exception::Simple->throw("transfer error");

    exists( $self->{'config'}->{'allowed_types'}->{ $head->header('content-type') } ) 
        || Exception::Simple->throw("invalid content-type");

    if (
        $head->header('content-length')
        && ( $head->header('content-length') > $self->{'config'}->{'max_filesize'} ) 
    ){
    #file too big
        Exception::Simple->throw("filesize exceeded");
    }

    return $head;
}

1;
サムネイルの作成

Imager - meta::cpan

サムネイルの作成などPerlで画像を編集する場合にはImage::Magickを使うことが多かったのですが、今回はImagerを使っています。特に理由は無かったのですが、Imagerの方がAPIがきれいという事で使ってみました。

実装

基本的にはFetch::Imageで得たテンポラリファイルのpathをImagerで読み込んで、サムネイルを作成後localに保存しているだけです。

MoroliciousではQudoを使ってジョブキューを処理しています。AnyEventのタイマー処理を使って、未キャッシュの動画情報をDBから探してキューを作成しています。

ジョブキューを見つけるとQudoのworkerが画像を読み込んでサムネイルを作成・保存する仕組みにしています。

恩恵を受けるのは新着一覧や動画の検索ページ等のサムネイルを多く表示するページのみですが、少しは快適になっているかな?

関連ビデオの無限スクロール部分を改善

昔のTwitter的な無限スクロールUIを使っていますが、ある程度無限スクロール情報を表示した後に画面遷移(ビデオページに移動)すると、ブラウザの戻るボタンを押した時に元のページが以前の状態で表示されない問題がありました。

Ajaxを使って動的に要素を追加すると起きる問題ですが、ブラウザのhistoryを操作することによって改善できるようです。

ブラウザのhistoryにアクセスするAPIには

window.history.pushState window.history.replaceState

というものがあります。pushStateは履歴に新しく追加します。replaceStateの場合は履歴を書き換えるようです。詳しくはこちらで解説されています。

今回は、今をときめくcookpadさんのモバイルサイトの「続きをみる」を参考に、次ページの情報を表示する時には bulk_load というクエリーを追加することにしました。

例えば、 http://morolicio.us/video/48653 というページの場合は、「もっと見る」ボタンを押した時に、 http://morolicio.us/video/48653?bulk_load=24 となります(PC版の場合)

ブラウザの履歴書き換えには window.history.replaceState を使用します。

window.history.replaceState(null, null, '?bulk_load=24');

戻る・進むボタンを押した時のイベントはこんな感じ。

$(window).bind("popstate", function() {
    $container.infinitescroll({
        state: { currPage: 2 }
    });
});

主要ブラウザ(SafariChromeFirefoxOpera)で確認してみましたが、そこそこ動いているようです。IEは当然確認していません。Operaだけはうまく動作していないようです。

参考サイトにはちゃんとしたコードがあるので、もう少しきちんと動作するようにJavaScript書き直したいと思います。

参考サイト

日本でのAlexa Traffic Rankが2000位台になってたのでサーバ構成等を書いてみる

Alexa Traffic Rank

Alexa Traffic Rank for morolicious

まだbeta版で機能も充実していないにもかかわらずたくさんアクセス頂いているようで、国内で2000位台になっているようです。

スクリーンショット

f:id:pinktx:20130430165439p:plain

サーバスペック

クラウド環境です。Web兼AppサーバはXenベース、DBサーバはKVMベースです。KVMの方がパフォーマンスがいいとされているようですが、個人的にはXenの方が柔軟な設定ができてパフォーマンス的にも満足ができるレベルじゃないかと思っています。

現在およそ48,000UU/day 、250,000PV/day位ですが、これくらいのスペックで捌けるよ、という参考程度にはなるのではないでしょうか。

web兼appサーバ

2コアにしたのでNginxのworkerは2プロセスにしてます。1コでも全然大丈夫な気がしますが。アプリケーションはstarmanで起動していて、--workers 7 と7コのプロセスで。もっと少なくても大丈夫なのですが、急なトラヒックがきても大丈夫なように。

CPU使用率

3月の初め頃に急にトラフィックが増えたので2コアにしました。また、4月の中旬頃にstarmanのプロセスを2個増やしたのでちょっと減っています。メモリもその時に増やしています。

こういう急激な時にサーバのスケールアップ出来るのもクラウドの良いところです。

f:id:pinktx:20130430172852p:plain

f:id:pinktx:20130430172401p:plain

ロードアベレージ

f:id:pinktx:20130430172623p:plain

f:id:pinktx:20130430172911p:plain

DBサーバ

CPU使用率、ロードアベレージ共に余裕です。

f:id:pinktx:20130430172144p:plain

f:id:pinktx:20130430171726p:plain

サイドバーコンテンツをキャッシュ化して描写を高速化

このエントリーは古い内容が含まれています。
Mojolicious 4.0がリリースされました - サンプルコードによるPerl入門

特に大きな変更として、render_data, render_json, render_partial, render_textメソッドがMojolicious::Controllerから取り除かれているので、この部分を変更する必要があります。$c->render(data => '...')という形式に書き換える必要があります。

      • 以下本分

moroliciousのサイドバーにある「関連語」リストが意外と描写に時間が掛かるので、師匠のページを参考にキャッシュを試してみる。


変数の値はアプリ終了まで永続されるのでそれを使ったもの。うちのアプリはとある事情で数時間ごとにrestartされる仕組みなので、問題ないけど、通常は$html_sidebarの中身が更新されないので注意。

MyApp/Web.pm

sub startup {
....
  # sidebar html cache
  my $html_sidebar;
  $self->hook(
    before_dispatch => sub{
      my $self = shift;
      unless ($html_sidebar) {
        $html_sidebar = $self->render_partial('/layouts/sidebar');
      }
      $self->stash->{html_sidebar} = $html_sidebar;
    }
  );
....
}

※師匠のページにはrender_partial が render_partical とtypoがあるので注意。
※Mojolicious 4.0ではrender_partialは無くなっています。$self->render('mail', partial => 1);とする必要があります。http://mojolicio.us/perldoc/Mojolicious/Guides/Rendering#Rendering_data

$self->stash->{html_sidebar} = ... とする事で、テンプレートからは $html_sidebar で使える。templateの方はこんな感じで書けば $html_sidebarが展開されてHTMLが描写される。

  <div id="main-content">  
    <!-- contents -->
    <%= content %>
  </div>

  <div id="sidebar-content">  
    <!-- side bar -->
    <%= $html_sidebar %>
  </div>

気づいた点

実はhookのbefore_dispatchを他にも使っているのだけれど、ちゃんと動作するみたい。

sub startup {
....
  # hook1
  $self->hook(
    before_dispatch => sub{
      my $self = shift;
      .....;
    }
  );

  # hook2
  $self->hook(
    before_dispatch => sub{
      my $self = shift;
      .....;
    }
  );

....
}


で、こんなふうにしてみたんだけどこっちはダメっぽい。

sub startup {
....
  $self->hook(
  # hook1
    before_dispatch => sub{
      my $self = shift;
      .....;
    },
  # hook2
    before_dispatch => sub{
      my $self = shift;
      .....;
    }
  );

....
}

jQuery ValidationプラグインでAjaxを使ってremoteバリデーションする

jQuery Validation Plugin jquery.validate.js

フォームのバリデーションにとても便利なjQueryプラグインですが、Ajaxを使った時にちょっとハマったのでメモ。

例えばユーザー名が使われているかチェックする時にクライアントサイドのチェックだけでは実装出来ないのでサーバに問い合わせる必要があります。

jQuery Validation Pluginではremoteを設定する事でデフォルトではonkeyup(一文字入力するごと)毎にサーバ側にリクエストしてチェックしてくれます。

コードサンプル

※Bootstrapを使っているのでCSSへの適応も考慮されています。

HTML

<form id="join-form">
	<div class="control-group">
	    <input id="username" name="username" type="text" placeholder="ユーザー名" />
	</div>
</form>

JavaScript

$("#join-form").validate({    
    errorClass: 'help-block',
    validClass: 'help-block',
    errorElement: 'span',
    validElemnt: 'span',
    errorPlacement: function(error, element){
        console.log(error);
        error.appendTo(element.parents(".control-group"));
    },
    highlight: function(element){
        $(element).parents('.control-group').removeClass('success').addClass('error');
    },
    success: function(element, validClass){
        element.text('OK').closest('.control-group').addClass('success');
    },
    rules: {
        username:{
            required: true,
            remote: {
                type: 'post',
                url: '/validate/member_name.cgi',
                dataType: 'json', //無くても動作するけど環境による?
                data: {
                    username: function(){
                        return $('#username').val();
                    }
                }
            }
        }
    }
});

サーバサイドの処理

レスポンスはJSONで返す必要があります(Mojoliciousならrender_json()で一発です)
レスポンスがtrueの場合のみsuccessとなります。


NginXが an upstream response is buffered to a temporary file... というエラーログを大量に吐く対処

Nginxのエラーログに以下の様なログが大量に吐かれていた。

2012/12/18 02:35:51 [warn] 1770#0: *1467277 an upstream response is buffered to a temporary file /var/cache/nginx/proxy_temp/7/00/0000002007 while reading upstream, client: ....."
2012/12/18 02:42:20 [warn] 1770#0: *1467895 an upstream response is buffered to a temporary file /var/cache/nginx/proxy_temp/8/00/0000002008 while reading upstream, client:....."
2012/12/18 02:43:01 [warn] 1770#0: *1467950 an upstream response is buffered to a temporary file /var/cache/nginx/proxy_temp/9/00/0000002009 while reading upstream, client: ....."


Nginx の Warningログ - Liquidfuncの日記

これはバックエンドサーバからのレスポンスをメモリ上にバッファリングしようとしたが、すでに設定値いっぱいまで使われているため、一時ファイルに保存したよ という意味。


なるほど。デフォルトだと4k/8k程度の設定になっているみたい。
http://wiki.nginx.org/HttpProxyModule#proxy_buffer_size を参考にproxy_buffer_sizeの値を増やすことで解決できる様子。

Gunma.web #11でLTしてきました「おとなのテキストマイニング」

Gunma.web #11 に参加させて頂きました。前回参加してから半年。今回ようやく動くものが出来たのでLTさせて頂きました。

前回のスライドと同様に、あおいひとのアカウントを借りてスライドアップしたので紹介させて頂きます。

 

 

アップしたスライドはちょっとロングバージョンになっています。5分で収まらなかったのでカットした部分をノーカットにて。

内容的にはサービスの紹介をメインに、作ったきっかけやこんな事してるよ、的な事。

LT(5分)という事でメインとなっている類似検索にしか触れていません。機会があったらサーバ構成とか、経過とかシャベってみたいです。

また、質問にあった辞書のダウンロードできますか?の件ですが、性癖バレそうなので勘弁して下さい(笑

ちなみにこちらの勉強会。こんな人ばかりが集まる...訳ではありませんので興味のある方は是非参加してみて下さい!