php-tokyo tyrant で tch だとtcrdbtblgenuid関数が失敗して Segmentation faultする件

php-tokyo tyrant を利用して、ディフォルトのままでsession.save_handler = tokyo_tyrant ととすると、Segmentation fault がおきる問題があったので書いてみる。

環境構築

tokyo tyrant をインストールする

インストールの前に、checkinstallというソフトを利用すると、パッケージを自動で作ってくれて便利です。
まずはこれを導入します。

checkinstallでぐぐってrpm見つけてきてねw //centso系
   or
apt-get install checkinstall             //debian系

//まずtokyo cabinetをインスコします。
cd
wget http://1978th.net/tokyocabinet/tokyocabinet-1.4.43.tar.gz
tar zxvf tokyocabinet-1.4.43.tar.gz
cd tokyocabinet-1.4.43
./configure
make
checkinstall

//次に tokyo tyrant をインスコします。
cd ..
wget http://1978th.net/tokyotyrant/tokyotyrant-1.1.40.tar.gz
tar zxvf tokyotyrant-1.1.40.tar.gz
cd tokyotyrant-1.1.40
./configure
make
checkinstall
cd ..

//ついでだから、シンボリックリンクを張る.
ln -s /usr/local/sbin/ttservctl /etc/init.d/ttservctl

//tokyo tyrant起動
/etc/init.d/ttservctl start
pecl php-tokyo tyrant をインストールする
#pecl install tokyo_tyrant-0.4.0
//ちなみに削除するときは、pecl uninstall tokyo_tyrant-0.4.0

//設定ファイルを書く
#vi /etc/php.d/tokyotyrant.ini                   //centos系
  or
#vi /etc/php5/apache2/conf.d/tokyotyrant.ini     //debian系

----------tokyotyrant.ini-------------------
extension=tokyo_tyrant.so
[tokyo_tyrant]
tokyo_tyrant.php_expiration=1
--------------------------------------------

//apache再起動
#/etc/init.d/httpd   restart   //centos系
  or
#/etc/init.d/apache2 restart   //debian系
動作させてクラッシュさせる

とりあえずこんな phpを作ります。

-----------test.php------------
<?
//tokyo tyrant を利用する
ini_set('session.save_handler', 'tokyo_tyrant');
ini_set('session.save_path', 'tcp://127.0.0.1:1978');

session_start();

$a = (int)@$_SESSION['a'];
@$_SESSION['a'] = $a + 1;

echo "Count : {$a}";
?>
---------------------------------

ブラウザでtest.phpを動かしてみると、不思議な挙動をするはずです。
真っ白のページになるか、いきなりダウンロードになったりするはずです。

これは phpがクラッシュしたときによく発生する状況です。
ログを見てみましょう。

tail /var/log/httpd/error.log    //centos系
   or
tail /var/log/apache2/error.log  //debian系

[Tue Apr 13 01:51:09 2010] [notice] child pid 18072 exit signal Segmentation fault (11)
[Tue Apr 13 01:51:31 2010] [notice] child pid 18073 exit signal Segmentation fault (11)
[Tue Apr 13 01:51:55 2010] [notice] child pid 18074 exit signal Segmentation fault (11)

おー落ちてますねー

解決

クラッシュする直接的名原因は、 php-tokyo_tyrant のチェックが甘いからです。
ただし、実質的な問題は、 tokyo_tyrant を .tch モードで動かしたことにあります。

tokyo tyrant を .tct モードで動かしてみる。

vi /usr/local/sbin/ttservctl
--------/usr/local/sbin/ttservctl---
dbname="$basedir/casket.tch#bnum=1000000"
↓
dbname="$basedir/casket.tct#bnum=1000000"
------------------------------------

//tokyo tyrant 再起動
/etc/init.d/ttservctl restart

ブラウザでtest.phpにアクセスしてみましょう、今度は大丈夫。
ページをリロードするたびに値が +1 されていくのが分かります。

php-tokyo_tyrant の公式では以下のパラメータが推奨されています。

http://www.php.net/manual/ja/tokyo-tyrant.installation.php

  • extpc expire 30.0 '/tmp/sessions.tct#idx=ts:dec'

オリジナルのttservctlには少し癖があって、 tokyo tyrant を普通に終了させなかった場合、二度と立ち上がらなくなったりします。(意図した動作?)

#ttservctl start
Starting the server of Tokyo Tyrant
Existing process: 0

見たいな感じで、実行された瞬間にアボートしてみたり。。。

