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

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

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

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

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

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

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


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

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



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

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



В случае с 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 — колонки — размер отдаваемых данных.



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

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

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

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



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

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

small test



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

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

Выводы


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

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

Подробнее
Реклама

Комментарии 14

    +2
    Ставить за нгинксом мамонтячий PHP и на этом мерять rps — ну такое.
      0

      А я наоборот — спасибо скажу. За фронтом почти всегда кто-то мамонтячий, от того и балансируют. Конечно, обычно нужен https, но вот за nftables в подобном примере (да ещё и с тестами) — спасибо.

        +1
        Никто не спорит, что гонять обратный трафик напрямую, а не через балансировщик — круто. Но крутость эта должна демонстрироваться не в увеличении производительности, потому что узким местом в тестовом стенде сабжа является не нгинкс и не пропускная способность локальной сети (я надеюсь, по крайней мере), а бэкенды. Оверхед, вносимый обработкой исходящих пакетов нгинксом, как мне кажется, несущественен.

        Ну и по настройке сети на линуксе не очень ясно — оптимизировались ли буферы, джамбо фреймы всякие, бэклог коннектов…
          0
          Какие предложения, что разумнее в качестве бекенда использовать для бенча, учитывая, что у современной пыхи выигрывает только нода?

          Сетевой стек не трогался вообще. Когда дойду до leastconn — буду крутить крутилки.
        0
        Тема интересная, результаты любопытные, спасибо. Смущает только то, что статью Вы начали с упоминания IPVS, как классического средства L4 балансировки, но в тестах в пару к NFT почему-то решили поставить Nginx, хотя логичнее было бы сравнивать именно с IPVS. Всё таки задачи, которые решаются балансировкой на IPVS\NFT, принципиально отличаются от тех, в которых применяется Nginx\HAproxy\любой другой L7 балансировщик.
          0
          Дело в том, что последний релиз ipvs был 7 лет назад, и на него забыли/забили. В наше время, обычно ставят nginx и не заморачиваются. NFT в данном случае — возможная альтернатива почившему ipvs.
            0

            ipvs живее всех живых и отлично работает с DR с различными типами балансировки.
            Гонять трафик через прокси в обе стороны глуповато в случае преобладания отдачи

          0
          Популярным инструментом проброски пакетов был IPVS

          Не был, а есть для тех, кто не может себе позволить XDP и прочие свежести в ядре. Джентельмены из Huawei даже притащили его в k8s, натурально устав ждать, пока изменения в iptables применятся в их кластерах на тысячу нод.

            0

            Как Xdp может заменить ipvs если работает в пределах одного интерфейса?

              0

              У меня прямо сейчас есть балансировщики на IPVS с одним интерфейсом (не считая lo), поэтому я не понял вопроса.

                0
                Я быстро глянул презентацию по XDP — на сколько я понял схема распределения нагрузки выглядит так: рутер шлет трафик на все воркеры а XDP скидывает лишний. Это несколько… ммм… глупо?
                Или есть другая схема?
                  0

                  Я не очень в XDP, но в katran, по-моему, не так.


                  Но это всё не нужно подавляющему большинству. 10 Gbit двудульная карта стоит 250 долларов и обслуживается буквально человеком из интернета после прочтения двух статей.

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

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

            Вот тут не совсем честно. У IPVS есть 3 метода доставки трафика до обслуживающих нод:


            1. Gatewaying, он же Direct Routing — пересылка пакетов в пределах одного броадкаст сегмента.
            2. Tunneling — туннелирование, работает в маршрутизируемых сетях, инкапсулирует пакеты в IPv4/IPv6.
            3. Masquerading — классический DNAT.

            Так вот обратный трафик идет через балансер только в случае Masquerading. В остальных двух случаях настройки на облуживающих нодах позволяют возвращать трафик клиенту без задействования балансировщика.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое