Minor Breakthrough in Thinking, Leads to Major Breakthrough in Performance
My Butterfly system has been in use since about 2008. Butterfly is somewhat akin to Erlang in Lisp, offering simplified command and coordination of multiple threads across distributed architectures, with no limits on separation of nodes.Up to now, Butterfly has used Lispwork threads, which since the early 2000’s have been native OS threads. Hence these are quite heavyweight, compared to Erlang’s green thread approach. As a result, where Erlang would spin off thousands of worker threads, we had to be more conservative in our thinking.
To make life easier for programmers, I developed the notion of a STANDARD-HANDLER, and a higher order version called PROTECTED-STANDARD-HANDLER. STANDARD-HANDLERS look a lot like Doug Hoyte’s DLAMBDA routines - a list of clauses that represent simple message headers and associated args, accompanied by routines that deal with each message kind. PROTECTED-STANDARD-HANDLERS envelope STANDARD-HANDLERS with around methods that understand the details of RPC packet handling so that STANDARD-HANDLERS can just blithely assume a “normal” working environment where whatever the routines return is their result. Not necessary to understand RPC packetizing in STANDARD-HANDLERS.
Last night it occurred to me... STANDARD-HANDLERS are essentially simple enter-at-the-top, dispatch on message header, execute some code, and return a value to the caller. The outer communication layers are unseen by STANDARD-HANDLERS — how messages are exchanged between threads / nodes / machines. Hence STANDARD-HANDLERS can just as easily operate at the direct function call level, over a Lispworks Mailbox, a synchronous Reppy-Channel, or a Butterfly asynchronous distribution. The STANDARD-HANDLER is just a computing element, nothing more.
Now when you examine an Erlang system with thousands of green threads in operation, many of those threads are nothing more than computing elements too. And many of them are very lightly loaded, in terms of communications per unit time. Hence, instead of launching a green thread to manage a minor computation, why not multiplex multiple handlers off a single communication node?
So I came up with an HMUX server - Handler Multiplexer - that is a simple layering of STANDARD-HANDLER. It is the main handler operated by some communication channel (function, mailbox, Reppy Channel, Butterfly connection), and it keeps a simple hash table to which any number of service handlers can be registered. It examines the prefix of every incoming message, taken as the name of the service to look up in the hash table, and then dispatches the remainder of the message to the intended service handler.
Hence, we can pack the equivalent of many many service threads into a single machine-level thread. Whereas previously you might form an RPC call to our TKV database handler as:
(!? :TKV ‘(read-key :DATAPOOL … ))
you can now reach the TKV manager as:
(!? :HMUX.TKV ‘(read-key :DATAPOOL … ))
The ideas works wonderfully well, and makes far better use of limited thread resources under Lisp.
Obviously, the decision of when and which MUX server to assign a particular service depends on its performance / communications load, and you wouldn’t want to just pile in all your handlers, willy nilly, without giving some serious consideration to the possible creation of communication bottlenecks. But many (most?) services that I encounter are quite lightly loaded - system logging, exit monitoring, NoSQL database queries, echo services, remote evaluation services, etc.
The difference between assigning a handler to a thread, or to a multiplexer, is either to call a SPAWN with the handler to create a new thread, or an RPC call directly to the multiplexer to register the handler under its service key.
You don’t really need tons of green threads. There are no continuations needing bookkeeping / tracking. Most situations have simple execution control structure.
- DM