Direct routing и балансировка с помощью NFT vs Nginx

в 14:08, , рубрики: highload, loadbalancing, nftables, Настройка Linux, Сетевые технологии

При разработке высоконагруженых сетевых приложений возникает необходимость в балансировке нагрузки.

Популярным инструментом L7 балансировки является Nginx. Он позволяет кешировать ответы, выбирать различные стратегии и даже скриптить на LUA. 

Несмотря на все прелести Nginx, если: 

  1. Не нужно работать с HTTP(s).
  2. Нужно выжать из сети максимум.
  3. Нет необходимости что либо кешировать - за балансером чистые API - сервера с динамикой.

Может возникнуть вопрос: а зачем нужен Nginx? Зачем тратить ресурсы на балансировку на L7, не проще ли просто пробросить SYN-пакет? (L4 Direct Routing).

Layer 4 balancing или как балансировали в древности

Популярным инструментом проброски пакетов был IPVS. Он выполнял задачи балансировки через тоннель и Direct Routing. 

В первом случае, для каждого соединения устанавливался TCP-канал и пакет от пользователя шел на балансер, потом на миньон, а потом в обратном порядке. 

Direct routing и балансировка с помощью NFT vs Nginx - 1

В этой схеме видна главная проблема: в обратную сторону данные идут вначале на балансер, а потом к пользователю (Nginx работает точно так же). Выполняется ненужная работа, учитывая тот факт, что к пользователю, обычно, идет больше данных, то такое поведение приводит к некоторой потере производительности.

Такого недостатка лишен (но наделен новыми) метод балансировки под названием Direct Routing. Схематично он выглядит следующим образом: 

Direct routing и балансировка с помощью NFT vs Nginx - 2

В случае с Direct Routing обратные пакеты идут напрямую к пользователю, минуя балансер. Очевидно, что экономятся как ресурсы балансера, так и сетевые. Под экономией сетевых ресурсов, подразумевается не столько экономия трафика, ведь обычная практика - соединить сервера в отдельную сетку и не аккаунтить трафик, а тот факт, что даже переброска через балансер - это потеря миллисекунд.

Данный метод накладывает определенные ограничения: 

  1. Датацентр, где расположена инфраструктура должен позволять спуфить локальные адреса. В схеме выше, каждый миньон должен отправлять обратно пакеты от имени IP 10.10.0.1, который закреплен за балансером. 
  2. Балансер ничего не знает о состоянии миньонов. Следовательно, стратегии Least Conn и Least Time не реализуемы «из коробки». В одной из последующих статей я попробую реализовать их и показать результат.

Here Comes NFTables

Несколько лет назад, в Linux начали активно продвигать NFTables, как замену IPTables, ArpTables, EBTables и всех остальных [a-z]{1,}Tables. В момент, когда у нас в Adramе, возникла необходимость выжать из сети каждую миллисекунду ответа, я решил вытащить шашку и поискать  —  а может быть, ipTables научился делать iphash форвардинг и на нем можно накостылить быструю балансировку. Тут я и наткнулся на nftables, который умеет и не только это, а iptables всё еще не умеет даже этого.
После нескольких дней разбирательств, я наконец-то смог завести в тестовой лаборатории Direct Routing и Channel Routing через NFTables а так же пробенчить их в сравнении с nginx.

Итак, тестовая лаборатория. Имеем 5 машин: 

  1. nft-router - роутер, выполняет задачу связи клиента и подсети AppServer. На нем 2 сетевых карты: 192.168.56.254 - смотрит на сеть аппсервера, 192.168.97.254 - смотрит на клиентов. Включен ip_forward и прописаны все маршруты.
  2. nft-client: клиент, с которого будет гоняться ab, ip 192.168.97.2
  3. nft-balancer: балансер. Имеет два IP: 192.168.56.4, к которому обращаются клиенты и 192.168.13.1, из подсетки миньонов.
  4. nft-minion-a и nft-minion-b: миньоны ипы: 192.168.56.2, 192.168.56.3 и 192.168.13.2 и 192.168.13.3 (я пробовал и через одну сеть и через разные балансировать). В тестах остановился на том, что миньоны имеют «внешние» ипы — в подсети 192.168.56.0/24

У всех интерфейсов MTU 1500.

Direct Routing

Настройки NFTables на балансере:

table ip raw {
        chain input {
                type filter hook prerouting priority -300; policy accept;
                tcp dport http ip daddr set jhash tcp sport mod 2 map { 0: 192.168.56.2, 1: 192.168.56.3 }
        }
}

Создается raw цепочка, на хуке прероутинг, с приоритетом -300.

Если приходит пакет с целевым адресом http, то в зависимости от исходного порта (сделано для тестирования с одной машины, в реальности нужен ip saddr), выбирается либо 56.2 либо 56.3 и устанавливается как целевой адрес в пакете, а потом отправляется далее по маршрутам. Грубо говоря для четных портов 56.2, для нечетных, соответственно, 56.3 (на самом деле нет, ибо для четных/нечетных хешей, но проще понимать именно так). После установки целевого IP, пакет уходит обратно в сеть. Никакого NAT не происходит, на миньоны пакет приходит с исходным IP клиента, а не балансера, что важно для Direct Routing.

Настройки NFT на миньонах:

table ip raw {
        chain output {
                type filter hook output priority -300; policy accept;
                tcp sport http ip saddr set 192.168.56.4
        }
}

Cоздается raw output hook с приоритетом -300 (здесь очень важен приоритет, на более высоких нужный менглинг не будет работать для reply-пакетов).

