Adding Latency and Limiting Bandwidth

Some aspects of linux have the reputation of being hard. Traffic
control via queueing disciplines for bandwidth management for example.
Even the title is enough to strike fear into the heart of a seasoned
system admin.

Which is a pity really, as the things outlined in chapter 9
of the lartc are very useful in practise.  The problem is the
documentation is very descriptive – which is good once you know roughly
what you’re doing – but which has quite a steep learning curve if you
don’t. In fact it’s pretty vertical if you don’t already know quite a
lot about networking. A few more worked examples would help over and
above those in the cookbook.

Instead, like most people in a rush, I have relied on attempting to
bash together snippets of code that are on random blogs to make /sbin/tc do what I want it to do, without really understanding what is going on. 

This time, when presented with a problem for which this is the exact
tool, I found I needed to dive deeper, and actually understand it, as
none of the precanned recipes worked. It was a case of “if all else
fails try the manual”.

So now I think I’ve got a vague handle on what is going on, I’m
documenting what I ended up doing because I’m sure I will need a worked
example when I come back to this in the future. If its useful to you
too, so much the better.

The Problem

We have a need to test the loading speed of our web page and trading
platform under a set of network conditions that approximate the
following;

  1. Local LAN, unrestricted
  2. “Europe”, 20ms round trip latency, limit of 512kbit/sec in and out
  3. “SE Asia”, 330ms round trip latency, limit of 128kbit/sec in and out.

In practise thats quite generous, particularly in the case of the
south east asia profile. There was no way I was getting 128kbit on the
wifi in shakey’s on Rizal Boulevard in Dumaguete earlier this month.
Which was better than the hotel wifi.

The Solution

Background

We have selenium to run the tests via webdriver/remotedriver to two
windows virtual machines, one running chrome and one running IE. They
run on a Linux host system, and can see a loadbalancer behind which lies
one of our performance test environments. We need to add latency and
bandwidth restrictions to their connections, effectively to put them
into each of the traffic classes above depending on which test our CI
system asks them to run.

The load balancer has been set up with three virtual servers, all listening on the same IP address but different ports.

  1. Local:  9090
  2. Europe: 9092
  3. SE Asia: 9091

Each virtual server has the same webserver pool behind it, so they’re
all the same from the point of view of the load balancer, but we’ll use
the different destination ports to switch the traffic between the
different sets of network latency and bandwidth restriction we need to
simulate the different customer locations.

The linux virtual machine host has the guests vnet network devices
attached to a bridge. In turn the bridge is attached to the network, via
a bonded interface. In our case bond0.30. 

To make this work for both machines, we’ll apply the traffic management on the bond0.30 side of the bridge.

Ascii art diagram of that;

    IE Windows VM - vnet0                               eth0
/
host bridge 30 - bond0.30
/
Chrome Windows VM - vnet1  eth1

 

Qdiscs and Classes

There are three creatures we’re dealing with here;

  • qdisc – a Queueing Discipline. These are the active things we’re
    going to use to control how the traffic is managed.  qdiscs can be
    classless or classful. We’re going to use a classful qdisc called htb
  • classes – We’ll use these to separate the traffic into its constituent flows and to apply different constraints on each flow.
  • filters  – Similarly to iptables, these allow us to specify which traffic ends up in which class.

Chapter 9 says that you can only shape transmitted traffic, which is
not 100% accurate, as we can do things to inbound traffic too, however
our options are very limited.

So, looking at the default qdiscs, classes and filters

[root@vm01 ~]# tc -s qdisc show dev bond0.30     
qdisc pfifo_fast 0: bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
 Sent 47844819829 bytes 140593932 pkt (dropped 0, overlimits 0 requeues 22)
 rate 0bit 0pps backlog 0b 0p requeues 22
[root@vm01 ~]# tc -s class show dev bond0.30
[root@vm01 ~]# tc -s filter show dev bond0.30
[root@vm01 ~]#

The “-s” option shows the statistics. So, but default, we have a queue discipline called pfifo_fast, which just passes traffic.

Each device has a default root which we use to build upon. We can
also attach handles to classes and qdiscs to allow us to relate each
part to the others and build up chains to process the packet stream.
“root” is shorthand for a handle of 1:0, or the top of the tree. 