実はこれって、 /var/ttserver/pid を見ているらしい。
普通は、 ttserver(tokyo tyrant)が終了するときに /var/ttserver/pid を削除するらしいけど、killall とかで殺してしまった場合とかで pid が残っていると、このファイルを手で削除しないと起動しない。

//手で残骸を削除する
rm  /var/ttserver/pid

/etc/init.d/ttservctl stop したときにいいようにしてほしいんだけどねぇ。。。

推奨パラメーターを埋め込んだ ttservctl を作ってみる。
vi /etc/init.d/ttservctl
--------/etc/init.d/ttservctl--------
#! /bin/sh

#----------------------------------------------------------------
# Startup script for the server of Tokyo Tyrant
#----------------------------------------------------------------


# configuration variables
prog="ttservctl"
cmd="ttserver"
basedir="/var/ttserver"
port="1978"
pidfile="$basedir/pid"
#logfile="$basedir/log"
#ulogdir="$basedir/ulog"
#ulimsiz="256m"
#sid=1
#mhost="remotehost1"
#mport="1978"
#rtsfile="$basedir/rts"
#dbname="$basedir/casket.tch#bnum=1000000"
dbname="$basedir/casket.tct#idx=ts:dec"
option="-extpc expire 30.0"
retval=0


# setting environment variables
LANG=C
LC_ALL=C
PATH="$PATH:/sbin:/usr/sbin:/usr/local/sbin"
export LANG LC_ALL PATH


# start the server
start(){
  printf 'Starting the server of Tokyo Tyrant\n'
  mkdir -p "$basedir"
  if [ -z "$basedir" ] || [ -z "$port" ] || [ -z "$pidfile" ] || [ -z "$dbname" ] ; then
    printf 'Invalid configuration\n'
    retval=1
  elif ! [ -d "$basedir" ] ; then
    printf 'No such directory: %s\n' "$basedir"
    retval=1
  elif [ -f "$pidfile" ] ; then
    pid=`cat "$pidfile"`
    printf 'Existing process: %d\n' "$pid"
    retval=1
  else
    cmd="$cmd -port $port -dmn -pid $pidfile"
    if [ -n "$logfile" ] ; then
      cmd="$cmd -log $logfile"
    fi
    if [ -n "$ulogdir" ] ; then
      mkdir -p "$ulogdir"
      cmd="$cmd -ulog $ulogdir"
    fi
    if [ -n "$ulimsiz" ] ; then
      cmd="$cmd -ulim $ulimsiz"
    fi
    if [ -n "$sid" ] ; then
      cmd="$cmd -sid $sid"
    fi
    if [ -n "$mhost" ] ; then
      cmd="$cmd -mhost $mhost"
    fi
    if [ -n "$mport" ] ; then
      cmd="$cmd -mport $mport"
    fi
    if [ -n "$rtsfile" ] ; then
      cmd="$cmd -rts $rtsfile"
    fi
    printf "Executing: %s\n" "$cmd"
    cmd="$cmd $option $dbname"
    $cmd
    if [ "$?" -eq 0 ] ; then
      printf 'Done\n'
    else
      printf 'The server could not started\n'
      retval=1
    fi
  fi
}


# stop the server
stop(){
  printf 'Stopping the server of Tokyo Tyrant\n'
  if [ -f "$pidfile" ] ; then
    pid=`cat "$pidfile"`
    printf "Sending the terminal signal to the process: %s\n" "$pid"
    kill -TERM "$pid"
    c=0
    while true ; do
      sleep 0.1
      if [ -f "$pidfile" ] ; then
        c=`expr $c + 1`
        if [ "$c" -ge 100 ] ; then
          printf 'Hanging process: %d\n' "$pid"
          retval=1
          break
        fi
      else
        printf 'Done\n'
        break
      fi
    done
  else
    printf 'No process found\n'
    retval=1
  fi
}


# send HUP to the server for log rotation
hup(){
  printf 'Sending HUP signal to the server of Tokyo Tyrant\n'
  if [ -f "$pidfile" ] ; then
    pid=`cat "$pidfile"`
    printf "Sending the hangup signal to the process: %s\n" "$pid"
    kill -HUP "$pid"
    printf 'Done\n'
  else
    printf 'No process found\n'
    retval=1
  fi
}


# check permission
if [ -d "$basedir" ] && ! touch "$basedir/$$" >/dev/null 2>&1
then
  printf 'Permission denied\n'
  exit 1
fi
rm -f "$basedir/$$"


