[PATCH] Ability to choose backend via cookie or hook

André Cruz andre.cruz at segula.pt
Thu May 3 16:27:49 UTC 2007


Hello.

Using this patch a specific backend from the pool can be specified to  
process the arriving client.

Either through a cookie whose name is configured like the high  
priority cookie (config var sticky_backend_cookie) or though a hook  
(select_backend) we can select a backend using the identifier ip:port.

With this functionality in place we can easily support sticky  
sessions, temporary or not, but setting the cookie/using the hook in  
other places of the processing as we like.

NOTES:
Using the cookie will expose the IP:PORT of the backend that will be  
used (this was not a problem here so I didn't worry about it but I  
guess another identifier, opaque to the user, can be used).

If perlbal has problems with a backend it will erase the preferences  
of all the clients in the queue which requested this backend. In  
practice they will be served by the next backend available.

I submit this patch here in the hope that it will be useful to  
someone else (maybe even be included in the svn?) and to hear  
comments/suggestions/bugs about it.

Regards,
André Cruz

PS - This patch is against 1.57.


-------------- next part --------------
diff -u -r -wB tmp/Perlbal-1.57/lib/Perlbal/ClientProxy.pm Perlbal-1.57/lib/Perlbal/ClientProxy.pm
--- tmp/Perlbal-1.57/lib/Perlbal/ClientProxy.pm	2007-04-16 21:55:27.000000000 +0100
+++ Perlbal-1.57/lib/Perlbal/ClientProxy.pm	2007-05-02 10:53:09.000000000 +0100
@@ -18,6 +18,7 @@
             'reconnect_count',     # number of times we've tried to reconnect to backend
             'high_priority',       # boolean; 1 if we are or were in the high priority queue
             'low_priority',        # boolean; 1 if we are or were in the low priority queue
+            'wanted_backend',      # ip:port; if a specific backend is requested
             'reproxy_uris',        # arrayref; URIs to reproxy to, in order
             'reproxy_expected_size', # int: size of response we expect to get back for reproxy
             'currently_reproxying',  # arrayref; the host info and URI we're reproxying right now
@@ -92,6 +93,8 @@
     $self->{high_priority} = 0;
     $self->{low_priority} = 0;
 
+    $self->{wanted_backend} = undef;
+
     $self->{responded} = 0;
     $self->{unread_data_waiting} = 0;
     $self->{content_length_remain} = undef;
@@ -461,6 +464,7 @@
     # if we get here we're being persistent, reset our state
     $self->{backend_requested} = 0;
     $self->{high_priority} = 0;
+    $self->{wanted_backend} = undef;
     $self->{reproxy_uris} = undef;
     $self->{reproxy_expected_size} = undef;
     $self->{currently_reproxying} = undef;
@@ -1212,6 +1216,8 @@
     }
     $ret .= "; highpri" if $self->{high_priority};
     $ret .= "; lowpri" if $self->{low_priority};
+    $ret .= "; wanted_backend=" . $self->{wanted_backend}
+        if defined $self->{wanted_backend};
     $ret .= "; responded" if $self->{responded};
     $ret .= "; waiting_for=" . $self->{content_length_remain}
         if defined $self->{content_length_remain};
@@ -1232,6 +1238,12 @@
     return;
 }
 
+sub set_wanted_backend {
+    my Perlbal::ClientProxy $self = shift;
+    my $wbe = shift;
+    $self->{wanted_backend} = $wbe;
+    return;
+}
 
 sub DESTROY {
     Perlbal::objdtor($_[0]);
diff -u -r -wB tmp/Perlbal-1.57/lib/Perlbal/Service.pm Perlbal-1.57/lib/Perlbal/Service.pm
--- tmp/Perlbal-1.57/lib/Perlbal/Service.pm	2007-04-16 21:10:09.000000000 +0100
+++ Perlbal-1.57/lib/Perlbal/Service.pm	2007-05-03 17:06:44.000000000 +0100
@@ -36,6 +36,7 @@
             'enable_delete', # bool: whether DELETE is supported
             'high_priority_cookie',          # cookie name to check if client can 'cut in line' and get backends faster
             'high_priority_cookie_contents', # aforementioned cookie value must contain this substring
+            'sticky_backend_cookie',         # cookie name to check if client wants a specific backend
             'backend_persist_cache',   # scalar: max number of persistent backends to hold onto while no clients
             'persist_client',  # bool: persistent connections for clients
             'persist_backend', # bool: persistent connections for backends
@@ -93,6 +94,7 @@
             'enable_error_retries',  # bool: whether we should retry requests after errors
             'error_retry_schedule',  # string of comma-separated seconds (full or partial) to delay between retries
             'latency',               # int: milliseconds of latency to add to request
+            'backend_interest', # { "ip:port" => 1 }; if on, spawn_backends prefer this ip:port combo
 
             # stats:
             '_stat_requests',       # total requests to this service
@@ -351,6 +353,11 @@
         check_role => "reverse_proxy",
     },
 
+    'sticky_backend_cookie' => {
+        des => "The cookie name to inspect to determine if the client wants a specific backend.",
+        check_role => "reverse_proxy",
+    },
+
     'trusted_upstream_proxies' => {
         des => "A Net::Netmask filter (e.g. 10.0.0.0/24, see Net::Netmask) that determines whether upstream clients are trusted or not, where trusted means their X-Forwarded-For/etc headers are not munged.",
         check_role => "reverse_proxy",
@@ -527,6 +534,7 @@
     $self->{extra_config} = {};
 
     $self->{backend_no_spawn} = {};
+    $self->{backend_interest} = {};
     $self->{generation} = 0;
 
     $self->{hooks} = {};
@@ -921,7 +929,9 @@
 }
 
 sub get_client {
-    my Perlbal::Service $self = shift;
+    my Perlbal::Service $self;
+    my Perlbal::BackendHTTP $be;
+    ($self, $be) = @_;
 
     my $ret = sub {
         my Perlbal::ClientProxy $cp = shift;
@@ -946,29 +956,107 @@
 
     # find a high-priority client, or a regular one
     my Perlbal::ClientProxy $cp;
-    while ($hp_first && ($cp = shift @{$self->{waiting_clients_highpri}})) {
-        next if $cp->{closed};
+    if ($hp_first) {
+        for (my $i = 0; $i < scalar @{$self->{waiting_clients_highpri}}; $i++) {
+            $cp = $self->{waiting_clients_highpri}->[$i];
+
+            if ($cp->{closed}) {
+                splice(@{$self->{waiting_clients_highpri}}, $i--, 1);
+                next;
+            }
+
+            if ($cp->{wanted_backend} &&                                    # wants a specific backend
+                $cp->{wanted_backend} ne $be->{ipport} &&                   # not this one
+                defined $self->{pool}->node_used($cp->{wanted_backend}) &&  # but is in pool
+                ! $self->{backend_no_spawn}->{$cp->{wanted_backend}}        # and it's not down
+                ) {
+                # got a client but he wants another backend
+                
+                if (Perlbal::DEBUG >= 2) {
+                    print "Got from fast queue, but wants another backend($cp->{wanted_backend}). I'm $be->{ipport}\n";
+                }
+                
+                # register interest
+                $self->{backend_interest}->{$cp->{wanted_backend}} = 1;
+                next;
+            }
+
         if (Perlbal::DEBUG >= 2) {
             my $backlog = scalar @{$self->{waiting_clients}};
             print "Got from fast queue, in front of $backlog others\n";
         }
+            
+            splice(@{$self->{waiting_clients_highpri}}, $i, 1);
         return $ret->($cp);
     }
+    }
 
     # regular clients:
-    while ($cp = shift @{$self->{waiting_clients}}) {
-        next if $cp->{closed};
+    for (my $i = 0; $i < scalar @{$self->{waiting_clients}}; $i++) {
+        $cp = $self->{waiting_clients}->[$i];
+        
+        if ($cp->{closed}) {
+            splice(@{$self->{waiting_clients}}, $i--, 1);
+            next;
+        }
+        
+        if ($cp->{wanted_backend} &&                                    # wants a specific backend
+            $cp->{wanted_backend} ne $be->{ipport} &&                   # not this one
+            defined $self->{pool}->node_used($cp->{wanted_backend}) &&  # but is in pool
+            ! $self->{backend_no_spawn}->{$cp->{wanted_backend}}        # and it's not down
+            ) {
+            # got a client but he wants another backend
+            
+            if (Perlbal::DEBUG >= 2) {
+                print "Got from normal queue, but wants another backend($cp->{wanted_backend}). I'm $be->{ipport}\n";
+            }
+
+            # register interest
+            $self->{backend_interest}->{$cp->{wanted_backend}} = 1;
+            next;
+        }
+        
         print "Backend requesting client, got normal = $cp->{fd}.\n" if Perlbal::DEBUG >= 2;
+        
+        splice(@{$self->{waiting_clients}}, $i, 1);
         return $ret->($cp);
     }
 
     # low-priority (batch/idle) clients.
-    while ($cp = shift @{$self->{waiting_clients_lowpri}}) {
-        next if $cp->{closed};
+    for (my $i = 0; $i < scalar @{$self->{waiting_clients_lowpri}}; $i++) {
+        $cp = $self->{waiting_clients_lowpri}->[$i];
+        
+        if ($cp->{closed}) {
+            splice(@{$self->{waiting_clients_lowpri}}, $i--, 1);
+            next;
+        }
+        
+        if ($cp->{wanted_backend} &&                                    # wants a specific backend
+            $cp->{wanted_backend} ne $be->{ipport} &&                   # not this one
+            defined $self->{pool}->node_used($cp->{wanted_backend}) &&  # but is in pool
+            ! $self->{backend_no_spawn}->{$cp->{wanted_backend}}        # and it's not down
+            ) {
+            # got a client but he wants another backend
+            
+            if (Perlbal::DEBUG >= 2) {
+                print "Got from low queue, but wants another backend($cp->{wanted_backend}). I'm $be->{ipport}\n";
+            }
+
+            # register interest
+            $self->{backend_interest}->{$cp->{wanted_backend}} = 1;
+            next;
+        }
+        
         print "Backend requesting client, got low priority = $cp->{fd}.\n" if Perlbal::DEBUG >= 2;
+        
+        splice(@{$self->{waiting_clients_lowpri}}, $i, 1);
         return $ret->($cp);
     }
 
+    # spawn more backends if here and waiting list is not empty
+    # means we want another backend
+    $self->spawn_backends if $self->{waiting_client_count};
+
     return undef;
 }
 
@@ -1011,7 +1099,7 @@
     return unless $self->verify_generation($be);
 
     # now try to fetch a client for it
-    my Perlbal::ClientProxy $cp = $self->get_client;
+    my Perlbal::ClientProxy $cp = $self->get_client($be);
     if ($cp) {
         return if $be->assign_client($cp);
 
@@ -1064,6 +1152,18 @@
     # FIXME: do something interesting (tell load balancer about dead host,
     # and fire up a new connection, if warranted)
 
+    # erase backend preference for this one that gave an error
+    foreach my Perlbal::ClientProxy $cp (@{$self->{waiting_clients_highpri}},
+                                         @{$self->{waiting_clients}},
+                                         @{$self->{waiting_clients_lowpri}}) {
+        
+        if ($cp->{wanted_backend} && 
+            $cp->{wanted_backend} eq $be->{ipport}) {
+            print "Erased preference for bad backend $be->{ipport}.\n" if Perlbal::DEBUG >= 2;
+            $cp->{wanted_backend} = undef;
+        }
+    }
+
     # makes a new connection, if needed
     $self->spawn_backends;
 }
@@ -1078,19 +1178,35 @@
     my $hi_pri = $cp->{high_priority};  # load values from the client proxy object
     my $low_pri = $cp->{low_priority};  # they are initialized as 0 during object creation, but hooks can override them
 
+    my $wbe;  # wanted backend
+
     # is there a defined high-priority cookie?
-    if (my $cname = $self->{high_priority_cookie}) {
-        # decide what priority class this request is in
+    my $cname = $self->{high_priority_cookie};
+    
+    # is there a defined sticky-backend cookie?
+    my $sticky = $self->{sticky_backend_cookie};
+
+    if ($cname || $sticky) {
+        # have to check cookies
         my $hd = $cp->{req_headers};
         my %cookie;
         foreach (split(/;\s+/, $hd->header("Cookie") || '')) {
             next unless ($_ =~ /(.*)=(.*)/);
             $cookie{Perlbal::Util::durl($1)} = Perlbal::Util::durl($2);
         }
+
+        if ($cname) {
+            # decide what priority class this request is in
         my $hicookie = $cookie{$cname} || "";
         $hi_pri = index($hicookie, $self->{high_priority_cookie_contents}) != -1;
     }
 
+        if ($sticky) {
+            # extract backend needed
+            $wbe = $cookie{$sticky} || "";
+        }
+    }
+
     # now, call hook to see if this should be high priority
     $hi_pri = $self->run_hook('make_high_priority', $cp)
         unless $hi_pri; # only if it's not already
@@ -1102,20 +1218,43 @@
     $cp->{high_priority} = 1 if $hi_pri;
     $cp->{low_priority} = 1 if $low_pri;
 
+    # call hook to check if a specific backend should be used unless already set
+    $wbe = $self->run_hook('select_backend', $cp)
+        unless $wbe;
+
+    if ($wbe) {
+        # some sanity checks
+        # ignore preference if backend is down or not in pool
+        $wbe = undef if $self->{backend_no_spawn}->{$wbe} ||
+            ! $self->{pool} ||
+            ! defined $self->{pool}->node_used($wbe);
+    }    
+
+    $cp->{wanted_backend} = $wbe;
+
     # before we even consider spawning backends, let's see if we have
     # some bored (pre-connected) backends that'd take this client
     my Perlbal::BackendHTTP $be;
     my $now = time;
-    while ($be = shift @{$self->{bored_backends}}) {
-        next if $be->{closed};
+    for (my $i = 0; $i < scalar @{$self->{bored_backends}}; $i++) {
+        $be = $self->{bored_backends}->[$i];
+
+        if ($be->{closed}) {
+            splice(@{$self->{bored_backends}}, $i--, 1);
+            next;
+        }
 
         # now make sure that it's still in our pool, and if not, close it
-        next unless $self->verify_generation($be);
+        unless ($self->verify_generation($be)) {
+            splice(@{$self->{bored_backends}}, $i--, 1);
+            next;
+        }
 
         # don't use connect-ahead connections when we haven't
         # verified we have their attention
         if (! $be->{has_attention} && $be->{create_time} < $now - 5) {
             $be->close("too_old_bored");
+            splice(@{$self->{bored_backends}}, $i--, 1);
             next;
         }
 
@@ -1123,9 +1262,16 @@
         # just about to kill the connection for being idle
         if ($be->{disconnect_at} && $now + 2 > $be->{disconnect_at}) {
             $be->close("too_close_disconnect");
+            splice(@{$self->{bored_backends}}, $i--, 1);
             next;
         }
 
+        # if the client requested a specific backend...
+        next if $wbe && $wbe ne $be->{ipport};
+
+        # we are going to use this backend
+        splice(@{$self->{bored_backends}}, $i, 1);
+
         # give the backend this client
         if ($be->assign_client($cp)) {
             # and make some extra bored backends, if configured as such
@@ -1148,7 +1294,10 @@
     $self->{waiting_client_count}++;
     $self->{waiting_client_map}{$cp->{fd}} = 1;
 
-    $self->spawn_backends;
+    # register interest
+    $self->{backend_interest}->{$wbe} = 1 if $wbe;
+
+    $self->spawn_backends();
 }
 
 # sees if it should spawn one or more backend connections
@@ -1183,7 +1332,7 @@
 
     my $now = time;
 
-    while ($to_create > 0) {
+    while (($to_create > 0) || scalar keys %{$self->{backend_interest}}) {
         $to_create--;
 
         # spawn processes if not a pool, else whine.
@@ -1198,7 +1347,15 @@
             return;
         }
 
-        my ($ip, $port) = $self->{pool}->get_backend_endpoint;
+        my ($ip, $port);
+        if (scalar keys %{$self->{backend_interest}}) {
+            my $ipport = each %{$self->{backend_interest}};
+            ($ip, $port) = split(':', $ipport);
+            delete $self->{backend_interest}->{$ipport};
+        } else {
+            ($ip, $port) = $self->{pool}->get_backend_endpoint;
+        }
+       
         unless ($ip) {
             Perlbal::log('crit', "No backend IP for service $self->{name}");
             # FIXME: register desperate flag, so load-balancer module can callback when it has a node


More information about the perlbal mailing list