Running catalyst applications under perlbal + starman + psgi

WTF are those?

  • Perlbal is a fantastic reverse proxy load balancer for HTTP, written in perl/XS. It handles balancing/HA and can function as a simple webserver perfect for hosting static content.
    It also supports some extra goodies such as buffering large user uploads before sending them to web processes, and the ability to write custom plugins to bypass catalyst altogether.

  • Starman is an excellent daemon for running plack applications. It is sorta like fcgi-manager, and speaks PSGI to your catalyst application, sorta like FastCGI. HTTP requests come in to perlbal, and if they are not requests for static content, they get reverse proxied to starman. Dispatches the request to your catalyst application using PSGI.

  • PSGI is simply a very basic standard for describing a web request. It is the protocol used for dispatching an HTTP request to a perl application of some sort.

Why do all that? Why not apache and FastCGI like everyone else?

  • Personally, I like this approach not only because it is fairly cutting-edge and new and shiny, but also because it is very fast. If you need to get a very sturdy and robust web production environment with maximum performance, this setup has proven to be the most powerful in my experience.
    In particular, perlbal allows for load balancing and high availability of your site, starman is just a really nice daemon that is perfectly suited to a typical catalyst application's needs, and PSGI is a powerful tool that bridges the gap between your HTTP request and your perl code while supporting all sorts of perl web application engines, even AnyEvent-based ones (twiggy).
    When pieced together properly, all of these components will provide you with a system that can be tweaked and extended to match any production perl web hosting needs or optimizations you may require.
    This setup also has the capacity to scale to a large degree, simply by adding more starman nodes to your reverse proxy pool.

  • My favorite trick that perlbal can do is static resource concatenation. It enables your application to compress a crapton of requests into one:

    <script src="/static/js/??jquery-1.4.2.min.js,jquery.dropshadow.js,jquery.easing.1.3.js,jquery.easing.compatibility.js,jquery.lavalamp.min.js,top_nav.js,jquery-ui-1.8.4/jquery.ui.core.min.js,jquery-ui-1.8.4/jquery.ui.widget.min.js,jquery-ui-1.8.4/jquery.ui.position.min.js,jquery-ui-1.8.4/jquery.ui.dialog.min.js,jquery.cycle.js,jquery.coda-slider-2.0.js,jquery.watermark.js,home.js" type="text/javascript" charset="utf-8"></script>

Setup

Perlbal

  • "documentation" on perlbal can be located as part of the cpan dist here with examples.
LOAD Vpaths

# starman webservers
CREATE POOL prod_starman
 POOL prod_starman ADD 127.0.0.1:5000

# static webserver
CREATE SERVICE web_static
  SET role                   = web_server
  SET docroot                = /home/myapp/prod/root
  SET dirindexing            = 0
  SET enable_concatenate_get = on
ENABLE web_static

# HTTP reverse proxy load balancer
CREATE SERVICE web_prod
 SET role                 = reverse_proxy
 SET pool                 = prod_starman
 SET buffer_uploads       = on
ENABLE web_prod

# HTTP selector
CREATE SERVICE myapp_selector
 SET listen              = 0.0.0.0:80
 SET role                = selector
 SET plugins             = vpaths
 VPATH ^/static/.*       = web_static
 VPATH .*                = web_prod
ENABLE myapp_selector
# disallow injection
HEADER myapp_selector REMOVE X-Forwarded-Proto

# HTTPS selector
CREATE SERVICE myapp_ssl_selector
 SET listen              = 0.0.0.0:443
 SET role                = selector
 SET plugins             = vpaths
 SET enable_ssl          = on
 SET ssl_key_file        = /etc/certs/myapp.com.key.plain
 SET ssl_cert_file       = /etc/certs/myapp.com.combined.pem
 VPATH ^/static/.*       = web_static
 VPATH .*                = web_prod
ENABLE myapp_ssl_selector
HEADER myapp_ssl_selector INSERT X-Forwarded-Proto: HTTPS

# management port, telnet in to chat with perlbal
CREATE SERVICE mgmt
 SET role   = management
 SET listen = 127.0.0.1:60000
ENABLE mgmt

Starman

  • Docs

  • This init-style bash script will start/stop/restart starman processes for you. It will also refuse to restart your application if it does not compile (big life-saver).

site-init.sh

#!/bin/bash

. /lib/lsb/init-functions

if [ ! $APP ]; then
    echo "\$APP is not defined, please do not call this script directly."
    exit 1
fi

export APPDIR="/home/myapp/$APP"
export PIDDIR=/tmp
export PIDFILE=$PIDDIR/${APP}.pid
export STARMAN="/home/myapp/prod/bin/starman-myapp.sh"

if [ ! -d $APPDIR ]; then
    echo "$APPDIR does not exist"
    exit 1
fi

check_running() {
    [ -s $PIDFILE ] && kill -0 $(cat $PIDFILE) >/dev/null 2>&1
}

check_compile() {
  if ( cd $APPDIR ; perl -Ilib -M$APPLIB -ce1 ) ; then
    return 1
  else
    return 0
  fi
}

_start() {

  /sbin/start-stop-daemon --start --pidfile $PIDFILE \
  --chdir $APPDIR --startas $STARMAN

  echo ""
  echo "Waiting for $APPNAME to start..."

  for i in 1 2 3 4 ; do
    sleep 1
    if check_running ; then
      echo "$APPNAME is now starting up"
      return 0
    fi
  done

  # sometimes it takes two tries.
  echo "Failed. Trying again..."
  /sbin/start-stop-daemon --start --pidfile $PIDFILE \
  --chdir $APPDIR --startas $STARMAN

  for i in 1 2 3 4 ; do
    sleep 1
    if check_running ; then
      echo "$APPNAME is now starting up"
      return 0
    fi
  done

  return 1
}

start() {
    log_daemon_msg "Starting $APP" $STARMAN
    echo ""

    if check_running; then
        log_progress_msg "already running"
        log_end_msg 0
        exit 0
    fi

    rm -f $PIDFILE 2>/dev/null

    _start
    log_end_msg $?
    return $?
}

stop() {
    log_daemon_msg "Stopping $APP" $STARMAN
    echo ""

    /sbin/start-stop-daemon --stop --oknodo --pidfile $PIDFILE
    sleep 3
    log_end_msg $?
    return $?
}

restart() {
    log_daemon_msg "Restarting $APP" $STARMAN
    echo ""

    if check_compile ; then
        log_failure_msg "Error detected; not restarting."
        log_end_msg 1
        exit 1
    fi

    /sbin/start-stop-daemon --stop --oknodo --pidfile $PIDFILE
    _start
    log_end_msg $?
    return $?
}


# See how we were called.
case "$1" in
    start)
        start
    ;;
    stop)
        stop
    ;;
    restart|force-reload)
        restart
    ;;
    *)
        echo $"Usage: $0 {start|stop|restart}"
        exit 1
esac
exit $?

bin/starman-myapp.sh:

#!/bin/bash

if [ ! $WORKERS ]; then
    echo "\$WORKERS is not defined"
    exit 1
fi

if [ ! $PORT ]; then
    echo "\$PORT is not defined"
    exit 1
fi

PSGIAPP="$APPDIR/script/myapp_psgi.psgi"
echo "Starting $PSGIAPP, pidfile $PIDFILE..."
starman -I$APPDIR/lib $PSGIAPP --workers $WORKERS --pid $PIDFILE --port $PORT --daemonize
  • To start your site, you simply set a few environment variables and run the init script. Like so:

    bin/starman-prod.sh


#!/bin/sh

# settings
export APP="prod"
export APPLIB="MyApp"
export WORKERS=5
export PORT=5000

# this runs site-init.sh, assuming it's in the same directory
. "$( cd "$( dirname "$0" )" && pwd )/site-init.sh"

PSGI

  • To connect catalyst to starman, you use PSGI glue. It looks something like this:

    script/myapp_psgi.psgi:

#!/usr/bin/env perl
use strict;
use warnings;

use FindBin;
use lib "$FindBin::Bin/../lib";

use MyApp;
use MyApp::Util;

use Catalyst::Engine::PSGI;
use FCGI::ProcManager;
use Plack::Builder;
use Plack::Middleware::AccessLog;
use Plack::Middleware::Debug;

# I load configuration info from my app. you are probably not cool enough to do this
my $config = MyApp::Util->get_config;
my $name = $config->{server_name} or die "server_name not set in config";
my $log_dir = MyApp::Util->log_dir or die "log_dir not set in config";
die "log_dir $log_dir does not exist\n" unless -d $log_dir;
die "log_dir $log_dir is not writable\n" unless -w $log_dir;

MyApp->setup_engine('PSGI');
my $app = sub { MyApp->run(@_) };

builder {
    my $logfh;
    my $access_logfile = "$log_dir/access-log-$name";
    my $error_logfile = "$log_dir/error-log-$name";
    open $logfh, ">>", $access_logfile or die $!;
    open STDERR, ">>", $error_logfile or die $!;
    $logfh->autoflush(1);

    enable "AccessLog", logger => sub { print $logfh @_ };

    # debug panel
    enable 'Debug', panels => $config->{plack_debug_panel}
    if $config->{plack_debug_panel};

    # if we're using perlbal, fix some request params. replace 12.34.56.78 with your public IP
    enable_if { $_[0]->{REMOTE_ADDR} eq '127.0.0.1'
                    || $_[0]->{REMOTE_ADDR} eq '12.34.56.78' }
    "Plack::Middleware::ReverseProxy";

    return $app;
};

My tags:
 
Popular tags:
 
Powered by Catalyst
Powered by MojoMojo Hosted by Shadowcat - Managed by Nordaaker