Ian Bicking: the old part of his blog

The Concurrency Model Debate

There's some discussion in the Rails world going on about FastCGI and scaling: pro FastCGI and anti FastCGI. Python's position here is kind of vague, because it supports both models reasonably well (though the Horrible Global Interpreter Lock makes the single process model slightly less scalable than it might otherwise be).

"FastCGI" is a misnomer here, and both sides seem rather misinformed. This is a discussion about the differences between threaded concurrency, with a pool of worker threads, and multi-process concurrency, with a pool of worker processes. FastCGI can do either, though it has extra (confusing) features for worker processes. AFAIK the only reason that most Java systems don't use FastCGI is because it's a pain in the ass to set it up, as the protocol is overdesigned and confuses the simplest aspect (sending a request to another process) with all sort of other features (sharing an error log, intercepting the request without responding to it, starting, restarting, and pooling worker processes, etc). Because of FastCGI's flaws, people constantly create and recreate the basic functionality -- SCGI, PCGI, mod_webkit, mod_skunk, AJP, and no doubt a whole slew of other similar things I don't know about.

OK, so ignore the FastCGI stuff. The issue is about concurrency. Apparently some Java-heads are having a hard time believing that a worker process model can be scalable. This is frankly bizarre, and I fear a sign of myopia in that community. Or just a sign of a few crackpots who are good typists -- probably best not to condemn the community for one guy.

The worker model is nothing new -- it's what Apache 1.3 does exclusively, and one of the concurrency options for Apache 2. The concurrency model for mod_ruby is the same as their FastCGI option, and mod_python if you happen to be using the worker model under Apache 2. In that model, you have multiple processes, and there's a queue of incoming requests -- a free process picks a request off the queue, processes only that request, and when finished grabs another request. In a threaded model almost the exact same thing happens, except there's worker threads. Not Very Different.

This multiprocess model has some advantages and disadvantages, none of which are nearly as extreme as anyone seems to think -- you still have to handle concurrency and contention for shared resources. Some in-process resources that are not threadsafe, like database connections, are automatically isolated. But other locking can be slightly harder, and generally shared resources are more difficult. One advantage is that you are encouraged from the beginning to share data in a way that scales across multiple machines. It's not really a choice for Ruby systems, because Ruby doesn't support OS-level threads, which I believe means that blocking I/O will block all threads in the process (among other issues).

In Python people go both ways. Threading seems to be more common, probably because it's more obvious and is easy to scale until you hit the one-process/one-machine barrier. Zope, Webware, and CherryPy are all threaded. SkunkWeb, CGI, and mod_python all use multiple processes (well, mod_python can use a single process with multiple interpreters, but I believe requests always are processed in separate interpreters). Quixote and now WSGIKit are fairly neutral on the matter. I'm not sure where other frameworks lie. And of course there is asynchronous (event driven) programming, which is non-threaded single-process. This is popular on the network level -- Twisted, Medusa, asyncore -- but relatively uncommon in higher level systems, with Nevow being the exception.

Created 13 Apr '05

Comments:

In respect of your comment:

mod_python can use a single process with multiple interpreters, but I believe requests always are processed in separate interpreters

Creation of separate interpreters in mod_python is to do with management of your URL namespace and is nothing really to do with threading. That is, if you want web applications hosted in different parts of your URL namespace to not intefere with each other, you designate those different parts of the URL namespace as running in distinct interpreters.

For a particular instance of an interpreter within a process, whether it be managing the whole URL namespace or just a part, when using a multithreaded MPM in Apache such as the default for Win32 or the worker MPM for UNIX, there can be multiple requests within distinct threads active within the same interpreter at the same time.

The only catch with all of this is that there are a couple of thread bugs in mod_python which you would want to patch if wanting to seriously use threading in any way. These are currently documented at "http://www.dscpl.com.au/projects/vampire/patches.html".

# Graham Dumpleton

Thanks. I guess this means that (probably like mod_ruby, mod_php, and a bunch of others) that it's safest to keep to keep to the multiprocess model.
# Ian Bicking

It is always going to be safer to avoid multithreading, but that is more to do with people not appreciating what it takes to write a robust multithreaded application and nothing to do with any fixable bugs.:-)
# Graham Dumpleton

Actually the 'worker' MPM in Apache 2 uses is mult-process and multi-thhreaded... Its a combination of the traditional 'pre-fork' model and a purely threaded system. It has some advantages, like a crash in one process will not destroy the entire server, and that for the most part you get the advantages of a purely-threaded server.
# Paul Querna

For me Zope, isn't the best thread example. In fact it use only a very small amout of threads, and doesn't really use it in the way everybody think. It only avoid the medusa to lock the whole suff.
# jkx

Well , it looks to me that fastcgi is rather ubiquitous , solid fastcgi support in Python is maybe a possible way forward to seriously take python web development out of the current ghetto. Out of Apache , the ruby train appears to move forward with just that (see lighttpd...).
# Marc

Personally it's the Apache side of FastCGI that has always been hard for me to set up, not the Python side. Other people are also using HTTP proxying, which is better supported still, but unfortunately doesn't have quite the same information as FastCGI passes through (SCRIPT_NAME and PATH_INFO particularly, but other information like REMOTE_USER is also lost -- resolveable through configuration, but then it's no longer as easy to configure).
# Ian Bicking

Apache 2.1 will finally add the ability to load-balance mod_proxy requests, and I trust HTTP more than a semi-orphaned protocol like FastCGI or its clones.

Until recently we were running mod_proxy on a farm of Apache 1.3 servers to funnel about 300,000 HTTP requests per day into a single multithreaded Python BaseHTTPServer process (using the PSP engine from WebWare, with the rest of WebWare ripped out), on a lowly P3/500, no less. GIL contention is probably less of an issue than many people believe in real-world applications, although it would help if there were a tool to measure it and/or something like an exponentially decaying average of the numbers of threads waiting to acquire the GIL.

# Fazal Majid

And, just in case you didn't know, there is now a way to write WSGI capable applications that use FastCGI and any capable web server (apache and lighttpd). I wrote a quick howto for setting up lighttpd to use it on my weblog:

http://www.cleverdevil.org/computing/24/python-fastcgi-wsgi-and-lighttpd

# Jonathan LaCour

Back-in-the-day, I worked at fastengines, the openmarket spinoff that productized the original fastcgi. One of the things we were very clear on is that the advantages were not actually about the concurrency itself, but came in a few areas:

All of this made it quite popular among people who were not advanced developers, who didn't have time or resources to invest in frameworks... but had just gotten an application off the ground using basic CGI :-) So, nothing you'd mistake for "high design"...

# Mark Eichin

Just a thing, AFAICT, ruby threads does not block the whole interpreter on I/O operations, this operations are automatically multiplexed, so if a thread is locked reading on a socket or whatever, another one can still does what he wants, just try this:

Thread.new {puts gets} while true

System Message: ERROR/3 (<string>, line 5)

Unexpected indentation.
puts "working flawlessly with locked IO" sleep 4

System Message: WARNING/2 (<string>, line 7)

Block quote ends without a blank line; unexpected unindent.

end

(there are pther issues with non native threads, but not this one)

# anonymous