これは mod_mruby ngx_mruby Advent Calendar 2014 の 15 日目(12/15) の投稿です。
mod_mruby を使った Web アプリ
きっかけは、ISUCON で mod_mruby を使ってみたいなーと思い、そのために高速できるポイントやボトルネックの探り方を見つけるための演習として考えたものです。
ただ、実際には ISUCON 予選には間に合わず、今回の Advent Calendar のために作成したものになります。
どういうアプリを作ったのか?
何らかのストレージに保存し、大量のアクセスを捌く Web アプリを想定して、今回は短縮 URL アプリを作成しました。長い URL から短い URL を生成し、長い URL を書かずに済むというものです。
また、これなら機能拡張をして検証するのが簡単かなと。例えば、ユーザーごとに短縮 URL を管理できるようにしたり、そのためにユーザーの認証を行ったり、アクセス制御をつけたり、はたまたリクエスト処理の効率を考慮した短縮URLハッシュの生成など、その後機能を拡張させて検証できると思います。
準備
今回使用した mrbgems は mruby-redis と mruby-json です。
mruby-redis には、今回必要だったハッシュ系の API がなかったので、少し手を入れました。
設計
入力フォームから URL を入力して、短縮 URL を生成する処理と短縮 URL にアクセスした時にオリジナルの URL にリダイレクトする処理を実装します。
Apache の設定
# オリジナルの URL から、短縮 URL を生成する
<Location /api/v1/shorten>
mrubyHandlerMiddle /home/vagrant/opt/apache/mruby/shorten.rb
</Location>
# 短縮 URL から、オリジナルの URL にリダイレクトさせる
<Location /go>
mrubyHandlerMiddle /home/vagrant/opt/apache/mruby/go_original_url.rb
</Location>
入力フォーム
ここで入力した URL を jQuery で /api/v1/shorten
にリクエストし、生成した URL を表示します。Web API とは JSON 形式でやりとりをします。
<!DOCTYPE html>
<html>
<head>
<title>Shorten URL</title>
</head>
<body>
<h1>Shorten URL</h1>
<input type="text" id="url" />
<button id="shorten-url">shrten</button>
=
<span id="result"></span>
</body>
<script type="text/javascript" src="https://code.jquery.com/jquery-1.11.1.js"></script>
<script type="text/javascript">
<!--
$(function () {
$("#shorten-url").on("click", function () {
var url = $("#url").val();
if (url === "") {
alert("Please enter the URL");
return;
}
$.ajax({
type: "POST",
contentType: 'application/json',
url: "/api/v1/shorten",
data: JSON.stringify({ url: url }),
}).done(function(res) {
$("#result").text(res.short_url);
});
});
})
//-->
</script>
</html>
短縮 URL を生成する
入力フォームから送られてきた JSON リクエストを解析し、短縮 URL を生成し、その URL を返します。
短縮 URL の生成は、シーケンシャルな番号を生成し、それをそのまま使うようにしています。
また、短縮 URL のリクエストを受け付けるサーバーの URL が固定で指定しています。
error_res = JSON::stringify({})
req = Apache::Request.new
req.content_type = "application/json"
unless req.method == "POST"
Apache.rputs(error_res)
Apache::return(Apache::OK)
end
post_data = JSON::parse(req.body)
url = post_data["url"]
# Redis config
host = "127.0.0.1"
port = 6379
database = 8
redis = Redis.new host, port
redis.select database
request_num = "request_number"
unless redis.exists?(request_num)
redis.set request_num, "0"
end
# generate a key
next_num = redis.incr(request_num)
url_key = "#{next_num}"
redis.hset "shorten_urls", url_key, url
Apache.rputs JSON::stringify({"url" => url, "short_url" => "http://localhost/go/#{url_key}"})
短縮 URL から、オリジナル URL にリダイレクトさせる
リクエストの URL からハッシュ値を取り出し、Redis からオリジナルの URL を取得し、リダイレクトします。
error_res = JSON::stringify({})
req = Apache::Request.new
unless req.path_info.index('/') == 0
req.content_type = "application/json"
Apache.rputs(error_res)
Apache::return(Apache::OK)
end
url_key = req.path_info[1..-1]
# Redis config
host = "127.0.0.1"
port = 6379
database = 8
redis = Redis.new host, port
redis.select database
unless redis.hexists?("shorten_urls", url_key)
req.content_type = "application/json"
Apache.rputs(error_res)
Apache::return(Apache::OK)
end
original_url = redis.hget "shorten_urls", url_key
Apache.rputs(original_url)
req.headers_out["Location"] = original_url
Apache::return(Apache::HTTP_MOVED_PERMANENTLY)
まとめ
予定では負荷を与えてどれくらいリクエストを処理できるかを調べてみたり、どういう部分に注意したらいいのか、MySQL との比較を載せる、予定でした。あと、ngx_mruby との比較も。それらの今回できなかったことは、今後やっていくこととします。(すみません・・・実装も幾つか手抜きになってしまっていて)
今回 mruby で Web アプリを作ってみましたが、Web API 側を mruby で実装するのは、簡単にできて良さそうだと思いました。mod_mruby と ngx_mruby で共通の作りにしておけば、アプリの特性や環境によって使い分けることができたりすると思います。
mod_mruby で Web アプリを作ることはあまりないかもしれませんが、HTML 生成などがないような Web API なら使いどころはあるのかなと。そして、mrbgems が充実することでさらに可能性が広がっていくような気がします。
template engine があったら、DB から取得した値を表示するのに楽になったりするのかなと思ったり。
明日は、hkusuさんの「mod_mrubyで静的ファイルをリバースプロキシする」です!