lua + FastCGI
I was looking for a FastCGI backend for lua to have a asynchronous brother for mod-magnet in lighttpd.
Same as mod-magnet this embedding of lua is not meant to replace Frameworks like Rails or Spring, nor do I want to write average PHP application in it. I use this magnet to write small scripts (for this project it was 140 lines of lua) which is going to be executed at least 500 times a second.
That is a range where the setup-cost for a request matters. I don't want to load the session, nor do I want cleanup the whole environment for each request. I need a way to store connections to the database over multiple requests and I have to cache content from the database in the application.
I needed: - a byte-code cache - a global environment to store the db-connection and a content-cache - SQL, FileSystem, ... functions
Based in my work on mod-magnet I wrote:
- a FastCGI wrapper for lua-scripts
To compile the magnet you need fastcgi and lua5.1 installed:
$ wget <a href="{filename}/downloads/lua/magnet.c">http://jan.kneschke.de/downloads/lua/magnet.c</a>
$ gcc -Wall -O2 -g -o magnet magnet.c -lfcgi -llua -lm -ldl
A simple config for lighttpd starts the script-environment:
fastcgi.server = ( ".lua" =>
(( "socket" => "/tmp/lua.socket",
"bin-path" => "/usr/local/bin/magnet",
"max-procs" => 8 ))
)
All script with the extension .lua will now be loaded and cache into the magnet. If the script changed between the requests it will be reloaded automaticly.
Request Setup Costs
A short benchmark on my rusty development machine:
- AMD Duron(tm) Processor, 1.3GHz
- 640Mb
- ...
should be enough to show the difference in the setup costs per request.
- both times it is FastCGI,
- both times a byte-code cache is used,
- both times the same byte-sequence is sent
On lua side we use the magnet from above:
$ cat 123.lua
header = {
["Content-Type"] = "text/html",
["Status"] = "200"
}
function cgi_send_response(cnt)
local o = ""
header["Content-Length"] = string.len(cnt)
for k, v in pairs(header) do
o = o .. k .. ": " .. v .. "\r\n"
end
o = o .. "\r\n"
print(o .. cnt)
end
cgi_send_response("123")
$ ab -n 10000 -c 8 -k http://127.0.0.1:1445/123.lua
...
Time taken for tests: 14.400517 seconds
...
Requests per second: 694.42 [#/sec] (mean)
Time per request: 11.520 [ms] (mean)
Time per request: 1.440 [ms] (mean, across all concurrent requests)
Transfer rate: 101.32 [Kbytes/sec] received
On PHP side it is:
The PHP was compiled with only the necessary extensions, to minimize the setup costs for each request in PHP itself.
$ cat 123.php
<?php
print "123";
?>
$ ab -n 10000 -c 8 -k http://127.0.0.1:1025/123.php
...
Time taken for tests: 29.783971 seconds
...
Requests per second: 335.75 [#/sec] (mean)
Time per request: 23.827 [ms] (mean)
Time per request: 2.978 [ms] (mean, across all concurrent requests)
Transfer rate: 57.21 [Kbytes/sec] received
Ruby plays in the same league as lua:
$ cat 123.rb
require 'fcgi';
FCGI.each { |request|
request.out.print "Content-Type: text/html\r\n\r\nHello, World!\n";
request.finish
}
$ ab -n 10000 -c 8 -k http://127.0.0.1:1446/123.rb
...
Time taken for tests: 15.751145 seconds
...
Requests per second: 634.87 [#/sec] (mean)
Time per request: 12.601 [ms] (mean)
Time per request: 1.575 [ms] (mean, across all concurrent requests)
Transfer rate: 101.52 [Kbytes/sec] received
Conclusion
Use Ruby, Python, ... (or this lua-magnet) if you have to maintain a very low response
times (below 1ms). As soon as you need the comfort of PHP ($_GET[]
, $_FILES[]
, ...) you
have to live with the extra overhead.
Byte-Code cache
As the cache itself is part of the lua environment you can take a look at its content with a lua-script:
header = {
["Content-Type"] = "text/html",
["Status"] = "200"
}
function cgi_send_response(cnt)
local o = ""
header["Content-Length"] = string.len(cnt)
for k, v in pairs(header) do
o = o .. k .. ": " .. v .. "\r\n"
end
o = o .. "\r\n"
print(o .. cnt)
end
o = [[<table border="1">
<tr><th width="50%">script-name</th>
<th width="20%">mtime</th><th width="20%">hits</th></tr>
]]
for k, v in pairs(magnet.cache) do
o = o .. string.format([[
<tr><td>%s</td>
<td align="right">%s</td>
<td align="right">%d</td></tr>]],
k, os.date("%c", v.mtime), v.hits)
end
o = o .. "</table>"
cgi_send_response(o)
Which generates this output:
script-name | mtime | hits |
---|---|---|
.../pages/cache-admin.lua | Thu Oct 5 20:28:21 2006 | 4 |
a global environment
I need a global storage which doesn't get cleaned up between script-runs. Instead of re-opening the connection to MySQL between script-runs I wanted to cache it. As querying the database also costs cpu-cycles I wanted to cache the results for one second if the same input data is requested again.
Coming from a PHP world you would solve the first issue with mysql_pconnect()
and the second
with a shm-segment or a session.
-- create our own area
prefix = "mysqlcache."
mysqlenv_ndx = prefix .. "mysqlenv"
mysqlcon_ndx = prefix .. "mysqlcon"
content_ndx = prefix .. "content"
-- create a db-connection if we don't have one up and running
if (not _G[mysqlenv_ndx] or not _G[mysqlcon_ndx]) then
env = assert(luasql.mysql())
con = assert(env.connect())
_G[mysqlenv_ndx] = env
_G[mysqlcon_ndx] = con
else
env = _G[mysqlenv_ndx]
con = _G[mysqlcon_ndx]
end
-- get the content-cache
content = {}
if (not _G[content_ndx]) then
_G[content_ndx] = content
else
content = _G[content_ndx]
end
-- real code ...
If you want, you can think about the _G[]
as $_SESSION[]
in PHP. Just that it is shared
between all scripts, all users and can store living objects like DB-connections. We are using
the prefix here to split the global-lua-env between different scripts.
Comments