# dispatch the command
case "$1" in
start)
  start
  ;;
stop)
  stop
  ;;
restart)
  stop
  start
  ;;
hup)
  hup
  ;;
*)
  printf 'Usage: %s {start|stop|restart|hup}\n' "$prog"
  exit 1
  ;;
esac


# exit
exit "$retval"



# END OF FILE
-------------------------------------

んなわけで、これでphp-tokyotyrant でコードを一切書かずに、セッションコントロールがでぎたわけです。
、、、コレにたどり着くのに1.5日ぐらいかかったのでデバッグの方法とかも書いてみる。

デバッグ

apachegdb でアタッチして動かしてみる。

//まず停止させる。
#/etc/init.d/httpd   stop   //centos系
  or
#/etc/init.d/apache2 stop   //debian系


//apacheをgdb経由で起動
#gdb httpd      //centos系
  or
#gdb apache2    //debian系

//gdbでア地上がったら、とりあえず実行させる
(gdb) run       //centos系
   or
(gdb) run -X    //debian系


Starting program: /usr/sbin/apache2
(no debugging symbols found)
(no debugging symbols found)
(no debugging symbols found)
(no debugging symbols found)
(no debugging symbols found)
(no debugging symbols found)
...

//と、色々流れるので、この間にWebページをリロードしてphpを動かす。
//すると、クラッシュしちゃったよーと表示される

Program exited normally.

//バックトレースを見る
(gdb) bt
#0  0xb7e40e18 in strcmp () from /lib/i686/cmov/libc.so.6
#1  0xb610084d in php_tt_get_sess_data (conn=0x841373c,
    sess_rand=0x8417334 "4179f2d042292a8fa77d9ed8ba3aafbb", pk=0x859def4 "(null)",
    pk_len=6, data_len=0xbfffbc30, mismatch=0xbfffbbab "")
    at /tmp/pear/temp/tokyo_tyrant/session_funcs.c:199
#2  0xb6101a86 in ps_read_tokyo_tyrant (mod_data=0xb7211470,
    key=0x859dd34 "4179f2d042292a8fa77d9ed8ba3aafbb-9828653bc8ae1b55377947ad743096f23ae011dc-0-(null)", val=0xbfffbc34, vallen=0xbfffbc30)
    at /tmp/pear/temp/tokyo_tyrant/session.c:184
#3  0xb6e432bf in php_session_start () from /usr/lib/apache2/modules/libphp5.so
#4  0xb6e439a0 in zif_session_start () from /usr/lib/apache2/modules/libphp5.so
#5  0xb6fb3e41 in execute_internal () from /usr/lib/apache2/modules/libphp5.so
#6  0xb68f59f0 in xdebug_execute_internal (current_execute_data=0xbfffbdfc,
    return_value_used=0) at /tmp/pear/temp/xdebug/xdebug.c:1631
#7  0xb6fccc80 in ?? () from /usr/lib/apache2/modules/libphp5.so
#8  0xbfffbdfc in ?? ()
#9  0x00000000 in ?? ()

strcmp で落ちているのが分かります。
それを呼び出したのは、php_tt_get_sess_data という php-tokyo_tyrant の関数だということがわかります。


gdbはもういいので終了すします。

php-tokyo_tyrant のソースは /tmp/pear/download/tokyo_tyrant-0.4.0 にあります。
とりあえずビルドしてみっか。

#cd /tmp/pear/download/tokyo_tyrant-0.4.0
#phpize
#./configure
#make
#make install

apache を再起動させて読み込ませる
#/etc/init.d/apache2 restart      //debian系
  or
#/etc/init.d/httpd restart        //centos系

とりあえず落ちないようにソースを書き換えてしまおう。

char *php_tt_get_sess_data(php_tt_conn *conn, char *sess_rand, const char *pk, int pk_len,
 int *data_len, zend_bool *mismatch TSRMLS_DC)
{
        TCMAP *cols;
        char *buffer = NULL;

        *data_len = 0;
        *mismatch = 0;

        cols = tcrdbtblget(conn->rdb, pk, pk_len);

        if (cols) {
                const char *checksum = tcmapget2(cols, "hash");
                /* Make sure that we get back the expected session */
                //↓ここで落ちてる! BUG!!!!
                if (strcmp(checksum, sess_rand) == 0) {               
                        buffer = estrdup(tcmapget2(cols, "data"));
                        *data_len = strlen(buffer);
                } else {
                        *mismatch = 1;
                }
                tcmapdel(cols);
        }
        return buffer;
}

ようするに、tcmapget2 命令で取ってきた値が NULL になっているわけだ。
NULL を strcmp しようとするから、ぬるぽになる。

char *php_tt_get_sess_data(php_tt_conn *conn, char *sess_rand, const char *pk, int pk_len,
 int *data_len, zend_bool *mismatch TSRMLS_DC)
{
        TCMAP *cols;
        char *buffer = NULL;

        *data_len = 0;
        *mismatch = 0;

        cols = tcrdbtblget(conn->rdb, pk, pk_len);

        if (cols) {
                const char *checksum = tcmapget2(cols, "hash");
                /* hacked by rti */
                if (checksum == NULL)
                {
                       return NULL;
                }
                /* hacked by rti */

                /* Make sure that we get back the expected session */
                if (strcmp(checksum, sess_rand) == 0) {               
                        buffer = estrdup(tcmapget2(cols, "data"));
                        *data_len = strlen(buffer);
                } else {
                        *mismatch = 1;
                }
                tcmapdel(cols);
        }
        return buffer;
}

これをリコンパイルしてインストールすると、とりあえず落ちなくなる。

万事解決、、、なわけねーよ。
この状態だとクラッシュはしないけどセッションが保存されないのだ。
セッションなのに、セッションが保存できないKVSなんて、、、サイテーだぜ。


で、なぜセッションが保存されないのかだけど、これがすごく深い問題だった。
結局、 tokyo tyrant の tcrdbtblgenuid 関数が .tch データベースだと常に -1 しか返さないのが問題らしい。
tcrdbtblgenuid関数は、ユニークな値を返す関数らしい。windowsでいうところのGUIDや mysqlのauto_incrementみたいなものだ。

php-tokyo_tyrant はこれを利用して、セッションIDを管理している。
だが、 tcrdbtblgenuid関数 が失敗し、 -1 を返したときは NULL を返すようになっている。

char *php_tt_create_pk(php_tt_conn *conn, int *pk_len)
{
        long pk = -1;
        char *pk_str;

        //これが失敗し -1 になる。
        pk = (long)tcrdbtblgenuid(conn->rdb);
        *pk_len = 0;

        if (pk == -1) {
                //↓ここにくる
                return NULL;
        }

        *pk_len = spprintf(&pk_str, 256, "%ld", pk);
        return pk_str;
}

これにより、セッション番号をパースする php_tt_tokenize 関数で正しくセッションIDがパースできなくて、エラーになってしまうのが問題らしい。

最後の桁に tcrdbtblgenuid関数で取得したユニークな数字が入る。
tcrdbtblgenuid関数が失敗して、 内部状態が NULL になってしまい問題がおきる。

× 4179f2d042292a8fa77d9ed8ba3aafbb-9828653bc8ae1b55377947ad743096f23ae011dc-0-(null) 
○ 4179f2d042292a8fa77d9ed8ba3aafbb-9828653bc8ae1b55377947ad743096f23ae011dc-0-12

tcrdbtblgenuid関数は、tokyo tyrant本体にこんな感じに定義されている。
うまく行かないときには、 -1 を返す仕様らしい。
エラーが発生したときの原因ぐらい教えてくれたっていいだけど、、、tokyo tyrant は無口なソフトウェアらしい。

/* Generate a unique ID number of a remote database object. */
int64_t tcrdbtblgenuid(TCRDB *rdb){
  assert(rdb);
  TCLIST *args = tclistnew2(1);
  TCLIST *rv = tcrdbmisc(rdb, "genuid", 0, args);
  tclistdel(args);
  if(!rv) return -1;
  int64_t uid = -1;
  if(tclistnum(rv) > 0) uid = tcatoi(tclistval2(rv, 0));
  tclistdel(rv);
  return uid;
}

tokyo tyrant をハックするときの注意点としては、tokyo tyrant をビルドしなおしたら、 php-tokyo_tyrant の方もビルドしなおさないとダメらしい。static linkしているっぽい。

んでー、穴居間は、 tokyo tyrant の仕様で .tch 形式でデータを保存する(ディフォルト)だと tcrdbtblgenuid関数というか、genuidは定義されていなくて、 失敗の -1 が帰ってくるようだ。
詳しいことはよく分からないけど、そういうものらしい。

今のところ、この問題が顕著に現れるのは、tcrdbtblgenuidでユニークなIDを取得してなんかやっているソフトウェアだけだ。
php-tokyo_tyrantも session.save_handler に記述しないければこの問題はおきない。

