2010-04-26

MySQL Proxyでできること、できないこと

はてなブックマーク   livedoor clip

こんにちは。リコーの井上です。今回から私もこのブログを書かせていただくことになりました。よろしくお願いします。

今回はMySQL Proxyについて書きたいと思います。MySQLを利用していると、WebアプリケーションなどのMySQLクライアントからサーバに送られるクエリを解析したい場合があります。あるいは、複数のMySQLサーバに処理を振り分けたいということがあります。

MySQL Proxyはその名の通り、MySQLクライアントとMySQLサーバの間に入って、接続やクエリで取り交わされるデータを参照・加工して、いろいろと役立つことをさせようというものです。動作内容は軽量スクリプト言語Luaにより柔軟に記述できますが、機能的制限もあります。

このエントリではMySQL Proxyとはどういうものか、どういうことができそうなのか、できないのかということを感じ取っていただければと思っています。

必要なものとインストール

MySQL Proxyを一からビルドするにはlibevent, glib, Luaなどの依存ライブラリについてもビルドする必要があります。気軽に試すには、ダウンロードサイトからお使いのOS用のバイナリパッケージをダウンロードするのがおすすめです。バイナリパッケージはtarballで提供されているので、これを/opt以下などに展開して利用します。

Red Hat系ディストリビューション用に、/etc/init.dに置くスクリプトなども別途公開されています。

ここでの動作確認は以下の組み合わせで行いました。

  • CentOS 5.4
  • MySQL Proxy 0.8.0(Red Hat Enterprise Linux用バイナリ)
  • MySQL 5.1.45

簡単なLuaスクリプト

MySQL Proxyの大きな特長は、軽量スクリプト言語Luaを利用して任意の処理を記述できることにあります。逆に、Luaを記述しなければ指定されたMySQLサーバに交互に接続を振り分ける単純なロードバランサとなります。

MySQL Proxyでクライアントから発行されたクエリの内容をファイルに出力するLuaスクリプトは以下のようになります。


 1  function read_query(packet)
 2    local fh
 3    if string.byte(packet) == proxy.COM_QUERY then
 4      fh = io.open("/var/tmp/query.log", "a")
 5      fh:write(string.sub(packet, 2) .. "\n")
 6      fh:close()
 7    end
 8  end

スクリプトをlog.luaという名前で保存し、以下のようにMySQL Proxyを起動します。


% /opt/mysql-proxy/bin/mysql-proxy \
          --proxy-backend-addresses=127.0.0.1:3306 \
          --proxy-lua-script=`pwd`/log.lua

オプション --proxy-backend-addresses で中継先のバックエンドつまりMySQLサーバを、 --proxy-lua-script でLuaスクリプトファイル名を指定します。Luaファイル名はフルパスで指定することに注意してください。

MySQLクライアントを起動してデータベースにSELECTを発行してみましょう。MySQL ProxyはデフォルトでTCPの4040ポートを利用します。なお、接続するDBとSELECTするテーブルは事前に作成しておいてください(ここではhogeとitems)。


% mysql -u root -p -h 127.0.0.1 -P 4040 hoge
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
...(省略)...
mysql> select * from items;
...(省略)...
mysql> exit
Bye
%

この後、 /var/tmp/query.log を開くと以下のような内容が記録されています。


select @@version_comment limit 1
select * from items

1行目はMySQLクライアントがサーバに接続したときに自動的に発行している文です。2行目は発行したSELECT文がそのまま出力されています。exitはクエリではないので、ログは出力されていません。

スクリプトの内容

MySQL ProxyはLua関数により拡張できるポイントをいくつか用意しており、read_queryはその一つです。read_query関数はクライアントからのリクエストがきたときに、そのデータが入ったパケットを引数として呼び出されます。

2行目ではファイルハンドルを格納するローカル変数を定義しています。localキーワードをつけないと変数はグローバルとなり、他のリクエストから呼ばれたLua関数の中でも共通に利用されることになります。なお、関数の外で定義されたローカル変数は、クライアントとの同一コネクションを通して有効です。

3行目のif文では、パケットの最初のバイトを見て、リクエストがクエリなのかどうかを確認しています。MySQL ProxyではCOM_QUERY, COM_QUITといったパケットのタイプを示す定数が定義されています。これをパケットの先頭バイトとマッチすることでパケットを識別することができます。なおstring.byte(str, i, j)はi文字目からj文字目の内部コードを数値として多値で返します。i, jのデフォルト値は1で、文字列の最初1文字ということになります。

4~6行目は実際にファイルを開き、クエリの内容を書き込んでいます。Luaの関数 string.subを使って、関数に渡されたパケットから2バイト目以降を取り出しています。

余談ですが、string.sub() の 「.」 はstringインスタンスのメソッドを呼び出しているように見えますが、Luaはクラスを持ちません。 string は「テーブル」と呼ばれるいわゆる連想配列で、Lua処理系が提供する文字列関数を格納しています。 string.sub() はキー”sub”に対応して格納された関数を呼び出すという意味になります。つまり string["sub"]() と同じ意味です。

MySQL Proxyを使いこなすにはLuaを理解する必要があります。とはいえ、Luaは大変理解しやすい言語なので、Lua 5.1 Reference Manual(日本語訳)などにざっと目を通すだけでかなりのことができるようになるでしょう。

カスタマイズできる箇所とその内容

MySQL Proxyが用意している拡張のための関数は以下の7つです。各関数からアクセスできるデータなどについてはスクリプティングのリファレンスマニュアルに詳しく書かれています。

接続に関わる関数

