Nginx + Lua から MySQL を使う

この記事は MySQL Casual Advent Calendar 2013 の 12 日目です。

みんな大好き Nginx + Lua ですが、その Lua から MySQL が叩けるとなると、Nginx だけでウェブアプリケーションが書けちゃうという夢が広がりますね。

難しそうというイメージがあるかもしれませんが、実は OpenResty を使うと、そんな環境が簡単に作れてしまうので、今日はその方法を紹介します。

ngx_openresty のインストール

今回は Ubuntu 12.04 での例ですが、ほとんど同様の手順で CentOS 6.5 でも動くことを確認しています。 *1

$ sudo apt-get -y install gcc make libpcre3-dev libssl-dev perl5 wget
$ sudo apt-get -y install libmysqlclient-dev
$ wget http://openresty.org/download/ngx_openresty-1.4.3.3.tar.gz
$ cd ngx_openresty-1.4.3.3
$ ./configure --with-luajit && make
$ sudo make install

上記の手順で、 /usr/local/openresty/nginx/ 以下に Nginx がインストールされます。ここでインストールされた nginx には、すでに Lua モジュールや lua-resty-mysql モジュールが含まれているので、すぐに Nginx + Lua + MySQL を使いはじめることができます。

なお今回は手抜きで libmysqlclient-dev パッケージを使っていますが、カジュアル勢の皆様におかれましては秘伝のビルドをリンクさせると良いと思います。

Lua + MySQL のコード例

簡単なアクセスカウンタを作ってみます。

/usr/local/openresty/nginx/conf/nginx.conf に以下のような記述を追加します。

location /counter {
  content_by_lua_file conf/counter.lua;
}

/usr/local/openresty/nginx/conf/counter.lua に以下のようなコードを記述します。

-- based on sample code at https://github.com/agentzh/lua-resty-mysql

local mysql = require "resty.mysql"
local db, err = mysql:new()
if not db then
  ngx.log(ngx.ERR, "failed to instantiate mysql: ", err)
  return
end
local ok, err, errno, sqlstate = db:connect{
  host = "127.0.0.1",
  port = 3306,
  database = "lua",
  user = "lua",
  password = "lua",
  max_packet_size = 1024 * 1024 }

if not ok then
  ngx.log(ngx.ERR, "failed to connect: ", err, ": ", errno, " ", sqlstate)
  return
end

res, err, errno, sqlstate =
db:query("UPDATE counters SET value = value + 1 WHERE id = 1")
if not res then
  ngx.log(ngx.ERR, "bad result: ", err, ": ", errno, ": ", sqlstate, ".")
  return
end

res, err, errno, sqlstate =
db:query("SELECT value FROM counters WHERE id = 1")
if not res then
  ngx.log(ngx.ERR, "bad result: ", err, ": ", errno, ": ", sqlstate, ".")
  return
end

ngx.say("counter: " .. res[1]["value"])

local ok, err = db:set_keepalive(10000, 100)
if not ok then
  ngx.log(ngx.ERR, "failed to set keepalive: ", err)
  return
end

MySQL の方は、以下のようにユーザとテーブルを作っておきましょう。

GRANT ALL PRIVILEGES ON *.* TO 'lua'@'localhost' IDENTIFIED BY 'lua';
CREATE DATABASE lua;
CREATE TABLE lua.counters(id INT PRIMARY KEY AUTO_INCREMENT, value INT NOT NULL);
INSERT INTO lua.counters (value) VALUES (0);

Nginx を起動します。

$ sudo /usr/local/openresty/nginx/sbin/nginx

これで、以下のように /counter でアクセスカウンタが動きます。

$ curl localhost/counter
counter: 1
$ curl localhost/counter
counter: 2
$ curl localhost/counter
counter: 3
$ curl localhost/counter
counter: 4

lua-resty-mysql の API

今回利用した API を紹介します。これらはほんの一部なので、詳しくはドキュメントを参照してください。

new

新しい MySQL アダプタオブジェクトを生成します。この時点ではまだ接続しません。

local mysql = require "resty.mysql"
local db, err = mysql:new()

Document (new)

connect

MySQL に接続します。後述するコネクションプールが残っていれば、そのコネクションが再利用されるようです。

local ok, err, errno, sqlstate = db:connect{
  host = "127.0.0.1",
  port = 3306,
  database = "lua",
  user = "lua",
  password = "lua",
  max_packet_size = 1024 * 1024 }

Document (connect)

query

SQL を実行します。

local res, err, errno, sqlstate =
db:query("SELECT value FROM counters WHERE id = 1")

Result Set は配列で返ってきます。上記の SELECT の結果からは、以下のようにして値を取り出せます。

ngx.say("counter: " .. res[1]["value"])

Document (query)

set_keepalive

コネクションプーリングの設定をします。

local ok, err = db:set_keepalive(10000, 100)

この例では、 10,000ms でタイムアウト、同時に 100 コネクションまでプールする、という設定をしています。

Document (set_keepalive)

おまけ: Docker でサンプルを動かす

今回のサンプルプログラムを再現できる Dockerfile を作ってみました。以下のリポジトリで公開しています。

https://github.com/mirakui/nginx-lua-mysql-example

Docker 自体のインストール手順については、公式ドキュメントを参照してください。

Docker がインストールされていれば、以下のような手順で、今回の環境が再現できます。

Docker イメージの作成

Dockerfile からイメージを作成します。

$ git clone https://github.com/mirakui/nginx-lua-mysql-example.git
$ cd nginx-lua-mysql-example
$ sudo docker build -t ngx .

Docker コンテナの起動

作成したイメージを使ってコンテナを起動します。 docker ps コマンドでコンテナが表示されていれば成功です。

$ sudo docker run -d ngx
b32680fbc44ee55d309e8377963f9015fcb3e6733f55dc580276c9eff1952ba5
$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
b32680fbc44e        ngx:latest          /bin/sh -c /usr/loca   4 seconds ago       Up 2 seconds        3306/tcp, 80/tcp    angry_heisenberg

サンプルコードの実行

立ち上がっているコンテナの IP アドレスを調べてから、その IP アドレスに対して /counter リクエストを発行してみると、アクセスカウンタが動くはずです。

$ sudo docker inspect b32680fbc44e | jq '.[0].NetworkSettings.IPAddress'
"172.17.0.17"
$ curl 172.17.0.17/counter
counter: 1
$ curl 172.17.0.17/counter
counter: 2
$ curl 172.17.0.17/counter
counter: 3
$ curl 172.17.0.17/counter
counter: 4

おわりに

このように Nginx + Lua + MySQL の環境は、 OpenResty を使えばとても簡単に構築できます。今回のような手順を覚えておけば、 isucon や、 isucon 等で役に立つかもしれません。

明日 12/13 は @meijik さんです。お楽しみに。

*1:Mac OSX でも試したのですが pcre まわりでエラーになってしまい、まだビルドできていません…