session.save_handlerを利用する利点

コードを書きたくないのと、 session.save_handler = tokyo_tyrant だと、 GC自動でをやってくれるっぽいからだ。
tokyo tyrant にはなぜか GCの機能がないので、不要になったデータは自分で消さないといけない。
そのコードを書くのが面倒で、 php-tokyo_tyrantのソースコードを眺めていたら、 それっぽいところがあったからだ。
まだどれくらいのタイミングで GC が走るかは検証していないのでなんともいえないけど。。。

php-tokyo_tyrantの session.c のコードを見てください。

PS_GC_FUNC(tokyo_tyrant)
{
        /* Handle expiration on PHP side? */
        if (TOKYO_G(php_expiration)) {
                TT_SESS_DATA;
                return php_tt_gc(session->pool TSRMLS_CC);
        }

        return SUCCESS;
}

そして、それが呼び出している session_func.c の php_tt_gc関数を見てください。

zend_bool php_tt_gc(php_tt_server_pool *pool TSRMLS_DC)
{
        int i;
        zend_bool overal_res = SUCCESS;
        char timestamp[64];

        memset(timestamp, '\0', 64);
        sprintf(timestamp, "%ld", SG(global_request_time));

        for (i = 0; i < pool->num_servers; i++) {
                php_tt_server *server;
                php_tt_conn *conn;

                RDBQRY *query;

                server = php_tt_pool_get_server(pool, i TSRMLS_CC);
                conn   = php_tt_conn_init(TSRMLS_C);

                if (!php_tt_connect_ex(conn, server->host, server->port, TOKYO_G(default_timeout), 1 TSRMLS_CC)) {
                        overal_res = FAILURE;
                        continue;
                }

                query = tcrdbqrynew(conn->rdb);
                tcrdbqryaddcond(query, "ts", RDBQCNUMLT, timestamp);

                if (!tcrdbqrysearchout(query)) {
                        php_tt_server_fail_incr(server->host, server->port TSRMLS_CC);
                        overal_res = FAILURE;
                        break;
                }

                tcrdbqrydel(query);
                php_tt_conn_deinit(conn TSRMLS_CC);
        }
        return overal_res;
}

どーみても、GCぽいです。

で、あとはこれが呼ばれる条件なんですが、
TOKYO_G(php_expiration) の定義を追いかけていくと、 tokyotyrant.c にあります。

PHP_INI_BEGIN()
        STD_PHP_INI_ENTRY("tokyo_tyrant.default_timeout", "0.0", PHP_INI_ALL, OnUpdateReal, default_timeout, zend_tokyo_tyrant_globals, tokyo_tyrant_globals)
        STD_PHP_INI_ENTRY("tokyo_tyrant.session_salt", "", PHP_INI_ALL, OnUpdateString, salt, zend_tokyo_tyrant_globals, tokyo_tyrant_globals)
        STD_PHP_INI_ENTRY("tokyo_tyrant.key_prefix", "", PHP_INI_ALL, OnUpdateKeyPrefix, key_prefix, zend_tokyo_tyrant_globals, tokyo_tyrant_globals)
        STD_PHP_INI_ENTRY("tokyo_tyrant.allow_failover", "1", PHP_INI_ALL, OnUpdateBool, allow_failover, zend_tokyo_tyrant_globals, tokyo_tyrant_globals)
        STD_PHP_INI_ENTRY("tokyo_tyrant.fail_threshold", "5", PHP_INI_ALL, OnUpdateLong, fail_threshold, zend_tokyo_tyrant_globals, tokyo_tyrant_globals)
        STD_PHP_INI_ENTRY("tokyo_tyrant.health_check_divisor", "1000", PHP_INI_ALL, OnUpdateLong, health_check_divisor, zend_tokyo_tyrant_globals, tokyo_tyrant_globals)
        STD_PHP_INI_ENTRY("tokyo_tyrant.php_expiration", "0", PHP_INI_ALL, OnUpdateBool, php_expiration, zend_tokyo_tyrant_globals, tokyo_tyrant_globals)
PHP_INI_END()

一番下のところに注目です。
php.ini で [tokyo_tyrant] の項目に書く奴っぽいですね。
ディフォルトが 0 で動かないんで、1 にしてあげればいいと思います。

extension=tokyo_tyrant.so

[tokyo_tyrant]
tokyo_tyrant.php_expiration=1

こんな感じで、 phpで楽チンに tokyo tyrant か利用できると思います。。。