Thursday, March 17, 2011

Speeding up Kahlua

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:


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


1 comment:

  1. 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