Synopsis

Today, almost any ecommerce website uses a load-balancer or an application delivery controller in front of it, in order to improve its availability and reliability.
In today’s article, I’ll explain how we can take advantage of ADCs’ layer 7 features to improve an ecommerce website performance and give the best experience to end-user in order to increase the revenue.
The points on which we can work are:

  • Network optimization
  • Traffic regulation
  • Overusage protection
  • User “tagging” based on cart content
  • User “tagging” based purchase phase
  • Blackout prevention
  • SEO optimization
  • Partner slowness protection

Note: the list is not exhaustive and the given example will be very simple. My purpose is not to create a very complicated configuration but give the reader clues on how he can take advantage of our product.


Note2: I won’t discuss about static content, there is already one article with a lot of details about it on this blog.


As Usual, the configuration example below applies on our ALOHA ADC appliance, but should work as well on HAProxy 1.5.

Network optimization

Client-side network latency have a negative impact on websites: the slowest the user connectivity is, the longest the connection will remain opened on the web server, the time for the client to download the object. This could last much longer if the client and server uses HTTP Keepalives.
Basically, this is what happens with basic layer 4 load-balancers like LVS or some other appliance vendors, when the TCP connection is established between the client and the server directly.
Since HAProxy works as a HTTP reverse-proxy, it breaks the TCP connection and enables TCP buffering between both connections. It means HAProxy reads the response at the speed of the server and delivers it at the speed of the client.
Slow clients with high latency will have no impact anymore on application servers because HAProxy “hides” it by its own latency to the server.
An other good point is that you can enable HTTP Keepalives on the client side and disable it on the server side: it allows a client to re-use a connection to download several objects, with no impact on server resources.
TCP buffering does not require any configuration, while enabling client side HTTP keep-alive is achieved by the line option http-server-close.
And The configuration is pretty simple:
[sourcecode language=”text”]
# default options
defaults
option http-server-close
mode http
log 10.0.0.1 local2
option httplog
timeout connect 5s
timeout client 20s
timeout server 15s
timeout check 1s
timeout http-keep-alive 1s
timeout http-request 10s # slowloris protection
default-server inter 3s fall 2 rise 2 slowstart 60s

# main frontend
frontend ft_web
bind 10.0.0.3:80
default_backend bk_appsrv

# application server farm
backend bk_appsrv
balance roundrobin
# app servers must say if everything is fine on their side and
# they are ready to process traffic
option httpchk GET /appcheck
http-check expect rstring [oO][kK]
cookie SERVERID insert indirect nocache
server s1 10.0.1.101:80 cookie s1 check
server s2 10.0.1.102:80 cookie s2 check
[/sourcecode]

Traffic Regulation


Any server has a maximum capacity. The more it handles requests, the slower it will be to process each request. And if it has too many requests to process, it can even crash and won’t obviously be able to answer to anybody!
HAProxy can regulate request streams to servers in order to prevent them from crashing or even slowing down. Note that when well set up, it can allow you to use your server at their maximum capacity without never being in trouble.
Basically, HAProxy is able to manage request queues.
You can configure traffic regulation with fullconn and maxconn parameters in the backend and with minconn and maxconn parameters on the server line description.
Let’s update our server line description above with a simple maxconn parameter:
[sourcecode language=”text”]
server s1 10.0.1.101:80 cookie s1 check maxconn 250
server s2 10.0.1.102:80 cookie s2 check maxconn 250
[/sourcecode]
Note: there would be many many things to say about queueing and the HAProxy parameter cited above, but this is not the purpose of the current article.

Over usage protection

By over usage, I mean that you want to be able to handle an unexpected flow of users and be able to classify users in 2 categories:

  1. Those who have already been identified by the website and are using it
  2. Those who have just arrived and wants to use it

