MySQLのシグナル処理解説

概要

main関数→init_signals関数にて設定されます。いろんなシグナルをトラップしています。

プラットフォームによっていくつか異なるinit_signals関数実装がありますが、ここではWindowsLinuxについて説明します。

Windows版でのシグナル処理

sql/mysqld.cc (init_signals関数全文)

#if defined(__WIN__) || defined(OS2)
static void init_signals(void)
{
  int signals[] = {SIGINT,SIGILL,SIGFPE,SIGSEGV,SIGTERM,SIGABRT } ;
  for (uint i=0 ; i < sizeof(signals)/sizeof(int) ; i++)
    signal(signals[i], kill_server) ;
#if defined(__WIN__)
  signal(SIGBREAK,SIG_IGN);     //ignore SIGBREAK for NT
#else
  signal(SIGBREAK, kill_server);
#endif
}

以下のシグナル受信時に、kill_server関数が呼ばれます。

  • SIGINT (ctrl+c)
  • SIGILL (無効命令)
  • SIGFPE (浮動小数点数例外)
  • SIGSEGV (セグメンテーション違反)
  • SIGTERM (終了要求)
  • SIGABORT (異常終了)

kill_server関数では以下の処理を行います。

  • SIGTERMであれば正常終了開始のログ、それ以外はシグナル受信によるエラー終了開始のログ
  • クライアントからの接続を全て切断(shutdown関数、closesocket関数を呼び強制切断)
  • 全てのクライアントスレッドにKILLマークを付ける(動作中のスレッドはこれで停止する)
  • slaveスレッドを停止
  • 待機中のクライアントスレッドを全て終了させる
  • SIGTERM以外ではAbortingのログ出力
  • リソースの解放(ログファイルを閉じる、メモリを解放するなど)

SIGSEGVの時にもちゃんとレプリケーションとかログとかの後始末を試みるんですね。

Linux版でのシグナル処理

Linuxでは以下のシグナル受信時に、handle_segfault関数が呼ばれます。

  • SIGSEGV
  • SIGABORT
  • SIGBUS
  • SIGILL
  • SIGFPE

handle_segfault関数では以下の処理を行います。

  • エラーログにメッセージを出力
  • エラーログにいくつかのサーバ変数(key_buffer_sizeなど)情報を出力
  • エラーログにstacktraceを出力
  • 直前まで実行していたSQL文をエラーログに出力
  • その他ライブラリ関連の情報をエラーログに出力
  • coreファイル出力設定が行われていればcoreファイルを出力

Linuxではこのパタンだととにかくエラーログに情報を出して終わりという感じですね。

上記以外のシグナルについては、起動時に作成されるシグナル処理用のスレッドにて処理されます。

シグナル処理用のスレッドはsigwait関数を呼び出し、シグナルの受信まで待機します。受信したシグナルが以下である場合には、ログに情報を出力し、kill_server関数が呼ばれます。

  • SIGTERM
  • SIGQUIT
  • SIGKILL

kill_server関数の振る舞いはWindowsと共通です。

受信したシグナルが以下である場合には、ステータス情報を標準出力し、様々なキャッシュのクリアやログのフラッシュを行います。

  • SIGHUP

クリア/フラッシュされるもの

  • ログファイルの再オープン
  • テーブルキャッシュ
  • 権限テーブル
  • スレッドキャッシュ
  • ホストキャッシュ

このSIGHUP受信時の動作ですが、なかなか面白いです。MySQLサーバを起動し、以下のコマンドを実行してみてください。

kill -SIGHUP `pgrep -x mysqld`

gitコマンド簡易一覧 for svnユーザ

gitを使いはじめたのですが、わからないことだらけなのでとりあえずsvnコマンドとの対応一覧。

svn checkout url git clone url
svn update git pull
svn status git status
svn revert path git checkout path
svn add file git add file
svn rm file git rm file
svn mv file git mv file
svn commit git commit -a
svn log git log

参考:http://www.tempus.org/n-miyo/git-course-trans-ja/svn.ja.html

mysqld エラーログソース解析

日本語版のPlanet MySQLが立ち上がりました。

