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-namemtimehits
.../pages/cache-admin.luaThu Oct 5 20:28:21 20064

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

Enable javascript to load comments.