Есть у меня проблема. Точнее была. Я ее успешно решил. У меня на шлюзе
(компьютер с FreeBSD) два канала интернет к разным провайдерам. Первый
(интерфейс А), с полосой пропускания порядка четверти мегабита, но
бесплатный. И Второй (интерфейс Б), с полосой пропускания порядка
мегабита, но учет идет по мегабайтам. И передо мной стоит задача, как
наверное перед многими - как максимально загрузить первый канал,
узкий и бесплатный, и разгрузить второй - широкий но платный. Или
хотя-б загрузить их пропорционально. И при этом, что-бы не сильно
страдали пользователи.
+-------+ Интерфейс А 10.1.1.1 -> 10.1.1.2
! Шлюз !-----------------------------------!Провайдер А
!FreeBSD!
! !-----------------------------------!Провайдер Б
+-------+ Интерфейс Б 192.168.1.1-> 192.168.1.2
Искал я в интернете решения этой проблемы, но не нашел подходящего.
Я сразу отбросил предложения, например все адреса вида
0.0.0.0-126.255.255.255 отправлять на один интерфейс, а остальные на
другой.
Были предложения, например, найти "жертву" в интернете, поднять к ней
два VPN-а и балансировать между ними. Это прекрасное решение, но
основная проблема в жертве. Которой нужно платить, что полностью
нивелирует все преимущества узкого бесплатного канала.
Остальные, более гибкие решения предполагали, что шлюз имеет ip-адрес
или один для всех интерфейсов, или, в случае, что каждый из интерфейсов
имеет свой отличный адрес (например разные адреса в одной подсети), что
пакеты отправленные через один канал (А) с исходящим адресом отличным от
адреса используемого интерфейса (например отправлен пакет с исходящим
адресом Б) вообще смогут уйти далее в интернет и ответ сможет вернется
на другой интерфейс (на Б). Ведь в моем случае, если попытаться
осуществить балансировку каналов, и при загруженном канале А отправить
через интерфейс Б пару пакетов (исходящий адрес, конечно, должен быть А,
т.к. пакеты с адресом отправителя Б получатель просто отбросит, т.к.
пакеты с исходящим адресом Б не участвуют в данном обмене данных между
клиентом с адресом А и сервером), то, даже если и вышестоящий Провайдер
Б пропустит сквозь себя пакеты с исходящим адресом А, то возникают
дополнительный проблемы - ответ от сервера придет, конечно, через канал
А, а он и так загружен. А, учитывая, что при структуре моего трафика,
исходящей информации в раз десять меньше чем входящей, то данное решение
не помогает.
Кроме того, решение этой проблемы предлагаемым способом, предполагает,
как минимум наличие "честного" статического интернетовского адреса и
договоренность с провайдерами хотя-бы одного канала.
А это не всегда возможно, а иногда и невозможно. Получить статический
адрес - не проблема. Проблема в том, что провайдер одного каналов у меня
- монополист Укртелеком, с которым разговаривать сложно по определению.
Они мне обратный резолв ДНС прописывают уже третий год :( И говорить о
том, что-бы один из них пропускал мои пакеты с адресами источника
отличными от тех, которыми он мне сам мне выделил - просто невозможно
(не говоря уже о том, что нужно будет решать и целый набор других
проблем, например нельзя посылать через канал Б самый первый пакет в
сеансе связи между клиентом с адресом А и сервером, т.к. фаерволы и
провайдера и шлюза отбросят ответ сервера на адрес А и так далее).
Но из этой ситуации я вышел. Решение в следующем. В фаерволе FreeBSD
есть такое прекрасное средство, как keep-state в правилах (Не знаю, есть
ли аналоги в других системах). Суть в том, что после первого прохождения
пакета по этому правилу, правило становится динамическим и в течении 300
секунд после получения (отправки) любого пакета по этому правилу (или
до закрытия сеанса) разрешен обмен информацией в обоих направлениях
между отправителем и получателем первого пакета (man ipfw). Причем если в
правиле не просто "разрешить" отправку/получение пакетов, но и
перенаправление (forward), то и это тоже учитывается. Эта особенность
здорово помогла, но и без нее можно обойтись.
Решение в следующем.
Состояние 1.
Шлюз по умолчанию - А (как самый дешевый). Все пакеты уходят и ответы
возвращаются через канал А и провайдера А. Существует скрипт который
постоянно анализирует состояние канала А и для каждого исходящего пакета
(например к 1.2.3.4) добавляет в фаервол следующее правило:
forward 10.1.1.2 all from me to 1.2.3.4 keep-state
Наш скрипт также запоминает все сделанное, что-бы не добавлять дважды
одно-и тоже и запоминает время прохождения последнего пакета к этому
адресу. (Можно использовать даже не адрес получателя, а превратить его в
подсеть, например не 1.2.3.4 а 1.2/24 Так даже лучше, далее пояснение).
Если к 1.2.3.4 шлюз ничего не отправлял более, например 300 секунд, то
правило с чистой совестью удаляем.
Также скрипт проверят загрузку канала. И когда становится ясно, что
канал не справляется, скрипт меняет шлюз по умолчанию на второй
интерфейс - Б.
Состояние 2.
Теперь шлюз по умолчанию - Б. Но, т.к. все текущие сеансы передачи
данных прописаны в правилах фаервола, то разрыва сеансов не происходит,
и старые данные продолжают уходить через А. На интерфейсе Б работает
второй скрипт аналогичный скрипту на интерфейсе А, за исключением того,
что он никуда ничего не переключает, а только прописывает в фаерволе
текущие соединения. И как только состояние на интерфейсе А
нормализуется, то скрипт А меняет шлюз по умолчанию обратно на интерфейс
А и возвращаемся к Состоянию 1. Аналогично, т.к. все текущие сеансы
передачи данных через шлюз Б прописаны в правилах фаервола, то разрыва
сеансов не происходит, и старые данные продолжают уходить через Б. И так
далее по кругу.
Данный алгоритм я уже продолжительное время эксплуатирую и должен
отметить, что пики нагрузки сглаживаются ним великолепно. Это позволило
мне если не отказался от перехода полностью на "широкий" канал, то, по
крайней мере, значительно отсрочить эту неизбежную, по сути, вещь и
сэкономить уйму денег. По мои подсчетам - только пятая часть трафика
уходила через "дорогой" канал Б, что я считаю весьма неплохим
результатом. Другими словами я экономлю до 4/5 стоимости всего своего
трафика.
Конечно, есть недостатки. Например сеансы начавшиеся до начала большой
загрузки канала А, даже и после полной его загрузки будут "ходить" через
А. Зато все остальные, "новые" уйдут на интерфейс Б. К плюсам алгоритма
следует отнести и то, что он легко адаптируется к различным
конфигурациям - к более чем двум интерфейсам и т.п.
Данный алгоритм, безусловно, требует улучшения. Если шлюз обслуживает
много абонентов, то стоит усовершенствовать скритпы, что-бы они
учитывали не только адрес получателя, но и адрес отправителя. Тогда, при
условиях загрузки канала А, клиент, начавший работу после установления
скриптом факта загрузки канала А, гарантировано получит в свое распоряжение
канал Б. Кроме того, у меня в скрипты встроена проверка
работоспособности каналов, что-бы исключить возможности переключения на
неработающий канал, и наоборот, быстро переключится на работающий. А
также реализован алгоритм разного порогового значения для переключения
каналов в разное время суток и дней недели. Например, днем и вечером
порог меньше, т.к. вероятность того что канал загрузят по полной -
весьма высока.
Сами скрипты написаны на скорую руку на основе результатов работы
tcpdump, которые далее перенаправляются, только не смейтесь, скрипту на
php, который и делает все работу. Я уверен, что найдутся умельцы которые
напишут это все и на перле и даже на Си, но меня устраивает и то, что я
имею. Я проверял работу скрипта на локальной 100-мегабитной сети при
100% загрузке сети - и top мне показал загрузку всей связки не более 8%
(на машине с AMD64 - 3 Ггц).
Теперь о keep-state. Вся надобность его сводится только к одному
моменту. Дело в том, что я не меняю шлюз по умолчанию буквально - в
смысле не правлю таблицы маршрутизации. Я просто перенаправляю пакеты
(только предназначенные для ухода в интернет) новым правилом фаервола
с keep-state. А т.к. скрипт имеет некоторую задержку в добавлении нового
правила, то не исключена такая ситуация, что пакеты уже ушли на один
интерфейс, а скрипт об этом еще не узнал и переключил шлюз по умолчанию
на уже на второй. В этом случае соединения с сервером у клиента не
произойдет, т.к. все последующие пакеты уйдут уже через другой
интерфейс. (это не есть большая проблема. Клиент после маленького
тайм-аута попробует еще раз - и тогда все получится) А правило с
keep-state само отреагирует на первый пакет и создаст правило и
изменение шлюза по умолчанию ничего не изменит. А когда скрипт через
секунду получит данные о новом коннекте, то он и пропишет новое правило.
И даже то, что по этом у правилу никакие пакеты могут не пройти (так-как
все пакеты будут ходить по динамическому правилу созданному правилом
фаервола, которое определяет шлюз по умолчанию), ничего не меняет.
Теперь о адресах получателя. Я сам нередко становился свидетелем криво
написанных систем авторизации и т.п. которые предполагали, что все
соединения от клиента должны приходить от одного ай-пи - речь идет в
первую очередь о WWW (простом - не сек'юрном - 80 порте). Другими
словами то, что первую страничку сайта клиент получил с одним ай-пи, а
вторую - с другим, может ввести в заблуждение многие алгоритмы на
серверах. Хотя, формально - это не есть правильно - нужно определять
пользователя по кукисам или по ссылкам. (Типа по PHP_SESSION_ID). А
так-как весьма часто сервера находятся на разных ай-пи в пределах своей
подсети и часто перебрасываю пользователя с одного на другой (просто
разные части сайта на разных серверах), то считаю весьма эффективным
указывать не ай-пи адрес сервера, а подсеть, образованную из него. Хотя
это только ухудшает характеристики общего алгоритма балансировки.
Также стоит отметить тот факт, что каждое правило требует отдельного
номера в фаерволе, т.к. иначе его нельзя отдельно удалить. С этим ничего
не поделаешь и приходится это учитывать в скрипте.
Вроде бы ничего не забыл.
На все вопросы с радостью отвечу.
С уважением, постоянный читатель OpenNet.ru - Radist (UA)