Earlier post Scriptable Object Cache
The first implementation of the lua based http proxy was fairly slow. On a 1.6Gz quad core intel processor, it could proxy about 1K requests per second. The final number was about 15K messages per second with 5KB request and 5KB response.
The first problem was LuaTableImpl. It is very slow. The problem is lua table is both a hashmap and a list with single iterator. One of the methods in the lua table interface is:
The first implementation of the lua based http proxy was fairly slow. On a 1.6Gz quad core intel processor, it could proxy about 1K requests per second. The final number was about 15K messages per second with 5KB request and 5KB response.
The first problem was LuaTableImpl. It is very slow. The problem is lua table is both a hashmap and a list with single iterator. One of the methods in the lua table interface is:
Object next(Object key);
Hack number one is to use default java Hashmap instead of the built in LuaTableImpl. Need to take care of making sure that "array" access works and "next" works.
Problem number two is the overhead of creating the runtime. In my case every request needs a new runtime or LuaState object which is associated with each request. This was very expensive. Here is what needs to be done to create luaState and set it up for the proxy code to execute.
String fileName = "/luaTestCode/test.lua";
LuaState state = new LuaState(System.out);
File luaFile = new File(fileName);
LuaClosure closure = LuaCompiler.loadis(new FileInputStream(luaFile),
luaFile.getName(), state.getEnvironment());
state.call(closure, null);
LuaConverterManager manager = new LuaConverterManager();
LuaNumberConverter.install(manager);
LuaTableConverter.install(manager);
LuaJavaClassExposer exposer = new LuaJavaClassExposer(state, manager);
exposer.exposeClass(LuaContext.class);
exposer.exposeClass(LuaHttpRequest.class);
exposer.exposeClass(LuaHttpResponse.class);
Object proxy = state.getEnvironment().rawget("proxy_function");
Object value = state.call(proxy, null);
As you can see, we have file read, followed by compiler, followed by some Reflection code which will setup java functions in the lua runtime. All of this is expensive and needs to be done for every message. To avoid this, I used couple of hacks. First one was to avoid the compilation again and again. This one was simple. The output of compilation step is LuaClosure. I changed the code so that instead of LuaClosure I would get LuaPrototype which is the real output of compilation. This could be easily reused to create as many LuaClosure objects without paying the price of file access and compilation. The second hack was to avoid setup cost of LuaState object. Digging into code revealed that LuaState (without the stack) is nothing but the environment or a luaTable. So I just added clone method to the LuaTable implementation and used it to cheaply construct new LuaStates which had all the java classes correctly exposed. The new code looked something like this.
public class LuaContext {
private LuaState state;
private LuaClosure closure;
private Object coroutine;
private static final LuaState BASE;
private static final LuaConverterManager MANAGER;
private static final LuaJavaClassExposer EXPOSER;
static {
BASE = new LuaState(System.out);
MANAGER = new LuaConverterManager();
LuaNumberConverter.install(MANAGER);
LuaTableConverter.install(MANAGER);
EXPOSER = new LuaJavaClassExposer(BASE, MANAGER);
EXPOSER.exposeClass(LuaContext.class);
EXPOSER.exposeClass(LuaHttpRequest.class);
EXPOSER.exposeClass(LuaHttpResponse.class);
}
public LuaContext(LuaPrototype proto)
throws IOException {
this.state = BASE.clone();
this.closure = new LuaClosure(proto, state.getEnvironment());
this.state.call(this.closure, null);
this.coroutine = this.state.getEnvironment().rawget("proxy_function");
}
That is it. After adding support for storing state (lua objects), lock and some basic json, I was able to write some cool proxy features in lua itself.
Here is how I could do redirect handling at server side...
function getServerAndPath(response)
local newURL = response:getHeader("Location")
local server = nil
local path = nil
if (newURL ~= nil) then
local h1, h2 = string.find(newURL, "http://")
local s1, s2 = string.find(newURL, "/", h2+1)
server = string.sub(newURL, h2+1, s1-1)
path = string.sub(newURL, s1)
end
return server, path
end
function isRedirectResponseCode(code)
if (code == 301) then return true end
if (code == 302) then return true end
if (code == 303) then return true end
if (code == 307) then return true end
return false
end
function proxy(context, httpRequest)
local targetServer = "yahoo.com"
local path = httpRequest:getUri()
local responseCode = 301
local httpResponse = nil
while (isRedirectResponseCode(responseCode)) do
httpRequest:setHeader("Host", targetServer)
httpRequest:setUri(path)
httpResponse = luaSendRequestAndGetResponse(context, httpRequest)
responseCode = httpResponse:getStatus()
print ("got response with status " .. responseCode)
targetServer, path = getServerAndPath(httpResponse)
end
luaSendResponse(context, httpResponse)
end
Here is another example...for doing least connection based server load balancing.
-- simple lb implementation
function newLB()
local object = {}
object.lock = NewLuaLock()
object.connections = {}
return object
end
function addServer(object, serverName)
object.lock:lock()
if (object.connections[serverName] == nil) then
object.connections[serverName] = 0
end
object.lock:unlock()
end
function findLeastUsedServer(object)
local min = 64 * 1024
local result = nil
object.lock:lock()
for k,v in pairs(object.connections) do
if (v < min) then
min = v
result = k
end
end
object.connections[result] = min + 1
object.lock:unlock()
return result
end
function decreaseServerCount(object, server)
object.lock:lock()
if (object.connections[server] ~= nil) then
if (object.connections[server] > 0) then
object.connections[server] = object.connections[server] -1
end
end
object.lock:unlock()
end
--------------------------------------------------------------
function getLBResource()
local objectMap = getObjectMap()
local lbResource = objectMap:get("lb")
if (lbResource == nil) then
lbResource = newLB()
addServer(lbResource, "google.com")
addServer(lbResource, "yahoo.com")
objectMap:put("lb", lbResource)
end
return lbResource
end
function proxy(context, httpRequest)
local lbResource = getLBResource()
local server = findLeastUsedServer(lbResource)
httpRequest:setHeader("Host", server)
local response = luaSendRequestAndGetResponse(context, httpRequest)
decreaseServerCount(lbResource, server)
luaSendResponse(context, response)
end
The final example show how to filter the twitter timeline of all the junk information and cache it for a while at the proxy.
function proxy(context, httpRequest)
local globalCache = getHttpResponseCache()
local twitterResponse = globalCache:get("timeline")
if (twitterResponse == nil) then
local twitterRequest = NewLuaHttpRequest("HTTP/1.1", "GET", "/1/statuses/public_timeline.json")
twitterRequest:setHeader("Host", "api.twitter.com")
twitterResponse = luaSendRequestAndGetResponse(context, twitterRequest)
local jsonArray = NewLuaJSONArrayFromText(twitterResponse:getContent())
local newJsonArray = NewLuaJSONArray()
for i=0,(jsonArray:length()-1) do
local current = jsonArray:getJSONObjectAtIndex(i)
local text = current:getString("text")
newJsonArray:putString(text)
end
twitterResponse:setContent(newJsonArray:toString())
-- cache for five minutes
globalCache:put("timeline", 300, twitterResponse)
end
luaSendResponse(context, twitterResponse)
end
Pretty cool. Recently, I saw a post on hackernews asking a question on how to store a set and manipulate in memcache. Obviously, that is not possible in memcache, but LuaProxy approach makes it pretty trivial.
ReplyDelete