The difference between both type of users can be done through the ecommerce CMS cookie: identified users owns a Cookie while brand new users doesn’t.
If you know your server farm has the capacity to manage 10000 users, then you don’t want to allow more than this number until you expand the farm.
Here is the configuration to protect against over-usage (The application Cookie is “MYCOOKIE”.):
[sourcecode language=”text”]
# default options
defaults
option http-server-close
mode http
log 10.0.0.2 local2
option httplog
timeout connect 5s
timeout client 20s
timeout server 15s
timeout check 1s
timeout http-keep-alive 1s
timeout http-request 10s # slowloris protection
default-server inter 3s fall 2 rise 2 slowstart 60s

# main frontend
frontend ft_web
bind 10.0.0.3:80
# update the number below to the number of people you want to allow
acl maxcapacity table_cnt(bk_appsrv) ge 10000
acl knownuser hdr_sub(Cookie) MYCOOK
# route any unknown user to the sorry page if we reached the maximum number
# of allowed users and the request does not have a cookie
use_backend bk_sorrypage if maxcapacity !knownuser
default_backend bk_appsrv

# appsrv backend for dynamic content
backend bk_appsrv
balance roundrobin
# define a stick-table with at most 10K entries
# cookie value would be cleared from the table if not used for 10 mn
stick-table type string len 32 size 10K expire 10m nopurge
stick store-response set-cookie(MYCOOK)
# Reset the TTL in the stick table each time a request comes in
stick store-request cookie(MYCOOK)
# app servers must say if everything is fine on their side and
# they are ready to process traffic
option httpchk GET /appcheck
http-check expect rstring [oO][kK]
cookie SERVERID insert indirect nocache
server s1 10.0.1.101:80 cookie s1 check maxconn 250
server s2 10.0.1.102:80 cookie s2 check maxconn 250

# sorry page management
backend bk_sorrypage
balance roundrobin
server s1 10.0.1.103:80 check maxconn 1000
server s2 10.0.1.104:80 check maxconn 1000
[/sourcecode]

User tagging based on cart content

When your architecture has enough capacity, you don’t need to classify users. But imagine if your platform runs out of capacity, you want to be able to reserve resources for users who have no article in the cart, that way the website looks very fast, hopefully these users will buy some articles.
Just configure your ecommerce application to setup a cookie with some information about the cart: either the number of article, the whole value, etc…
In the example below, we’ll consider the application creates a cookie named CART and the number of articles as a value.
Based on the information provided by this cookie, we’ll take routing decision and choose different farms with different capacity.
[sourcecode language=”text”]
# default options
defaults
option http-server-close
mode http
log 10.0.0.2 local2
option httplog
timeout connect 5s
timeout client 20s
timeout server 15s
timeout check 1s
timeout http-keep-alive 1s
timeout http-request 10s # slowloris protection
default-server inter 3s fall 2 rise 2 slowstart 60s

# main frontend
frontend ft_web
bind 10.0.0.3:80
# update the number below to the number of people you want to allow
acl maxcapacity table_cnt(bk_appsrv) ge 10000
acl knownuser hdr_sub(Cookie) MYCOOK
acl empty_cart hdr_sub(Cookie) CART=0
# route any unknown user to the sorry page if we reached the maximum number
# of allowed users and the request does not own a cookie
use_backend bk_sorrypage if maxcapacity !knownuser
# Once the user have something in the cart, move it to a farm with less resources
# only when there are too many users on the website
use_backend bk_appsrv if maxcapacity !empty_cart
default_backend bk_appsrv_empty_cart

# Default farm when everything goes well
backend bk_appsrv_empty_cart
balance roundrobin
# create the entry in the table when the server generates the cookie
stick store-response set-cookie(MYCOOK) table bk_appsrv
# Reset the TTL in the stick table each time a request comes in
stick store-request cookie(MYCOOK) table bk_appsrv
# app servers must say if everything is fine on their side
# and they can process requests
option httpchk GET /appcheck
http-check expect rstring [oO][kK]
cookie SERVERID insert indirect nocache
server s1 10.0.1.101:80 cookie s1 check maxconn 200
server s2 10.0.1.102:80 cookie s2 check maxconn 200

