前々回、前回の続きです。次はMySQLドライバを調べてみます。
ActiveRecordからRuby/MySQLへ
さてアクティブチェックしては、connection_adaptors/mysql_adaptor.rbのactive?メソッドでした。
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
| def active?
if @connection.respond_to?(:stat)
@connection.stat
else
@connection.query 'select 1'
end
# mysql-ruby doesn't raise an exception when stat fails.
if @connection.respond_to?(:errno)
@connection.errno.zero?
else
true
end
rescue Mysql::Error
false
end |
ここでは、@connection.statか@connection.query 'select 1'が呼び出されます。その前に、まず@connectionとはなにかと言うと、親クラスであるAbstractAdapterの38行目にあるinitializeメソッドで作成されるインスタンス変数です。ファイルはconnection_adaptors/abstract_adaptor.rbです。
38 39 40 41 42 43
| def initialize(connection, logger = nil) #:nodoc:
@connection, @logger = connection, logger
@runtime = 0
@last_verification = 0
@query_cache_enabled = false
end |
このメソッドはconnection_adaptors/mysql_adaptor.rbの200行目で呼び出されるわけです。このクラスは同じファイルの75行目でインスタンスを生成しています。
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| # Establishes a connection to the database that's used by all Active Record objects.
def self.mysql_connection(config) # :nodoc:
config = config.symbolize_keys
host = config[:host]
port = config[:port]
socket = config[:socket]
username = config[:username] ? config[:username].to_s : 'root'
password = config[:password].to_s
database = config[:database]
# Require the MySQL driver and define Mysql::Result.all_hashes
unless defined? Mysql
begin
require_library_or_gem('mysql')
rescue LoadError
$stderr.puts '!!! The bundled mysql.rb driver has been removed from Rails 2.2. Please install the mysql gem and try again: gem install mysql.'
raise
end
end
MysqlCompat.define_all_hashes_method!
mysql = Mysql.init
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0
options = [host, username, password, database, port, socket, default_flags]
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
end
end |
この引数mysqlはその前のMySQL.initで生成されています。このMySQLはRubyのMySQLドライバであるMySQL/Rubyのことです。
さてこのMySQL/Rubyですが、拡張ライブラリなのでC言語で書かれています。また内部的にはMySQL APIを呼び出しているので、適宜両方見ていきます。
まず、statメソッドがあればそれが呼ばれるので、そちらを調べると、ext/mysql_api/mysql.cの1978行目にstatメソッドの定義があります。
1
| rb_define_method(cMysql, "stat", my_stat, 0); |
細かいことは省きますが、MySQL#statが呼ばれるとmy_statが実行されます。ではそのmy_statは693行目にありました。
693 694 695 696 697 698 699 700 701
| /* stat() */
static VALUE my_stat(VALUE obj)
{
MYSQL* m = GetHandler(obj);
const char* s = mysql_stat(m);
if (s == NULL)
mysql_raise(m);
return rb_tainted_str_new2(s);
} |
ここでGetHandlerはマクロで、構造体の中にある接続ハンドラを取得しています。で、mysql_statという関数を実行して結果を判定しているわけですね。ではこのmysql_statはと言うと、MySQL 5.0のリファレンスマニュアルには「mysqladmin statusと同じ結果を返す」とだけ書かれています。
MySQLのソースを読む
あまりC/C++をやった経験がないのですが、とりあえず調査続行と言うことでMySQL本体のソースを追ってみます。まずはダウンロードして展開すると、libmysql/libmysql.cと言うファイルが見つかります。前述のは、いわゆる共有ライブラリ経由での接続なので、ここを見ればいいと思われます。このファイルの1407行目にmysql_statがありました。
1407 1408 1409 1410 1411 1412 1413 1414
| const char * STDCALL
mysql_stat(MYSQL *mysql)
{
DBUG_ENTER("mysql_stat");
if (simple_command(mysql,COM_STATISTICS,0,0,0))
DBUG_RETURN(mysql->net.last_error);
DBUG_RETURN((*mysql->methods->read_statistics)(mysql));
} |
どうやらsimple_commandを呼び出しているだけのようです。simple_commandはinclude/mysql.hの852行目に定義されているマクロです。
852 853 854
| #define simple_command(mysql, command, arg, length, skip_check) \
(*(mysql)->methods->advanced_command)(mysql, command, 0, \
0, arg, length, skip_check, NULL) |
mysql経由でadvanced_commandを呼び出しています。mysqlはinclude/mysql.hの258行目で定義されている構造体です。
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
| typedef struct st_mysql
{
NET net; /* Communication parameters */
unsigned char *connector_fd; /* ConnectorFd for SSL */
char *host,*user,*passwd,*unix_socket,*server_version,*host_info;
char *info, *db;
struct charset_info_st *charset;
MYSQL_FIELD *fields;
MEM_ROOT field_alloc;
my_ulonglong affected_rows;
my_ulonglong insert_id; /* id if insert on table with NEXTNR */
my_ulonglong extra_info; /* Not used */
unsigned long thread_id; /* Id for connection in server */
unsigned long packet_length;
unsigned int port;
unsigned long client_flag,server_capabilities;
unsigned int protocol_version;
unsigned int field_count;
unsigned int server_status;
unsigned int server_language;
unsigned int warning_count;
struct st_mysql_options options;
enum mysql_status status;
my_bool free_me; /* If free in mysql_close */
my_bool reconnect; /* set to 1 if automatic reconnect */
/* session-wide random string */
char scramble[SCRAMBLE_LENGTH+1];
/*
Set if this is the original connection, not a master or a slave we have
added though mysql_rpl_probe() or mysql_set_master()/ mysql_add_slave()
*/
my_bool rpl_pivot;
/*
Pointers to the master, and the next slave connections, points to
itself if lone connection.
*/
struct st_mysql* master, *next_slave;
struct st_mysql* last_used_slave; /* needed for round-robin slave pick */
/* needed for send/read/store/use result to work correctly with replication */
struct st_mysql* last_used_con;
LIST *stmts; /* list of all statements */
const struct st_mysql_methods *methods;
void *thd;
/*
Points to boolean flag in MYSQL_RES or MYSQL_STMT. We set this flag
from mysql_stmt_close if close had to cancel result set of this object.
*/
my_bool *unbuffered_fetch_owner;
/* needed for embedded server - no net buffer to store the 'info' */
char *info_buffer;
void *extension;
} MYSQL; |
この中のmethodsはst_mysql_methodsと言う構造体で、753行目に定義されています。
753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782
| typedef struct st_mysql_methods
{
my_bool (*read_query_result)(MYSQL *mysql);
my_bool (*advanced_command)(MYSQL *mysql,
enum enum_server_command command,
const unsigned char *header,
unsigned long header_length,
const unsigned char *arg,
unsigned long arg_length,
my_bool skip_check,
MYSQL_STMT *stmt);
MYSQL_DATA *(*read_rows)(MYSQL *mysql,MYSQL_FIELD *mysql_fields,
unsigned int fields);
MYSQL_RES * (*use_result)(MYSQL *mysql);
void (*fetch_lengths)(unsigned long *to,
MYSQL_ROW column, unsigned int field_count);
void (*flush_use_result)(MYSQL *mysql);
#if !defined(MYSQL_SERVER) || defined(EMBEDDED_LIBRARY)
MYSQL_FIELD * (*list_fields)(MYSQL *mysql);
my_bool (*read_prepare_result)(MYSQL *mysql, MYSQL_STMT *stmt);
int (*stmt_execute)(MYSQL_STMT *stmt);
int (*read_binary_rows)(MYSQL_STMT *stmt);
int (*unbuffered_fetch)(MYSQL *mysql, char **row);
void (*free_embedded_thd)(MYSQL *mysql);
const char *(*read_statistics)(MYSQL *mysql);
my_bool (*next_result)(MYSQL *mysql);
int (*read_change_user_result)(MYSQL *mysql, char *buff, const char *passwd);
int (*read_rows_from_cursor)(MYSQL_STMT *stmt);
#endif
} MYSQL_METHODS; |
だんだん長くなってきました。さてこれはMYSQL_METHODS型なのですが、定義しかありません。実体はと言うと、sql-common/client.cの1885行目で代入されている、client_methodsです。
1885 1886 1887 1888 1889
| /* Don't give sigpipe errors if the client doesn't want them */
set_sigpipe(mysql);
mysql->methods= &client_methods;
net->vio = 0; /* If something goes wrong */
mysql->client_flag=0; /* For handshake */ |
で、このclient_methodsは1763行目で定義されています。
1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783
| static MYSQL_METHODS client_methods=
{
cli_read_query_result, /* read_query_result */
cli_advanced_command, /* advanced_command */
cli_read_rows, /* read_rows */
cli_use_result, /* use_result */
cli_fetch_lengths, /* fetch_lengths */
cli_flush_use_result /* flush_use_result */
#ifndef MYSQL_SERVER
,cli_list_fields, /* list_fields */
cli_read_prepare_result, /* read_prepare_result */
cli_stmt_execute, /* stmt_execute */
cli_read_binary_rows, /* read_binary_rows */
cli_unbuffered_fetch, /* unbuffered_fetch */
NULL, /* free_embedded_thd */
cli_read_statistics, /* read_statistics */
cli_read_query_result, /* next_result */
cli_read_change_user_result, /* read_change_user_result */
cli_read_binary_rows /* read_rows_from_cursor */
#endif
}; |
もうちょい続きそうです。で、ここにあるcli_advanced_commandはと言うと、764行目にありました。
1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829
| my_bool
cli_advanced_command(MYSQL *mysql, enum enum_server_command command,
const uchar *header, ulong header_length,
const uchar *arg, ulong arg_length, my_bool skip_check,
MYSQL_STMT *stmt)
{
NET *net= &mysql->net;
my_bool result= 1;
init_sigpipe_variables
my_bool stmt_skip= stmt ? stmt->state != MYSQL_STMT_INIT_DONE : FALSE;
DBUG_ENTER("cli_advanced_command");
/* Don't give sigpipe errors if the client doesn't want them */
set_sigpipe(mysql);
if (mysql->net.vio == 0)
{ /* Do reconnect if possible */
if (mysql_reconnect(mysql) || stmt_skip)
DBUG_RETURN(1);
}
if (mysql->status != MYSQL_STATUS_READY ||
mysql->server_status & SERVER_MORE_RESULTS_EXISTS)
{
DBUG_PRINT("error",("state: %d", mysql->status));
set_mysql_error(mysql, CR_COMMANDS_OUT_OF_SYNC, unknown_sqlstate);
DBUG_RETURN(1);
}
net_clear_error(net);
mysql->info=0;
mysql->affected_rows= ~(my_ulonglong) 0;
/*
We don't want to clear the protocol buffer on COM_QUIT, because if
the previous command was a shutdown command, we may have the
response for the COM_QUIT already in the communication buffer
*/
net_clear(&mysql->net, (command != COM_QUIT));
if (net_write_command(net,(uchar) command, header, header_length,
arg, arg_length))
{
DBUG_PRINT("error",("Can't send command to server. Error: %d",
socket_errno));
if (net->last_errno == ER_NET_PACKET_TOO_LARGE)
{
set_mysql_error(mysql, CR_NET_PACKET_TOO_LARGE, unknown_sqlstate);
goto end;
}
end_server(mysql);
if (mysql_reconnect(mysql) || stmt_skip)
goto end;
if (net_write_command(net,(uchar) command, header, header_length,
arg, arg_length))
{
set_mysql_error(mysql, CR_SERVER_GONE_ERROR, unknown_sqlstate);
goto end;
}
}
result=0;
if (!skip_check)
result= ((mysql->packet_length=cli_safe_read(mysql)) == packet_error ?
1 : 0);
end:
reset_sigpipe(mysql);
DBUG_PRINT("exit",("result: %d", result));
DBUG_RETURN(result);
} |
だんだん面倒になってきたので、これぐらいにしておいて上から順に見ていくと、いろいろ初期化して何らかの事情で切断されたことを知っていれば(mysql->net.vio == 0)再接続。続いて準備をしてコマンド(COM_STATISTICS)送信(net_write_command)し、結果を返して終わる。
まとめ
長くなりましたが、MySQL/RubyのstatメソッドではCOM_STATISTICSを送信していると言うことがわかりました。これは統計情報を取得しようとしていると言うことで、件の負荷上昇に原因となります。ただ、じゃあ外せばいいかというと、Rails + MySQL (+ Mongrel?) でDB接続の通信が無い状態が続くとデッドロックする。 – こせきの技術日記と言うこともあるのでそう簡単ではないようです。今回の場合はRubyではTCP接続をしていないのでMongrelの問題は発生しませんが、負荷軽減を目論んでアクティブチェックを外す場合には、フレームワークやドライバまでちゃんと調べた方が良さそうだと感じました。
Tags:
MySQL,
Rails,
Ruby,
Ruby on Rails,
ネットワーク