RubyのMySQLドライバを見る - アクティブチェック問題

前々回、前回の続きです。次はMySQLドライバを調べてみます。

ActiveRecordからRuby/MySQLへ

さてアクティブチェックしては、connection_adaptors/mysql_adaptor.rbactive?メソッドでした。

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です。

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行目でインスタンスを生成しています。

# 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を見てみる

さてこのMySQL/Rubyですが、拡張ライブラリなのでC言語で書かれています。また内部的にはMySQL APIを呼び出しているので、適宜両方見ていきます。

まず、statメソッドがあればそれが呼ばれるので、そちらを調べると、ext/mysql_api/mysql.cの1978行目にstatメソッドの定義があります。

rb_define_method(cMysql, "stat", my_stat, 0);

細かいことは省きますが、MySQL#statが呼ばれるとmy_statが実行されます。ではそのmy_statは693行目にありました。

/* 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がありました。

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_commandinclude/mysql.hの852行目に定義されているマクロです。

#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を呼び出しています。mysqlinclude/mysql.hの258行目で定義されている構造体です。

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;

この中のmethodsst_mysql_methodsと言う構造体で、753行目に定義されています。

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です。

/* 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行目で定義されています。

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行目にありました。

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の問題は発生しませんが、負荷軽減を目論んでアクティブチェックを外す場合には、フレームワークやドライバまでちゃんと調べた方が良さそうだと感じました。

 
comments powered by Disqus