2010/08/09  |  Written by  |  under Blog


仕事で使うJRuby! そこが知りたい技術・開発・案件の最新情報

  • 2010/08/05 19:00-
  • ジュンク堂 池袋本店
  • 抽象エア社員として参戦

はじめに

  • 自己紹介
    • 大場夫妻
    • 橋本さん
    • 中村さん(nahiさん)
      • OpenSSLとかの人

本を出した経緯

  • 大場さん
    • 原書は2007年に発刊
    • @takai さん経由で知らせてもらった
    • だいたい3部構成
      • を使いアプリを開発しながら、それとは気づかずにEEにはまる感じ
      • Java -> / -> Java の章がある
        • 黒魔術から白魔術まで
    • 原著作者に推薦文を書いてもらった!
    • みどころ
      • のリファレンスがある
  • 橋本さん
    • DB Magazineでの連載
    • 2部構成
      • 後半にエンタープライズでRailsを使う上で注意すること
        • Javaシステムとの連携
    • Thomas Enebo に推薦文もらった!
  • 結論
    • 両方買うといいよ!
  • 総括 by nahi さん
    • Olavini の本は入門として買うとかなりつらい
      • ただし細かい部分も書いてあるので、さらに進むにはいいのではないか。
    • 橋本さんの方が入門としては良い

JRuby の魅力

  • 大場さん
    • 好きなメソッド : to_java
      • Ruby のオブジェクトを Java のオブジェクトにいい案配に変えてくれる黒魔術
  • 橋本さん
    • 聞き逃したorz
  • nahi さん
    • JRuby の JIT コンパイラ

仕事で使えるか

  • 橋本さん
    • 沖縄に幽閉されて地方自治体向けのシステムを作っていた
      • Thomas らに用賀でたまたますれ違って、「使ってみてよ〜」と言われたので
    • Ruby でもあるから危なくなったら切り替えられる
    • 1.2 のころだったので、かなりもっさりしていた
      • 1.4, 1.5 で早くなった
    • Java の利点
      • SOAP のライブラリの運用などで便利だった

最近の Java の動向

  • Java EE6 がでました
    • アプリケーションサーバ用のAPI
      • WebLogic とか
    • SOAP が大ブーム
      • Java のアプリケーションサーバがちゃんと SOAP 喋れるようになってきた
      • REST 厨的には SOAP は滅んだと思ってたのに….

Rails の魅力

  • 最初に Rails が動くのを見たときは感動した
    • Olavini が YAML の parser を書いて去っていった….
      • 4回もあったらしい
    • String を実装してあった
    • 鬼車を Java で実装しある
    • CRuby に忠実
      • バグさえも
    • Rails 3.0 も対応している

JRuby を採用するには

  • いつも JRuby と言い続けたので
    • 変更に強い部分とそうでない部分を切り分けてみたり
      • Java で書く部分と Ruby で書く部分を分ける
    • インフラでは JRuby がいいんじゃないか
      • Windows サーバでは Java の方が有利
      • クラスタリングなどやりやすいんじゃないか
    • ただしすべての利点を活用できる訳じゃないので、そこは注意すべき
    • 地雷は根本的に避ける

XML & Webサービス

  • 他社とつなぐのに堅くやるには Java の XML でやるのが利点

JMS

  • 非同期通信のためにメッセージングキューの、標準化されたAPI

JRuby Kaigi 2010

  • 2010/08/28 に Ruby Kaigi 2010 内で開催
    • JRuby Pony Show
      • JRuby の現状と今後を JRuby の開発者が話します
      • 9月に1.6が出るらしい
      • Android / GAE
    • RedBridge で Java -> Ruby を
      • 原田さん
        • Java アプリで Ruby を活用したい
    • JRuby on GAE/J のノウハウ
      • 13個の TIPS
    • JRuby on Rails の力を JavaEE 技術を使ってさらに高める 10 の方法
      • 本に書けなかったことを書きます
    • Ruby と DSL で MapReduce – Hadoop-papyrus
      • JRuby 上で Ruby で書いてしまえ
    • JRuby ソースコード詳説
      • JRuby のソースコードのどこに何があるかを示して、デバッグしやすいように解説します
    • LT募集
    • Tシャツ購入募集
      • ファイナンシャルサポートも混みで 4000 円

最後に

  • JRuby 1.5 on Rails 2.3.x という安定していく環境の入門本を是非買いましょう
 
このエントリーを含むはてなブックマークはてなブックマーク - JRubyKaigi前夜祭に行ってきた この記事をクリップ!Livedoorクリップ - JRubyKaigi前夜祭に行ってきた Googleブックマークに追加 Digg This
Tags: , , ,

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

