NginScript to Progressively Transition Clients to a New Server
NginScript is a unique JavaScript implementation for NGINX and NGINX Plus, designed specifically for server‑side use cases and per‑request processing.
Join the DZone community and get the full member experience.
Join For FreeWith the launch of NGINX Plus R10, we introduced a preview release of our next‑generation programmatic configuration language, nginScript. nginScript is a unique JavaScript implementation for NGINX and NGINX Plus, designed specifically for server‑side use cases and per‑request processing.
One of the key benefits of nginScript is the ability to read and set NGINX configuration variables. The variables can then be used to make custom routing decisions unique to the needs of the environment. This means that you can use the power of JavaScript to implement complex functionality that directly impacts request processing.
The Use Case: Transitioning to a New Application Server
In this blog post, we show how to use nginScript to implement a graceful switchover to a new application server. Instead of taking a disruptive “big‑bang approach” where everything is transitioned all at once, we define a time window during which all clients are progressively transitioned to the new application server. In this way, we can gradually — and automatically — increase traffic to the new application server.
We’re defining a two‑hour window during which we want our progressive switchover to take place, in this example from 5-7 p.m. After the first 12 minutes, we should expect 10% of clients to be directed to the new application server, then 20% of clients after 24 minutes, and so on. The following graph illustrates the transition.
One important requirement of this “progressive transition” configuration is that transitioned clients don’t revert back to the original server – once a client has been directed to the new application server, it continues to be directed there for the remainder of the transition window (and afterward, of course).
We will describe the complete configuration below, but in brief, when NGINX or NGINX Plus processes a new request that matches the application that is being transitioned, it follows these rules:
- If the transition window has not started, direct the request to the old application server.
- If the transition window has elapsed, direct the request to the new application server.
- If the transition is in progress:
- Calculate the current (time) position in the transition window.
- Calculate a hash for the client IP address.
- Calculate the position of the hash in the range of all possible hash values.
- If the hash position is greater than the current position in the transition window, direct the request to the new application server; otherwise direct the request to the old application server.
Let’s get started!
Enabling NginScript for NGINX Plus
nginScript is available as a free dynamic module for NGINX Plus subscribers (for open source NGINX, see Enabling nginScript for Open Source NGINX below).
- Obtain the module itself by installing it from the NGINX Plus repository.For Ubuntu and Debian systems:
$ sudo apt-get install nginx-plus-module-njs
For RedHat, CentOS, and Oracle Linux systems:$ sudo yum install nginx-plus-module-njs
- Enable the module by including a
load_module
directive for it in the top‑level (“main”) context of the nginx.conf configuration file (that is, not in thehttp
orstream
contexts).load_module modules/ngx_http_js_module.so;
- Reload NGINX Plus to load the nginScript module into the running instance.
$ sudo nginx -s reload
- Two changes are required: The sample configuration in NGINX and NGINX Plus Configuration for HTTP Applications is appropriate when NGINX or NGINX Plus acts as a reverse proxy for an HTTP application server. We can adapt the configuration for TCP and UDP applications by moving the entire configuration snippet to the
stream
context.
NGINX and NGINX Plus Configuration for TCP and UDP Applications
Finally, we compare the hashed value’s position within the output range with our current position within the transition time window and return the name of the corresponding upstream group.
2016/09/08 17:44:48 [info] 41325#41325: *84 js: timepos = 0.373333, hashpos = 0.840858
At this point we invoke req.log() to log the hash position and the current position of the transition time window. This is logged at the info level to the error_log defined in our NGINX or NGINX Plus configuration and produces log entries such as the following example. The js: prefix serves to denote a log entry resulting from JavaScript code.We then calculate where the hashed value sits within the range of all possible values. Because the FNV‑1a algorithm returns a 32‑bit integer we can simply divide the hashed value by 4,294,967,295 (the decimal representation of 32 bits).
The outer if block determines whether the transition window has started, finished, or is in progress. If it’s in progress, we obtain the hash of the client IP address by passing req.remoteAddress to the fnv32a function. The transitionStatus function has a single parameter, req, which is the JavaScript object representing the HTTP request. The variables
property of the request object contains all of the NGINX configuration variables, including the two we set to define the transition window, $transition_window_start and $transition_window_end.
function transitionStatus(req) {
var vars, window_start, window_end, time_now, timepos, numhash, hashpos;
// Get the transition window from NGINX/NGINX Plus configuration
vars = req.variables;
window_start = new Date(vars.transition_window_start);
window_end = new Date(vars.transition_window_end);
// Are we in the transition time window?
time_now = new Date.now();
if ( time_now < window_start ) {
return "old";
} else if ( time_now > window_end ) {
return "new";
} else { // We are in the transition window
// Calculate our position in the window (0-1)
timepos = (time_now - window_start) / (window_end - window_start);
// Get numeric hash for this client's IP address
numhash = fnv32a(req.remoteAddress);
// Calculate the hash's position in the output range (0-1)
hashpos = numhash / 4294967295; // Upper bound is 32 bits
req.log("timepos = " + timepos + ", hashpos = " + hashpos); //error_log [info]
// Should we transition this client?
if ( timepos > hashpos ) {
return "new";
} else {
return "old";
}
}
}
Next, we define the function, transitionStatus, that sets the $upstream variable in the js_set directive in our NGINX or NGINX Plus configuration.
function fnv32a(str) {
var hval = 2166136261;
var i = 0;
for (; i < str.length; ++i ) {
hval ^= str.charCodeAt(i);
hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
}
return hval >>> 0;
}
In this example, we are using the FNV‑1a hash algorithm, which is compact, fast, and has reasonably good distribution characteristics. Its other advantage is that it returns a positive 32‑bit integer, which makes it trivial to calculate the position of each client IP address in the output range. The following code is a JavaScript implementation of the FNV‑1a algorithm. Dependent functions must appear before those that call them, so we start by defining a function that returns a hash of the client’s IP address. If our application server is predominantly used by users on the same LAN then all of our clients have very similar IP addresses, so we need the hash function to return an even distribution of values even for a small range of input values.We put our nginScript code in /etc/nginx/functions.js as specified by the js_include directive. All of our functions appear in this file.
NginScript Code
Finally, the error_log
directive enables nginScript logging of events with severity level info and higher (by default only events at level warn and higher are logged). By placing this directive inside the location block and naming a separate log file, we avoid cluttering the main error log with all info messages.
The proxy_pass
directive directs the request to the upstream group that is calculated by the transitionStatus function.
The js_set directive names transitionStatus as the nginScript function that sets the value of the $upstream variable. Note that this directive is not an instruction to call transitionStatus. All NGINX variables are evaluated on demand, that is, at the point during request processing when they are used. So the js_set directive tells NGINX or NGINX Plus how to evaluate the $upstream variable when it is needed.
The location
block applies to the entire application space (/). Within this block, we use the set
directive to define the transition window with two new variables, $transition_window_start and $transition_window_end. Note that dates can be specified in RFC 2822 format (as in the snippet) or ISO 8601 format (with milliseconds). Both formats must include their respective local time zone designator. This is because the JavaScript Date.now() function always returns the UTC date and time and so an accurate time comparison is possible only if the local time zone is specified.
The js_include directive specifies the location of that file. This directive can be specified only once, so all of our nginScript code must be placed in this file.
js_include /etc/nginx/functions.js;
server {
listen 80;
location / {
set $transition_window_start "Thu, 08 Sep 2016 17:00:00 +0100";
set $transition_window_end "Thu, 08 Sep 2016 19:00:00 +0100";
# Returns "old|new" based on window position
js_set $upstream transitionStatus;
proxy_pass http://$upstream;
# Enables nginScript logging
error_log /var/log/nginx/transition.log info;
}
}
Next, we define the frontend service that NGINX or NGINX Plus presents to clients.
upstream old {
server 10.0.0.1;
server 10.0.0.2;
}
upstream new {
server 10.0.0.9;
server 10.0.0.10;
}
First, we define separate upstream
blocks for the sets of servers that host our old and new application code respectively. Even with our progressive transition configuration, NGINX will continue to load balance between the available servers during the transition window.In this example we are using NGINX or NGINX Plus as a reverse proxy to a web application server, so all of our configuration is in the HTTP context. For details about the configuration for TCP and UDP applications in the stream context, see NGINX and NGINX Plus Configuration for TCP and UDP Applications below.
NGINX and NGINX Plus Configuration for HTTP Applications
Compile the nginScript module from source at the open source repository. Copy the module binary ngx_http_js_module.so to the modules subdirectory of the NGINX root (usually /etc/nginx/modules). Perform Steps 2 and 3 in the previous instructions for NGINX Plus to configure NGINX and reload.
Enabling NginScript for Open Source NGINX
- In Step 2 of Enabling nginScript for NGINX Plus, enable the nginScript Stream module by including a
load_module
directive for it in the top-level (“main”) context of the nginx.conf configuration file. Then reload NGINX Plus as directed in Step 3.load_module modules/ngx_stream_js_module.so;
- Define the
$transition_window_start
and$transition_window_end
variables in thetransitionStatus
function instead of with theset
directive in the NGINX or NGINX Plus configuration, because theset
directive is not yet supported in thestream
context.
Summary
We’ve presented the progressive transition use case in this blog post as an illustration of the type of complex programmatic configuration you can achieve with nginScript. Deploying custom logic to control the selection of the most appropriate upstream group is one of a large number of solutions now possible with nginScript.
We will continue to extend and enhance the capabilities of nginScript to support ever‑more useful programmatic configuration solutions for NGINX and NGINX Plus. In the meantime, we would love to hear about the problems you are solving with nginScript — please add them to the comments section below.
Published at DZone with permission of Liam Crilly, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments