Session persistence is a common challenge for any transaction that involves two or more requests, whether it be a complex shopping-cart application or a simple paginated search.

Many complex web applications store state locally and can fail or underperform if a load balancer distributes requests in a user’s session across different servers instead of directing all requests to the server that responded to the initial request.

When session persistence is configured, the NGINX Plus load balancer identifies user sessions and pins all requests in a session to the same upstream server. This avoids fatal errors for applications that only store state locally, and it significantly improves performance for applications that share state across a cluster.

Session persistence can make it more complicated to take an upstream server down for maintenance or upgrade. NGINX Plus’ session draining capability enables you to take a server down without interrupting established sessions.

Session Persistence

Despite HTTP being a ‘stateless’ protocol, many applications store state locally and don’t perform well in a load-balanced environment. Once a user commences a complex transaction with an upstream server (for example, putting the first item in his shopping basket, or starting a paginated search), it is often optimal to direct all of the user’s requests to that same server, because sharing state between servers is slow or even impossible.

NGINX Plus tracks user sessions and pins them to the correct upstream server. It provides three methods for identifying user sessions: Inserting its own tracking cookie into the traffic, Learning when servers create a session and detecting requests in that session, and tracking specific data in a request (such as the jvmRoute).

Session Persistence Method: Cookie Insertion

To configure NGINX Plus’ simplest session persistence method, include the cookie parameter to the sticky directive. NGINX Plus adds a session cookie to the first response from the upstream group to a given client, identifying which server generated the response (in an encoded fashion). Subsequent requests from the client include the cookie value and NGINX Plus uses it to route the request to the same upstream server:

upstream backend {
    server webserver1;
    server webserver2;

    sticky cookie srv_id expires=1h domain=.example.com path=/;
}

Session Persistence Method: Learn

When you include the learn parameter to the sticky directive, NGINX Plus inspects requests and responses to find session identifiers, which servers most commonly set in cookies. NGINX Plus learns which upstream server is associated with a session identifier, and when a subsequent request contains a known session identifier, NGINX Plus forwards it to the corresponding server.

upstream backend {
   server webserver1;
   server webserver2;

   sticky learn create=$upstream_cookie_sessionid
       lookup=$cookie_sessionid
       zone=client_sessions:1m
       timeout=1h;
}

Whereas the cookie method is completely stateless and stores session data in a client-side cookie, the learn method does modify server responses in some way, and stores the state locally in a shared memory zone.

Session Persistence Method: Sticky Routes

Sometimes you want precise control over which server is selected from an upstream group. Include the route parameter to the sticky directive to achieve this:

upstream backend {
   server webserver1 route=a;
   server webserver2 route=b;

   # $var1 and $var2 are run-time variables, calculated for each request
   sticky route $var1 $var2;
}

With this method, NGINX Plus searches the parameters to the sticky route directive ($var1, $var2, and so on) and compares the first non-empty value to the route value for each server. If there’s a match, NGINX Plus chooses the matching server; otherwise, it uses the configured load-balancing algorithm to decide where to route the request.

This feature can be used to great effect with NGINX Plus’ run-time interpretation of configuration. For example, if you have two Java servers, you can configure each of them to append an identifier (a or b) to the end of its JSESSIONID cookie value using the jvmRoute attribute:

<Engine name="Catalina" defaultHost="www.example.com" jvmRoute="a">

NGINX Plus can inspect the JSESSIONID cookie or URI parameter in each request, extract the jvmRoute value, and select the upstream server accordingly:

# In the cookie named JSESSIONID, extract the data after the final 
# period (.) and store it in the $route_cookie variable

map $cookie_jsessionid $route_cookie {
    ~.+\.(?P<route>\w+)$ $route;
}

# Search the URL for a trailing jsessionid parameter, extract the data 
# after the final period (.), and store it in the $route_uri variable

map $request_uri $route_uri {
    ~jsessionid=.+\.(?P<route>\w+)$ $route;
}

upstream backend {
    server backend1.example.com route=a;
    server backend2.example.com route=b;

    sticky route $route_cookie $route_uri;
}

For more information on session persistence in NGINX Plus, refer to the documentation for the sticky directive.

Session Draining

Sometimes you need to take an upstream node down for maintenance or upgrade. When you configure session draining by setting the drain parameter on a server directive in the upstream context, NGINX Plus gracefully prepares the server to be taken out of service: it allows existing requests and sessions on the draining server to complete, but directs new requests that are not part of an established session to other servers in the upstream group.

draining-nginx-plus

Session draining takes a server out of service without interrupting established sessions

To set the drain parameter, you can either edit the configuration file and have NGINX Plus reload it, or use the on-the-fly configuration API as illustrated by the following example. The first command reports the IDs for the servers in the backends upstream group, and the second puts the server with ID 1 into the draining state. The output from the second command confirms the status change for that server. (The example uses the conventional name, upstream_conf, for the location context where the upstream_conf directive is included to activate the API handler.)

$ curl http://localhost/upstream_conf?upstream=backend
server 192.168.56.101:80; # id=0
server 192.168.56.102:80; # id=1
server 192.168.56.103:80; # id=2
$ curl http://localhost/upstream_conf?upstream=backend\&id=1\&drain=1
server 192.168.56.102:80; # id=1 draining

To confirm that all sessions have completed on the drained server, use NGINX Plus’ live activity monitoring interface to retrieve the selected statistic, which reports the UNIX time when NGINX Plus last directed a request to the server. The following examples retrieve the selected statistic for the server with ID 1. They use the conventional name, status, for the location context where the status directive is included to activate the live activity monitoring handler.

# Return the UNIX time (expressed in milliseconds) when 
# server 1 in upstream group 'backends' was last used
$ curl http://localhost:8080/status/upstreams/backends/1/selected
1440636514000

# Calculate how long the server has been idle (in milliseconds)
$ expr `date +%s000` - `curl -s http://localhost:8080/status/upstreams/backends/1/selected`

Note that for live activity monitoring statistics NGINX Plus reports UNIX time in milliseconds rather than seconds. Most tools that convert UNIX times to date strings use seconds, so when converting the selected value you need to remove the final three digits (000 in the sample output). Similarly, in the expr command you need to include the +%s000 formatting parameter to have the date command use milliseconds rather than seconds.

Once you are sure all sessions have completed, you can take the server offline. If you have configured health checks, also change the drain parameter to down in the configuration to mark the server as offline; otherwise, NGINX Plus continues to send health checks to it and reports errors once the server is offline.