# Reserve resources for the few users which have something in their cart
backend bk_appsrv
balance roundrobin
# define a stick-table with at most 10K entries
# cookie would be cleared from the table if not used for 10 mn
stick-table type string len 32 size 10K expire 10m nopurge
# create the entry in the table when the server generates the cookie
stick store-response set-cookie(MYCOOK)
# Reset the TTL in the stick table each time a request comes in
stick store-request cookie(MYCOOK)
cookie SERVERID insert indirect nocache
server s1 10.0.1.101:80 cookie s1 track bk_appsrv_empty_cart/s1 maxconn 50
server s2 10.0.1.102:80 cookie s2 track bk_appsrv_empty_cart/s2 maxconn 50

backend bk_sorrypage
balance roundrobin
server s1 10.0.1.103:80 check maxconn 1000
server s2 10.0.1.104:80 check maxconn 1000
[/sourcecode]

User tagging based on purchase phase

The synopsis of this chapter is the same as the precedent chapter: behing able to classify users and ability to reserve resources.
But this time, we’ll identify users based on the phase they are. Basically, we’ll consider two phases:

  1. browsing phase, when people add articles in the cart
  2. purchasing phase, when people have finished filling up the cart and start providing billing, delivery and payment information

In order to classify users, we’ll use the URL path. It starts by /purchase/ when the user is in the purchasing phase. Any other URLs are considered as browsing.
Based on the information provided by requested URL, we’ll take routing decision and choose different farms with different capacity.
[sourcecode language=”text”]
# defaults options
defaults
option http-server-close
mode http
log 10.0.0.2 local2
option httplog
timeout connect 5s
timeout client 20s
timeout server 15s
timeout check 1s
timeout http-keep-alive 1s
timeout http-request 10s # slowloris protection
default-server inter 3s fall 2 rise 2 slowstart 60s

# main frontend
frontend ft_web
bind 10.0.0.3:80
# update the number below to the number of people you want to allow
acl maxcapacity table_cnt(bk_appsrv) ge 10000
acl knownuser hdr_sub(Cookie) MYCOOK
acl purchase_phase path_beg /purchase/
# route any unknown user to the sorry page if we reached the maximum number
# of allowed users and the request does not own a cookie
use_backend bk_sorrypage if maxcapacity !knownuser
# Once the user is in the purchase phase, move it to a farm with less resources
# only when there are too many users on the website
use_backend bk_appsrv if maxcapacity purchase_phase
default_backend bk_appsrv_browse

# Default farm when everything goes well
backend bk_appsrv_browse
balance roundrobin
# create the entry in the table when the server generates the cookie
stick store-response set-cookie(MYCOOK) table bk_appsrv
# Reset the TTL in the stick table each time a request comes in
stick store-request cookie(MYCOOK) table bk_appsrv
# app servers must say if everything is fine on their side
# and they can process requests
option httpchk GET /appcheck
http-check expect rstring [oO][kK]
cookie SERVERID insert indirect nocache
server s1 10.0.1.101:80 cookie s1 check maxconn 200
server s2 10.0.1.102:80 cookie s2 check maxconn 200

# Reserve resources for the few users in the purchase phase
backend bk_appsrv
balance roundrobin
# define a stick-table with at most 10K entries
# cookie would be cleared from the table if not used for 10 mn
stick-table type string len 32 size 10K expire 10m nopurge
# create the entry in the table when the server generates the cookie
stick store-response set-cookie(MYCOOK)
# Reset the TTL in the stick table each time a request comes in
stick store-request cookie(MYCOOK)
cookie SERVERID insert indirect nocache
server s1 10.0.1.101:80 cookie s1 track bk_appsrv_browse/s1 maxconn 50
server s2 10.0.1.102:80 cookie s2 track bk_appsrv_browse/s2 maxconn 50

backend bk_sorrypage
balance roundrobin
server s1 10.0.1.103:80 check maxconn 1000
server s2 10.0.1.104:80 check maxconn 1000
[/sourcecode]

Blackout prevention

