Load balancing
Setup
We will be using a virtual machine in the faculty's cloud.
When creating a virtual machine in the Launch Instance window:
- Name your VM using the following convention:
scgc_lab<no>_<username>
, where<no>
is the lab number and<username>
is your institutional account. - Select Boot from image in Instance Boot Source section
- Select SCGC Template in Image Name section
- Select the m1.large flavor.
In the base virtual machine:
-
Download the laboratory archive from here in the
work
directory. Use:wget https://repository.grid.pub.ro/cs/scgc/laboratoare/lab-lb.zip
to download the archive. -
Extract the archive. The
.qcow2
files will be used to start virtual machines using therunvm.sh
script. -
Start the virtual machines using
bash runvm.sh
. -
The username for connecting to the nested VMs is
student
and the password isstudent
.
$ # change the working dir
$ cd ~/work
$ # download the archive
$ wget https://repository.grid.pub.ro/cs/scgc/laboratoare/lab-lb.zip
$ unzip lab-lb.zip
$ # start VMs; it may take a while
$ bash runvm.sh
$ # check if the VMs booted
$ virsh net-dhcp-leases labvms
Load balance topology
The machines in the topology (the three KVM machines and the host) have the following roles:
- load-balancer is the director. It handles the load balancing for the other two virtual machines (the real servers);
- real-server-1 and real-server-2 are the real servers;
- the host is the client that requests data from the servers.
Linux Virtual Server (LVS)
Linux Virtual Server (LVS) is an advanced, open source load balancing solution. It is also integrated in the Linux kernel. The IP virtual server that we will present in this section is an extension of LVS and is a transport-layer load balancer. This type of load balancer is also known as a Layer 4 LAN switch, since it does not use application layer information.
In LVS terminology, the load balancing server is called the Director, whereas the machines that respond to requests are named Real Servers (RS). A client will access the service exclusively using the address of the director.
LVS has three operation modes:
- LVS-NAT - the director performs NAT for the real servers. It is useful when the RSs do not have public IP addresses and are part of the same network. It does not scale well because the entire traffic must go through the director;
- LVS-TUN - the director tunnels the client packets and the real servers communicate directly with the client. It scales better than LVS-NAT because only the client traffic goes through the director, but it requires implementing a tunneling configuration on the real servers;
- LVS-DR - the director routes the packets towards the real servers without tunneling and the real servers communicate with the client directly. It removes the tunneling prerequisite, but both the director and the real servers must have their network interfaces in the same LAN. Additionally, the real servers must be able to answer requests sent to the director, since the request destination addresses are not overwritten.
LVS-DR (direct routing)
An HTTP service will be load balanced. The nginx
web server is already
installed on the real servers. The director will split the client requests to
the two real servers.
To manage the IP virtual server, you must first install the ipvsadm
package on
the director system.
student@load-balancer:~$ sudo apt update
student@load-balancer:~$ sudo apt install ipvsadm
We will first configure a virtual address on the director machine. We will add
the 192.168.100.251/24
address on the eth0:1
sub-interface on the
load-balancer
machine.
student@load-balancer:~$ sudo ip addr add dev eth0 192.168.100.251/24 label eth0:1
Configure IPVS in DR mode
We will configure the HTTP service as a virtual service. To do this, we need to
specify the virtual address, port and transport protocol used (TCP, in our
case). The following command creates a service (the -A
parameter) that
handles requests coming to the 192.168.100.251
IP address on TCP port 80:
student@load-balancer:~$ sudo ipvsadm -A -t 192.168.100.251:80
Once the virtual service has been configured, we can also add the real servers:
student@load-balancer:~$ sudo ipvsadm -a -t 192.168.100.251:80 -r 192.168.100.72:80 -g
student@load-balancer:~$ sudo ipvsadm -a -t 192.168.100.251:80 -r 192.168.100.73:80 -g
In the commands above we add the real servers (with IP addresses
192.168.100.72
and 192.168.100.73
) to the service that was created on
192.168.100.251:80
. Routing will be done in direct route (DR) mode (set using
the -g
parameter).
We also need to make the real servers answer to requests that are meant for the virtual address. There are two ways of achieving this:
- configure the virtual address on a loopback interface on the real servers. A disadvantage of doing this is that the real servers might respond to ARP messages that are actually meant for the director. This issue is known as the ARP problem;
- configure an
iptables
rule that causes the real server to accept packets even though the virtual address is not configured on any interface. We will use this approach.
student@real-server-1:~$ sudo iptables -t nat -A PREROUTING -d 192.168.100.251 -j REDIRECT
student@real-server-2:~$ sudo iptables -t nat -A PREROUTING -d 192.168.100.251 -j REDIRECT
The virtual service is now completely configured and we can use it.
To test the functionality, fetch the HTTP page from 192.168.100.251
. Download
the page repeatedly and notice how the service behaves.
We can use tcpdump
to start a capture of the packets that traverse the
virbr-labs
bridge on the host, with link-level header inspection (the -e
parameter). You could optionally add the -A
parameter to also print the packet
bodies.
student@lab-lb-host:~/work$ sudo tcpdump -i virbr-labs -e src port 80 or dst port 80
Check the IP and MAC addresses of the packets:
- sent from the client towards the director;
- sent from the director towards the real servers;
- replied by the real servers.
The configuration of the virtual server can be inspected using the -l
parameter:
student@load-balancer:~$ sudo ipvsadm -l
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 192.168.100.251:http wlc
-> lab-lb-2:http Route 1 0 0
-> lab-lb-3:http Route 1 0 0
A list of connections that are managed by the virtual server can be obtained
using the -c
parameter:
student@load-balancer:~$ sudo ipvsadm -l -c
Adding a connection threshold
We can update the basic configuration to change the server's behaviour.
Edit the configuration to set a maximum number of connections redirected towards each real server. Set the limit to three for each server.
The edit command is similar to the one that added the real servers,
but the -a
parameter is changed to -e
. You must add the proper parameter to
set the connection threshold.
Start a large number of connections (e.g., using a for
loop) and see how the
servers respond to the requests.
For real servers with different hardware configurations, a different number of maximum connections can be configured. Alternatively, we can use different types of schedulers (e.g., weighted round-robin with different weights for each server).
Cleanup
To delete the service, use the -D
parameter:
student@load-balancer:~$ sudo ipvsadm -D -t 192.168.100.251:80
You must also delete the iptables rules on the real servers:
student@real-server-1:~$ sudo iptables -t nat -F
student@real-server-2:~$ sudo iptables -t nat -F
LVS-TUN (tunneling)
For this task we will configure the IP virtual server in tunneling mode.
Configure IPVS in TUN mode
Similar to the previous task, configure the virtual service on the director and add the real servers in tunneling mode.
You must replace the -g
parameter.
As mentioned before, when using the tunneling mode we must configure a tunnel interface on both real servers. The tunnel interface's IP address must be the same as the IP address of the director.
To create an IP-IP tunnel interface on the first real server you can use the following command:
student@real-server-1:~$ sudo ip tunnel add tun0 mode ipip local 192.168.100.72
Create a tunnel interface on the second real server. Make sure to set the
correct local IP address. After creating the interfaces, add the director's IP
address (192.168.100.251/24
) on the tunnel interfaces and activate them on
both servers.
When adding the IP address to the IP-IP tunnel, make sure to set a metric of
32768
(or larger than the 100
used for the eth0
interface). Otherwise, the
server will prefer using the tunnel for the 192.168.100.0/24
network and will
become inaccessible.
If you have set the wrong metric, you can reboot the virtual machine using
virsh reboot lab-vm-X
. Alternatively, you can access its console using VNC and
removing the tunnel.
Check how connections are handled using tcpdump
. Compare the connections
handling to how connections were handled in direct routing mode.
Cleanup
Remove the service on the director and the tunnel interfaces on the real servers.
student@real-server-1:~$ sudo ip tunnel del tun0
student@real-server-2:~$ sudo ip tunnel del tun0
HAProxy
HAProxy is a service that can function as a high-availability load balancer and proxy. HAProxy can function as a TCP load balancer, meaning that it can proxy random TCP streams, such as SSH connections, but for the purposes of this lab, we are interested in configuring it as an HTTP proxy.
HAProxy as load balancer
To use HAProxy, it must first be installed on the load balancer system:
student@load-balancer:~$ sudo apt update
student@load-balancer:~$ sudo apt install haproxy
Make sure to enable and start the HAProxy.
student@load-balancer:~$ sudo systemctl enable --now haproxy
We can configure HAProxy as a load balancer and reverse proxy for the two HTTP
servers, real-server-1
and real-server-2
. To do this, edit the
/etc/haproxy/haproxy.cfg
file and configure the server to listen for HTTP
connections and use the two servers as the backends (append the following
configuration at the end of the file):
frontend www
bind *:80
default_backend realservers
backend realservers
mode http
balance roundrobin
server realserver-1 192.168.100.72:80
server realserver-2 192.168.100.73:80
Restart the HAProxy whenever you edit the settings file to reload the changes.
Check how connections are handled using tcpdump
. Compare the connections
handling to how they were handled when using IP virtual server.
Health checks
HAProxy can automatically detect when the backend servers are down and remove them from the pool of available servers to avoid sending requests to the servers when they are unavailable.
To add health checks, edit the configuration files and add the
httpchk
option in the backend
section, and then enable checks on the backend servers.
Set the check interval to 3 seconds.
To enable health checks on the server, you must use the
check setting, and
configure the interval using the inter
keyword.
Since the backend servers only have a basic configuration that serves static
pages (an index.html
file), you cannot use the default health check method
(OPTIONS
). You will have to use the GET
method instead. To change the used
method, use the http-check send directive and configure it to use the GET
.
A fully functional application written using a programming language / framework
like PHP or NodeJS will likely handle the OPTIONS
HTTP method without issues.
After adding the health checks, see how HAProxy behaves when stopping the
nginx
service on one of the real servers. Verify the service's logs and how
new requests are directed through the load balancer.
HAProxy response caching
HAProxy can cache server responses to reduce the load on the web servers. This is an important functionality since some files (e.g., CSS style pages, JavaScript script, static images) are the same for all users. Consequently such pages can be cached by the load balancer and served without requesting them from the backend.
Configure HAProxy caching
The cache must first be created at the top level of the configuration file, and
then used in the backend
section. You can find more details in the
cache section of the
documentation.
We will configure a 256MB cache that will be used to store objects of a maximum of 20MB. Only responses that are smaller than the maximum size (and are cacheable according to the conditions defined in the documentation) will be added to the cache. Additionally, we configure the cache such that objects that are older than five seconds will expire. When an object's cache entry has expired, it will be requested again from the backend servers.
backend realservers
[...]
http-request cache-use main
http-response cache-store main
server [...]
cache main
total-max-size 256
max-object-size 20971520
max-age 5
Observe the behaviour of the load balancer after the cache has been enabled. Send multiple requests to the server and see how the responses change when using, or not using the cache. Keep sending requests for more than 10 seconds.
We can extract information about the cached data using the administration
socket. To do this, we will use socat
to connect to the socket and enter
commands:
student@load-balancer:~$ echo 'show cache' | sudo socat - /run/haproxy/admin.sock
Blocking response caching
The web server can decide that some responses should not be cached, or that the response must be re-validated by the client before reusing. We can use the Cache-Control HTTP header to change how the response is be cached.
For example, we can set the no-cache
policy on the responses coming from the
second web server. Edit the /etc/nginx/sites-available/default
page on the
real-server-2
server and add the following configuration in the server
section.
location /index.html {
add_header Cache-Control no-cache;
}
After editing the configuration, restart the nginx
service. Observe what
responses the client receives and the stats about HAProxy caches.
On production servers it usually does not make sense to set the no-cache
parameter on plain HTML files. However, some response categories, such as
transitory messages used in authentication protocols should not be cached, since
they should not be reusable, and caching them may leak confidential information.
Performance improvements using caching
To get a better view of the load on the systems, we must first configure HAProxy
to only use one of the backend servers. Remove the realserver-2
from the
configuration and restart HAProxy. Afterwards, confirm that you only get replies
from the first server.
We will use the httperf
tool to test performance. Install the tool on the host
system:
student@lab-lb-host:~$ sudo apt install httperf
To start testing using the index file, use the following command:
student@lab-lb-host:~$ httperf --server 192.168.100.251 --port 80 --num-conns 10000 --rate 1000 --timeout 5
Observe the load on the load balancer and the real server using htop
. When the
test is finished, see the numbers printed by httperf
for Connection rate
,
Request rate
, Net I/O
.
Disable response caching and repeat the tests. See how the numbers change.
Securing connections with HTTPS
HTTPS is a protocol that encrypts communication between the client and the server. This makes using HTTPS a requirement on all sites that require authentication, or handle any sort of confidential information. Anyone with access to the network can insert sniffing tools or implement various types of attacks that force traffic to go through their system; without the encryption provided by HTTPS they can view the raw packets and extract any transmitted information.
Currently, almost all sites implement HTTPS, by either using a paid certificate from a certification authority, or free certificates from authorities like Let's Encrypt.
For the purposes of this lab, since we do not have a (public) domain for our services, we will use locally signed certificates - either a self-signed certificate, or certificates signed by an internal certification authority. Review the generating a certificate section for more details.
Using HTTPS with HAProxy
Before enabling HTTPS, create a self-signed certificate in the
/etc/ssl/private
directory (see the crt-base
directive in the HAProxy
configuration). For this example, we will assume that you will create the
load-balancer.pem
file as the certificate.
To enable HTTPS you must use the ssl
setting and specify the certificate file
using the crt
setting on the bind
directive. The default port for HTTPS is
443
.
frontend www
bind *:80
bind *:443 ssl crt load-balancer.pem
[...]
The crt directive
expects all certificate information to be placed in the same file (i.e., the
certificate, private key, issuer certificate chain, etc., must be concatenated
in a larger *.pem
file) and HAProxy will automatically extract the data from
that file.
If the certificate file does not contain some of the expected information, it
will attempt to use some pre-defined file extensions to look for it. For
example, you could have the key in a separate file called
load-balancer.pem.key
.
Both wget
and curl
support connecting to an HTTPS server and expect the
server to use a certificate that has been signed by a trusted authority. Since
we do not have a trusted certificate, you can pass the -k
parameter to curl
,
or the --no-check-certificate
parameter to wget
.
Automatic HTTPS redirects
A good practice is redirecting requests coming to the HTTP port (80) to use
HTTPS. To do this, we can add the following line to the frontend
section of
the HAProxy configuration file:
frontend www
[...]
redirect scheme https if !{ ssl_fc }
To follow redirects, you can use the -L
flag for curl
. wget
should
automatically follow redirects and download the page.
Internal HTTPS servers
Even when using HTTPS for client connections, establishing connections to the backend servers using plain HTTP implies that we trust our internal network. We can reduce the risk of having internal traffic intercepted (at the cost of computational resources required for encryption) by adding TLS encryption to all internal servers.
Begin by enabling HTTPS on the two backend servers. You will have to create certificates for both backend servers. You can choose to either create a self-signed certificate for each server, or create a CA on the host system that you will use to sign the certificates.
We will assume that the certificates are created as
/etc/ssl/private/real-server.pem
and the key is
/etc/ssl/private/real-server.key
. Add the following configuration in the
/etc/nginx/sites-enables/ssl
file:
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_certificate /etc/ssl/private/real-server.pem;
ssl_certificate_key /etc/ssl/private/real-server.key;
root /var/www/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
After the configuration has been added, restart the nginx
service on both
servers and check that you can send HTTPS requests to them.
At this point all that is left is configuring HAProxy to connect to the backend servers using HTTPS. You must set the correct port and enable the use of SSL when connecting.
Since the backend servers do not use certificates that are signed by a trusted
authority, you will have to let HAProxy know that it can trust them. Use the
ca-file setting
on the server
directives to specify the certificate of the CA (or the
certificate files in case of self-signed certificates) that HAProxy should
trust.