SLB |
|
Simple Load Balancer |
Sergey Poznyakoff |
SLB User Manual (split by chapter): | ? |
This chapter guides you through the most important features of
slb
and explains its configuration. Start here if you have
never used slb
before.
It omits some complicated details and rarely used features, which will be discussed further, in SLB Configuration File.
Options start with a dash. Most of them have two forms, called short and long forms. Both forms are absolutely identical in function; they are interchangeable.
The short form is a traditional form for UNIX utilities. In this form, the option consists of a single dash, followed by a single letter, e.g. ‘-c’.
Short options which require arguments take their arguments immediately following the option letter, optionally separated by white space. For example, you might write ‘-o name’, or ‘-oname’. Here, ‘-o’ is the option, and ‘name’ is its argument.
Short options' letters may be clumped together, but you are not required to do this. When short options are clumped as a set, use one (single) dash for them all, e.g. ‘-ne’ is equivalent to ‘-n -e’. However, only options that do not take arguments may be clustered this way. If an option takes an argument, it can only be the last option in such a cluster, otherwise it would be impossible to specify the argument for it. Anyway, it is much more readable to specify such options separated.
The long options consist of two dashes, followed by a multi-letter option name, which is usually selected to be a mnemonics for the operation it requests. Long option names can be abbreviated, provided that such an abbreviation is unique among the options understood by the program.
Arguments to long options follow the option name being separated from it either by an equal sign, or by any amount of white space characters. For example, the option ‘--eval’ with the argument ‘test’ can be written either as ‘--eval=test’ or as ‘--eval test’.
The configuration file defines most parameters needed for the normal
operation of slb
. The program will not start if its
configuration file does not exist, cannot be read, or contains some errors.
The configuration file is located in your system configuration
directory (normally ‘/etc’) and is named ‘slb.conf’. You
can place it elsewhere as well, but in this case you will need to
explicitly inform slb
about its actual location, using the
‘--config-file’ (‘-c’) command line option:
slb --config-file ./new.conf |
Before actually starting the program, it is wise to check the configuration file for errors. To do so, use the ‘--lint’ (‘-t’) command line option:
slb --lint |
When started with this option, slb
parses the configuration,
reports any errors on the standard error and exits. If parsing
succeeds, it exits with code 0. Otherwise, if any errors have been
found, it exits with code 78 (configuration error).
The ‘--lint’ option can, of course, be used together with ‘--config-file’, e.g.:
slb --lint --config-file ./new.conf |
If you are unsure about the correct configuration syntax, you can obtain a concise summary any time, by running:
slb --config-help |
The summary is printed to the standard output and includes all configuration statements with short descriptions of their purpose and arguments.
In this section we will provide a quick start introduction to the
slb
configuration. For a more detailed and formal
discussion, refer to SLB Configuration File.
The configuration file consists of statements. There are two kinds of statements, called simple and block statements. A simple statement consists of a keyword and value, or values, separated by any amount of whitespace and terminated with a semicolon, for example:
wakeup 15; |
A block statement is used for logical grouping of other statements. It consists of a keyword, optionally followed by a value, and a set of other statements, enclosed in a pair of curly brackets. For example:
syslog { facility local1; tag slb; } |
A semicolon may follow the closing ‘}’, although this is not required.
Note that whitespace (i.e. space characters, tabs and newlines) has no special syntactical meaning, except that it serves to separate otherwise adjacent tokens. For example, the following form of the ‘syslog’ statement is entirely equivalent to the one shown above:
syslog { facility local1; tag slb; } |
Several types of comments are supported. A single-line comment starts with ‘#’ or ‘//’ and continues to the end of the line. A multi-line or C-style comment starts with the two characters ‘/*’ (slash, star) and continues until the first occurrence of ‘*/’ (star, slash). Whatever comment type are used, they are removed from the configuration prior to parsing it.
After comment removal, the configuration is preprocessed using
m4
. This is a highly useful feature, which allows for
considerable simplification of configuration files. It is described
in Preprocessing with m4.
The most important daemon parameters are the operation mode and wakeup interval. You are not required to explicitly configure them, but you may want to do so, if their default values don't suit you.
There are two operation modes: ‘standalone’ and
‘cron’. In standalone mode, slb
detaches itself from
the controlling terminal and continues operation in the background, as
a daemon. It will periodically wake up, poll the servers,
compute load table and format it to the output file or pipe. This
is the default operation mode.
In cron mode, slb
starts, polls the servers, computes
and formats load table and exits when done. This mode is designed
for use from crontabs, hence its name. It has some limitation,
compared to the standalone mode. Most notably, the d()
function (derivative) is not available in this mode.
If you wish to explicitly set the operation mode, use the ‘standalone’ configuration statement. The statement:
standalone yes; |
configures the standalone mode, whereas the statement:
standalone no; |
configures cron mode. The later can also be requested from the command line, using the ‘--cron’ option.
When operating in standalone mode, the wakeup interval specifies
the amount of time, in seconds, between successive polls. The default
value is 5 minutes. To set another value, use the ‘wakeup’
statement. For example, the following configures slb
to do
a poll each minute:
wakeup 60; |
Normally, this does not require additional configuration, unless you want to load some custom MIBs.
To load an additional MIB file, use the ‘add-mib’ statement. Its argument is the name of the file to load.
add-mib "MY-MIBS.txt"; |
Unless full pathname is specified, the file is searched in the SNMP search path. To modify the search path, use the ‘mib-directory’. Each ‘mib-directory’ adds a single directory to the end of the path:
mib-directory "/usr/share/slb/mibs"; |
Server definitions are principal part of any slb
configurations. They define remote servers for which slb
is to compute the load table. A server definition is a block statement
which begins as:
server id { |
The id argument specifies server ID, an arbitrary string of characters uniquely identifying this server. This ID will be used by log messages concerning this server. It can also be referred to in the output format definition (see section Output Configuration).
As usual, the definition ends with a closing ‘}’.
Statements inside the ‘server’ block supply configuration parameters for this server. Two parameters are mandatory for each server: its IP address and SNMP community:
host ip; community string; |
The ip argument can be either the IP address of the server in dotted-quad notation, or its host name. For example:
host 192.168.10.6; community "public"; |
The most important part of a server definition is its load estimation function. It is basically an arithmetic expression of arbitrary complexity, with SNMP variables serving as its terms (see section Expression). It is defined using the ‘expression’ statement.
To begin with, suppose you want to use 1-minute load average as relative load. Let's name it ‘la1’. Then, the expression is very simple and can be defined as:
expression "la1"; |
Now, ‘la1’ is a variable name, which should be bound to the corresponding SNMP variable. This binding is declared with the ‘variable’ statement:
variable la1 "UCD-SNMP-MIB::laLoadFloat.1"; |
The ‘variable’ statement has two arguments. The first one is the name of a variable used in the expression and the second one is the SNMP OID which is bound to this variable. This OID is added to the list of OIDs queried during each poll of this server. When a SNMP reply is received, all instances of that variable in the expression are replaced with the value of the corresponding SNMP variable. Once all variables have been thus resolved, the expression is evaluated and its result is taken as the relative load for the given server.
Let's take a more complex example. Suppose you define the relative load to be a function of outgoing data transfer rate through the main network interface and the load average of the server. Data transfer rate is defined as first derivative of data transfer through the interface with respect to time. Let ‘out’ be the outgoing data transfer, ‘la1’ be the server's 1-minute load average, ‘k’ and ‘m’ be two server-dependent constants (weight coefficients). Given that, we define relative load as
expression "sqrt(k * d(out)**2 + m * la1**2)"; |
The ‘**’ operator raises its left operand to the power given by its second operand. The two constructs ‘d(…)’ and ‘sqrt(…)’ are function calls. The ‘d()’ function computes first derivative of its argument with respect to time. The ‘sqrt()’ function computes the square root of its argument.
Now, let's define variable bindings:
variable out "IF-MIB::ifOutOctets.3"; variable la1 "UCD-SNMP-MIB::laLoadFloat.1"; |
The constants ‘k’ and ‘m’ are defined using the following statements:
constant k 1.5; constant m 1.8; |
The ‘constant’ statement is similar to ‘variable’, except that its second argument must be a floating-point number. Of course, in this particular example, the two constants could have been placed directly in the expression text, as in:
expression "sqrt(1.5 * d(out)**2 + 1.8 * la1**2)"; |
However, defining them on a per-server basis is useful when the same expression is used for several different servers, as explained in the following section.
To conclude, here is our sample server definition:
server srv01 { host 192.168.10.6; community "public"; expression "sqrt(k * d(out)**2 + m * la1**2)"; variable out "IF-MIB::ifOutOctets.3"; variable la1 "UCD-SNMP-MIB::laLoadFloat.1"; constant k 1.5; constant m 1.8; } |
In most cases, load estimation function is common for all servers (perhaps with server-dependent set of coefficients and/or variable bindings). To simplify configuration, such a common function can be defined once and then used by another ‘server’ definitions. It is defined as a named expression, i.e. an expression that can be referred to by its name.
Named expressions are defined using the ‘expression’ statement outside of ‘server’ block. Such statements take two arguments: the name of the expression and its definition, e.g.:
expression load "sqrt(k * d(out)**2 + m * la1**2)"; |
Named expressions can be invoked from another expressions using the ‘@’ operator, followed by expression's name. For example, the following statement in ‘server’ block:
expression "@load"; |
is in fact equivalent to the expression defined above. The ‘@’ construct can, of course, also be used as a term in arithmetic calculations, e.g.:
expression "2 * @load + 1"; |
A default expression is a named expression which is implicitly applied for ‘server’ statements that lack ‘expression’ statement. Default expression is defined using the ‘default-expression’ statement:
default-expression "load"; |
To finish this section, here is an example configuration declaring two servers which use the same default expression, but supply different sets of coefficients:
expression load "sqrt(k * d(out)**2 + m * la1**2)"; default-expression "load"; server srv01 { host 192.168.10.6; community "public"; variable out "IF-MIB::ifOutOctets.3"; variable la1 "UCD-SNMP-MIB::laLoadFloat.1"; constant k 1.5; constant m 1.8; } server srv02 { host 192.168.10.1; community "private"; variable out "IF-MIB::ifOutOctets.3"; variable la1 "UCD-SNMP-MIB::laLoadFloat.1"; constant k 2.5; constant m 2.2; } |
In the previous section we have seen how to define two servers which
use the same expression to compute relative load. In real
configurations, the number of servers is likely to be considerably
greater than that, and adding each of them to the configuration soon
becomes a tedious and error-prone task. This task is greatly
simplified by the use of preprocessor. As mentioned earlier,
the configuration file is preprocessed using m4
, a
traditional UNIX macro processor. It provides a powerful framework
for implementing complex slb
configurations.
For example, note that in our sample configuration the ‘server’
statements differ by only three values: the server ID, its IP and
community. Taking this into account, we may define the following
m4
macro:
m4_define(`defsrv',`server $1 { host $2; community "m4_ifelse(`$3',,`public',`$3')"; variable out "IF-MIB::ifOutOctets.3"; variable la1 "UCD-SNMP-MIB::laLoadFloat.1"; constant k 2.5; constant m 2.2; }') |
The ‘defsrv’ macro takes two mandatory and one optional argument. Mandatory arguments are the server ID and its IP address. Optional argument is SNMP community; if it is absent, the default community ‘public’ is used.
Notice, that we use m4_define
, instead of the familiar
define
. It is because the default slb
setup
renames all m4
built-in macro names so they all start with
the prefix ‘m4_’. This avoids possible name clashes and makes
preprocessor statements clearly visible in the configuration.
Using this new macro, the above configuration is reduced to the following two lines:
defsrv(srv01, 192.168.10.6) defsrv(srv02, 192.168.10.1, private) |
Declaring a new server is now a matter of adding a single line of text.
The default preprocessor setup defines a set of useful macros, among
them m4_foreach
(see (m4)Improved foreach section `foreach' in GNU M4 macro processor). This macro can be used to further simplify the
configuration, as shown in the example below:
m4_foreach(args, ``srv01, 192.168.10.6', `srv02, 192.168.10.1, private', `srv03, 192.168.0.3', `srv04, 192.168.100.1, foo', `srv05, 192.168.100.2, bar'', `defserv(args) ') |
The second argument to m4_foreach
is a comma-separated list of
values. The expansion is as follows: for each value from this list,
the value is assigned to the first argument (‘args’). Then the
third argument is expanded and the result is appended to the overall
expansion.
In this particular example, each line produces an expansion of the ‘defserv’ macro with the arguments taken from that line. Note, that each argument in the list must be quoted, because it contains commas. Note also the use of the optional third arguments to supply community names that differ from the default one.
For a detailed information about slb
preprocessor feature,
see Preprocessor.
When slb
has finished building load table, it sends it to
the output, line by line, using the output format string. This
format string is similar to the format argument of printf(1): any
characters, except ‘%’ are copied to the output verbatim; the
‘%’ introduces a conversion specifier. For example,
‘%i’ expands to the ID of the server this line refers to. The
‘%w’ specifier expands to the computed relative load for that
server, etc. There is a number of such specifiers, they are all
described in detail in Output Format String.
The default output format is ‘%i %w\n’. This produces a table, each line of which shows a server ID and its relative load. This default is suitable for testing purposes, but for real configurations you will most probably need to create a custom output format. You do so using the ‘output-format’ statement:
output-format "id=%i host=%h\n"; |
The output format string can be quite complex as shown in the following example:
output-format <<EOT server 10.0.0.1 update add www.example.net 60 IN A %h send EOT; |
This format creates, for each line from the load table, a set of
commands for nsupdate
(1). The ‘<<’ block introduces a here-document, a construct
similar to that of shell and several other programming languages. It
is discussed in here-document.
You may need to include server-dependent data in the corresponding
output lines. For this purpose, slb
provides server
macros. Server macros are special text variables, defined in the
‘server’ block, which can be accessed from output format string.
Macros are defined using the ‘macro’ statement, whose syntax is similar to that of ‘variable’ or ‘constant’. The first argument supplies the name of the macro, and the second one, its contents, or expansion, e.g.:
server srv01 { host 192.168.10.6; community "public"; macro description "some descriptive text"; } |
Macros are accessed using the following conversion specifier:
%(name) |
where name is the name of the macro. This specifier is replaced with the actual contents of the macro. The following example uses the ‘description’ macro to create a TXT record in the DNS:
output-format <<EOT server 10.0.0.1 update add www.example.net 60 IN A %h update add %i.example.net 60 IN TXT "%(description)" send EOT; |
Sometimes you may need to produce some output immediately before the load table or after it. Two configuration statements are provided for that purpose: ‘begin-output-message’ and ‘end-output-message’. Both take a single string as argument. The string supplied with ‘begin-output-message’ is output before formatting the load table, and the one supplied with ‘end-output-message’ is output after formatting it.
Continuing our ‘nsupdate’ example, the following statement will remove all existing A records from the DNS prior to creating new ones:
begin-output-message <<EOT server 10.0.0.1 update delete www.example.net send EOT; |
You may want to output only a part of the load table. Two statements are provided for this purpose. The ‘head N’ statement outputs at most N entries from the top of the table. The ‘tail N’ statement outputs at most N entries from the bottom of the table. For example, to print only one entry corresponding to the least loaded server, use
head 1; |
By default slb
prints results to the standard output. To
change this, use the ‘output-file’ statement. Its argument
specifies the name of a file where slb
output will be
directed. If this name starts with a pipe character (‘|’),
slb
treats the remaining characters as the shell command.
This command is executed (using /bin/sh -c
), and the output
is piped to its standard input. Thus, the following statement
output-file "| /usr/bin/nsupdate " "-k /usr/share/slb/Kvpn.+157+45756.private"; |
directs the formatted output to the standard input of
nsupdate
command.
The slb
configuration can be quite complex, therefore it is
important to verify that it behaves as expected before actually
implementing it in production. Three command line options are provided
for that purpose.
The ‘--dry-run’ (or ‘-n’) option instructs the program to start in foreground mode and print to the standard output what would have otherwise been printed to the output file. Additional debugging information is displayed on the standard error.
The ‘--eval’ option initiates expression evaluation mode. In
this mode slb
evaluates the expression whose name is
supplied as argument to the option. Actual values of the variables
and constants used in the expression are supplied in the command
line in form of variable assignments. The result is printed to the
standard output. For example, if the configuration file contains
expression load "(la1/100 + usr/1024)/2"; |
then the command
slb --eval=load la1=30 usr=800 |
will print ‘.540625’.
If the expression contains calls to ‘d()’ (derivative), you will need several evaluations to compute its value. The minimal number of evaluations equals the order of the highest derivative computed by the expression, plus 1. Thus, computing the following expression:
expression load "sqrt(k * d(out)**2 + m * la1**2)"; |
requires at least two evaluations. To supply several values to a single variable, separate them with commas, e.g.: ‘out=16000,20000’. This will run first evaluation with ‘out=16000’ and the second one with ‘out=20000’. For example:
$ slb --eval=load k=1.5 m=3 out=16000,20000 la1=0.4 16.3446 |
Notice, that you don't need to supply the same number of values for each variable. If a variable is assigned a single value, this value will be used in all evaluations.
A more elaborate test facility is enabled by the ‘--test’
option. This option instructs slb
to read SNMP variables
and their values from a file and act as if they were returned by SNMP.
The output is directed to the standard output, unless the
‘--output-file’ option is also given.
The input file name is taken from the first non-option argument. If it is ‘-’ (a dash) or if no arguments are given, the standard input will be read:
slb --test input.slb |
The input file consists of sections separated by exactly one empty
line. A section contains assignments to SNMP variables in the style
of snmpset
(2). Such assignments form several server groups, each
group being preceded by a single word on a line, followed by a
semicolon. This word indicates the ID of the server to which
the data in the group belong. For example:
srv01: UCD-SNMP-MIB::laLoadFloat.1 F 0.080000 IF-MIB::ifOutUcastPkts.3 c 346120120 srv02: UCD-SNMP-MIB::laLoadFloat.1 F 0.020000 IF-MIB::ifOutUcastPkts.3 c 2357911693 |
This section supplies data for two servers, named ‘srv01’ and ‘srv02’.
? |
Verbatim copying and distribution of this entire article is permitted in any medium, provided this notice is preserved.