[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