One of the most useful pages I found is here; http://luxik.cdi.cz/~devik/qos/htb/manual/userg.htm

Things worth repeating from that link are;

  • tc tool (not only HTB) uses shortcuts to denote units of rate. kbps means kilobytes and kbit means kilobits
  • Note: In general (not just for HTB but for all qdiscs and
    classes in tc), handles are written x:y where x is an integer
    identifying a qdisc and y is an integer identifying a class belonging to
    that qdisc. The handle for a qdisc must have zero for its y value and
    the handle for a class must have a non-zero value for its y value. The
    “1:” above is treated as “1:0”

The whole page is worth reading carefully.

The Design

 .

The Pentagons are filters, the circles represent qdiscs, and the
rectangles are classes. One important point is that this diagram in no
way implies flow. This is hard to get, and I had problems understanding
the comments in section 9.5.2.1 “How filters are used to classify
traffic” – particularly;

“You should *not* imagine the kernel to be at the apex of
the tree and the network below, that is just not the case. Packets get
enqueued and dequeued at the root qdisc, which is the only thing the
kernel talks to.”

.

 The way I squared it in the end was to think of it as an order of application for traffic flowing through the root qdisc.

So in the above we have the root qdisc, which is an instance of the
HTB qdisc. From that depends each of the classes we set up to handle the
three different classes of traffic. We use htb to limit the outbound
bandwidth for each of the classes (1:10, 1:11, 1:12). When we define the
root qdisc we specify that class 1:10 will be our default class for the
bulk of the traffic we don’t want to delay.

Setting up the root qdisc;

INTERFACE=bond0.30
tc qdisc add dev $INTERFACE root handle 1:0 htb default 10

 “root” is a synonym for handle 1:0.  $INTERFACE is defined
in the shell script to make the porting from machine to machine easier.
This installs the htb qdisc on the root for our bond interface, and
tells it that by default all traffic should be put in a class called
1:10.

Now we add classes for each of the types of traffic, along with the
bandwidth limits we want to enforce on each of the traffic classes.

# default class
tc class add dev $INTERFACE parent 1:0 classid 1:10 htb rate 1024mbit

# "europe" traffic class - outbound bandwidth limit
tc class add dev $INTERFACE parent 1:0 classid 1:11 htb rate 512kbit

# "se asia" traffic class - outbound bandwidth limit
tc class add dev $INTERFACE parent 1:0 classid 1:12 htb rate 128kbit

 We now attach the network emulator qdisc, netem, which we will use to introduce latency into each of the classes;

# network emulation - add latency.
tc qdisc add dev $INTERFACE parent 1:11 handle 11:0 netem delay 20ms 5ms 25%
 distribution normal
tc qdisc add dev $INTERFACE parent 1:12 handle 12:0 netem delay 330ms 10ms 25%
distribution normal

This attaches the emulator instances to their parent classes, with
handles that match the parents Y value, for ease of tracing.The netem
parameters break down as follows.

  • delay 20ms – This is pretty self explanatory.
  • 5ms – this is a jitter on the latency to give a bit of variation
  • 25% – this indicates how much the variation in the latency of each packet will depend on its predecessor
  • distribution normal – how the variation is distributed. 

The netem module is described completely here:  http://www.linuxfoundation.org/collaborate/workgroups/networking/netem

One thing  that could be improved here is that we’re adding all
the latency on the outbound leg. Ideally we’d add 165 ms on the way
there and on the way back for the SEAsia traffic (and 10ms for the EU
traffic). To do that means applying latency to the outbound interfaces
in both directions. In our case that would mean applying 165ms of
latency to both of the vnet interfaces as well as the bond0.30
interface. However that is tricky to do simply as the virtual machine
interface names may change as they get rebooted. Instead this way we end
up with the same result for far less faffing about.

Now, all we need to do is add the filters that classify the packets into their classes

SEASIAIP=172.16.10.10
SEASIAPORT=9091
EUIP=172.16.10.10
EUPORT=9092

