Around IT in 256 seconds

Writing a download server. Part V: Throttle download speed

July 06, 2015 | 6 Minute Read

In the age of botnets that you can rent for few hundred bucks and run your very own distributed-denial-of-service attack, having emergency switches that selectively turn off expensive functionality or degrade performance gratefully is a huge win. Your application is still operational while you mitigate the problem. Of course such safety measure are also valuable under peaks or business hours. One of such mechanisms applying to download servers is throttling download speed dynamically. In order to prevent distributed denial of service attack and excessively high cloud invoices, consider built-in download throttling, that you can enable and fine-tune at runtime. The idea is to limit maximum download speed, either globally or per client (IP? Connection? Cookie? User agent?).

I must admit, I love java.io design with lots of simple Input/OutputStream and Reader/Writer implementations, each having just one responsibility. You want buffering? GZIPing? Character encoding? File system writing? Just compose desired classes that always work with each other. All right, it's still blocking, but it was designed before reactive hipsters were even born. Nevermind, java.io also follows open-closed principle: one can simply enhance existing I/O code without touching built-in classes - but by plugging in new decorators. So I created a simple decorator for InputStream that slows down reading resource on our side in order to enforce given download speed. I am using my favorite RateLimiter class:

public class ThrottlingInputStream extends InputStream {

private final InputStream target;
private final RateLimiter maxBytesPerSecond;

public ThrottlingInputStream(InputStream target, RateLimiter maxBytesPerSecond) {
this.target = target;
this.maxBytesPerSecond = maxBytesPerSecond;
}

@Override
public int read() throws IOException {
maxBytesPerSecond.acquire(1);
return target.read();
}

@Override
public int read(byte[] b) throws IOException {
maxBytesPerSecond.acquire(b.length);
return target.read(b);
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
maxBytesPerSecond.acquire(len);
return target.read(b, off, len);
}

//less important below...

@Override
public long skip(long n) throws IOException {
return target.skip(n);
}

@Override
public int available() throws IOException {
return target.available();
}

@Override
public synchronized void mark(int readlimit) {
target.mark(readlimit);
}

@Override
public synchronized void reset() throws IOException {
target.reset();
}

@Override
public boolean markSupported() {
return target.markSupported();
}

@Override
public void close() throws IOException {
target.close();
}
}
Arbitrary InputStream can be wrapped with ThrottlingInputStream so that reading is actually slowed down. You can either create new RateLimiter per each ThrottlingInputStream or one global, shared by all downloads. Of course one might argue that simple sleep() (what RateLimiter does underneath) wastes a lot of resources, but let's keep this example simple and avoid non-blocking I/O. Now we can easily plug decorator in:

private InputStreamResource buildResource(FilePointer filePointer) {
final InputStream inputStream = filePointer.open();
final RateLimiter throttler = RateLimiter.create(64 * FileUtils.ONE_KB);
final ThrottlingInputStream throttlingInputStream = new ThrottlingInputStream(inputStream, throttler);
return new InputStreamResource(throttlingInputStream);
}
Example above limits download speed to 64 KiB/s - obviously in real life you would want to have such number configurable, preferably at runtime. BTW we already talked about the importance of Content-Length header. If you monitor the progress of download with pv, it will correctly estimate remaining time, which is a nice feature to have:

~ $ curl localhost:8080/download/4a8883b6-ead6-4b9e-8979-85f9846cab4b | pv > /dev/null
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 71.2M 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 16kB 0:00:01 [14,8kB/s]
0 71.2M 0 40960 0 0 30097 0 0:41:21 0:00:01 0:41:20 30095 80kB 0:00:02 [ 64kB/s]
0 71.2M 0 104k 0 0 45110 0 0:27:35 0:00:02 0:27:33 45106 144kB 0:00:03 [ 64kB/s]
0 71.2M 0 168k 0 0 51192 0 0:24:18 0:00:03 0:24:15 51184 208kB 0:00:04 [ 64kB/s]
0 71.2M 0 232k 0 0 54475 0 0:22:51 0:00:04 0:22:47 54475 272kB 0:00:05 [63,9kB/s]
0 71.2M 0 296k 0 0 56541 0 0:22:00 0:00:05 0:21:55 67476 336kB 0:00:06 [ 64kB/s]
0 71.2M 0 360k 0 0 57956 0 0:21:28 0:00:06 0:21:22 65536 400kB 0:00:07 [ 64kB/s]
0 71.2M 0 424k 0 0 58986 0 0:21:06 0:00:07 0:20:59 65536 464kB 0:00:08 [ 64kB/s]
0 71.2M 0 488k 0 0 59765 0 0:20:49 0:00:08 0:20:41 65536 528kB 0:00:09 [ 64kB/s]
0 71.2M 0 552k 0 0 60382 0 0:20:36 0:00:09 0:20:27 65536 592kB 0:00:10 [ 64kB/s]
0 71.2M 0 616k 0 0 60883 0 0:20:26 0:00:10 0:20:16 65536 656kB 0:00:11 [ 64kB/s]
0 71.2M 0 680k 0 0 61289 0 0:20:18 0:00:11 0:20:07 65536 720kB 0:00:12 [ 64kB/s]
As an extra bonus pv proved our throttling works (last column). Now without Content-Length pv is clueless about the actual progress:

~ $ curl localhost:8080/download/4a8883b6-ead6-4b9e-8979-85f9846cab4b | pv > /dev/null
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 16384 0 16384 0 0 21116 0 --:--:-- --:--:-- --:--:-- 21113 32kB 0:00:01 [ 31kB/s]
100 81920 0 81920 0 0 46149 0 --:--:-- 0:00:01 --:--:-- 46126 96kB 0:00:02 [ 64kB/s]
100 144k 0 144k 0 0 53128 0 --:--:-- 0:00:02 --:--:-- 53118 160kB 0:00:03 [ 64kB/s]
100 208k 0 208k 0 0 56411 0 --:--:-- 0:00:03 --:--:-- 56406 224kB 0:00:04 [ 64kB/s]
100 272k 0 272k 0 0 58328 0 --:--:-- 0:00:04 --:--:-- 58318 288kB 0:00:05 [ 64kB/s]
100 336k 0 336k 0 0 59574 0 --:--:-- 0:00:05 --:--:-- 65536 352kB 0:00:06 [ 64kB/s]
100 400k 0 400k 0 0 60450 0 --:--:-- 0:00:06 --:--:-- 65536 416kB 0:00:07 [ 64kB/s]
100 464k 0 464k 0 0 61105 0 --:--:-- 0:00:07 --:--:-- 65536 480kB 0:00:08 [ 64kB/s]
100 528k 0 528k 0 0 61614 0 --:--:-- 0:00:08 --:--:-- 65536 544kB 0:00:09 [ 64kB/s]
100 592k 0 592k 0 0 62014 0 --:--:-- 0:00:09 --:--:-- 65536 608kB 0:00:10 [ 64kB/s]
100 656k 0 656k 0 0 62338 0 --:--:-- 0:00:10 --:--:-- 65536 672kB 0:00:11 [ 64kB/s]
100 720k 0 720k 0 0 62612 0 --:--:-- 0:00:11 --:--:-- 65536 736kB 0:00:12 [ 64kB/s]
We see that the data is flowing, but we have no idea how much has left. Thus Content-Length is a really important header to have.


Writing a download server

The sample application developed throughout these articles is available on GitHub.

Tags: HTTP, scalability, spring, spring mvc

Be the first to listen to new episodes!

To get exclusive content: