オープンソースこねこね

Webプログラミングなどについてあれこれ。

PHP5.5+OPcacheでシンボリックリンクでデプロイするとキャッシュが消えない

PHP

という問題にぶち当たりました。 どういうことかを話す前にPHPアプリケーションの個人的なデプロイ構成について説明します。 要はCapistranoのデプロイと同様なのですが、デプロイ先は以下のようなディレクトリ構成になっています。

.
|-- current -> /path/to/releases/20141011000001
|-- releases
|   |-- 20141011000000
|   `-- 20141011000001

currentアプリケーションサーバ(apachephp-fpm)から参照される箇所で、releases配下の最新のアプリケーションへのシンボリックリンクになっている。新しくアプリをデプロイするとreleases配下に20141011000002のようなタイムスタンプで新しくディレクトリが作成され、そこにアプリケーションのコードが配置される。その後currentシンボリックリンクが新しいアプリケーションへと切り替えられる。これによってダウンタイムなしでPHPアプリのコードをデプロイできます。

OPcacheのキャッシュ

ところが、PHP5.5+php-fpm+OPcacheという環境でこの構成のデプロイをためしたところ、実行中のアプリケーションにデプロイコードが反映されないという問題が生じました。 (※OPcacheはPHP5.5の新しいコードキャッシュシステムでPHP5.4以前のAPCに替わるものです)

調査していたら以下の解説記事を発見しました。

OpCache and symlink-based deployments

要約すると

  • OPcacheはAPCとはファイルのキャッシュの仕方が違う。
  • OPcacheはシンボリックを解決して、実ファイルパスの状態でキャッシュする。
  • よってシンボリックリンクを更新しても、実ファイルパスのキャッシュが保持されてしまう。

というわけだそうです。(このへんをちゃんとPHPのソース読んで、自分で裏付けとれるといいと思うのですが。。。技術力の低さが露呈しますね。。。(ー_ー;))

上記の解説記事では対応方法もいくつか提示されていて

  • webサーバの設定を直接書き換える(シンボリックリンクの代わりにweb(AP)サーバが参照するパスを直接書き換える)
  • OPcacheをリセットする
  • php-fpmを再起動する(多少のダウンタイムあり)

ansibleやpuppetを使っていればデプロイでwebサーバの設定を書き換えるのは簡単かも、と言及されています。 ただ、シンボリックリンクを使い続けたいならキャッシュをリセットするか、php-fpmを再起動する。 OPcacheはキャッシュをリセットする関数opcache_resetをもっているので、 これを使用した対応方法も(Laravelを使った例で)示されています。

さて、自分はどーしようか悩み中。 デプロイタスクがちょっとだけど複雑になるのと、コードキャッシュはアプリケーションより一段下の技術レイヤと考えているので、 それのリセット処理をアプリケーションのコードに直接入れるような対応はやりたくない。

この際、最近盛り上がってきているdockerでBlue-Green Deployment的なことをやってしまうのもアリかなと考えています。

nginxでerror_log出力先を設定ファイルで指定しても`/var/log/nginx/error.log`を読みにいってしまう件

nginx.conf

error_log  /path/to/error.log;

とか書いて、デフォルトと異なる場所にエラーログを出力するようにしてみたら、起動時に

nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (13: Permission denied)

とか警告が出てしまってました。なんで出力先変更したのに/var/log/nginx/error.logを読みに行ってしまうのか〜、と調べていたら いつものようにStackOverflowで質問が見つかって、ありがとうございます、と。

なんでも、起動してコンフィグファイルを読みに行くまでの間にnginxはコンパイル時に指定したエラーログ出力先(私の環境の場合(CentOS)は/var/log/nginx/error.log)を使うとのこと。

How to turn off or specify the nginx error log location? - Stack Overflow

上記から、リンクされている公式ドキュメントにもちゃんと記載がありました。

CoreModule - Nginx Community

というわけで、起動時のエラーも/var/log/nginx/error.log以外に向けたい場合はconfigureオプションを指定してソースから入れなおすしかないわけだけど、個人的にやりたくないんですよねソースからのインストール。悩ましいぃぃ。

Laravelのバリデーション拡張を作った

LaravelでWebアプリを書いていて、ややバリデーション周りの機能が薄いと感じたので拡張パッケージを書きました。

LaravelValidatorExtension

Laravel標準のバリデーションについては公式のドキュメントを見ればいいかと思います。この場合Validator::makeメソッドに入力データ、バリデーションルール、デフォルトから変える必要があるならエラー時のメッセージ、をそれぞれ配列で渡してvalidatorオブジェクトを作成し、その後failsメソッドで検証が通るかどうかを確認します。例えば以下のようなコードになります。

<?php
$rules = array(
    'username' => 'required|alpha',
    'password' => 'required|alpha|min:8',
);

$validator = Validator::make(Input::all(), $rules);

if ($validator->fails()) {
    return Redirect::back()->withErrors($validator);
}

シンプルなのはいいのですが、実際のアプリケーションを書くとなると更に以下のようなことがしたくなります。

  • バリデーションルールの定義箇所をメインロジックから個別のクラスに外出ししたい。
  • バリデーションの前後で値の変換処理を行いたい。例えばバリデーションの前に値をtrimしたり年月日で個別のフィールドに入力された値を結合して日付形式にするなど。

このへんの仕組みが標準では用意されていなかったので、前述の拡張を書きました。

使い方

バリデータクラスをこんな感じに定義して。。。

<?php
// app/validators/BlogValidator.php
class BlogValidator extends BaseValidator
{
    protected function configure()
    {
        $this
            ->rule('title', 'required', 'Title is required.')
            ->rule('title', 'max:100', 'Title must not be greater than 100 characters.')
            ->rule('body', 'pass')
            ;
    }
}

以下のように使います。

<?php
$validator = BlogValidator::make(Input::all());
if ($validator->fails()) {
    return Redirect::back()->withInput(Input::all())->withErrors($validator);
}

$data = $validator->onlyValidData();

バリデーション定義がメインロジックから切りだされてスッキリ。

また$validator->onlyValidDataメソッドはバリデートが行われた項目の値のみを配列で戻すメソッドなので、DB更新時などはこの値をEloquentモデルのマスアサインメントでまるっと設定してやればいいかと思います。

フィルタとカスタムバリデーションルール

バリデーション前後に何らかの処理を入れたいときはbeforeFilterafterFilterクロージャを登録します。

<?php
class BlogValidator extends BaseValidator
{
    protected function configure()
    {
        $this->beforeFilter(function($validator){
            // your code
        });

        $this->afterFilter(function($validator){
            // Modify title after validation.
            $title = $validator->title;
            $title .= " created by kohki";
            $validator->title = $title;
        });
    }
}

独自のバリデーションルールを定義したいときはvalidateXXXというメソッドを作ればOKです。メソッドの規約は標準のカスタムバリデーションルールの定義方法を同じですので、公式ドキュメントを参考にしてください。

<?php
class BlogValidator extends BaseValidator
{
    protected function configure()
    {
        $this
            ->rule('title', 'required', 'Title is required.')
            ->rule('title', 'max:100', 'Title must not be greater than 100 characters.')
            ->rule('body', 'foo', 'Body must be foo only!')
            ;
    }

    protected function validateFoo($attribute, $value, $parameters)
    {
        return $value == 'foo';
    }
}

インストール方法

composerでインストールします。composer.jsonに以下を記述して

"require": {
    "kohkimakimoto/laravel-validator-extension": "0.*"
}

composer updateします。

$ composer update

ServiceProviderとBaseValidatorのエイリアスapp/config/app.phpに登録します。

'providers' => array(
    ...
    'Kohkimakimoto\ValidatorExtension\ValidatorExtensionServiceProvider',
}
'aliases' => array(
    ...
    'BaseValidator' => 'Kohkimakimoto\ValidatorExtension\Validator',
),

また、私は今のところapps/validatorsディレクトリを切ってそこに個々のバリデータクラスを作成しているので、オートロードされるようにLaravel(app/start/global.php)とcomposer(composer.json)にオートロード設定を追加します。

ClassLoader::addDirectories(array(
    ...
    app_path().'/validators',
));
"autoload": {
    "classmap": [
        ...
        "app/validators"
    ]
}

Laravelかわいいよ、Laravelヽ(´ー`)ノ

pecoでカレントディレクトリごとによく使うコマンドを呼び出せるようにする

pecoをランチャーのようにして使う

の続き。ちょいちょいスクリプトをカスタマイズしていたので。

ところで最近の開発はコードを書いているときだいたい開発中アプリケーションのトップディレクトリにいて、そこでいつもいくつか決まったコマンドを実行する、ということがよくあります。gruntとかcomposer installとかphp vendor/bin/phpunitとか。

私はPHPをよく書くのでcomposer installなどは手に馴染んでいて入力するのに困ったりしないのですが、たまにRubyのプロジェクトをいじるときBundlerの使い方を忘れていてbundle install --path=vendor/bundle --binstubs=vendor/binとかを毎回、Gistにメモっておいたスニペットから引っ張ってきて入力してたりしました。

アプリケーションごとのスニペットファイル

そこでpecoをランチャーのようにして使うで書いたスクリプトをちょっと拡張して、カレントディレクトリに.snippetsファイルをおいておくと、そこからもスニペットを取得するようにしました。

# snippets
function peco-snippets() {

    local line
    local snippet
    local cwd
    local local_snippet
    if [ ! -e "~/.snippets" ]; then
        echo "~/.snippets is not found." >&2
        return 1
    fi

    # Get snippets in the current directory if it exists.
    cwd=`pwd`
    if [ -e "$cwd/.snippets" ]; then
      local_snippet="$cwd/.snippets"
    else
      local_snippet=""
    fi

    line=$(cat $local_snippet ~/.snippets | grep -v "^\s*#" | grep -v '^\s*$' | peco --query "$LBUFFER")
    if [ -z "$line" ]; then
        return 1
    fi
    
    snippet=$(echo "$line" | sed "s/^[ |\*]*\[[^]]*\] *//g")
    if [ -z "$snippet" ]; then
        return 1
    fi

    BUFFER="$snippet"
    zle clear-screen
}
zle -N peco-snippets
bindkey '^x^x' peco-snippets