ActiveRecordからRuby/MySQLへ

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

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のことです。

/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_commandinclude/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を呼び出しています。mysqlinclude/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;

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

 
このエントリーを含むはてなブックマークはてなブックマーク - RubyのMySQLドライバを見る – アクティブチェック問題 この記事をクリップ!Livedoorクリップ - RubyのMySQLドライバを見る – アクティブチェック問題 Googleブックマークに追加 Digg This
Tags: , , , ,

前回の話を受けて、Railsの場合はどうなのかを調べてみたいと思います。ターゲットはRails 2.3.5です。

ActiveRecordに潜る

RailsのO/R MapperはActiveRecordです。今回はActiveRecordでMySQLの場合について調べます。

まずはアクティブチェックしている部分を探します。ざっと見ると、connection_adaptors/mysql_adaptor.rbの275行目に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

うぉ、SELECT 1とかそのままじゃないか。とりあえず次はこれを呼び出しているところを探します。するとconnection_adaptors/abstract_adaptor.rbの149行目にverify!(*ignored)メソッドがありました。

147
148
149
150
151
152
      # Checks whether the connection to the database is still active (i.e. not stale).
      # This is done under the hood by calling <tt>active?</tt>. If the connection
      # is no longer active, then this method will reconnect to the database.
      def verify!(*ignored)
        reconnect! unless active?
      end

アクティブでなければ再接続するようです。ではこのメソッドはどこで呼ばれているのかというと、connection_adaptors/abstract/connection_pool.rbの255行目にあるcheckout_and_verifyメソッドです。

255
256
257
258
259
260
      def checkout_and_verify(c)
        c.verify!
        c.run_callbacks :checkout
        @checked_out << c
        c
      end

ここで接続確認して接続を取得、それをインスタンス変数@checked_outに保存しているわけです。まだ続きますが、ではこのメソッドはと言うと、すぐ上にあるchechout_new_connectioncheckout_existing_connectionメソッドで呼び出されています。

244
245
246
247
248
249
250
251
252
253
      def checkout_new_connection
        c = new_connection
        @connections << c
        checkout_and_verify(c)
      end

      def checkout_existing_connection
        c = (@connections - @checked_out).first
        checkout_and_verify(c)
      end

まだ続きそうです。では、これはと言うと同じファイルの181行目にあるcheckoutメソッドで呼び出されているようです。

181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
      def checkout
        # Checkout an available connection
        @connection_mutex.synchronize do
          loop do
            conn = if @checked_out.size < @connections.size
                     checkout_existing_connection
                   elsif @connections.size < @size
                     checkout_new_connection
                   end
            return conn if conn
            # No connections available; wait for one
            if @queue.wait(@timeout)
              next
            else
              # try looting dead threads
              clear_stale_cached_connections!
              if @size == @checked_out.size
                raise ConnectionTimeoutError, "could not obtain a database connection#{" within #{@timeout} seconds" if @timeout}.  The max pool size is currently #{@size}; consider increasing it."
              end
            end
          end
        end
      end

さてさて、ではこのメソッドはどこから呼ばれるかというと、同じファイルにあるconnectionwith_connectionメソッドのようです。

89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
      # Retrieve the connection associated with the current thread, or call
      # #checkout to obtain one if necessary.
      #
      # #connection can be called any number of times; the connection is
      # held in a hash keyed by the thread id.
      def connection
        if conn = @reserved_connections[current_connection_id]
          conn
        else
          @reserved_connections[current_connection_id] = checkout
        end
      end

      # Signal that the thread is finished with the current connection.
      # #release_connection releases the connection-thread association
      # and returns the connection to the pool.
      def release_connection
        conn = @reserved_connections.delete(current_connection_id)
        checkin conn if conn
      end

      # Reserve a connection, and yield it to a block. Ensure the connection is
      # checked back in when finished.
      def with_connection
        conn = checkout
        yield conn
      ensure
        checkin conn
      end

とまあだいたいこんな感じで、ActiveRecord::Base.connection.execute("SELECT * FROM users")が実行されるときには、active?メソッドが呼び出されているわけです。実際に、例えばp "Active Check!"なんてのを仕込むと毎回表示されます。これはproduction環境でも変わりませんでした。

さて次回はactive?のところでは実際は何が呼び出されるのかを調べるために、MySQLドライバを見てみたいと思います。

 
このエントリーを含むはてなブックマークはてなブックマーク - ActiveRecordでの接続チェックはどこでやっているのか この記事をクリップ!Livedoorクリップ - ActiveRecordでの接続チェックはどこでやっているのか Googleブックマークに追加 Digg This
Tags: , , , ,

例によって気になったので意訳。

Checking for a live database connection considered harmful on Performance Blog

顧客のデータベースでよく見かけて注意するのだが、クエリーを送信する前にデータベース接続がアクティブかどうかをチェックするのは大きなオーバーヘッドになる。これは、次のような擬似コードで書かれるデザインパターンに由来する。

1
2
3
4
5
6
function query_database(connection, sql)
   if !connection.is_alive() and !connection.reconnect() then
      throw exception
   end
   return connection.execute(sql)
end

多くの開発環境やフレームワークで、こういうコードになっている。これには、実際には期待したとおりには動かないと言うことと、大きなパフォーマンスのオーバーヘッドがあると言う2点で間違っている。
2つの間違いがあり

実はちゃんと動かない

このコードはレースコンディション(競合状態)によって動作しない。もしチェックしたときに接続がアクティブだとしても、connection.execute(sql)を実行するときにアクティブだとは保証されない。さらにもし非アクティブで再接続したとしても、同様にアクティブであるとは保証されない。

チェックして実行するのは実用的ではない。代わりに、次のように書き換えるべきだ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function query_database(connection, sql, retries=1)
   while true
      try
         result=connection.execute(sql)
         return result
      catch InactiveConnectionException e
         if retries> 0 then
            retries = retries - 1
            connection.reconnect()
         else
            throw e
         end
      end
   end
end

is_active()が無くなったのに気付いただろうか。接続がアクティブであればクエリーが実行され、そうでなければ失敗して再接続し、再度実行しようとする。

このコードだと、必要に応じてロック待ちのタイムアウトやデッドロックのときに再試行することができるようになっている。私の経験上、多くのアプリケーションで有効である。ほとんどのアプリケーションでは、こう言うときには単純に再試行するだけで、ちゃんと扱おうとはしていない。

パフォーマンスのオーバーヘッド

アクティブチェックには大抵の場合、MySQLのプロトコルレベルのコマンドであるpingstatisticsの呼び出しか、SELECT 1のような自明なクエリーが実行される。前者のコマンドはSHOW GLOBAL STATUSで表示されるCom_admin_commandsをインクリメントし、後者のクエリーは診断を難しくする。これは多くのアプリケーションで非常に高コストとなる。ここには2つのコストがある。1つはネットワーク通信とクエリー実行時間のアプリケーションへのコストで、もう1つはデータベースサーバの負荷上昇。このデータベースサーバへの負荷はかなり大きい。何日か前に、「管理コマンドのstatistics」を使うRuby on Railsアプリケーションを見たが、全クエリー実行時間の40%がこのコマンドだった。この不必要な接続チェックを削除したところ、データベースの負荷が半分程度に削減できた。これは普通じゃない!

アプリケーションのクエリーが長いとき、追加のクエリーはノイズの中で消えてしまう。しかし高トラフィックアプリケーションはクエリー実行時間を短くするのに途方もない努力を費やし、いくつかのチューニングしたアプリケーションでは、クエリーの実行時間がミリ秒より長くならないかドキドキしている。もしデータベースで毎秒20,000クエリー走っているなら、コネクションチェックも毎秒20,000回行われていることになる。これらpingstatisticsと言うクエリーは、アプリケーションで実行すべきクエリーと同じぐらいコストがかかっているんだ。

これはデータベースサーバへの負荷であった。アプリケーション側では、クエリー実行時間の2倍の遅延があるのがわかるだろう。クエリーを実行するときには、そのアプリケーションフレームワークがチェックのためにネットワーク通信をして、さらに別のネットワーク通信でクエリーを実行する。これもやっぱり問題だ。

問題なのは、さっきの擬似コードがレアケースに気を取られて一般的な場合にペナルティーを課しているところ。普通は接続は生きていて、確認したり再接続したりはしなくてもいい。良い方法は、レースコンディションを解決したコードを使うことだ。もし接続が切れていても、クエリーを実行するときに探せばいいだけだ。そのときまでは、全てがOKで、クエリーを実行できる。

問題となるライブラリのアップストリームメンテナが、この問題を見つけ出して解決することを願っている。アプリケーションが成長するときの大きな手助けになるからだ。ラボではうまくいっていて、現場でもそうであったとしても、パフォーマンスはすぐに問題となる。そしてそれはすごく目立つんだ。

追記

この馬鹿げた結果を見て欲しい。