Весь исходящий трафик с http порта подписывается 56.4 (айпи балансера) и отправляется прямиком клиенту, в обход балансера.

Чтоб проверить будет ли корректно всё работать, я завел клиента в другую сеть и пустил через маршрутизатор.

Так же я отключил arp_filter, rp_filter (чтоб работал спуфинг) и включил ip_forward как на балансере так и на роутере.

Для бенчей, в случае NFT, использован Nginx + php7.2-FPM через unix socket на каждом миньоне. На балансере не было ничего.

В случае с Nginx использован: nginx на балансере и php7.2-FPM через TCP на миньонах. В итоге, я балансировал не веб-сервера за балансером, а сразу FPM-ки (что будет более честно по отношению к nginx, и больше соответствовать реальной жизни).

Для NFT использовался только стратегия hash (в таблице — nft dr), для nginx: hash (ngx eq) и least conn (ngx lc)

Было сделано несколько тестов.

  1. Маленький быстрый скрипт (small).
    <?php
    system('hostname');
    

  2. Скрипт со случайной задержкой (rand).
    <?php
    usleep(mt_rand(100000,200000));
    echo "ok";
    
  3. Скрипт с отправкой большого объема данных (size).
    <?php
    $size=$_GET['size'];
    $file='/tmp/'.$size;
    if (!file_exists($file))
    {
            $dummy="";
            exec ("dd if=/dev/urandom of=$file bs=$size count=1 2>&1",$dummy);
    }
    fpassthru (fopen($file,'rb'));
    

    Были использованы следующие размеры:
    512,1440,1460,1480,1500,2048,65535,655350 байт.
    Перед тестами, я прогрел файлики со статикой, на каждом миньоне.

Тестировал ab, три раза каждый тест:

#!/bin/bash
function do_test()
{
        rep=$3
        for i in $(seq $rep)
        do
                echo "testing $2 # $i"
                echo "$2 pass $i" >> $2
                ab $1 >> $2
                echo "--------------------------" >> $2
        done
}

do_test " -n 5000 -c 100 http://192.168.56.4:80/rand.php" "ngx_eq_test_rand" 3
do_test " -n 10000 -c 100 http://192.168.56.4:80/" "ngx_eq_test_small" 3
size=512
do_test " -n 10000 -c 100 http://192.168.56.4:80/size.php?size=$size" "ngx_eq_test_size_$size" 3
size=1440
do_test " -n 10000 -c 100 http://192.168.56.4:80/size.php?size=$size" "ngx_eq_test_size_$size" 3
size=1460
do_test " -n 10000 -c 100 http://192.168.56.4:80/size.php?size=$size" "ngx_eq_test_size_$size" 3
size=1480
do_test " -n 10000 -c 100 http://192.168.56.4:80/size.php?size=$size" "ngx_eq_test_size_$size" 3
size=1500
do_test " -n 10000 -c 100 http://192.168.56.4:80/size.php?size=$size" "ngx_eq_test_size_$size" 3
size=2048
do_test " -n 10000 -c 100 http://192.168.56.4:80/size.php?size=$size" "ngx_eq_test_size_$size" 3
size=65535
do_test " -n 10000 -c 100 http://192.168.56.4:80/size.php?size=$size" "ngx_eq_test_size_$size" 3
size=655350
do_test " -n 10000 -c 100 http://192.168.56.4:80/size.php?size=$size" "ngx_eq_test_size_$size" 3

Изначально, планировал привести время теста, миллисекунды и остальное, в итоге остановился на RPS — они репрезентативны и коррелируют с временными показателями.

Получили следующие результаты:

Тест Size — колонки — размер отдаваемых данных.

Direct routing и балансировка с помощью NFT vs Nginx - 3

Как видно, nft direct routing выигрывает с огромным отрывом.

Я рассчитывал на несколько другие результаты, связанные с размером ethernet фрейма, но корреляции не обнаружилось. Возможно, 512 body не влазит в 1500 MTU, хотя, сомневаюсь, тест small — будет показательным.

Заметил, что на больших объемах (650к) nginx уменьшает отрыв. Возможно, это как-то связано с буферами и TCP Windows size.

Результат rand теста. Показывает как справляется least conn в условиях разной скорости выполнения скриптов на разных миньонах.

Direct routing и балансировка с помощью NFT vs Nginx - 4

Удивительно, но nginx hash отработал быстрее, нежели least conn, и только в финальном проходе least conn немного вырвался вперед, что не претендует на статистическую значимость.
Цифры проходов сильно отличаются за счет того, что уходит сразу 100 потоков, а FPM-ок со старта грузится около 10. К третьему проходу они успели нафоркаться — что показывает применимость стратегий при берстах.

NFT ожидаемо проиграл этот тест. Nginx хорошо оптимизирует взаимодействие с FPMами в таких ситуациях.

small test

Direct routing и балансировка с помощью NFT vs Nginx - 5

nft незначительно выигрывает по RPS, least conn опять в аутсайдерах.

Кстати, в этом тесте видно, что выдается 400-500RPS, хотя, на тесте с отправкой 512 байт было за 1500 — похоже, system сжирает эту тысячу.

Выводы

NFT хорошо себя показал в ситуации оптимизации равномерных нагрузок: когда отдается много данных, а время работы приложения детерминировано и ресурсов кластера хватает на отработку входящего потока без ухода в штопор.

В ситуации, когда нагрузка по каждому запросу хаотична и невозможно равномерно сбалансировать нагрузку серверов примитивным остатком от деления хеша, то NFT будет проигрывать.

Автор: AlexKMK

Источник


* - обязательные к заполнению поля