ちょっとしたニュース
今年11月にアメリカ東海岸のシャーロットで開催されるRubyConf 2007の発表枠応募にAP4Rが通りました! いい機会をもらえたので、
- Agenda for RubyConf 2007 :
Introduction to AP4R, Asynchronous Processing for Ruby
手を動かしてみましょう
第1回では、
そして、
今回は、
アプリケーションの機能、動作
ウェブ上のお店のアプリケーションを作成します。簡単のため、
仮想的な問題点、非同期化するところ
さて、
そこで、
加えて、
プロセス構成
今回作成するアプリケーションのプロセス構成を図に示します。

利用するソフトウェア
アプリケーションで利用するソフトウェアをまとめておきます。なお、
- Mac OS X 10.
4.10 - Ruby 1.
8.6 (2007-06-07 patchlevel 36) - RubyGems 0.
9.4 - MySQL 5.
0.16 - Ruby on Rails 1.
2.3 - AP4R 0.
3.2 - reliable-msg 1.
1.0 - mongrel 1.
0.1
Ruby
Rubyは、
インストール前にRubyのプログラムに触れてみたい場合、
Rubyのインストールは、
Ruby on Rails
Ruby on Railsは、
インストールは、
% gem install rails --include-dependencies
- (注)
- HTTPのアクセスにプロキシを経由する場合、
--http-proxy=http:// your. proxy. host:port/のオプションを付け加えて下さい。
Unix系のOSで、インストールにroot権限が必要な場合は、 suするかsudoで実行してください。
MySQL
業務DBおよびメッセージDBとして、
AP4R
AP4Rは、
% gem install ap4r --include-dependencies
AP4Rの情報は以下の場所にあります。
また、
- kiwamu日記
(加藤) - いたわさににほんしゅ
(篠原)
同期アプリケーションの作成
それでは、
まずはrailsコマンドでプロジェクトを作成します。
% cd async_shop % rails as_rails
テーブル設計
テーブル数が多くなると本質的な部分が見えにくくなるので、orders
)と決済テーブルpayments
)の2つを用意します。商品や顧客などの管理テーブルや注文明細テーブルは省略します。