A website blackout is the worst thing that could happen: something has crashed and the application does not work anymore, or none of the servers are reachable.
When such thing occurs, it is common to get 503 errors or a blank page after 30 seconds.
In both cases, end users have a negative feeling about the website. At least an excuse page with an estimated recovery date would be appreciated. HAProxy allows to communicate to end user even if none of the servers are available.
The configuration below shows how to do it:
[sourcecode language=”text”]
# defaults options
defaults
option http-server-close
mode http
log 10.0.0.2 local2
option httplog
timeout connect 5s
timeout client 20s
timeout server 15s
timeout check 1s
timeout http-keep-alive 1s
timeout http-request 10s # slowloris protection
default-server inter 3s fall 2 rise 2 slowstart 60s

# main frontend
frontend ft_web
bind 10.0.0.3:80
# update the number below to the number of people you want to allow
acl maxcapacity table_cnt(bk_appsrv) ge 10000
acl knownuser hdr_sub(Cookie) MYCOOK
acl purchase_phase path_beg /purchase/
acl no_appsrv nbsrv(bk_appsrv_browse) eq 0
acl no_sorrysrv nbsrv(bk_sorrypage) eq 0
# worst case management
use_backend bk_worst_case_management if no_appsrv no_sorrysrv
# use sorry servers if available
use_backend bk_sorrypage if no_appsrv !no_sorrysrv
# route any unknown user to the sorry page if we reached the maximum number
# of allowed users and the request does not own a cookie
use_backend bk_sorrypage if maxcapacity !knownuser
# Once the user is in the purchase phase, move it to a farm with less resources
# only when there are too many users on the website
use_backend bk_appsrv if maxcapacity purchase_phase
default_backend bk_appsrv_browse

# Default farm when everything goes well
backend bk_appsrv_browse
balance roundrobin
# create the entry in the table when the server generates the cookie
stick store-response set-cookie(MYCOOK) table bk_appsrv
# Reset the TTL in the stick table each time a request comes in
stick store-request cookie(MYCOOK) table bk_appsrv
# app servers must say if everything is fine on their side
# and they can process requests
option httpchk GET /appcheck
http-check expect rstring [oO][kK]
cookie SERVERID insert indirect nocache
server s1 10.0.1.101:80 cookie s1 check maxconn 200
server s2 10.0.1.102:80 cookie s2 check maxconn 200

# Reserve resources for the few users in the purchase phase
backend bk_appsrv
balance roundrobin
# define a stick-table with at most 10K entries
# cookie would be cleared from the table if not used for 10 mn
stick-table type string len 32 size 10K expire 10m nopurge
# create the entry in the table when the server generates the cookie
stick store-response set-cookie(MYCOOK)
# Reset the TTL in the stick table each time a request comes in
stick store-request cookie(MYCOOK)
cookie SERVERID insert indirect nocache
server s1 10.0.1.101:80 cookie s1 track bk_appsrv_browse/s1 maxconn 50
server s2 10.0.1.102:80 cookie s2 track bk_appsrv_browse/s2 maxconn 50

backend bk_sorrypage
balance roundrobin
server s1 10.0.1.103:80 check maxconn 1000
server s2 10.0.1.104:80 check maxconn 1000

backend bk_worst_case_management
errorfile 503 /etc/haproxy/errors/503.txt
[/sourcecode]

And the content of the file /etc/haproxy/errors/503.txt could look like:
[sourcecode language=”text”]
HTTP/1.0 200 OK
Cache-Control: no-cache
Connection: close
Content-Type: text/html
Content-Length: 246

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Maintenance</title>
</head>
<body>
<h1>Maintenance</h1>
We’re sorry, ecommerce.com is currently under maintenance and will come back soon.
</body>
</html>
[/sourcecode]

SEO optimization

Most search engines takes now into account pages response time.
The configuration below redirects search engine bots to a dedicated server and if it’s not available, then it is forwarded to the default farm. The bot is identified by its User-Agent header.
[sourcecode language=”text”]
# defaults options
defaults
option http-server-close
mode http
log 10.0.0.2 local2
option httplog
timeout connect 5s
timeout client 20s
timeout server 15s
timeout check 1s
timeout http-keep-alive 1s
timeout http-request 10s # slowloris protection
default-server inter 3s fall 2 rise 2 slowstart 60s

