こんにちは、@tkuchiki です。
このエントリーは tech.kayac.com Advent Calendar 2013、9 日目のエントリーです。
本エントリーでは、弊社で主に利用している Web サーバ (Nginx) の bundle OpenResty について簡単に紹介したいと思います。
OpenResty
OpenResty (ngx_openresty) は、lua-nginx-module を始めとする多数の 3rd party module を内包した Nginx です。
作者は、lua-nginx-module のメンテナを務める agentzh さんです。
lua-nginx-module
Nginx を Lua / LuaJIT で処理するためのモジュールです。
API は 全て Non-Blocking I/O で書かれています。
使用したモジュール
以下のモジュールを使用しました。
各モジュールの詳細な API 仕様はそれぞれのページをご確認ください。
OpenResty の build
drizzle-nginx-module は、デフォルトで有効になっていないので、
--with-http_drizzle_module
をつけて build してください。
また、以下の例では使用していない module も組み込んでいますので、必要のないものは消したほうが速く build できるでしょう。
yum install -y pcre pcre-devel openssl openssl-devel zlib zlib-devel readline readline-devel libxml2 libxml2-devel libxslt-devel perl perl-devel perl-ExtUtils-Embed GeoIP-devel gd-devel # install libdrizzle curl -O http://agentzh.org/misc/nginx/drizzle7-2011.07.21.tar.gz tar zxvf drizzle7-2011.07.21.tar.gz cd drizzle7-2011.07.21/ ./configure --without-server make libdrizzle-1.0 sudo make install-libdrizzle-1.0 # install ngx_openresty export LIBDRIZZLE_LIB=/usr/local/lib export LIBDRIZZLE_INC=/usr/local/include/libdrizzle-1.0 ./configure \ --prefix=/usr/local/openresty \ --conf-path=/etc/nginx/nginx.conf \ --sbin-path=/usr/sbin/nginx \ --http-log-path=/var/log/nginx/access.log \ --error-log-path=/var/log/nginx/error.log \ --pid-path=/var/run/nginx.pid \ --lock-path=/var/lock/subsys/nginx \ --http-client-body-temp-path=/var/tmp/nginx/client_temp \ --http-proxy-temp-path=/var/tmp/nginx/proxy_temp \ --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \ --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \ --http-scgi-temp-path=/var/cache/nginx/scgi_temp \ --user=nginx \ --group=nginx \ --with-file-aio \ --with-ipv6 \ --with-http_ssl_module \ --with-http_realip_module \ --with-http_addition_module \ --with-http_xslt_module \ --with-http_image_filter_module \ --with-http_geoip_module \ --with-http_sub_module \ --with-http_dav_module \ --with-http_flv_module \ --with-http_mp4_module \ --with-http_gzip_static_module \ --with-http_random_index_module \ --with-http_secure_link_module \ --with-http_degradation_module \ --with-http_stub_status_module \ --with-http_perl_module \ --with-http_iconv_module \ --with-http_drizzle_module \ --with-mail \ --with-mail_ssl_module \ --with-luajit gmake sudo gmake install
使用した設定ファイル(共通)
nginx.conf
今回使用する nginx.conf です。
各 Section で抜粋しながら説明を行います。
lua_package_path '/usr/local/openresty/lualib/resty/?.lua;/usr/local/openresty/lualib/magick/?.lua;;'; lua_shared_dict upload_cnt 100k; init_by_lua_file '/usr/local/openresty/lua/init.lua'; upstream backend { drizzle_server 127.0.0.1:3306 dbname=test_db password=test user=test_user protocol=mysql; } server { listen 80; server_name localhost; client_max_body_size 20M; location /login { content_by_lua_file /usr/local/openresty/lua/login.lua; } location /login_query { internal; set_quote_sql_str $quoted_username $arg_username; set_quote_sql_str $quoted_password $arg_password; set $sql "SELECT COUNT(id) as cnt FROM users WHERE username=$quoted_username AND password=$quoted_password;"; drizzle_query $sql; drizzle_pass backend; rds_json on; } location /upload { content_by_lua_file /usr/local/openresty/lua/file_upload.lua; } location ~ ^/img/(.+\.(gif|jpe?g|png)(\?.+)?$) { content_by_lua_file /usr/local/openresty/lua/resize.lua; } location ~ ^/(raw|s|m|l)/(.+\.(gif|jpe?g|png)(\?.+)?$) { alias /tmp/$1/$2; } }
lua の変数定義
モジュールの require など、共通して使いたい変数は以下のように定義しました。
nginx.conf の init_by_lua_file ‘/usr/local/openresty/lua/init.lua’; で読み込んでいます。
# init.lua cjson = require "cjson" magick = require "magick" resty_sha1 = require "resty.sha1" resty_str = require "resty.string" upload = require "resty.upload" img_dir = '/tmp' raw_dir = img_dir .. "/raw" s_dir = img_dir .. "/s" m_dir = img_dir .. "/m" l_dir = img_dir .. "/l" size_s = "25%x25%" size_m = "50%x50%" size_l = "100%x100%"
MySQL アクセス
Drizzle は、MySQL から fork してクラウドに最適化して開発されたプロダクトです。
どのようなものかは、クラウドに最適化したMySQLのフォーク「Drizzle」が正式版公開、Introducing the Drizzle Project が参考になります。
今回使うのは Drizzle ではなく、OpenResty build 前にインストールした libdrizzle を使います。
libdrizzle は Drizzle と MySQL プロトコルに対応したライブラリで、これを使って、Nginx から MySQL にアクセスします。
使用した MySQL のバージョンは、5.5.35 です(MySQL 5.6 では動作しませんでした)。
MySQL の準備
# setup MySQL $ mysql -u root mysql> CREATE DATABASE test_db DEFAULT CHARACTER SET utf8; mysql> CREATE TABLE test_db.users (id int(11) unsigned AUTO_INCREMENT, username varchar(512), password varchar(512), PRIMARY KEY (id)); mysql> INSERT INTO test_db.users (username, password) VALUES ('foo', '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'), ('bar', '62cdb7020ff920e5aa642c3d4066950dd1f01f4d'); mysql> GRANT ALL PRIVILEGES ON *.* TO test_user@127.0.0.1 IDENTIFIED BY 'test' WITH GRANT OPTION; mysql> FLUSH PRIVILEGES;
nginx.conf(抜粋)
# in http directive upstream backend { drizzle_server 127.0.0.1:3306 dbname=test_db password=test user=test_user protocol=mysql; } # in server directive location /login { content_by_lua_file /usr/local/openresty/lua/login.lua; } location /login_query { internal; set_quote_sql_str $quoted_username $arg_username; set_quote_sql_str $quoted_password $arg_password; set $sql "SELECT COUNT(id) as cnt FROM users WHERE username=$quoted_username AND password=$quoted_password;"; drizzle_query $sql; drizzle_pass backend; rds_json on; }
login.lua
drizzle-nginx-module を使って MySQLにアクセスして、ログインの判定を行っています。
- upstream backend
- MySQL の接続情報を書いておく
- drizzle_pass
- upstream を参照
- drizzle_query
- クエリ実行
- ngx.location.capture
- location を 擬似 http 通信でアクセスして結果を受け取る
- internal をつけて内部からしかアクセス出来ない状態にした /login_query を実行して結果を受け取っている
- rds_json on
- MySQL に投げたクエリの結果を JSON 形式で出力
- set_quote_sql_str
- 第二引数に渡した文字列をクォートして第一引数に代入
ngx.req.read_body() local post_args = ngx.req.get_post_args() local _sha1 = resty_sha1:new() _sha1:update(post_args["password"]) local password = _sha1:final() password = resty_str.to_hex(password) local username = post_args["username"] local res = ngx.location.capture("/login_query?username=" .. username .. "&password=" .. password) local result = string.match(res.body, '%[%{"cnt":(.+)%}%]') -- login fail if result == "0" then -- do something -- login success else -- do something end
画像アップロード・変換
画像に便利な、lua-resty-upload モジュールを使います。
lua-resty-upload は、file upload 時の header や body を 簡単に読み取るためのモジュールです。
例では、アップロードした画像を ImageMagick で加工しています。
Lua の ImageMagick バインディングには 公式サイト(ImageMagick: Application Program Interfaces) に載っている、magick を使用しました。
nginx.conf(抜粋)
# in server directive client_max_body_size 20M; location /upload { content_by_lua_file /usr/local/openresty/lua/file_upload.lua; } location ~ ^/img/(.+\.(gif|jpe?g|png)(\?.+)?$) { content_by_lua_file /usr/local/openresty/lua/resize.lua; } location ~ ^/(raw|s|m|l)/(.+\.(gif|jpe?g|png)(\?.+)?$) { alias /tmp/$1/$2; }
lua code
image_upload.lua
- form:read()
- HTTP Header や Body を読み込む
- ngx.shared.VALUE:{get,set}()
- Nginx で共有する変数を get, set
- nginx.conf に lua_shared_dict upload_cnt 100k; のように書いておく必要あり
- os.time() は、同じ時間(秒単位)でリクエストがくると同じ値を返すので、ngx.shared.VALUE をインクリメントして重複しないようにしている
- magick.thumb
- ImageMagick で画像をリサイズ
local chunk_size = 4096 local form = upload:new(chunk_size) local sha1 = resty_sha1:new() local file, file_name, file_path, cnt while true do local typ, res, err = form:read() if not typ then ngx.say("failed to read: ", err) return end if typ == "header" then local image_type = string.match(res[2], "image/(.+)") if image_type then cnt = ngx.shared.upload_cnt:get("value") and ngx.shared.upload_cnt:get("value") or 0 ngx.shared.upload_cnt:set("value", cnt + 1) math.randomseed(os.time()) sha1:update(tostring(os.time()) .. tostring(cnt)) local binary_digest = sha1:final() local hex = resty_str.to_hex(binary_digest) if image_type == "jpeg" then file_name = hex .. ".jpg" elseif image_type == "png" then file_name = hex .. ".png" elseif image_type == "gif" then file_name = hex .. ".gif" end file_path = raw_dir .. "/" .. file_name end if file_path then file = io.open(file_path, "w+") if not file then ngx.say("failed to open file : ", file_path) return end end elseif typ == "body" then if file then file:write(res) end elseif typ == "part_end" then file:close() sha1:reset() file = nil if cnt > 100000 then ngx.shared.upload_cnt:set("value", 0) end elseif typ == "eof" then magick.thumb(file_path, "25%x25%", s_dir .. "/" .. file_name) magick.thumb(file_path, "50%x50%", m_dir .. "/" .. file_name) ngx.say(cjson.encode({image = file_name})) break end end
resize.lua
- ngx.var[‘arg_GET_PARAM’] – GET パラメータを取得
- string.gsub(ngx.var.request_uri, “(./)(.)(%?.*)”, “%2”) – URI から ファイル名だけを抽出
- io.open(file_path, “r”) – ファイルが無ければ size= で指定したサイズにリサイズ
local size = ngx.var['arg_size'] local file_name, file_path, resize -- type あり if size then file_name = string.gsub(ngx.var.request_uri, "(.*/)(.*)(%?.*)", "%2") -- type から file_path を生成 if string.find(size, "[sml]") then file_path = img_dir .. "/" .. size .. "/" .. file_name else ngx.header.content_type = 'text/plain' ngx.say('invalid parameter "type=' .. size .. '"') ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end -- 画像がなければ生成する if io.open(file_path, "r") == nil then if size == "s" then resize = size_s elseif size == "m" then resize = size_m elseif size == "l" then resize = size_l end magick.thumb(raw_dir .. "/" .. file_name, resize, file_path) end ngx.exec("/" .. size .. "/" .. file_name) -- type なし else file_name = string.gsub(ngx.var.request_uri, "(.*/)(.*)", "%2") ngx.exec('/raw/' .. file_name) end
今回の例では、ファイルを一つ送る場合のみに対応しています。
複数ファイルを読み取るには、工夫が必要なようです。
まとめ
nginx から MySQL にアクセスする例と、nginx だけでアップロードした画像の保存、リサイズする例を交えながら、
使用したモジュールの紹介を行いました。
画像加工はハードウェアリソースの消費が激しく、画像を頻繁に加工するシステムではフロントで処理が遅くなってしまう可能性があるので、実際は Nginx ではなくバックエンドで行うのが望ましいと思いますが、通常 perl や ruby を動かす必要がある部分を Nginx で任せられるのは面白いと思いました。
他にも redis や memcached にアクセスするモジュールや、lua-nginx-module 自体にも多くの API が用意されていますので、
興味のある方はドキュメントを一読してみるところから初めてみてはいかがでしょうか?
明日は、ISUCON3 出題者のお一人 @acidlemon さんです。