В эпоху ботнетов, которые вы можете арендовать за несколько сотен долларов и проводить свою собственную атаку с распределенным отказом в обслуживании, наличие аварийных переключателей, которые выборочно отключают дорогостоящую функциональность или с благодарностью снижают производительность, является огромной победой. Ваше приложение все еще работает, пока вы решаете проблему. Конечно, такие меры безопасности также ценны в часы пик или в рабочее время. Одним из таких механизмов, применяемых к серверам загрузки, является динамическое регулирование скорости загрузки. Чтобы предотвратить распределенную атаку на отказ в обслуживании и чрезмерно высокие облачные счета, рассмотрите встроенное регулирование загрузки, которое можно включить и настроить во время выполнения. Идея состоит в том, чтобы ограничить максимальную скорость загрузки, как глобально, так и для каждого клиента (IP? Соединение? Cookie? Пользовательский агент?).
Должен признать, мне нравится дизайн OutputStream
с множеством простых реализаций Input
/ OutputStream
и Reader
/ Writer
, каждая из которых несет только одну ответственность. Вы хотите буферизацию? GZIPing? Кодировка символов? Запись файловой системы? Просто составьте нужные классы, которые всегда работают друг с другом. Хорошо, это все еще блокирует, но это было разработано до того, как реактивные хипстеры даже родились. Несмотря ни на что, java.io
также следует принципу открытого-закрытого : можно просто улучшить существующий код ввода-вывода, не касаясь встроенных классов — но подключив новые декораторы. Поэтому я создал простой декоратор для InputStream
который замедляет чтение ресурсов с нашей стороны, чтобы обеспечить заданную скорость загрузки. Я использую мой любимый класс RateLimiter
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
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(); } } |
Произвольный InputStream
может быть обёрнут с помощью ThrottlingInputStream
так что чтение на самом деле замедляется. Вы можете создать новый RateLimiter
для каждого ThrottlingInputStream
или один глобальный, общий для всех загрузок. Конечно, можно утверждать, что простая RateLimiter
sleep()
(которую RateLimiter
) тратит много ресурсов, но давайте оставим этот пример простым и избегаем неблокирующих операций ввода-вывода. Теперь мы можем легко подключить декоратор к:
1
2
3
4
5
6
|
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); } |
Приведенный выше пример ограничивает скорость загрузки до 64 КиБ / с — очевидно, в реальной жизни вы бы хотели, чтобы такое число настраивалось, предпочтительно во время выполнения. Кстати, мы уже говорили о важности заголовка Content-Length
. Если вы следите за ходом загрузки с помощью pv
, он правильно оценит оставшееся время, что очень удобно:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
~ $ 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 ] |
В качестве дополнительного бонуса pv
доказал наши работы по дросселированию (последний столбец). Теперь без Content-Length
pv
имеет никакого представления о реальном прогрессе:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
~ $ 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 ] |
Мы видим, что данные текут, но мы не знаем, сколько осталось. Таким образом, Content-Length
— действительно важный заголовок.
Написание сервера загрузки
- Часть I: Всегда передавайте, никогда не храните полностью в памяти
- Часть II: заголовки: последние изменения, ETag и If-None-Match
- Часть III: заголовки: длина содержимого и диапазон
- Часть IV: Реализация операции
HEAD
(эффективно) - Часть V: Скорость загрузки газа
- Часть VI. Опишите, что вы отправляете (Content-type, et.al.)
- Пример приложения, разработанного в этих статьях, доступен на GitHub.
Ссылка: | Написание сервера загрузки. Часть V: Скорость загрузки газа от нашего партнера JCG Томаша Нуркевича из блога Java и соседей . |