# main frontend
frontend ft_web
bind 10.0.0.3:80
# update the number below to the number of people you want to allow
acl maxcapacity table_cnt(bk_appsrv) ge 10000
acl knownuser hdr_sub(Cookie) MYCOOK
acl purchase_phase path_beg /purchase/
acl bot hdr_sub(User-Agent) -i googlebot bingbot slurp
acl no_appsrv nbsrv(bk_appsrv_browse) eq 0
acl no_sorrysrv nbsrv(bk_sorrypage) eq 0
acl no_seosrv nbsrv(bk_seo) eq 0
# worst caperformancese management
use_backend bk_worst_case_management if no_appsrv no_sorrysrv
# use sorry servers if available
use_backend bk_sorrypage if no_appsrv !no_sorrysrv
# redirect bots
use_backend bk_seo if bot !no_seosrv
use_backend bk_appsrv if bot no_seosrv
# route any unknown user to the sorry page if we reached the maximum number
# of allowed users and the request does not own a cookie
use_backend bk_sorrypage if maxcapacity !knownuser
# Once the user is in the purchase phase, move it to a farm with less resources
# only when there are too many users on the website
use_backend bk_appsrv if maxcapacity purchase_phase
default_backend bk_appsrv_browse

# Default farm when everything goes well
backend bk_appsrv_browse
balance roundrobin
# create the entry in the table when the server generates the cookie
stick store-response set-cookie(MYCOOK) table bk_appsrv
# Reset the TTL in the stick table each time a request comes in
stick store-request cookie(MYCOOK) table bk_appsrv
# app servers must say if everything is fine on their side
# and they can process requests
option httpchk GET /appcheck
http-check expect rstring [oO][kK]
cookie SERVERID insert indirect nocache
server s1 10.0.1.101:80 cookie s1 check maxconn 200
server s2 10.0.1.102:80 cookie s2 check maxconn 200

# Reserve resources for the few users in the purchase phase
backend bk_appsrv
balance roundrobin
# define a stick-table with at most 10K entries
# cookie would be cleared from the table if not used for 10 mn
stick-table type string len 32 size 10K expire 10m nopurge
# create the entry in the table when the server generates the cookie
stick store-response set-cookie(MYCOOK)
# Reset the TTL in the stick table each time a request comes in
stick store-request cookie(MYCOOK)
cookie SERVERID insert indirect nocache
server s1 10.0.1.101:80 cookie s1 track bk_appsrv_browse/s1 maxconn 50
server s2 10.0.1.102:80 cookie s2 track bk_appsrv_browse/s2 maxconn 50

# Reserve resources search engines bot
backend bk_seo
option httpchk GET /appcheck
http-check expect rstring [oO][kK]
server s3 10.0.1.103:80 check

backend bk_sorrypage
balance roundrobin
server s1 10.0.1.103:80 check maxconn 1000
server s2 10.0.1.104:80 check maxconn 1000

backend bk_worst_case_management
errorfile 503 /etc/haproxy/errors/503.txt
[/sourcecode]

Partner slowness protection

Some ecommerce website relies on partners for some product or services. Unfortunately, if the partner’s webservice application slows down, then our own application will slow down. Even worst, we may see sessions pilling up and server crashes due to lack of resources…
In order to prevent this, just configure your appserver to pass through HAProxy to reach your partners’ webservices. HAProxy can shut a session if a partner is too slow to answer. If the partner complain you don’t send them enough deals, just tell him to improve his platform, maybe using a ADC like HAProxy / ALOHA Load-Balancer 😉
[sourcecode language=”text”]
frontend ft_partner1
bind 10.0.0.3:8001
use_backend bk_partner1

backend bk_partner1
# the partner has 2 seconds to answer each requests
timeout server 2s
# you can add a maxconn here if you’re not supposed to open
# too many connections on the partner application
server partner1 1.2.3.4:80 check
[/sourcecode]

Related links

Links