1
2
3
4
5
6
7
8
9
# Rank Query ID           Response time    Calls  R/Call   Item
 # ==== ================== ================ ====== ======== ===============
 #    1 0x5E796D5A4A7D1CA9 10651.0708 73.1% 120487   0.0884 ADMIN STATISTICS
 #    2 0x85FFF5AA78E5FF6A  1090.0772  7.5%  23621   0.0461 BEGIN
 #    3 0x6E85B9A9C9FF813E   868.0335  6.0%   6923   0.1254 UPDATE scores
 #    4 0xA3A0423749EC0E37   851.0152  5.8%   6020   0.1414 UPDATE user_datas
 #    5 0x813031B8BBC3B329   822.0041  5.6%  23299   0.0353 COMMIT
 #    6 0xA873BBC4583C4C85   278.4533  1.9%   6985   0.0399 SELECT users user_devices
That's right, 73% of the server's load is consumed by checking to see if the connection is still alive

まとめ

これが本当だとすると、Rubyで使われているMySQLライブラリや、RailsのMySQLアダプタとかをちゃんと調べないとやばいんじゃないか?と言うわけで調査にはいります。

 
このエントリーを含むはてなブックマークはてなブックマーク - データベースアクティブチェックは負荷を上昇させるだけ この記事をクリップ!Livedoorクリップ - データベースアクティブチェックは負荷を上昇させるだけ Googleブックマークに追加 Digg This
Tags: , , , ,


Ruby on Rails携帯サイト開発技法

  • 著者/訳者:伊藤 祐策 富田 陽介 三上 喜之
  • 出版社:ソフトバンククリエイティブ( 2010-04-30 )
  • 大型本:312 ページ

@yoshukiさんに献本していただきました。ありがとうございます。と言うわけで、書評を。

またもや端末識別番号問題

まず最初に残念な点を。最後の章でjpmobileを使わないで自前で機能を実装しているのですが、セッション管理に端末識別番号を利用してしまっています。これは高木さんがよく言われている由々しき問題で、エンジニアとしてはやってはいけないことのはずです。

ここまで破綻しているケータイID認証(簡単ログイン) on 高木浩光@自宅の日記

せっかくその前の章でSession Fixationの話がでたのに、最後にそれを台無しにしてしまう内容は、少し残念でした。Railsで携帯サイト開発の初の本なのでよけいに。書評でこんなこと書いてしまうのはどうかとも思ったのですが、携帯サービス開発をしている身としては、書かざるを得ませんでした。

あと途中から、あまりRubyのコードを書かない人なのかな?と思わせる部分が見受けられました。ifthenだったり、文中のメソッドがget_mobile_id()と括弧がついていたり。気になったのはその辺りでしょうか。

Railsで携帯サイト開発する人は一通り目を通すべき

とは言え、端末識別番号のこと以外ではお勧めできる本となっています。jpmobileの使い方からPCで絵文字を出す方法まで、かなり実践的な内容です。特にPCで絵文字は携帯/PC両対応のサイトを作るときには必ず出てくる問題で、それが詳しく書かれています。Railsで携帯サイト開発をする人は、一度は目を通しておいて損はしないでしょう。

そしてまとめ

これはもうjpmobile on 3という本を書くしかないですよね(多少違う)。先を越された感をぬぐい去れないので、@yoshukiさんとはどこかで飲み明かしたいと思いました。さてjpmobileがんばろう。

 
このエントリーを含むはてなブックマークはてなブックマーク - Railsで携帯サイトを作るには &#8211; Ruby on Rails携帯サイト開発技法 この記事をクリップ!Livedoorクリップ - Railsで携帯サイトを作るには &#8211; Ruby on Rails携帯サイト開発技法 Googleブックマークに追加 Digg This
Tags: , , ,
2010/04/04  |  Written by  |  under
コメントは受け付けていません。


自己紹介

京都出身、大阪育ちの世田谷系Rubyistです。携帯向けWebサービス開発・運営会社に勤務しており、新機能開発やインフラ設計、負荷分散など全般のいろいろなことをやっていたりします。

開発言語はRubyでフレームワークはもちろんRuby on 。最近はjpmobileを利用しつつ、Railsを最新版に上げるのが主なお仕事です。

以下、雑多な属性などつらつらと。

個人属性

作家とか漫画家とか

主に竹本泉島本和彦。とくに逆境ナインは人生のバイブルです。これなしには生きていけません。

音楽とか

ECHOESDCIが好きです。中学生の頃に辻仁成のオールナイトニッポンでECHOESに出会って以来、歌詞を完全に覚えるぐらいなんども聴いた気がします。DCIはマーチングバンドやってたこともあるので、その影響でしょうか。たまに演奏を見たくなります。いつかはDCI tourに行ってみたいな。

以下続く。

 
このエントリーを含むはてなブックマークはてなブックマーク - About Me この記事をクリップ!Livedoorクリップ - About Me Googleブックマークに追加 Digg This
Tags: , , , , , , , , ,