FastCGI experiments
It's not particularly important - i'm lucky to get more than one-non-bot hit in a given day - but I thought i'd have a look into FastCGI. If in the future I do use a database backend or even a Java one it should be an easy way to get some performance while leveraging the simplicity of CGI and leaving the protocol stuff to apache.
After a bit of background reading and looking into some 'simple' implementations I decided to just roll my own. The 'official' fastcgi.com site is no longer live so I didn't think it worth playing with the official sdk. The way it handled stdio just seemed a little odd as well.
With the use of a few GNU libc extensions for stdio (cookie streams) and memory (obstacks) I put together enough of a partial (but robust) implementation to serve output-only pages from the fcgid module in a few hundred lines of code.
This is the public api for it.
struct fcgi_param { char *name; char *value; }; struct fcgi { // Active during cgi request FILE *stdout; FILE *stderr; // Current request info unsigned char rid1, rid0; unsigned char flags; unsigned char role; // Current request params (environment) size_t param_length; size_t param_size; struct fcgi_param *param; struct obstack param_stack; // Internal buffer stuff int fd; size_t pos; size_t limit; size_t buffer_size; unsigned char *buffer; }; typedef int (*fcgi_callback_t)(struct fcgi *, void *); struct fcgi *fcgi_alloc(void); void fcgi_free(struct fcgi *cgi); int fcgi_accept_all(struct fcgi *cgi, fcgi_callback_t cb, void *data); char *fcgi_getenv(struct fcgi *cgi, const char *name);
I didn't bother to implement concurrent requests, the various access control roles, or STDIN messages. The first doesn't appear to be used by mod_fcgi (it handles concurrency itself) and I don't need the rest (yet at least). As previously stated I used GNU libc extensions to implement custom stdio streams for stdout and stderr, although I used a custom 'zero-copy' buffer implementation for the protocol handling (wherein the calls can access the internal buffer address rather than having to copy data around).
Converting a CGI program is a little more involved than using the original SDK because it doesn't hide the i/o behind macros or use global variables to pass information. Instead via a context-specific handle it provides stdio compatible FILE handles and a separate environmental variable lookup function. Of course it is possible to write a handler callback which can implement such a solution.
The main function of a the fast cgi program just allocates the context, calls accept_all and then free. The callback is invoked for each request and can access stdout/stderr from the context using stdio calls as it wishes.
Apache config
Here's the basic apache config snipped I used to hook it into `/blog' on a server (I did this locally rather than live on this site though).
ScriptAlias /blog /path/fcgi-test.fcgi FcgidCmdOptions /path/fcgi-test MaxProcesses 1 <Directory "/path"> AllowOverride None Options +ExecCGI Require all granted </Directory>
Custom streams and cookies
Using a GNU extension it is trivial to hook up custom stdio streams - one gets all the benefits of libc's buffering and formatting and one only has to write a couple of simple callbacks.
#define _GNU_SOURCE #include <sys/types.h> #include <sys/uio.h> #include <stdio.h> #include <unistd.h> static ssize_t fcgi_write(void *f, const char *buf, size_t size, int type) { struct fcgi *cgi = f; size_t sent = 0; FCGI_Header header = { .version = FCGI_VERSION_1, .type = type, .requestIdB1 = cgi->rid1, .requestIdB0 = cgi->rid0 }; while (sent < size) { size_t left = size - sent; ssize_t res; struct iovec iov[2]; if (left > 65535) left = 65535; header.contentLengthB1 = left >> 8; header.contentLengthB0 = left & 0xff; iov[0].iov_base = &header; iov[0].iov_len = sizeof(header); iov[1].iov_base = (void *)(buf + sent); iov[1].iov_len = left; res = writev(cgi->fd, iov, 2); if (res < 0) return -1; sent += left; } return size; } static int fcgi_close(void *f, int type) { struct fcgi *cgi = f; FCGI_Header header = { .version = FCGI_VERSION_1, .type = type, .requestIdB1 = cgi->rid1, .requestIdB0 = cgi->rid0 }; if (write(cgi->fd, &header, sizeof(header)) < 0) return -1; return 0; }
Well perhaps the callbacks are more `straightforward' than simple
in this case. FastCGI has a payload limit of 64K so any larger
writes need to be broken up into parts. I use writev
to write the header and content directly from the library buffer
in a single system call (a pretty insignificant performance
improvment in this case but one nonetheless). I might need to
handle partial writes but this works so far - in which case
the writev
approach gets too complicated to bother
with.
The actual 'cookie' callbacks just invoke the functions above with the FCGI channel to write to.
static ssize_t fcgi_stdout_write(void *f, const char *buf, size_t size) { return fcgi_write(f, buf, size, FCGI_STDOUT); } static int fcgi_stdout_close(void *f) { return fcgi_close(f, FCGI_STDOUT); } const static cookie_io_functions_t fcgi_stdout = { .read = NULL, .write = fcgi_stdout_write, .seek = NULL, .close = fcgi_stdout_close };
And opening a custom stream is as as simple as opening a regular file.
static int fcgi_begin(struct fcgi *cgi) { cgi->stdout = fopencookie(cgi, "w", fcgi_stdout); ...; return 0; }
Example
Here's a basic example that just dumps all the parameters to the client. It also maintains a count to demonstrate that it's persistent.
I went with a callback mechanism rather than the polling mechanism of the original SDK mostly to simplify managing state. Shrug.
#include "fcgi.h" static int cgi_func(struct fcgi *cgi, void *data) { static int count; fprintf(cgi->stdout, "Content-Type: text/plain\n\n"); fprintf(cgi->stdout, "Request %d\n", count++); fprintf(cgi->stdout, "Parameters\n"); for (int i=0;i<cgi->param_length;i++) fprintf(cgi->stdout, " %s=%s\n", cgi->param[i].name, cgi->param[i].value); return 0; } int main(int argc, char **argv) { struct fcgi * cgi = fcgi_alloc(); fcgi_accept_all(cgi, cgi_func, NULL); fcgi_free(cgi); }
Notes
I haven't worked out how to get the CGI script to 'exit' when the MaxRequestsPerProcess limit has been reached without causing service pauses. Whether I do nothing or whether I exit and close the socket at the right time it still pauses the next request for 1-4 seconds.
I haven't converted my blog driver to use it yet - maybe later on tonight if I keep poking at it.
Oh and it is quite fast, even with a trivial C program.