- 注文テーブル
- 注文情報を保存するためのテーブルです。注文を受け付けるとレコードが生成されます。注文明細テーブルはつくらないので、
1つの注文につき1つの商品となります。カラムは、 注文ID ( id
)、商品名 ( item
)、注文日時 ( created_
)です。at - 決済テーブル
- 他社のシステムにて決済が無事完了すると、
このテーブルにレコードが生成されます。カラムは、 決済ID ( id
)、注文ID ( order_
)、id 決済日時 ( created_
)です。at
モデルとテーブルの作成
- データベースの準備
MySQLにdevelopment、
test、 production環境用のデータベースを作成します。それぞれのデータベースに作成されるテーブルへのアクセス権限は、 デフォルトで出力されるconfig/ database. ymlの値に合わせておきます。必要であれば適宜変更してください。 % mysqladmin ping -u root mysqld is alive % mysql -u root mysql> create database as_
rails_ development default character set utf8; mysql> create database as_ rails_ test default character set utf8; mysql> create database as_ rails_ production default character set utf8; mysql> grant all privileges on as_ rails_ development.* to root@localhost identified by ""; mysql> grant all privileges on as_ rails_ test.* to root@localhost identified by ""; mysql> grant all privileges on as_ rails_ production.* to root@localhost identified by ""; mysql > \q - モデルの作成
以下のコマンドを実行すると、
モデル、 モデルのテストファイル、 migration定義ファイルが作成されます。 % ruby script/
generate model Order item:string created_ at:datetime % ruby script/ generate model Payment order_ id:integer created_ at:datetime - テーブルの作成
migration定義ファイルを実行します。
% rake db:migrate
- モデルの関連
注文モデルと決済モデルに関連を定義します。
- as_
rails/ app/ models/ order. rb -
class Order < ActiveRecord::Base has_
one :payment end - as_
rails/ app/ models/ payment. rb class Payment < ActiveRecord::Base belongs_
to :order end コントローラとビューの作成
のちの拡張を見越してコントローラ名はAsyncShopとしておきます。
% ruby script/generate controller AsyncShop index order_form order destroy_order payment
本連載はRailsアプリケーションの解説がメインの目的ではありませんので、
アクションやビューの実装はscaffoldコマンドで作成されるものを適宜流用してしまいます。 - as_
rails/ app/ controllers/ async_ shop_ controller. rb -
同期アプリケーションなので、
order
アクション内にて注文テーブルと決済テーブルの両方にレコードを生成したのち、ユーザーに応答 ( index
アクションの結果)が返ります。 他社システムと連携している決済処理が 「重い」 という想定なので、 該当する payment
メソッドのなかで5秒間のsleep
をいれています。class AsyncShopController < ApplicationController def index @order_
pages, @orders = paginate :orders, :per_ page => 10 end def order_ form @order = Order. new end def order begin Order. transaction do @order = Order. new(params[:order]) @order. save! payment(@order[:id]) flash[:notice] = 'Order was successfully created.' redirect_ to :action => 'index' end rescue Exception flash[:notice] = 'Order was failed.' render :action => 'order_ form' end end def destroy_ order Order. find(params[:id]).destroy redirect_ to :action => 'index' end private def payment(order_ id) sleep 5 payment = Payment. new payment. order_ id = order_ id payment. save! end end - as_
rails/ app/ views/ async_ shop/ index. rhtml -
scaffoldで生成したものとの違いは、
show/ editのリンクがなくなっていることと、 注文テーブルと決済テーブルのそれぞれのレコードの作成日時が表示されることです。上記のとおり payment
アクションのなかで5秒間のsleep
があるので、その差を反映した日時が表示されることになります。 <h1>Listing orders</h1> <table> <tr> <th>Item</th> <th>Ordered at</th> <th>Payed at</th> </tr> <% for order in @orders %> <tr> <td><%=h order.
item %></td> <td><%=h order. created_ at %></td> <td><%=h begin order. payment. created_ at rescue "not yet." end %></td> <td><%= link_ to 'Destroy', { :action => 'destroy_ order', :id => order }, :confirm => 'Are you sure?', :method => :post %></td> </tr> <% end %> </table> <%= link_ to 'Previous page', { :page => @order_ pages. current. previous } if @order_ pages. current. previous %> <%= link_ to 'Next page', { :page => @order_ pages. current. next } if @order_ pages. current. next %> <br /> <%= link_ to 'New order', :action => 'order_ form' %>
あとは、
注文内容の入力画面です。 - as_
rails/ app/ views/ async_ shop/ order_ form. rhtml -
<h1>New order</h1> <% form_
tag :action => 'order' do %> <%= error_ messages_ for 'order' %> <p><label for="order_ item">Item</label><br/> <%= text_ field 'order', 'item' %></p> <%= submit_ tag "Order" %> <% end %> <%= link_ to 'ordered list', :action => 'index' %> - as_
rails/ app/ views/ layouts/ async_ shop. rhtml -
<!DOCTYPE html PUBLIC "-//
W3C// DTD XHTML 1. 0 Transitional// EN" "http:// www. w3. org/ TR/ xhtml1/ DTD/ xhtml1-transitional. dtd"> <html xmlns="http:// www. w3. org/ 1999/ xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="content-type" content="text/ html;charset=UTF-8" /> <title>AsyncShop: <%= controller. action_ name %></title> </head> <body> <p style="color: green"><%= flash[:notice] %></p> <%= yield %> </body> </html>
同期アプリケーションの一連のコードはこちらから取得できます。
% svn checkout http://ap4r.rubyforge.org/svn/tags/200709_gihyo_sync_shop
実行
動作を確認してみましょう。
% ruby script/server
次のURLにアクセスします。
「New order」
を選択し、 適当なアイテム名を入力したのち、 「Order」 ボタンで注文を確定します。決済処理に時間がかかっているので、 5秒ほど待つと注文結果のリストが表示されます。注文処理と決済処理は続けて一気に処理されていますので、 レコードの生成時刻である 「Ordered at」 と 「Payed at」 の値は、 おおむね5秒の差となっていることでしょう。 図3 同期アプリケーションの実行結果 ベンチマークをとってみる
irbにて以下のコードを実行してみましょう。アイテム名
「hoge」 で、 「Order」 ボタンが5回押された状態です。いずれも 「重い」 決済処理に影響されて実行時間は5秒以上かかっています。 (注) - このアプリケーションでは簡単のために、
HTTP GETで注文処理を行いましたが、 一般にはPOSTで処理すべきです。
% irb >> require 'benchmark' >> require 'open-uri' >> Benchmark.bm do |b| ?> 5.times {b.report {open "http://localhost:3000/async_shop/order?order[item]=hoge"}} >> end user system total real 0.010000 0.010000 0.020000 ( 5.348240) 0.010000 0.000000 0.010000 ( 5.760167) 0.000000 0.010000 0.010000 ( 5.509365) 0.010000 0.000000 0.010000 ( 5.839032) 0.010000 0.010000 0.020000 ( 5.377185) => true >>
以上で、
同期アプリケーションの作成が終わりました。次にこのアプリケーションをAP4Rを利用して非同期化します。 非同期化の準備
AP4RのRailsプラグインをインストール
下記のコマンドでインストールできます。tags以下のap4r-0.
3.2は執筆段階でのAP4Rのバージョンを表しています。バージョンが異なる場合には、 適宜読み変えてください。 % cd as_rails % ruby script/plugin install http://ap4r.rubyforge.org/svn/tags/ap4r-0.3.2/samples/HelloWorld/vendor/plugins/ap4r
また、
インターネット環境に接続できない場合は、 RubyGemsでインストールされたAP4Rパッケージのgems/ ap4r-0. 3.2/ rails_ plugin/以下をコピーしてご利用ください。 AP4R の作業ディレクトリを作成
冒頭で説明したとおり、
AP4Rのルートディレクトリとしてas_ ap4rを作成します。次のコマンドを実行するだけです。 % cd async_shop % ap4r_setup as_ap4r
このコマンドにより以下のディレクトリ、
およびファイルが生成されます。 as_ap4r +-- config +-- log +-- public +-- script +-- tmp
メッセージ格納用のテーブルを作成
MySQLにユーザーとデータベースを作成します。
% mysql -u root mysql> create database ap4r default character set utf8; mysql> grant all privileges on ap4r.* to ap4r@localhost identified by "ap4r"; mysql> use ap4r
次の2つのSQLを実行してください。メッセージ格納用のテーブルです。ファイルは、
gems/ reliable-msg-1. 1.0/ lib/ reliable-msg/ mysql. sqlにあります。 CREATE TABLE `reliable_msg_queues` ( `id` varchar(255) NOT NULL default '', `queue` varchar(255) NOT NULL default '', `headers` text NOT NULL, `object` blob NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=binary; CREATE TABLE `reliable_msg_topics` ( `topic` varchar(255) NOT NULL default '', `headers` text NOT NULL, `object` blob NOT NULL, PRIMARY KEY (`topic`) ) ENGINE=InnoDB DEFAULT CHARSET=binary;
AP4Rの起動確認
以下のコマンドで、
AP4Rを起動します。 % cd as_ap4r % ruby script/mongrel_ap4r start -A config/queues_mysql.cfg
終了するには、
コンソールで Ctrl+C
としてください。Unix系のOSでは、-d
オプションをつけるとデーモンモードで起動することもできます。irbで疎通確認しましょう。
% irb >> require 'rubygems' >> require 'reliable-msg' >> q = ReliableMsg::Queue.new "test" => #<ReliableMsg::Queue:0x26fdea0 @queue="test"> >> q.put "hoge" => "37a13880-3736-012a-cf51-001IRI6cb9ad524"
irbやAP4Rの起動コンソールになにもエラーが出力されなければ、
疎通できているはずです。 UUIDの生成に失敗した場合は、
ディレクトリの権限モードを変更, または, UUIDの設定ファイルを手動で生成する必要があります。また、 コネクションの生成に失敗した場合は、 ネットワーク環境の設定に問題がある可能性があります。詳細はFAQをご覧ください。 FAQ:http://
ap4r. rubyforge. org/ wiki/ wiki. pl?FAQ 次のページからは、
いよいよこの同期アプリケーションを非同期に拡張していきます。 非同期アプリケーションへの拡張
非同期化の方針のおさらい
決済処理を注文処理から分離します。注文処理と決済処理の疎結合化です。時間のかかっていた決済処理を実行することなく応答を返すことができるようになるので、
お客さんのイライラ解消につながるでしょう。また、 他社のシステムと連携した決済処理は、 これまで他社のシステムメンテナンスや障害に影響されていましたが、 今後はそのような場合でも注文を受け付けることが可能になります。 非同期化のAPI
非同期メッセージ送信のAPIは、
Railsプラグインで提供されるヘルパーメソッドを使います。 ap4r.async_to({...}, # 次の処理を指定するオプション {...}) # リクエストするデータ
ap4r
が、AP4Rのプラグインで提供されるメソッドです。 async_
メソッドには、to 引数として、 非同期処理として実行されるアクションの指定、 及びパラメータを渡します。アクションの指定は、 Railsの url_
と同様に指定するか、for :url
キーにURLを指定することが出来ます。URLを直接指定することで、Railsではないサービスにも非同期処理を行わせることができます。 いざ、
コード拡張 今回の同期アプリケーションの場合、
変更箇所はasync_ shop_ controller. rbの order
アクションとpayment
アクションになります。以下に修正前のものと修正後のものを並べます。- 修正前
-
class AsyncShopController < ApplicationController (省略) def order begin Order.
transaction do @order = Order. new(params[:order]) @order. save! payment(@order[:id]) # ...(1) flash[:notice] = 'Order was successfully created.' redirect_ to :action => 'index' end rescue Exception flash[:notice] = 'Order was failed.' render :action => 'order_ form' end end private def payment(order_ id) sleep 5 payment = Payment. new payment. order_ id = order_ id # ...(2) payment. save! end (省略) end - 修正後
-
class AsyncShopController < ApplicationController (省略) def order begin Order.
transaction do @order = Order. new(params[:order]) @order. save ap4r. async_ to({:action => 'payment'}, {:order_ id => @order. id}) # ...(3) flash[:notice] = 'Order was successfully created.' redirect_ to :action => 'index' end rescue Exception flash[:notice] = 'Order was failed.' render :action => 'order_ form' end end def payment sleep 5 payment = Payment. new payment. order_ id = params[:order_ id] # ...(4) payment. save render :text => "true" # ...(5) end (省略) end
いかがでしょう?
修正前後であまり大きな違いはないように見えるのではないでしょうか。変更箇所を順に見ていきます。
修正前に
payment
アクション(メソッド) を呼び出していた部分 (1) が非同期化にともなってなくなり、 代わりに ap4r.
メソッドasync_ to (3) となります。次の処理として同じコントローラ内の payment
アクションを指定し、@order.
をリクエストデータとして渡しています。非同期メッセージ送信する側の変更は以上で終了です。id 次に非同期として処理される側、
payment
アクション(privateメソッドからアクションに変わっています) をみてみましょう。修正前は @order.
を引き数として受け取り、id payment
にセット(2) していました。修正後にはこの引き数がなくなっています。リクエストデータとして渡されたものは、 画面から渡るデータと同様に params
でアクセス可能なので、(4) のようにして payment
にセットしています。キーは、(3) で指定していたものとなります。 最後に、
非同期処理が問題なく終了したことをAP4Rサービスに伝えるために (5) の記述が必要になります。 非同期アプリケーションの一連のコードはこちらから取得できます。
% svn checkout http://ap4r.rubyforge.org/svn/tags/200709_gihyo_async_shop
実行
Railsプロセスを起動します。
% cd as_rails % ruby script/server
AP4Rプロセスを起動します。
% cd as_ap4r % ruby script/mongrel_ap4r start -A config/queues_mysql.cfg
画面から注文処理を実行します。
「Order」
ボタンを押すとすぐに注文結果のリストが表示されるでしょう。決済日時 (Payed_ at) は"not yet."となっています。非同期アプリケーションに拡張した結果、 注文情報の保存処理が実行されるとすぐにユーザーに応答が返るようになりました。 「重い」 決済処理はユーザーの知らぬ間に実行されているはずです。画面をリロードすると、 決済日時の表示が変わり、 決済処理が無事に終了したことを確認できると思います。 図4 非同期アプリケーションの実行結果 ベンチマークをとってみる
さきほどと同様にirbから実行します。
% irb >> require 'benchmark' >> require 'open-uri' >> Benchmark.bm do |b| ?> 5.times {b.report {open "http://localhost:3000/async_shop/order?order[item]=hoge"}} >> end user system total real 0.010000 0.000000 0.010000 ( 0.602230) 0.010000 0.010000 0.020000 ( 5.549839) 0.000000 0.000000 0.000000 ( 5.395502) 0.010000 0.010000 0.020000 ( 5.592141) 0.010000 0.010000 0.020000 ( 5.566676) => true
タイミングや各自の環境によってベンチマークの結果は変わります。筆者の環境では、
はじめのリクエストの返りはたしかに早くなりましたが、 それ以外は以前と同じくらいでした。これは、 Railsプロセス1台で処理を行なっているため、 「重い」 決済処理が実行されている間、 ほかの処理が堰き止められてしまっているからです。つまり、 index
アクション後のリダイレクトより先にpayment
アクションのリクエストが実行されているので、こうした結果になっています。 リダイレクトの時間を含めずに計測すると、
サーバ側での処理時間が確実に短縮されていることがよりはっきりします。 % irb >> require 'benchmark' >> require 'net/http' >> Net::HTTP.version_1_2 >> Benchmark.bm do |b| ?> 5.times {b.report { ?> Net::HTTP.start('localhost', 3000) {|http| ?> response = http.get('/async_shop/order?order[item]=hoge') ?> } ?> } } >> end user system total real 0.000000 0.000000 0.000000 ( 0.081017) 0.000000 0.010000 0.010000 ( 0.385568) 0.000000 0.000000 0.000000 ( 5.103064) 0.010000 0.000000 0.010000 ( 0.372144) 0.000000 0.010000 0.010000 ( 5.638147) => true
payment
アクションの実行待ちでやはり時間のかかったものもいくつかありますが、下図のようなリバースプロキシを使った構成では、 通常、 リクエストが適切に振り分けられることでユーザーは遅延を感じることなく処理を実行できるでしょう。バックエンドにフットプリントの小さいRailsプロセスをたくさん立ちあげておくことで、 ユーザーからのリクエストとAP4Rからの非同期メッセージ処理のリクエストをともに分散させ、 負荷の平準化を図るのが一般的な構成です。 図5 リバースプロキシを利用した複数プロセス構成 非同期処理用のRailsプロセスを分けてみる
また、
リバースプロキシを使わずに、 非同期メッセージ処理のリクエストを明示的に別のRailsプロセスにリクエストすることもできます。非同期処理用のプロセスへの負荷が少ない場合はもったいない構成ですが、 別な機能も紹介したいので、 最後にこの方法を試してみましょう。 決済処理を別のRailsプロセスで実行するよう拡張します。構成は下図のようになります。AP4Rでは設定によりこうした構成も簡単に組むことができます。
図6 非同期処理用のRailsプロセスを別にたてる構成 設定ファイル
AP4Rの設定ファイルを確認してみましょう。
- as_
ap4r/ config/ queues_ mysql. cfg -
--- store: type: mysql host: localhost database: ap4r username: ap4r password: ap4r drb: host: port: 6438 acl: allow 127.
0.0. 1 allow ::1 dispatchers: - targets: queue.* threads: 1
store
は、メッセージの永続化先の情報です。 drb
は、メッセージの受け口となるdruby関連の情報です。 acl
(=access control list)は、 allow
で接続可能なアドレスを,deny
で接続不可のアドレスを指定します。dispatchers
は、非同期メッセージ処理のリクエストを行なうスレッドの設定です。 targets
で指定されたチャネルにはいったメッセージを、threads
で指定されたスレッド数で処理します。リクエスト処理が完了するまでスレッドは占有されます。さて、
非同期メッセージ処理のリクエストが、 dispatchers
によって行われるのはすでに見てきたとおりですが、リクエスト先のURLはどう決まっているのでしょうか? ap4r.
の第1引き数で次の処理を指定していますが、async_ to ここで指定された値をActionControllerの url_
の引数に与えた結果がURLとなりますfor ( :controller
の指定がない場合は、自身のコントローラ名を利用します)。今回の例では、 下記のURLが非同期メッセージ処理のリクエスト先となります。 http://localhost:3000/{:controller で指定された値}/{:action で指定された値}
localhost:3000が変わらないので、
同一のRailsプロセスに非同期メッセージ処理のリクエストは飛ぶことになります。この部分を書き替えるのが次に紹介するURL変換フィルタの機能です。 URL変換フィルタ
その名のとおり、
非同期メッセージ処理のリクエスト先URLを書き替えるためのフィルタです。次のように、 dispatchers
の設定に追加します。Procオブジェクトの文字列を指定することで、ユーザーが自在に書き替えルールを設定できます。以下の設定では、 ホスト名はそのままで、 ポート番号のみを4001から4003の範囲でランダムに変化させています。これにより、 同期処理用のRailsプロセスと非同期処理用のRailsプロセスが分離されたことになります。また、 非同期処理用のプロセスでは 「重い」 処理が実行されるので複数のRailsプロセスに処理を分散しています。 さらに、
threads
の設定値も1から3に変わっています。非同期メッセージング処理のリクエストが完了するまでスレッドが占有されるので、非同期処理用のRailsプロセス分だけスレッドも用意したわけです。 dispatchers: - targets: queue.* threads: 3 modify_rules: url: "proc {|url| url.port = 4001 + rand(3)}"
上の設定をqueues_
mysql. cfgに反映し、 AP4Rを起動します。 % cd as_ap4r % ruby script/mongrel_ap4r start -A config/queues_mysql.cfg
複数のRailsプロセスを起動
では、
この設定のもとでこれまで作成してきた非同期アプリケーションを動かしてみましょう。複数のRailsプロセスを扱うにはmongrel_ clusterが便利です。ただし、 Linux環境下である必要があるため、 Windows環境で動かしている方は、 コマンド プロンプト4つに、 それぞれにRailsプロセスをポート指定 ( -p
オプション)で起動してください。 % gem install mongrel_cluster % cd as_rails % mongrel_rails cluster::configure -e development -p 4000 -N 4 % mongrel_rails cluster::start starting port 4000 starting port 4001 starting port 4002 starting port 4003
同期処理である注文情報の保存処理には4000番ポートのRailsプロセスが利用され、
非同期処理である決済処理には4001から4003番ポートのプロセスが利用されます。 次のURLで画面をひらき、
動きを確認してみてください。 ベンチマークをとってみる
さきほどと同様にirbから実行します。ポート番号が3000から4000に変わっている部分だけご注意ください。
% irb >> require 'benchmark' >> require 'open-uri' >> Benchmark.bm do |b| ?> 5.times {b.report {open "http://localhost:4000/async_shop/order?order[item]=hoge"}} >> end user system total real 0.010000 0.000000 0.010000 ( 0.567744) 0.010000 0.000000 0.010000 ( 0.714265) 0.000000 0.010000 0.010000 ( 0.407419) 0.010000 0.000000 0.010000 ( 0.715437) 0.000000 0.000000 0.000000 ( 0.708147) => true
4000番ポートの同期処理用のRailsプロセスでは、
軽い、 注文情報の保存処理のみが実行されるので、 ユーザーへの応答はいずれもすぐに返ってきているのがわかるでしょう。また、 後続の非同期処理も3台のRailsプロセスで着々と実行されます。ベンチマークを実行したのち、 画面を何度かリロードしているとその様を確認できます。 URL変換フィルタの機能を利用した非同期アプリケーションの一連のコードはこちらから取得できます。
% svn checkout http://ap4r.rubyforge.org/svn/tags/200709_gihyo_async_shop_with_url_rewrite
今回は、
同期アプリケーションに対するAP4Rを利用した非同期拡張をみてきました。 async_
メソッドによるコード拡張のポイントと、to 非同期化によってユーザーのイライラが解消される様子が伝わったでしょうか。リバースプロキシを利用してバックエンドに複数のRailsプロセスが存在する構成では、 「重い」 非同期処理とユーザーにすぐ応答したい処理をうまく両立させながら実行できることになります。また、 非同期メッセージ処理のリクエスト先を自在に変更可能なURL変換フィルタ機能を利用した構成も紹介しました。 次回は、
AP4Rの 「堅牢」 さを支えるSAF機能の利用やテストサポートについて触れてみたいと思います。 - as_
- as_
テーブルができたかどうかを確認してみましょう。今はdevelopment環境のみで十分です。
% mysql -u root mysql> use as_rails_development Database changed mysql> show tables; +--------------------------------+ | Tables_in_as_rails_development | +--------------------------------+ | orders | | payments | | schema_info | +--------------------------------+ 3 rows in set (0.00 sec)