connect_server
クライアントからMySQL Proxyに接続が行われた際、バックエンドとの接続を行う前に呼び出されます。実際に接続および認証を行うサーバを選択できる唯一の場所で、落ちているとマークされているバックエンドを避けるといったことができます。ただしクライアントに関する情報は利用できません。
read_handshake
MySQL Proxyとバックエンドとの接続が行われた後、バックエンドから送り返されたハンドシェイクのための情報をクライアントに転送する前に呼び出されます。クライアントのIPなどの情報が得られる状態になっているので、IPを見て切断するといった制御ができます。
read_auth
ハンドシェイクに続き、クライアントから受けとった認証情報をバックエンドに転送する前に呼び出されます。クライアント側のユーザ名にアクセスできますが、認証が成功するのかどうかはこの時点ではわかりません。
read_auth_result
バックエンドでの認証が終了した後、バックエンドから受けとった認証結果をクライアントに通知する前に呼び出されます。認証の成否がわかるので、認証成功(失敗)後に行いたい処理を記述できます。

認証が終了したコネクションはそのままそのクライアントからのリクエストで利用してもいいですし、バックエンドと結びつけられたコネクションプールに入れて後から利用することもできます。

リクエストに関わる関数

read_query
クライアントから何らかのリクエストが送られたとき、それをバックエンドに転送する前に呼び出されます。「簡単なLuaスクリプト」で利用していたのもこの関数です。SQLを解析して書き換えたり、結果を作って返したりできます。
read_query_result
クエリ結果がバックエンドから返されたとき、それをクライアントに転送する前に呼び出されます。結果を操作したり任意の結果を返すようにすることができます。この関数は結果セットにアクセスしたい旨を前述のread_query()の中で明示的に指定しなければ呼び出されません。

その他の関数

disconnect_client
サーバとクライアントの接続が切断されるときに呼び出されます。実際には切断せずにコネクションをプールするといった処理を記述します。

コネクションプールへのアクセス

少し細かい話になりますが、MySQL Proxyのスクリプトで一番わかりにくいと感じたのがコネクションプールとのやりとりなので、説明を加えておきます。これは proxy.connection.backend_ndx という変数によって制御します。

現在利用している(バックエンドとの)コネクションをコネクションプールに入れるには、 proxy.connection.backend_ndx に0を代入します。 代入した段階でプールへの格納が行われます。

逆にプールにあるコネクションを利用するには、対応するバックエンドの番号 (proxy.global.backends配列の添え字) を proxy.connection.backend_ndx に代入します。 代入と同時に、プールにあるアイドルなコネクションのどれか一つが、現在のコンテキストとなっているクライアントとのコネクションに対応づけられます。

Lua層のこの変数への代入が、Cレベルではコネクションプールを操作する関数呼び出しを起こすような実装となっている、ということを覚えておくと多少理解しやすくなります。

MySQL Proxyでできること、できないこと

MySQL Proxyの用途として、サイトでは以下のようなものが例として挙げられています。

  • 負荷分散
  • フェイルオーバー(冗長化)
  • クエリの解析
  • クエリのフィルタリングや書き換え

Luaでかなり自由なことができそうなのですが、現行のバージョンでは機能にいくつかの制限があり、これらを念頭に置いて使い方を考えなければなりません。

  • Luaスクリプトが呼ばれるのはクライアントがアクションを起こしたときのみです。そのため、クライアントからの指示なしに必要な数のコネクションを作ってプールすることや、バックエンドの生死確認をアクティブに行うといったことはできません。
  • コネクションプールからのコネクションの割り当ては接続してきたユーザの権限に関係なく行われるようです。コネクションの割り当て時にCHANGE_USERコマンドで権限を変更することもできるようなのですが、期待通りの動作を確認できませんでした。そのため、権限の高いユーザと低いユーザが混在するような利用法はうまく動作しません。
  • これはコネクションプールをアプリケーションと独立に持つ場合の宿命かも知れませんが、コネクション固有の状態をうまくサポートできません。プールに入っているコネクションに固有の状態を設定しても、次にそのクライアントに対して同じコネクションが割り当てられる保証はないからです。コネクション固有の状態としては、文字エンコーディングやセッション変数、そしてテンポラリテーブルなどがあります。

R/W Splittingへの適用

MySQL Proxyの利用法として、更新系と参照系のクエリをそれぞれマスタとスレーブに振り分けて負荷分散する、いわゆるR/W Splittingがあります。これが完璧に動作すれば、アプリ側での制御が不要になるため開発が楽になります。MySQL Proxyのパッケージに含まれるサンプルスクリプトの中にも rw-splitting.lua としてコンセプト実証のコードが入っています。(0.8.0に付属のものはいくつかバグがあってそのままでは動作しません。例: Bug #39629)

実現方法は、認証済みのコネクションをマスタ・スレーブそれぞれに対して一定数プールしておき、クエリがきた段階で書き込みならマスタへの、読み込みならスレーブへのコネクションを使うというものです。LAST_INSERT_ID()やトランザクションに対する考慮もされています。

しかし、前節に書いたような制限があるため、バグを修正して使う場合でもアプリ側で以下のような注意が必要になります。

  • 文字エンコーディングなどコネクション固有になる設定はバックエンド側で一括して設定する
  • MySQL Proxy起動初期にはコネクションプールに書き込みのためのコネクションが不足することがあるため、エラー時に再接続するようアプリ側で制御する

R/W SplittingについてはMySQL Proxy RW Splittingに制限を含めた記載があります。

まとめ

以上、MySQL Proxyの機能やその制限について書いてみました。開発においてクエリを解析したり、特定の条件で書き換えたりということには利用できるレベルになっていると思います。ただ、MySQLサーバ群の手前に配置してすべてのアプリからの読み書きを分散、という使い方はまだ荷が重いようです。今後の開発に期待したいところです。