忘れやすいコマンドなどは.snippetsファイルを以下のように記述しておけば、

EArray (https://github.com/kohkimakimoto/EArray)
 * [phpunit test] php vendor/bin/phpunit
 * [fix code] php vendor/bin/php-cs-fixer fix src
 * [composer] composer update

cmd-x+cmd-xのショートカットでpecoの選択インターフェースが起動して、アプリごとに必要なコマンドが選択肢に表示されるので、あとはそこから選べばよくなりました。

f:id:kohkimakimoto:20140727114321g:plain

pecoをランチャーのようにして使う

前回に引き続き、pecoが大変気に入ったので、その後もいろいろネットで情報さがしたりしてました。それで

peco/percolでCUIなスニペットツールを作ってみる

の記事を見て同じこと導入してみました。いやはや便利。元記事に感謝。ついでに多少カスタマイズして、スニペットの先頭にラベルをつけてみました。

手順

zshの場合は以下のような関数を.zshrcなどに記述しておく

function peco-snippets() {

    local line
    local snippet

    if [ ! -e ~/.snippets ]; then
        echo "~/.snippets is not found." >&2
        return 1
    fi

    line=$(grep -v "^#" ~/.snippets | peco --query "$LBUFFER")
    if [ -z "$line" ]; then
        return 1
    fi
    
    snippet=$(echo "$line" | sed "s/^\[[^]]*\] *//g")
    if [ -z "$snippet" ]; then
        return 1
    fi

    BUFFER=$snippet
    zle clear-screen
}

zle -N peco-snippets
bindkey '^x^x' peco-snippets

bashの場合は以下のような関数を.bashrcなどに記述

function peco-snippets() {

    local line
    local snippet

    if [ ! -e ~/.snippets ]; then
        echo "~/.snippets is not found." >&2
        return 1
    fi

    line=$(grep -v "^#" ~/.snippets | peco --query "$READLINE_LINE")
    if [ -z "$line" ]; then
        return 1
    fi
    
    snippet=$(echo "$line" | sed "s/^\[[^]]*\] *//g")
    if [ -z "$snippet" ]; then
        return 1
    fi
    READLINE_LINE="$snippet"
    READLINE_POINT=${#READLINE_LINE}
    clear
}

bind -x '"\C-x\C-x":peco-snippets'

あとはスニペットファイル.snippet[***]の形式でラベルをつけたコマンドを記述しておく。

# SSH
[ローカル仮想環境へSSH接続:local-server01:connect] ssh kohkimakimoto@192.168.56.21 -p 22

# Vagrant
[ローカル仮想環境Vagrant状態確認:status] cd /Users/kohkimakimoto/Documents/vagrant/hk && vagrant status && cd -
[ローカル仮想環境Vagrant起動:up] cd /Users/kohkimakimoto/Documents/vagrant/hk && vagrant up && cd -
[ローカル仮想環境Vagrant停止:halt] cd /Users/kohkimakimoto/Documents/vagrant/hk && vagrant halt && cd -

# etc
[Snippetsファイルを開く:open] subl ~/.snippets
[Shell拡張設定ファイルを開く:open] subl ~/.shell_extention

2014/06/30編集

.snippetsを読みに行く前にファイルの存在チェックをいれました。

2014/07/01編集

コマンド内に[]があるとラベル削除がうまくいかないところを直しました。

pecoでコマンドラインからファイルやディレクトリを開いたりしてみる

すこし乗り遅れた感じですが、最近話題のpecoをさわってみました。 pecoがなんなのかは以下のページなどを参照してください。

このツールはGoで書かれていて、しかも各種プラットフォーム向けにバイナリファイルを配布しているので、パスの通ったディレクトリにそのバイナリファイルをおくだけで動作するという手軽さがいいです。導入が楽なのは個人的にすごく重要なので。

機能をざっくりいうと、コマンド標準出力の行に対して選択機能のインターフェースを差し込めるというモノです。 文字にすると簡素なものですが、使ってみるとものすごく応用性の高いツールです。これホントすげー。でもこのすごさが言葉で説明できん。ツール自体のシンプルさとそれ故の応用性の高さが見事で、なんというか美しいのです。たぶんあれだ、確かUnixの哲学に「パイプやリダイレクトでつなげられるように、プログラムは最小の単位で設計すべし」みたいなものがあったハズで、それをドンピシャで体現しているんじゃないかと。pecoはpercolという同様のツールをもとに作られているとのことですが、この仕組みを考えた人は相当頭いいなーと、思わせてくれる逸品です。

さて、調べてみたらみんな自分の目的にあわせて、いろいろラッパーの関数やエイリアスを書いたりしてるみたいなので、私もちょこっと書いてみました。なおMacの環境用です。

function peco-cd()
{
    local var
    local dir
    if [ ! -t 0 ]; then
    var=$(cat -)
    dir=$(echo -n $var | peco)
    else
        return 1
    fi

    if [ -d "$dir" ]; then
        cd "$dir"
    else
        echo "'$dir' was not directory." >&2
        return 1
    fi
}

これはcdで移動したいディレクトリをfindコマンドなどで検索して、検索結果からpecoで選択、移動する、というのをやります。以下のように使っています。

find . | peco-cd

同様にfindコマンドで探したファイルを開くような関数も書きました。

function peco-open()
{
    local var
    local file
    local command="open"
    if [ ! -t 0 ]; then
        var=$(cat -)
        file=$(echo -n $var | peco)
    else
        return 1
    fi

    if [ -n "$1" ]; then
      command="$1"
    fi

    if [ -e "$file" ]; then
        eval "$command $file"
    else
        echo "Could not open '$file'." >&2
        return 1
    fi
}

これは

find . | peco-open

のように使います。またpeco-openの第二引数にコマンドを指定すれば、そのコマンドで選択したファイルを開きます。 たとえば、テキストファイルをコマンドラインで検索して、SublimeTextで開くには以下のようにやります。

find . | peco-open subl

これでもはや、Finderでちまちまディレクトリをおりていく必要はなくなったのでした。(^O^)

Bashスクリプトで実行ファイルのディレクトリを取得する

よくやるのが以下の記述だったのだけれど

SCRIPT_DIR=$(cd $(dirname $0); pwd)

これだと、このスクリプトシンボリックリンクから呼び出したときに、 実ファイルのパスでなくリンク先のディレクトリが取得されてしまっていた。 シンボリックリンクから呼び出されたときも、実ファイルのパスを返すようにするには、

SCRIPT_DIR=$(cd $(dirname $(readlink $0 || echo $0));pwd)

のようにすればよさげ。

追記)id:ngyukiさんにご指摘をいただいたので修正。

スクリプトシンボリックリンクなディレクトリにあることを考慮して readlink -f とか cd -P とか pwd -P とかにするとなお良いのかな

おお、なるほど。やってみたら確かにシンボリックリンクなディレクトリにあるとうまくいかなかった。なので修正した以下の書き方がベターです。

SCRIPT_DIR=$(cd $(dirname $(readlink -f $0 || echo $0));pwd -P) 

これならうまくいきました。

追記)2014-06-25

上記の記事はシンボリックリンク相対パスだとやっぱりうまくいかなかったり、readlink -fmacでは使えなかったりでいろいろ至らない。 その後もいろいろ調べたら、rbenvスクリプト内にあったabs_dirnameがいい感じでした。 ちょこっと手直ししたのが以下になります。

abs_dirname() {
  local cwd="$(pwd)"
  local path="$1"

  while [ -n "$path" ]; do
    cd "${path%/*}"
    local name="${path##*/}"
    path="$(readlink "$name" || true)"
  done

  pwd -P
  cd "$cwd"
}

script_dir="$(abs_dirname "$0")"