# filter packets into appropriate traffic classes.
tc filter add dev $INTERFACE protocol ip parent 1:0 prio 1
u32 match ip dst $SEASIAIP match ip dport $SEASIAPORT 0xffff flowid 1:12
tc filter add dev $INTERFACE protocol ip parent 1:0 prio 1
u32 match ip dst $EUIP match ip dport $EUPORT 0xffff flowid 1:11

The action is mainly in the second line of each command, where we
match the target IP of the load balancer, and the ports we’ve setup. The
flowid is the class handle for the appropriate classes. We don’t need
to set up a filter for the “normal” traffic, as it is covered by the
“default 10” part of the original htb root qdisc declaration.

And that takes care of the outbound traffic shaping and latency.

We now need to handle inbound.

For this we use the special ingress qdisc. There’s very little we can
actually do with this qdisc. It has no classes, and all you can really
do is to attach a filter to it. Usefully we can use the “police” key
word to restrict (by packet dropping) the inbound flow. Its not exact,
but its good enough for our purposes.

# inbound qdisc.
tc qdisc add dev $INTERFACE handle ffff: ingress

# attach a policer for "se asia" class.
tc filter add dev $INTERFACE protocol ip parent ffff: prio 1
u32 match ip src $SEASIAIP  match ip sport $SEASIAPORT 0xffff
police rate 128kbit burst 10k drop flowid :1

# attach a policer for "europe" traffic class.
tc filter add dev $INTERFACE protocol ip parent ffff: prio 1
 u32 match ip src $EUIP match ip sport $EUPORT 0xffff
police rate 512kbit burst 10k drop flowid :2

The handle ffff: is a synonym for the inbound traffic root. All you
can do is attach ingress to it as shown. To be frank I’ve not dived into
exactly how the burst keyword affects things. Essentially the above
filter rule is the same as the one we used on the outbound side except
we now match the source ports and IPs rather than the destination ports
and IPs. Then rather than using the flowid argument we use police to
instruct the kernel to drop packets from each of our loadbalancer ports
if they exceed the stated rates.

Cleanup

To clean up after all of this, its sufficient to just remove the root
and ingress qdiscs. Removing the top of the tree removes all the other
configuration.

# remove any existing ingress qdisc.
tc qdisc del dev $INTERFACE ingress
# remove any existing egress qdiscs
tc qdisc del dev $INTERFACE root

 Which cleans up all classes and filters.

Conclusion

There’s an init script that encapsulates all of the above which can be downloaded from here.

[root@vm01 ~]# chkconfig latency on
[root@vm01 ~]# /etc/init.d/latency      
Usage: /etc/init.d/latency {start|stop|restart|condrestart|status}
[root@vm01 ~]# /etc/init.d/latency start
[root@vm01 ~]# /etc/init.d/latency stop
[root@vm01 ~]# /etc/init.d/latency status
 Active Queue Disciplines for bond0.10

 Active Queueing Classes for bond0.10

 Active Traffic Control Filters for bond0.10
[root@vm01 ~]#

And thats it.

This mainly suits a static configuration, as is the case with our
load balancer and continuous integration environment. However for web
development use, this approach lacks flexibility, particularly if you
don’t have root access. For our developers, I looked at ipdelay but
eventally settled with charles which was adequate for our purposes.

 HTH.

Any opinions, news, research, analyses, prices or other information ("information") contained on this Blog, constitutes marketing communication and it has not been prepared in accordance with legal requirements designed to promote the independence of investment research. Further, the information contained within this Blog does not contain (and should not be construed as containing) investment advice or an investment recommendation, or an offer of, or solicitation for, a transaction in any financial instrument. LMAX Group has not verified the accuracy or basis-in-fact of any claim or statement made by any third parties as comments for every Blog entry.

LMAX Group will not accept liability for any loss or damage, including without limitation to, any loss of profit, which may arise directly or indirectly from use of or reliance on such information. No representation or warranty is given as to the accuracy or completeness of the above information. While the produced information was obtained from sources deemed to be reliable, LMAX Group does not provide any guarantees about the reliability of such sources. Consequently any person acting on it does so entirely at his or her own risk. It is not a place to slander, use unacceptable language or to promote LMAX Group or any other FX and CFD provider and any such postings, excessive or unjust comments and attacks will not be allowed and will be removed from the site immediately.