Planet MySQLというのはMySQLに関連するブログを集めたアグリゲーションサイトで、Sun Microsystems(MySQL開発元)が運営しています。英語版は以前からありましたが、今回新しく日本語版が登場し、当ブログも登録しましたのでそれを記念してエントリを書いてみたいと思います。

公式マニュアルによるエラーログの解説

MySQLのエラーログについては以下の公式リファレンスマニュアルに説明がありますが、

ログのレベルの話だとか、どんなエラーが出力されるのかだとかの詳細は無いようでしたのでソース解析してみました。

エラーログを実際に出力している関数

いきなり底から始めますが、エラーログは以下のprint_buffer_to_file関数がfprintfで出力しています。(sql/log.cc)

static void print_buffer_to_file(enum loglevel level, const char *buffer)
{
  time_t skr;
  struct tm tm_tmp;
  struct tm *start;
  DBUG_ENTER("print_buffer_to_file");
  DBUG_PRINT("enter",("buffer: %s", buffer));

  VOID(pthread_mutex_lock(&LOCK_error_log));

  skr=time(NULL);
  localtime_r(&skr, &tm_tmp);
  start=&tm_tmp;
  fprintf(stderr, "%02d%02d%02d %2d:%02d:%02d [%s] %s\n",
          start->tm_year % 100,
          start->tm_mon+1,
          start->tm_mday,
          start->tm_hour,
          start->tm_min,
          start->tm_sec,
          (level == ERROR_LEVEL ? "ERROR" : level == WARNING_LEVEL ?
           "Warning" : "Note"),
          buffer);

  fflush(stderr);

  VOID(pthread_mutex_unlock(&LOCK_error_log));
  DBUG_VOID_RETURN;
}

出力中はmutexで排他制御を行い、fprintfした後はfflushですぐに書き込みを行うようです。出力先はstderr(=標準エラー)です。

ちなみに今回ソース解析を行った動機のひとつである「エラーログの出力フォーマット」「エラーログのレベル」ですが、この関数にばっちり書いてありますね。

エラーログの出力フォーマット

以下が出力フォーマットになります。

"%02d%02d%02d %2d:%02d:%02d [%s] %s\n"

サイズ固定で

YYMMDD hh:mm:ss [ログのレベル] エラーメッセージ

ということですね。これで安心してエラーログに対するテキスト処理ができますね!!

エラーログのレベル

エラーログのレベルは"ERROR"、"Warning"、"Note"の3つです。

level == ERROR_LEVEL ? "ERROR" : level == WARNING_LEVEL ? "Warning" : "Note"

3項演算子を2つ繋げて書いているのでなれない人には分かりにくいかもですが、if文で書き直すと以下のような感じです。

if (level == ERROR_LEVEL) {
  ERRORとする
} else {
  if (level == WARNING_LEVEL) {
    Warningとする
  } else {
    Noteとする
  }
}

print_buffer_to_file関数が呼ばれる流れ

次に流れを説明します。MySQLの各ソースコードはprint_buffer_to_file関数を直接は呼びません。ERROR, Warning, Noteの各レベルのログを出力するための関数が用意されていて、その関数がprint_buffer_to_file関数を呼び出すことになります。

  • ERRORの場合 : sql_print_error関数 → vprint_msg_to_log関数 → print_buffer_to_file関数
  • Warningの場合: sql_print_warnig関数 → vprint_msg_to_log関数 → print_buffer_to_file関数
  • Noteの場合 : sql_print_information関数 → vprint_msg_to_log関数 → print_buffer_to_file関数

※全てsql/log.ccに実装。

つまりこれらの関数を呼び出しているコードを探せば、どんな場合にERROR/Warning/Noteになるかが分かりますね!

grepしてもいいし、emacs+cscopeなら"C-c s c sql_print_error"すればいい。

ERRORレベルの出力例

かなりたくさんあります。以下はsql/mysqld.ccの一部です。他のソースからもガンガン出力してます。

*** sql/mysqld.cc:
close_connections[757]         sql_print_error("Got error %d from pthread_cond_timedwait",error);
kill_mysql[981]                sql_print_error("Can't create thread to kill server");
kill_server[1026]              sql_print_error(ER(ER_GOT_SIGNAL),my_progname,sig);
unireg_abort[1120]             sql_print_error("Aborting\n");
check_user[1381]               sql_print_error("Fatal error: Please read \"Security\" section of th\
e manual to find out how to run mysqld as root!\n");
check_user[1407]               sql_print_error("Fatal error: Can't change to run as user '%s' ; Ple\
ase check that the user exists!\n",user);
network_init[1546]             sql_print_error("Do you already have another mysqld server running o\
n port: %d ?",mysqld_port);
network_init[1552]             sql_print_error("listen() on TCP/IP failed with error %d",

Warningレベルの出力例

*** sql/mysqld.cc:
close_connections[870]         sql_print_warning(ER(ER_FORCING_CLOSE),my_progname,
print_signal_warning[1079]     sql_print_warning("Got signal %d from thread %ld",
check_user[1371]               sql_print_warning(
network_init[1645]             sql_print_warning("listen() on Unix socket failed with error %d",
console_event_handler[1829]    sql_print_warning("CTRL-C ignored during startup");
init_signals[2432]             sql_print_warning("setrlimit could not change the size of core files\
 to 'infinity'; We may not be able to generate a core file on signals");
init_common_variables[2876]    sql_print_warning("gethostname failed, using '%s' as hostname",
init_common_variables[2948]    sql_print_warning("Changed limits: max_open_files: %u max_connection\
s: %ld table_cache: %ld",
init_common_variables[2952]    sql_print_warning("Could not increase number of max_open_files to mo\
re than %u (request: %u)", files, wanted_files);

Noteレベルの出力例

こちらは主に起動/停止ログですね。

*** sql/mysqld.cc:
kill_server[1024]              sql_print_information(ER(ER_NORMAL_SHUTDOWN),my_progname);
clean_up[1214]                 sql_print_information(ER(ER_SHUTDOWN_COMPLETE),my_progname);
network_init[1538]             sql_print_information("Retrying bind on TCP/IP port %u", mysqld_port\
);
win_main[3838]                 sql_print_information(ER(ER_STARTUP),my_progname,server_version,
main[3838]                     sql_print_information(ER(ER_STARTUP),my_progname,server_version,

というわけで個別に追うしかない感じですね。

留意事項 - InnoDBの独自ログ

InnoDBはこんな感じの独自のエラーログを出す場合があります。

090108 14:58:25  InnoDB: ERROR: the age of the last checkpoint is 9433838,
InnoDB: which exceeds the log group capacity 9433498.
InnoDB: If you are using big BLOB or TEXT rows, you must set the
InnoDB: combined size of log files at least 10 times bigger than the

これはInnoDBがstderrを直接使用して出力しています。なのでこれらは上記説明からは例外。innobaseディレクトリをstderrでgrepすると特定できます。

MyISAMには独自のエラーログはありません。

Java - JNI - C/C++ デバッグ入門

明けましておめでとうございます。今年も宜しくお願いします。



というわけで昨年末に調べていたJNIプログラムデバッグ方法のまとめ。

これが一番参考になった。Debugging integrated Java and C/C++ code

やりたい事

  • JNIプログラムのデバッグをしたい(javaからJNI経由で呼ばれたC/C++プログラムのデバッグをしたい)
  • CUI環境で完結したい(対象マシンはサーバで遠隔地にありGUIは使用不可)

まとめ

こんな感じ。

  • javac -g
  • gcc -g
  • JPDA(java)
  • jdb -attach
  • gdb --pid=

※JPDAはJava Platform Debugger Architectureの略、JVMの機能の一つ。jdbはJDK付属のJava用デバッガ。gdbはいつものGNU Debugger。

senna-javaでの例

JavaプログラムとC/C++プログラムをデバッグビルド。

Javaプログラムではantを使っているのでbuild.xmlにて設定。

  <property name="debug" value="true" />

C/C++プログラムはMakefileを設定。

OPT = -Wall -fPIC -g

リビルド

ant clean test

JPDAを使ってjavaを実行。

LD_LIBRARY_PATH="." \
java -Xdebug -Xnoagent -Djava.compiler=none \
  -Xrunjdwp:transport=dt_socket,server=y,suspend=y \
  -cp "lib/junit.jar:senna-java-0.01.jar:classes:tests" \
  junit.textui.TestRunner senna.SnippetTest

こんな感じで待ちうけポート番号が示される。

Listening for transport dt_socket at address: 34436

jdbを実行。ポート番号を指定する。

tritonn@dev32:/srv/senna-java/trunk$ jdb -sourcepath "src/java" -attach 34436
uncaught java.lang.Throwable を設定しました
保留した uncaught java.lang.Throwable を設定しました
jdb の初期化中です...
>
VM が起動しました: 現行の呼び出しスタックにはフレームがありません

main[1]

まだjavaプログラムが開始されていない = System.loadLibrary()が呼ばれていない = C/C++プログラム(共有ライブラリ)もロードされていない状態。

javaプログラム側の良さそうな場所にjdbでブレークポイントを入れる。

main[1] stop in senna.SnippetTest.testSnippet
ブレークポイント senna.SnippetTest.testSnippet を保留しています。
クラスがロードされた後に設定されます。
main[1]

そこまで実行。設定したところで停止する。

main[1] cont
> 保留した ブレークポイント senna.SnippetTest.testSnippet を設定しました

ブレークポイントのヒット: "スレッド=main", senna.SnippetTest.testSnippet(), line=40 bci=0

main[1]

gdbjavaプロセスにアタッチ。

tritonn@dev32:/srv/senna-java/trunk/src/jni$ gdb --pid=`pgrep -x java`
GNU gdb Red Hat Linux (6.3.0.0-1.143.el4rh)
Copyright 2004 Free Software Foundation, Inc.
..(中略)...
Loaded symbols for /lib/libgcc_s.so.1
0x006367a2 in _dl_sysinfo_int80 ()
   from /lib/ld-linux.so.2
(gdb)

System.loadLibrary()が呼ばれた後なら、ここで自由にbreakpointを設定できる。(呼ばれる前にも"予約"は可能ですが)

(gdb) b Java_senna_Snippet_exec
Breakpoint 1 at 0xb39b259e: file senna_Snippet.c, line 107.
(gdb) c
Continuing.

breakpointを設定したらcontinueしておく。

jdb側でもcontinueする。

main[1] cont
>

gdb側のbreakpointがヒット。後はお好きに。

(gdb) c
Continuing.
[Switching to Thread -1208027456 (LWP 648)]

Breakpoint 1, Java_senna_Snippet_exec (env=0x8057b0c, obj=0xbfffcec4, jstr=0xbfffcec0)
    at senna_Snippet.c:107
107         snip = this_sen_snip(env, obj);
(gdb) list
102         sen_rc rc;
103         jclass string_class;
104         jobjectArray array;
105         char **results;
106
107         snip = this_sen_snip(env, obj);
108         string = (*env)->GetStringUTFChars(env, jstr, NULL);
109         string_len = (*env)->GetStringUTFLength(env, jstr);
110         SEN_LOG(sen_log_debug, "sen_snip_exec: snip=%p, string=%s, string_len=%d",
111                 snip,string,string_len);
(gdb)

※もっと手軽な方法を募集中です(CUI完結可能なものに限る)

fopen関係のメモ

fopenしたファイルを横から削除(rm)した場合、fprintf/fflush/fclose等の関数はそれを関知(エラーに)せず、通常通りの戻り値を返す。

つまりプログラムからみると関数呼び出しが成功してるので書き込みできていると誤認している。errnoも0のままになっている。

ただし、削除してしまったら実際にはファイル書き込みは行われない。(例えば自動的にファイルを新規作成して書いたりはしない。)

Connector/J 5.1とServer Side Prepared Statement

ここ数日「MySQL + Connector/J(JDBCドライバ) + プリペアードステートメント」の話題がちらほら出ています。正確に把握はしていないですがSQLインジェクション対策→PreparedStatementという流れできた話のようです。

自分は元Connector/J開発メンバ(※インターン生として)でもありとても気になる話題なので、Connector/Jのソース解析も含めた説明をここで行いたいと思う。

プリペアードステートメントとは?

基本的な説明は割愛。RDBMSの世界では元々は毎回のSQL文のパース、コンパイルといった処理を軽くすることで性能向上を実現するために導入された仕組み。最近ではSQLインジェクション対策の面で注目もされている機能。

client-sideとserver-side

Connector/JでのPrepared Statementには2種類のモードが存在する。

  • client-side Prepared Statement
  • server-side Prepared Statement

client-sideとは、その名の通りMySQLサーバから見たクライアント相当であるConnector/J自身により実現しているPrepared Statement。

Connector/JはJDBC Type4 Driver(=Pure Javaで実装されている)であり、Connector/J自身にはSQL文のパース機能等は無い。従って、client-sideのPrepared Statementでは、単純なプレースホルダ機能のみが実際には提供されている。要するに、"?"の部分に引数を文字列連結させて、それをMySQLサーバに投げているということ。

これは、MySQLサーバがver4.1になるまでPrepared Statement機能を提供していなかったことにも関係している(と推測)。

一方、Connector/Jは(正確な記憶はないが)かなり昔からJDBC 3.0をサポートしており、Prepared StatementはJDBC 2.0の仕様なので、その都合上、MySQLサーバ側にPrepared Statementが無い時期からAPIとして提供するためにclient-side実装を行っていたものと考えられる。

一方、server-sideのPrepared StatementはMySQL 4.1でサポートされた機能であり、サーバ側でSQL文のパースをスキップできる、(他のRDBMSと比べるとかなり簡潔だが)いわゆるPrepared Statementとなっている。

プロトコルの違い

client-side Prepared Statementでは通常のSQL文を実行するのと同じ(Java的に言うと、java.sql.Statementを用いた実行と同じ)プロトコルが用いられる。

server-side Prepared Statementでは専用のプロトコル(正確には専用のコマンド)を使用する。

MySQL Internalsドキュメントにclient-serverプロトコルの解説がある。

SQLの実行にはCommand Packetを使用するが、

  • 通常のSQL文(client-side Prepared Statement含む)ではCOM_QUERYコマンドを使用
  • server-side Prepared StatementではCOM_STMT_PREPARE、COM_STMT_EXECUTE、COM_STMT_CLOSEなどを使用

といったような違いがある。

ちなみにこうしたSQLCOMコマンドの違いは一般クエリログにも出力されるので、それでも確認ができる。

SQLインジェクション対策

server-side Prepared StatementではSQL文のフォーマットとパラメータ値がこのように別々のPacketを使用して送信される仕組みとなっており、これによりSQLインジェクションを防ぐことが可能になっている(と思われる)。

Connector/Jにおける各機能の実装クラス

次にConnector/JにおけるPrepared Statement関連のクラスを説明する。

関連クラス一覧

Connection

java.sql.Connectionインタフェースを拡張したインタフェース。

ConnectionImpl

com.mysql.jdbc.Connectionインタフェースを実装したクラス。

Statementオブジェクト(通常のSQL文用)の作成はConnectionオブジェクトを通じて実行する。

Statement stmt = conn.createStatement("SQL文");

PreparedStatementオブジェクトの作成も同様にConnectionオブジェクトを通じて行うことになる。

PreparedStatement pstmt = conn.prepareStatement("SQL文");
Statement

java.sql.Statementインタフェースを拡張したインタフェース。

StatementImpl

com.mysql.jdbc.Statementインタフェースを実装したクラス。

PreparedStatement

com.mysql.jdbc.StatementImplクラスを拡張したクラス。これがclient-side Prepared Statementの時に使われる。

ServerPreparedStatement

com.mysql.jdbc.PreparedStatementクラスを拡張したクラス。これがserver-side Prepared Statementの時に使われる。Connector/Jの歴史的にclient-sideの実装が先なので、こういう継承関係になっていると思われる。

抑えておきたいところ

次の話に進む上で抑えておきたいのは、server-sideのPrepared Statementは"Statment←StatementImpl←PreparedStatement←ServerPreparedStatement"という継承関係があるということと、これらのStatement系オブジェクトはConnectionオブジェクトから作られるということである。

Connector/Jのパラメータとclient-side/server-side制御方法

Connector/J 5.0/5.1のデフォルト設定では、server-sideではなくclient-sideのPrepared Statementが使用されるようになっている。

この振る舞いを制御するためのJDBC接続文字列用パラメータがuseServerPrepStmtsである。

通常のJDBC文字列同様、以下のように使用する。

String url = "jdbc:mysql://localhost/test?useServerPrepStmts=false";

もちろん、Propertyオブジェクトにputしても良い。

server-side Prepared Statementの是非とuseServerPrepStmtsパラメータ

このuseServerPrepStmtsパラメータはConnector/J 3.1.0にてデフォルト値はtrueで登場したものの、Connector/J 5.0.5/5.1にてfalseとなった。

このデフォルト値変更の理由はConnector/J開発者Mark Matthews氏がConnector/J 5.0.5をリリースした際のRelease Noteに記述されている。(2007年3月 http://lists.mysql.com/java/9035)

Important change: Due to a number of issues with the server's implementation of
server-side prepared statements, Connector/J 5.0.5 has disabled their use by defa

The disabling of server-side prepared statements does not affect the operation of the
connector in any way, except for the case where the connection was configured with
"useTimezone=true". If so, see the bugfix note for "useSSPSCompatibleTimezoneShift".

To enable server-side prepared statements you must add the following configuration
property to your connector string: 

useServerPrepStmts=true

The default value of this property is false (i.e. Connector/J does not use server-side
prepared statements, and won't until they've matured). If you have an application that
already works well with server-side prepared statements, it is reasonable to enable them.

簡単にまとめると、

  • いろいろサーバ側のPrepared Statement実装には問題がある
  • 枯れてくるまでデフォルト値=falseにしておく
  • 既にserver-side Prepared Statementを使っていて問題がでてないならtrueにして使ってもOK

という内容。

「サーバ側実装に問題がある」の具体的な内容が触れられていないので、そのバグがMySQLサーバのバージョンで既に直ったのかとかは不明。

またMySQLアーキテクチャ担当責任者であるBrian Aker氏は、Drizzleの機能説明において以下のように触れている。(2008年6月 http://krow.livejournal.com/599921.html)※DrizzleではPrepared Statementはサポートされない予定。

So why are prepared statements a problem?

Because users do not clean up/close unused prepared statements. 
Multiply the number of prepared statements times the number of 
open connections and you begin to see the problem.

What do you do about this?

Turn them off. The Java driver and a few other drivers turn them off automatically.
  • Prepared Statementが問題となるのはプログラマがclean up/closeをしてくれないから
  • clean up/closeしないコードが多数のConnectionによって実行されると問題が出る(補足:メモリリークやmysqldダウン)
  • Connector/Jとか、デフォルトでOFFにしているものもあるし

Drizzleは"文字コードはutf8のみ対応、他は無し"にしたりするような、シンプルさを追求するRDBMSなので「Prepared Statementを切る」というのは理解できる。

しかしBrianが言うところの理由は一言で言うと「プログラマがちゃんとcloseしてくれないから」ということであり、

  • つまり利用者が悪い
  • とは言うものの製品提供側はトラブルの種を極力少なくしたいので機能OFFで回避(MySQLではちゃんと使える人だけONにすべし)

ということなのではと思う。

PreparedStatement.close()に関連するソースを読んでみる

Connectionオブジェクトのみならず、Statementオブジェクト、PreparedStatementオブジェクトについても、はたまたResultSetオブジェクトについても、不要となったらclose()を呼ぶのはJDBCプログラミングでは基本だと思うが(新入社員研修で習った記憶あり)、それがなかなか徹底されないと言うことらしい。

となると「PreparedStatement.close()を必ず呼べば解決するはず」というのも難しいので、いくつか調べてみた。

先に結論を述べると、

  • Connection.close()が呼ばれれば、ServerPreparedStatement.close()も呼ばれ、MySQLサーバにCOM_STMT_CLOSEコマンドが送信され、メモリリークは発生しない

ということが判明した。

以下、結論に至る説明を記す。

JDBCプログラミングではPreparedStatemtを利用するためには以下のようにConnectionクラスのprepareStatementメソッドを使用する必要がある。

java.sql.PreparedStatement pstmt = conn.prepareStatement("XXXX");

このConnectionオブジェクト生成時にJDBC接続文字列に"useServerPrepStmts=true"が指定されていれば、prepareStatementメソッドの戻り値はcom.mysql.jdbc.ServerPreparedStatementオブジェクトとなる。

ServerPreparedStatementはインスタンス化される際、コンストラクタの中で、親クラスであるPreparedStatementのコンストラクタを呼び出す。

PreparedStatementのコンストラクタは、さらにその親のStatementImplクラスのコンストラクタを呼び出す。

StatementImplのコンストラクタは、dontTrackOpenResourcesパラメータがfalse(=デフォルト値)の場合、ConnectionクラスのregisterStatemet()メソッドを使用して、Connectionオブジェクトに自身を登録する。

    if (!this.connection.getDontTrackOpenResources()) {
        this.connection.registerStatement(this);
    }

このようにして、Connectionオブジェクトには生成済みのStatement/PreparedStatement/ServerPreparedStatementが登録されている。

ここで、Connectionがclose()されるとどうなるか。

  1. Connection.close()
  2. Connection.realClose()
  3. Connection.closeAllOpenStatements()

という順序で呼び出しが行われ、closeAllOpenStatements()内でIterator経由でServerPreparedStatement.close()も呼ばれる仕組みになっている。

for (Iterator iter = this.openStatements.keySet().iterator(); iter.hasNext();) {
    currentlyOpenStatements.add(iter.next());
}
int numStmts = currentlyOpenStatements.size();
for (int i = 0; i < numStmts; i++) {
    StatementImpl stmt = (StatementImpl) currentlyOpenStatements.get(i);
    try {
        stmt.realClose(false, true);
    } catch (SQLException sqlEx) {
        postponedException = sqlEx; // throw it later, cleanup all
        // statements first
    }
}		

そしてServerPreparedStatement.close()にて、MySQLサーバにCOM_CLOSE_STATEMENTコマンドを送信、となる。

MysqlIO mysql = this.connection.getIO();
Buffer packet = mysql.getSharedSendPacket();
packet.writeByte((byte) MysqlDefs.COM_CLOSE_STATEMENT);
packet.writeLong(this.serverStatementId);
mysql.sendCommand(MysqlDefs.COM_CLOSE_STATEMENT, null,
    packet, true, null, 0);

結論:ではJava+MySQLではServerSidePreparedStatementを使うべきか

というわけでだらだら書いてしまったがここいらで結論。

使うべきか使うべきでないかのYes/Noで言うと「微妙」としか言えない。

使うべき理由
  • 上記で引用したBrianのエントリにも書いてあるように、ServerPreparedStatementはBlobを使用している場合の性能向上、Blob以外の場合も少しだけ性能向上が期待できる
  • http://d.hatena.ne.jp/mir/20060213/p1でも書いたけどServerPreparedStatementは大量データ送信時に相対的にプロトコルが軽い(上記と同じ話かも)
  • Connection.close()できていればメモリリークの心配はないはず(サーバ自身に問題がある場合を除く)
  • SQLインジェクション対策になる
使うべきでない理由
  • MySQLサーバのPreparedStatementはSQLパーサーのオーバーヘッドを軽くするが、オプティマイザ結果のキャッシュとかはしてくれないのであまり大した物ではない
  • BrianやMarkがあまり推奨していない(理由は前述の通り「プログラマがミスる」だけど、他にもある?)という事実
  • Connectionプーリング(java.sql.DataSource)を使っていると、ds.close()してもconn.close()されるとは限らない。

という感じでしょうか。SQLインジェクションをアプリ側で完璧にできるなら、性能面で困っていない限り、使わなくても良いかも。でもSQLインジェクション対策になるのは捨てがたいかも・・と結論がでません。

というわけでおしまいおしまい。

senna-java-0.02 ログ制御機能の設計

senna-javaにDB APIを実装するに先立ってsenna.log制御機能が欲しいと思い、まずログ制御機能を追加することに。

Sennaクラスに追加すべきか新しくクラスを作るべきか悩んだ末、新しく作ることに。

  • /var/senna/logがあるとsen_log_defaultでsenna.logが出力される実装はそのままlibsenna.soに残っている
  • それとは別にJava API側でログを制御したい。回帰テスト実行時とか、debugレベルで出したい。
  • senna.Loggerクラスの新設
  • Logger#open(String path),open(String path, int level)でログファイルのオープン。出力場所は相対パス/絶対パス
  • Logger#setLevel(int level)で出力レベルの制御、SEN_LOG_XXXをsenna.Sennaに追加。
  • Logger#close()でログのクローズ
  • append書き込み
  • ログの書式指定はとりあえず放置

追記:とりあえず実装できた。クラス名をやっぱりSennaLoggerにしようかな。