Хочу рассказать историю жизни сервера в кампусной сети Новосибирского университета, которая началась в далеком 2004 году, а так же этапы его оптимизации и даунгрейдинга.
Многие вещи в статье покажутся общеизвестными хотя бы по той причине, что речь пойдет о событиях почти десятилетней давности, хотя на тот момент это были передовые технологии. По той же причине кое что вообще потеряло актуальность, но далеко не все, так как сервер до сих пор живет и обслуживает сетку из 1000 машин.
Сама сеть существует с 1997г — это дата, когда все общежития были объединены в единую сеть и получили доступ в Интернет. До 2004 года кампусная сеть была построена полностью на меди, между общежитиями линки были проброшены кабелем П270 (благо, расстояние между общежитиями не превышало 350м, и линк, при использовании карт 3c905 поднимался «на сотке»). В каждом здании стоял свой сервер, в котором стояло по 3 сетевых карты. Две из них «смотрели» на соседние сервера, к третьей подключалась «локалка» общежития. Итого, все шесть (а столько было общаг в нашем университете) были замкнуты в кольцо, а маршруты между ними строились по протоколу OSPF, что позволяло, при обрыве линии запустить трафик в обход выпавшего звена. А обрывы случались часто: то гроза разразится и линк сляжет, то электрики нахимичат. Да и обслуживать сами сервера было не очень удобно, тем более они все были разношерстные: в разные годы от 486DX4 (да-да, с 8Мб ОЗУ, на ядре 2.0.36 и картах 3com она тянула два линка 100Мбит, правда загрузка была в потолок на голом роутинге без всяких ipfwadm) до AMD K6-2 и даже P4 2.8Ghz.
Минусы такой организации (помимо уж очень ненадежного по сегодняшним меркам линка) налицо: уж очень неудобно управлять пользовательской базой. Адреса белые, их количество ограничено. Договор привязывался к IP «пожизненно», то есть на весь срок обучения студента. Сеть нарезана на сегменты, размеры которых изначально были сделаны с учетом численности студентов в каждом общежитии. Но то студенты переезжают из общаги в общагу, то факультету Автоматики понадобилось заметно больше адресов, чем филологам — в общем жуть.
Ограничения доступа были по паре MAC-IP, ибо в времена «зачатия» сети сменить MAC-адрес сетевой карты, не имея под рукой программатора (а то и паяльника) было очень проблематично, а то и невозможно. Поэтому было достаточно держать актуальным файлик /etc/ethers и спасаться от 99% любителей халявы. О управляемых коммутаторах в те времена только мечтали, и уж точно ставить их в качестве абонентского оборудования было не по карману (так как сеть развивалась 100% на деньги самих студентов, а студенты, как известно, народ небогатый)
В 2004 году подвернулась хорошая возможность: один из городских провайдеров предложил в обмен на пиринг между своей сетью и сетью кампуса безвозмездно соединить все здания по оптике. Ну как соединить — непосредственно монтажом оптики занималась инициативная группа студентов, а технари провайдера ее только разварили. В итоге, с использованием этой оптики удалось построить не кольцо, а звезду!
И тут родилась идея — поставить один хороший сервер, с несколькими гигабитными сетевыми картами, связать все линки в один bridge и сделать одну плоскую сеть, что позволило бы избавиться от головной боли с нарезанным на подсети адресным пространством, а так же позволило управлять доступом из одного места.
Так как шина PCI не смогла бы прокачать такой трафик, да и требуемых 6-8 гигабитных портов нельзя было получить за отсутствием такого количества PCI-разъемов на материнских платах, то было решено брать 2x Intel Quad Port Server Adapter на шине PCI-X 133Mhz. К таким сетевкам пришлось взять материнскую плату Supermicro X6DHE-XG2 по причине наличия аж трех PCI-X 133, ну и процессоры на нее, Xeon 3Ghz 2 шт (это те, что можно найти на ark.intel.com в разделе Legacy Xeon)
И понеслось: на сервер устанавливается RHELAS 2.1, заводится bridge, сеть склеивается в одну большую /22. И тут выясняется, что если ограничивать доступ паре сотен адресов с помощью правил типа:
то загрузка на сервере подпрыгивает до неприличных значений. Сервер не справляется?
Поиск в Интернете подсказывает только появившийся тогда проект — ipset. Да, оказалось что это именно то, что нужно. В дополнение к тому, что можно было избавиться от большого количества однотипных записей в iptables, появилась возможность сделать привязку IP-MAC с использованием macipmap.
Одной из особенностей бриджа было то, что пакет, проходящий через бридж в каких-то случаях попадал в цепочку FORWARD, а в каких-то — нет. Оказалось, что в FORWARD попадают «маршрутизирующиеся» между интерфейсами пакеты, а «бриджующиеся» (то есть, входящие в br0 тут же и выходящие из br0) — не попадают.
Решением стало использование таблицы mangle вместо filter.
Так же получилось сделать привязку конкретного адреса не только к MAC, но и к общежитию, в котором проживал пользователь сети. Сделано было с использованием модуля iptables physdev и выглядело примерно так:
Так как оптическая «звезда» была построена с помощью оптоконвертеров, то на каждое здание «смотрела» своя сетевая карта. И нужно было добавить всего лишь в сет IPMAC_H1 пары MAC-IP пользователей первого общежития, в сет IPMAC_H2 — второго общежития и так далее.
Порядок самих правил внутри iptables попытался сделать таким, чтобы выше были те правила, описывающие общежития, где пользователи более активные, что позволило пакетам быстрее проходить цепочки.
Так как в итоге весь межобщажный и внешний трафик в итоге стал проходить через сервер то появилась идея в случае, если абонент отключен, или в случае несовпадения пары IP-MAC — отображать пользователю некую страничку с информацией, в которой бы разъяснялось, почему, собственно, не работает сеть. Показалось, что это не сложно. Нужно было вместо DROP пакетов, идущих на 80-й порт, сделать MARK пакета, а потом перенаправить помеченные пакеты с помощью DNAT на локальный веб-сервер.
Первая проблема оказалась в том, что если просто редиректить пакеты на вебсервер — вебсервер в 99% случаях отвечает, что страница не найдена. Потому как, если пользователь шел на ark.intel.com/products/27100, а вы завернули его на свой веб сервер, то маловероятно, что там будет страница products/27100, и вы в лучшем случае получите ошибку 404. Поэтому был написан простенький демон на С, который на любой запрос выдавал
В последствии этот костыль был заменен на более красивое решение с mod_rewrite.
Вторая, и наиболее существенная, проблема была в том, что как только в ядро был загружен модуль nat, то нагрузка опять подпрыгнула. Виновата конечно же таблица conntrack, а при таком количестве соединений и pps существующее железо не вывозило в часы максимальной нагрузки.
Сервер не справляется?
Начинаем думать. Поставленная цель довольно интересна, но на существующем железе не работает. Использование
которая просто подменяла адрес шлюза и отправляла пакет дальше в обход остальных цепочек.
Вообще, очень удобно было посылать «неугодные» пакеты таким образом на обработку на сторонний сервер, не заботясь о прохождении этим пакетом остальных цепочек типа filter, но с изменением архитектуры ядра этот патч из patch-o-matic стал не поддерживаемым.
Решение: маркировать нужные пакеты маркой 0x1, потом, с помощью ip rule fw посылать пакет в «другую» таблицу маршрутизации, где единственный маршрут — это наш сервер с NAT
В итоге «хороший» трафик пропускался, а «плохим» пользователям показывалась страничка с информацией о блокировке. А так же, в случае несовпадения IP-MAC пользователь мог введя логин/пароль провести перепревязку на свой текущий МАС.
Действие происходит во времена помегабайтного трафика, в общежитии. То есть в безденежной, интернетоактивной и IT-продвинутой среде пользователей. Значит простой привязки IP-MAC уже недостаточно, а случаи хищения интернет-трафика становятся повсеместными.
Единственный вменяемый по затратам вариант — vpn. Но, учитывая то, что к тому времени у сети кампуса появился бесплатный пиринг с полудюжиной городских операторов, гонять пиринговый трафик через vpn-сервер — не получится, он просто не вывезет. Конечно был возможен получивший распространение метод: в интернет — через vpn, в пиринг и локалку — батник с маршрутами. Но мне батник казался уж очень некрасивым решением. Рассматривался вариант с RIPv2, который на тот момент был «встроен» в большинство используемых ОС, но там оставался открытый вопрос с подлинностью анонсов. Без дополнительной настройки кто угодно мог рассылать маршруты, а в популярной тогда WindowXP и ее «Слушателе RIP» вообще никакой настройки не было.
Тогда был «придуман» «ассиметричный VPN». Клиент для выхода в интернет устанавливает обычное vpn-pptp соединение к серверу с логином/паролем, при этом снимая в настройках галочку «Использовать шлюз в удаленной сети». На клиентский конец тоннеля выдавался адрес 192.0.2.2, причем всем клиентам одинаковый, и как будет показано далее, вообще не имевший никакого значения.
На стороне VPN-сервера был модифицирован скрипт /etc/ppp/ip-up, выполняемый после аутентификации и поднятия интерфейса
То есть в переменную PEERIP из базы скриптом выдергивался IP-адрес, который должен быть у пользователя с PEERNAME (логином, под которым он подключился), и если этот адрес совпадает с IP, с которого произошло подключение (LOGDEVICE) к VPN-серверу, то весь трафик до этого IP маршрутизируется в интерфейс IFNAME посредством таблицы 101. Так же в таблице 101 шлюзом по умолчанию установлен 127.0.0.1
Весь маршрутизируемый трафик заворачивается в таблицу 101 правилом
В итоге получаем, что трафик, пришедший на vpn-сервер, и следующий НЕ на vpn сервер (тот пойдет в таблицу local, которая есть по умолчанию) уходит в таблицу 101. А там «разбредется» по ppp-тоннелям. А если не найдет нужного, то просто дропнется.
Пример того, что получается в итоге в табличке 101 (ip r sh ta 101)
Теперь всего лишь остается завернуть на центральном маршрутизаторе весь трафик с «интернетного» интерфейса на vpn-шлюз, и без подключения к VPN интернета у пользователей не будет. Причем остальной трафик (пиринговый) будет бегать IPoE (то есть «обычным» способом), и не будет нагружать VPN-сервер. При появлении дополнительных пиринговых сетей пользователю не придется править никакие bat-файлы. Опять же, доступ до некоторых ресурсов внутренних, хоть IP, хоть портов, можно сделать через VPN, достаточно завернуть пакет на VPN-сервер.
При использовании данной методики злоумышленник конечно может путем подмены IP-MAC послать в интернет трафик, но не может получить ничего обратно, так как не поднят vpn-тоннель. Что практически напрочь убивает смысл подмены — «посидеть в интернете» с чужого IP теперь нельзя.
Для того, чтобы клиентские компьютеры могли получать пакеты через vpn-тоннель, нужно было в Windows выставить в реестре ключ IPEnableRouter=1, а в linux — rp_filter=0. Иначе ОС не принимали ответы не с тех интерфейсов, куда отправляли запросы.
Затраты на реализацию почти нулевые, на ~700 одновременных подключений к vpn хаватло сервера уровня celeron 2Ghz, так как интернет трафик внутри ppp во времена помегабайтных тарифов был не очень большой. При этом пиринговый трафик бегал на скоростях до 6Гбит/с суммарно (через Xeon на S604)
Проработало все это чудо лет 8. Году в 2006 RHELAS 2.1 был заменен на свежевышедший CentOS 4. Центральные коммутаторы в зданиях поменяны на DES-3028, абонентскими остались DES-1024. Управление доступом на DES-3028 сделать не получалось толком. Для того, чтобы сделать привязку ip-mac к порту с использовании ACL не хватало 256 записей, ведь в некоторых общежитиях было более 300 компьютеров. Менять оборудование стало проблемой, так как университет «легализовал» сеть, и теперь за нее нужно было платить в кассу университета, при обратно денег на оборудование не выделялось, а если и выделялось, то очень скупо, через год и через конкурс (когда покупают не то что нужно, а то, что дешевле, или где откат больше).
И тут сервер сломался. Вернее сгорела материнская плата (по заключению из мастерской — умер северный мост). Нужно собрать что-то на замену, денег нет. То есть неплохо бы бесплатно. И чтобы можно было вставить сетевки PCI-X. Благо знакомый отдавал списанный из банка сервер, в нем как раз было пару слотов PCI-X 133. Но материнка однопроцессорная, и в ней не Xeon, а Socket 478 Pentium 4 3Ghz
Перекидываем винты, сетевые карты. Стартуем — вроде работает.
Но softirq «съедает» 90% в сумме от двух псевдоядер (в процессоре одно ядро, и включен гипертрединг), пинг подскакивает до 3000, даже консоль «тупит» до невозможности.
![Стало плохо](https://habrastorage.org/r/w1560/getpro/habr/post_images/948/f03/dea/948f03dea60165c27dbdc62995877bcc.png)
Казалось бы вот оно, сервер устарел, пора на покой.
Вооружаюсь oprofile, начинаю «выкашивать лишнее». Вообще oprofile в процессе «общения» с этим сервером пользовался довольно часто, и не раз он выручал. К примеру, даже используя ipset стараюсь пользоваться ipmap, а не iphash (если это возможно), так как с oprofile действительно видно НАСКОЛЬКО разительна разница в производительности. По моим данным получилось на два порядка, то есть плавало от 200 до 400 раз. Так же при подсчете трафика в разное время переходил с ipcad-ulog на ipcad-pcap, и далее на nflow, ориентируясь на профайлинг. ipt_NETFLOW уже не использовал, так вошли в век «безлимитного интернета», а пишет там наш провайдер верхнего уровня netflow для СОРМ или нет — его проблемы. Собственно, используя oprofile выявил, что ip_conntrack был главным пожирателем ресурсов при включении nat.
В общем oprofile в этот раз говорит мне, что 60% циклов процессора занимает модуль ядра e1000 (сетевой карты). Ну что с ним делать? Рекомендованные в e1000.txt
вписаны еще в 2005 году.
Беглый просмотр git.kernel.org/cgit/linux/kernel/git/stable/linux-stable.git на предмет сколько нибудь значимых изменений в e1000 не дал результатов (то есть изменения конечно есть, но либо исправление ошибок, либо расстановка пробелов в коде). На всякий случай ядро все-таки обновил, но результатов это не дало.
В ядре так же стоит
Еще Oprofile заявляет, что довольно большую долю циклов занимает модуль bridge. И, казалось бы, без него уже никуда, так как IP-адреса размазаны в беспорядке по нескольким зданиям, и снова разбить их на сегменты уже не возможно (вариант — объединить все в один сегмент без сервера не рассматривается, так как теряется управление)
Подумываю «разбить» бридж, и использовать proxy_arp. Тем более давно хотелось это сделать, после обнаружения баги в DES-3028 с flood_fdb. В принципе есть возможность загрузить все адреса в таблицу маршрутизации в виде:
потому как известно, какой абонент где должен быть (хранится в базе)
Но так же давно хотелось реализовать привязку IP-MAC не только к зданию, но и к порту узлового свича на здании (повторюсь, на абонентов стоят неуправляшки типа DES-1024)
И тут доходят руки поразбираться с dhcp-relay и dhcp-snooping.
На свичах включил:
На сервере интерфейсы вывел из бриджа, убрал IP-адреса на них (интерфейсы без IP), включил arp_proxy
Настройка isc-dhcp
В файле dhcp-event производится проверка agent.circuit-id, agent.remote-id, IP и MAC на валидность, и если все ок, то добавляется машрут до этого адреса через нужный интерфейс
Примитивный пример dhcp-event:
здесь проверяется только $ARGV[3] (то есть agent.remote-id, или МАС свича, через который получен DHCP-запрос), но можно проверять так же все остальные поля получая их валидные значения, к примеру, из базы
В итоге получаем:
1) клиент, не запросивший адрес по DHCP — не выходит дальше своего неуправляемого свича, его не пропускает IP-MAC-PORT-BINDING;
2) клиент, у которого МАС известен (есть в базе), но запрос не соответствует порту или коммутатору — получит IP привязанный к этому МАС, но маршрут до него не будет добавлен, соответственно proxy_arp «ответит», что адрес уже занят, и адрес будет тут же освобожден;
3) клиент, у которого МАС не известен, получит адрес из временного пула. С этих адресов происходит перенаправление на страничку с информацией, здесь же можно перерегистрировать свой МАС использую логин/пароль;
4) и наконец клиент, у которого МАС известен, и совпадает привяка к коммутатору и порту — получит свой адрес. Dhcp-snooping добавит динамическую привязку в таблицу impb на коммутаторе, сервер добавит маршрут до этого адреса через нужный интерфейс бывшего бриджа.
При окончании аренды или освобождении адреса вызывается скрипт /etc/dhcp/dhcp-release, содержание которого донельзя примитивно:
Есть небольшой изъян в безопасности, конкретно в п.2. Если использовать нестандартный dhcp-client, который не проверяет, занят ли выданный dhcp-сервером адрес, то освобождения адреса не произойдет. Конечно выхода во внешнюю сеть, дальше маршрутизатора, у пользователя не появится, так как сервер не добавит маршрут на этот адрес через нужный интерфейс, но коммутатор разблокирует эту пару MAC-IP на своем порту.
Обойти этот изъян можно, используя классы в dhcpd.conf, но это заметно усложняет конфигурационный файл, и, соответственно, увеличивает нагрузку на наш старенький сервер. Потому как для каждого абонента придется создать свой класс, с довольно сложным условием попадания в него, а потом свой пул. Хотя в планах попробовать на практике, как сильно это увеличит нагрузку.
Таким образом получилось, что за соответствием пары IP-MAC теперь «следит» DHCP при выдаче адреса, доступ с «невалидных» MAC-IP ограничивает коммутатор. С сервера теперь можно было убрать не только bridge, но и macipmap, заменив 6 сетов (из «Оптимизации 1») на один ipmap, в котром содержатся все открытые IP-адреса. А так же удалив -m physdev.
Еще интерфейсы сервера перешли из promisc mode в обычный, что тоже несколько снизило нагрузку.
А именно, вся эта процедура с разборкой бриджа снизила общую нагрузку на сервер почти в 2 раза! Softirq теперь не 50-100%, а 25-50%. При этом контроль доступа к сети стал только лучше.
После проведения последней оптимизации, хотя нагрузка заметно упала, была замечена странность: возрос iowait. Не то чтобы сильно, с 0-0.3% до 5-7%. Это с учетом того, что на данном сервере вообще каких бы то ни было дисковых операций практически нет — он просто перекидывает пакетики.
![iowait](https://habrastorage.org/r/w1560/getpro/habr/post_images/2d7/016/f2a/2d7016f2adee88ecbdddb30e3b9add0f.png)
(синий юзертайм — компиляция ядра)
iostat показывал, что есть постоянная нагрузка на диск в 800-820 Blk_wrtn/s
Начал поиск процессов, которые могли бы заниматься записью. Выполнение
дало странный результат: виновниками оказались
Ext3 находится в режиме
Начал по шагам прокручивать в голове, что я делал, и что могло повлиять на iowait. В итоге остановил syslog — и iowait резко упал до ~0%
Для того, чтобы dhcp не захламял messages своими сообщениями, я отправил их в log-facility local6, а в syslog.conf вписал:
оказалось, что при записи через syslog для каждой строки делается sync. Запросов к dhcp-серверу довольно много, генерируется много событий, которые попадают в лог, и вызывается много sync.
Исправление на
снизило iowait в моем случае 10-100 раз, вместо 5-7% стало 0-0.3%
Результат оптимизации:
![Результат оптимизации](https://habrastorage.org/r/w1560/getpro/habr/post_images/ec0/241/62a/ec024162a5db7686827523355c9456ad.png)
К чему эта статья.
Во-первых, возможно кто-то почерпнет из нее полезные для себя решения. Америку я тут не открыл, хотя бОльшая часть описанных тут событий была в дохабровую эпоху, и «гугление рецептов» мало помогало, доходил до всего своей головой.
Во-вторых, постоянно приходится сталкиваться с тем, что вместо оптимизации кода разработчики занимаются наращиванием вычислительных мощностей, и эта статья может быть примером того, что при желании можно сколь угодно долго находить ошибки в своем, казалось бы много раз проверенном, коде и справлять их. 90% разработчиков сайтов проверяют у себя на машине работу сайта, выкладывают в продкашн. Потом все это дело тормозит под нагрузкой. Теребят админа сервера. Оптимизацией сервера в этому случае мало что можно добиться, если код написан изначально не оптимально. И покупается новый сервер, или еще один сервер и балансировщик. Потом балансировщик балансировщика и так далее. А код внутри как был неоптимальным, так и остается навеки. Я конечно понимаю, что в нынешних реалиях оптимизация кода стоит дороже, чем экстенсивное наращивание мощностей, но с появлением огромного числа «доморощенных» айтишников объемы «плохого кода» приобретют серьезные масштабы.
Из ностальгического: имею на полке старый ноутбук, Р3-866 2001гв (Panasonic CF-T1 если кому-то это что-то скажет), но сейчас на нем невозможно даже сайты смотреть, хотя смысла на этих сайтах за 10 лет больше не стало. С любовью вспоминаю интересные игрушки на ZX-Spectrum по геймплею не уступающим сегодняшним монстрам, требующим 4 ядра/4гига
Многие вещи в статье покажутся общеизвестными хотя бы по той причине, что речь пойдет о событиях почти десятилетней давности, хотя на тот момент это были передовые технологии. По той же причине кое что вообще потеряло актуальность, но далеко не все, так как сервер до сих пор живет и обслуживает сетку из 1000 машин.
Сеть
Сама сеть существует с 1997г — это дата, когда все общежития были объединены в единую сеть и получили доступ в Интернет. До 2004 года кампусная сеть была построена полностью на меди, между общежитиями линки были проброшены кабелем П270 (благо, расстояние между общежитиями не превышало 350м, и линк, при использовании карт 3c905 поднимался «на сотке»). В каждом здании стоял свой сервер, в котором стояло по 3 сетевых карты. Две из них «смотрели» на соседние сервера, к третьей подключалась «локалка» общежития. Итого, все шесть (а столько было общаг в нашем университете) были замкнуты в кольцо, а маршруты между ними строились по протоколу OSPF, что позволяло, при обрыве линии запустить трафик в обход выпавшего звена. А обрывы случались часто: то гроза разразится и линк сляжет, то электрики нахимичат. Да и обслуживать сами сервера было не очень удобно, тем более они все были разношерстные: в разные годы от 486DX4 (да-да, с 8Мб ОЗУ, на ядре 2.0.36 и картах 3com она тянула два линка 100Мбит, правда загрузка была в потолок на голом роутинге без всяких ipfwadm) до AMD K6-2 и даже P4 2.8Ghz.
Минусы такой организации (помимо уж очень ненадежного по сегодняшним меркам линка) налицо: уж очень неудобно управлять пользовательской базой. Адреса белые, их количество ограничено. Договор привязывался к IP «пожизненно», то есть на весь срок обучения студента. Сеть нарезана на сегменты, размеры которых изначально были сделаны с учетом численности студентов в каждом общежитии. Но то студенты переезжают из общаги в общагу, то факультету Автоматики понадобилось заметно больше адресов, чем филологам — в общем жуть.
Ограничения доступа были по паре MAC-IP, ибо в времена «зачатия» сети сменить MAC-адрес сетевой карты, не имея под рукой программатора (а то и паяльника) было очень проблематично, а то и невозможно. Поэтому было достаточно держать актуальным файлик /etc/ethers и спасаться от 99% любителей халявы. О управляемых коммутаторах в те времена только мечтали, и уж точно ставить их в качестве абонентского оборудования было не по карману (так как сеть развивалась 100% на деньги самих студентов, а студенты, как известно, народ небогатый)
Звезда
В 2004 году подвернулась хорошая возможность: один из городских провайдеров предложил в обмен на пиринг между своей сетью и сетью кампуса безвозмездно соединить все здания по оптике. Ну как соединить — непосредственно монтажом оптики занималась инициативная группа студентов, а технари провайдера ее только разварили. В итоге, с использованием этой оптики удалось построить не кольцо, а звезду!
И тут родилась идея — поставить один хороший сервер, с несколькими гигабитными сетевыми картами, связать все линки в один bridge и сделать одну плоскую сеть, что позволило бы избавиться от головной боли с нарезанным на подсети адресным пространством, а так же позволило управлять доступом из одного места.
Так как шина PCI не смогла бы прокачать такой трафик, да и требуемых 6-8 гигабитных портов нельзя было получить за отсутствием такого количества PCI-разъемов на материнских платах, то было решено брать 2x Intel Quad Port Server Adapter на шине PCI-X 133Mhz. К таким сетевкам пришлось взять материнскую плату Supermicro X6DHE-XG2 по причине наличия аж трех PCI-X 133, ну и процессоры на нее, Xeon 3Ghz 2 шт (это те, что можно найти на ark.intel.com в разделе Legacy Xeon)
И понеслось: на сервер устанавливается RHELAS 2.1, заводится bridge, сеть склеивается в одну большую /22. И тут выясняется, что если ограничивать доступ паре сотен адресов с помощью правил типа:
iptables -A FORWARD -s a.b.c.d -j REJECT
то загрузка на сервере подпрыгивает до неприличных значений. Сервер не справляется?
Оптимизация 1
Поиск в Интернете подсказывает только появившийся тогда проект — ipset. Да, оказалось что это именно то, что нужно. В дополнение к тому, что можно было избавиться от большого количества однотипных записей в iptables, появилась возможность сделать привязку IP-MAC с использованием macipmap.
Одной из особенностей бриджа было то, что пакет, проходящий через бридж в каких-то случаях попадал в цепочку FORWARD, а в каких-то — нет. Оказалось, что в FORWARD попадают «маршрутизирующиеся» между интерфейсами пакеты, а «бриджующиеся» (то есть, входящие в br0 тут же и выходящие из br0) — не попадают.
Решением стало использование таблицы mangle вместо filter.
Так же получилось сделать привязку конкретного адреса не только к MAC, но и к общежитию, в котором проживал пользователь сети. Сделано было с использованием модуля iptables physdev и выглядело примерно так:
iptables -t mangle -A PREROUTING -m physdev --physdev-in eth1 -m set --set IPMAC_H1 src -j ACCEPT
iptables -t mangle -A PREROUTING -m physdev --physdev-in eth2 -m set --set IPMAC_H2 src -j ACCEPT
...
iptables -t mangle -A PREROUTING -i br0 -j DROP
Так как оптическая «звезда» была построена с помощью оптоконвертеров, то на каждое здание «смотрела» своя сетевая карта. И нужно было добавить всего лишь в сет IPMAC_H1 пары MAC-IP пользователей первого общежития, в сет IPMAC_H2 — второго общежития и так далее.
Порядок самих правил внутри iptables попытался сделать таким, чтобы выше были те правила, описывающие общежития, где пользователи более активные, что позволило пакетам быстрее проходить цепочки.
Оптимизация 2
Так как в итоге весь межобщажный и внешний трафик в итоге стал проходить через сервер то появилась идея в случае, если абонент отключен, или в случае несовпадения пары IP-MAC — отображать пользователю некую страничку с информацией, в которой бы разъяснялось, почему, собственно, не работает сеть. Показалось, что это не сложно. Нужно было вместо DROP пакетов, идущих на 80-й порт, сделать MARK пакета, а потом перенаправить помеченные пакеты с помощью DNAT на локальный веб-сервер.
Первая проблема оказалась в том, что если просто редиректить пакеты на вебсервер — вебсервер в 99% случаях отвечает, что страница не найдена. Потому как, если пользователь шел на ark.intel.com/products/27100, а вы завернули его на свой веб сервер, то маловероятно, что там будет страница products/27100, и вы в лучшем случае получите ошибку 404. Поэтому был написан простенький демон на С, который на любой запрос выдавал
Location: myserverru
В последствии этот костыль был заменен на более красивое решение с mod_rewrite.
Вторая, и наиболее существенная, проблема была в том, что как только в ядро был загружен модуль nat, то нагрузка опять подпрыгнула. Виновата конечно же таблица conntrack, а при таком количестве соединений и pps существующее железо не вывозило в часы максимальной нагрузки.
Сервер не справляется?
Начинаем думать. Поставленная цель довольно интересна, но на существующем железе не работает. Использование
-t raw -j NOTRACK
помогало, но не сильно. Решение была найдено такое: NAT-ить пакеты не на центральном маршрутизаторе, а на одной из старых машинок, которые еще оставались и использовались для различных сервисов типа p2p-сервера, игрового сервера, jabber-сервера, а то и просто стоящих без дела. В случае всплеска нагрузки на этом сервере в худшем случае абонент бы не получил сообщение в окне браузера, что он отключен (или что его IP не соответствует зарегистрированному MAC), и это не повлияло бы на работу остальных пользователей сети. А для того, чтобы доставить трафик пользователя на этот сервер с NAT использовалась команда:iptables -t mangle -A POSTROUTING -p tcp --dport 80 -j ROUTE --gw a.b.c.d
которая просто подменяла адрес шлюза и отправляла пакет дальше в обход остальных цепочек.
Вообще, очень удобно было посылать «неугодные» пакеты таким образом на обработку на сторонний сервер, не заботясь о прохождении этим пакетом остальных цепочек типа filter, но с изменением архитектуры ядра этот патч из patch-o-matic стал не поддерживаемым.
Решение: маркировать нужные пакеты маркой 0x1, потом, с помощью ip rule fw посылать пакет в «другую» таблицу маршрутизации, где единственный маршрут — это наш сервер с NAT
iptables -t mangle -A PREROUTING -p tcp --dport 80 -j MARK --set-mark 0x1
ip route flush table 100
ip route add via a.b.c.d table 100
ip rule add fwmark 0x1 lookup 100
В итоге «хороший» трафик пропускался, а «плохим» пользователям показывалась страничка с информацией о блокировке. А так же, в случае несовпадения IP-MAC пользователь мог введя логин/пароль провести перепревязку на свой текущий МАС.
Оптимизация 3
Действие происходит во времена помегабайтного трафика, в общежитии. То есть в безденежной, интернетоактивной и IT-продвинутой среде пользователей. Значит простой привязки IP-MAC уже недостаточно, а случаи хищения интернет-трафика становятся повсеместными.
Единственный вменяемый по затратам вариант — vpn. Но, учитывая то, что к тому времени у сети кампуса появился бесплатный пиринг с полудюжиной городских операторов, гонять пиринговый трафик через vpn-сервер — не получится, он просто не вывезет. Конечно был возможен получивший распространение метод: в интернет — через vpn, в пиринг и локалку — батник с маршрутами. Но мне батник казался уж очень некрасивым решением. Рассматривался вариант с RIPv2, который на тот момент был «встроен» в большинство используемых ОС, но там оставался открытый вопрос с подлинностью анонсов. Без дополнительной настройки кто угодно мог рассылать маршруты, а в популярной тогда WindowXP и ее «Слушателе RIP» вообще никакой настройки не было.
Тогда был «придуман» «ассиметричный VPN». Клиент для выхода в интернет устанавливает обычное vpn-pptp соединение к серверу с логином/паролем, при этом снимая в настройках галочку «Использовать шлюз в удаленной сети». На клиентский конец тоннеля выдавался адрес 192.0.2.2, причем всем клиентам одинаковый, и как будет показано далее, вообще не имевший никакого значения.
На стороне VPN-сервера был модифицирован скрипт /etc/ppp/ip-up, выполняемый после аутентификации и поднятия интерфейса
PATH=/sbin:/usr/sbin:/bin:/usr/bin
export PATH
LOGDEVICE=$6
REALDEVICE=$1
[ -f /etc/sysconfig/network-scripts/ifcfg-${LOGDEVICE} ] && /etc/sysconfig/network-scripts/ifup-post ifcfg-${LOGDEVICE}
[ -x /etc/ppp/ip-up.local ] && /etc/ppp/ip-up.local "$@"
PEERIP=`/usr/local/bin/getip.pl $PEERNAME`
if [ $LOGDEVICE == $PEERIP ] ; then
ip ro del $PEERIP table vpn > /dev/null 2>/dev/null&
ip ro add $PEERIP dev $IFNAME table 101
else
ifconfig $IFNAME down
kill $PPPD_PID
fi
exit 0
То есть в переменную PEERIP из базы скриптом выдергивался IP-адрес, который должен быть у пользователя с PEERNAME (логином, под которым он подключился), и если этот адрес совпадает с IP, с которого произошло подключение (LOGDEVICE) к VPN-серверу, то весь трафик до этого IP маршрутизируется в интерфейс IFNAME посредством таблицы 101. Так же в таблице 101 шлюзом по умолчанию установлен 127.0.0.1
Весь маршрутизируемый трафик заворачивается в таблицу 101 правилом
ip ru add iif eth0 lookup 101
В итоге получаем, что трафик, пришедший на vpn-сервер, и следующий НЕ на vpn сервер (тот пойдет в таблицу local, которая есть по умолчанию) уходит в таблицу 101. А там «разбредется» по ppp-тоннелям. А если не найдет нужного, то просто дропнется.
Пример того, что получается в итоге в табличке 101 (ip r sh ta 101)
[root@vpn ~]# ip route show table 101
a.b.c.d dev ppp2 scope link
a.b.c.e dev ppp6 scope link
a.b.c.f dev ppp1 scope link
default via 127.0.0.1 dev lo
Теперь всего лишь остается завернуть на центральном маршрутизаторе весь трафик с «интернетного» интерфейса на vpn-шлюз, и без подключения к VPN интернета у пользователей не будет. Причем остальной трафик (пиринговый) будет бегать IPoE (то есть «обычным» способом), и не будет нагружать VPN-сервер. При появлении дополнительных пиринговых сетей пользователю не придется править никакие bat-файлы. Опять же, доступ до некоторых ресурсов внутренних, хоть IP, хоть портов, можно сделать через VPN, достаточно завернуть пакет на VPN-сервер.
При использовании данной методики злоумышленник конечно может путем подмены IP-MAC послать в интернет трафик, но не может получить ничего обратно, так как не поднят vpn-тоннель. Что практически напрочь убивает смысл подмены — «посидеть в интернете» с чужого IP теперь нельзя.
Для того, чтобы клиентские компьютеры могли получать пакеты через vpn-тоннель, нужно было в Windows выставить в реестре ключ IPEnableRouter=1, а в linux — rp_filter=0. Иначе ОС не принимали ответы не с тех интерфейсов, куда отправляли запросы.
Затраты на реализацию почти нулевые, на ~700 одновременных подключений к vpn хаватло сервера уровня celeron 2Ghz, так как интернет трафик внутри ppp во времена помегабайтных тарифов был не очень большой. При этом пиринговый трафик бегал на скоростях до 6Гбит/с суммарно (через Xeon на S604)
Работает
Проработало все это чудо лет 8. Году в 2006 RHELAS 2.1 был заменен на свежевышедший CentOS 4. Центральные коммутаторы в зданиях поменяны на DES-3028, абонентскими остались DES-1024. Управление доступом на DES-3028 сделать не получалось толком. Для того, чтобы сделать привязку ip-mac к порту с использовании ACL не хватало 256 записей, ведь в некоторых общежитиях было более 300 компьютеров. Менять оборудование стало проблемой, так как университет «легализовал» сеть, и теперь за нее нужно было платить в кассу университета, при обратно денег на оборудование не выделялось, а если и выделялось, то очень скупо, через год и через конкурс (когда покупают не то что нужно, а то, что дешевле, или где откат больше).
Сервер сломался
И тут сервер сломался. Вернее сгорела материнская плата (по заключению из мастерской — умер северный мост). Нужно собрать что-то на замену, денег нет. То есть неплохо бы бесплатно. И чтобы можно было вставить сетевки PCI-X. Благо знакомый отдавал списанный из банка сервер, в нем как раз было пару слотов PCI-X 133. Но материнка однопроцессорная, и в ней не Xeon, а Socket 478 Pentium 4 3Ghz
Перекидываем винты, сетевые карты. Стартуем — вроде работает.
Но softirq «съедает» 90% в сумме от двух псевдоядер (в процессоре одно ядро, и включен гипертрединг), пинг подскакивает до 3000, даже консоль «тупит» до невозможности.
![Стало плохо](https://habrastorage.org/getpro/habr/post_images/948/f03/dea/948f03dea60165c27dbdc62995877bcc.png)
Казалось бы вот оно, сервер устарел, пора на покой.
Оптимизация 4
Вооружаюсь oprofile, начинаю «выкашивать лишнее». Вообще oprofile в процессе «общения» с этим сервером пользовался довольно часто, и не раз он выручал. К примеру, даже используя ipset стараюсь пользоваться ipmap, а не iphash (если это возможно), так как с oprofile действительно видно НАСКОЛЬКО разительна разница в производительности. По моим данным получилось на два порядка, то есть плавало от 200 до 400 раз. Так же при подсчете трафика в разное время переходил с ipcad-ulog на ipcad-pcap, и далее на nflow, ориентируясь на профайлинг. ipt_NETFLOW уже не использовал, так вошли в век «безлимитного интернета», а пишет там наш провайдер верхнего уровня netflow для СОРМ или нет — его проблемы. Собственно, используя oprofile выявил, что ip_conntrack был главным пожирателем ресурсов при включении nat.
В общем oprofile в этот раз говорит мне, что 60% циклов процессора занимает модуль ядра e1000 (сетевой карты). Ну что с ним делать? Рекомендованные в e1000.txt
options e1000 RxDescriptors=4096,4096,4096,4096,4096,4096,4096,4096,4096,4096 TxDescriptors=4096,4096,4096,4096,4096,4096,4096,4096,4096,4096 InterruptThrottleRate=3000,3000,3000,3000,3000,3000,3000,3000,3000,3000
вписаны еще в 2005 году.
Беглый просмотр git.kernel.org/cgit/linux/kernel/git/stable/linux-stable.git на предмет сколько нибудь значимых изменений в e1000 не дал результатов (то есть изменения конечно есть, но либо исправление ошибок, либо расстановка пробелов в коде). На всякий случай ядро все-таки обновил, но результатов это не дало.
В ядре так же стоит
CONFIG_HZ_100=y
, при большем значении результаты еще хуже.Еще Oprofile заявляет, что довольно большую долю циклов занимает модуль bridge. И, казалось бы, без него уже никуда, так как IP-адреса размазаны в беспорядке по нескольким зданиям, и снова разбить их на сегменты уже не возможно (вариант — объединить все в один сегмент без сервера не рассматривается, так как теряется управление)
Подумываю «разбить» бридж, и использовать proxy_arp. Тем более давно хотелось это сделать, после обнаружения баги в DES-3028 с flood_fdb. В принципе есть возможность загрузить все адреса в таблицу маршрутизации в виде:
ip route add a.b.c1.d1 dev eth1 src 1.2.3.4
ip route add a.b.c1.d2 dev eth1 src 1.2.3.4
...
ip route add a.b.c2.d1 dev eth2 src 1.2.3.4
ip route add a.b.c2.d2 dev eth2 src 1.2.3.4
...
потому как известно, какой абонент где должен быть (хранится в базе)
Но так же давно хотелось реализовать привязку IP-MAC не только к зданию, но и к порту узлового свича на здании (повторюсь, на абонентов стоят неуправляшки типа DES-1024)
И тут доходят руки поразбираться с dhcp-relay и dhcp-snooping.
На свичах включил:
enable dhcp_relay
config dhcp_relay option_82 state enable
config dhcp_relay option_82 check enable
config dhcp_relay option_82 policy replace
config dhcp_relay option_82 remote_id default
config dhcp_relay add ipif System 10.160.8.1
enable address_binding dhcp_snoop
enable address_binding trap_log
config address_binding ip_mac ports 1-28 mode acl stop_learning_threshold 500
config address_binding ip_mac ports 1-24 state enable strict allow_zeroip enable forward_dhcppkt enable
config address_binding dhcp_snoop max_entry ports 1-24 limit no_limit
config filter dhcp_server ports 1-24 state enable
config filter dhcp_server ports 25-28 state disable
config filter dhcp_server trap_log enable
config filter dhcp_server illegal_server_log_suppress_duration 1min
На сервере интерфейсы вывел из бриджа, убрал IP-адреса на них (интерфейсы без IP), включил arp_proxy
Настройка isc-dhcp
log-facility local6;
ddns-update-style none;
authoritative;
use-host-decl-names on;
default-lease-time 300;
max-lease-time 600;
get-lease-hostnames on;
option domain-name "myserver.ru";
option ntp-servers myntp.ru;
option domain-name-servers mydnsp-ip;
local-address 10.160.8.1;
include "/etc/dhcp-hosts"; #здесь лежат привязки MAC-IP в виде "host hostname {hardware ethernet AA:BB:CC:55:92:A4; fixed-address w.x.y.z;}"
on release {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
log(info, concat("***** release IP " , ClientIP));
execute("/etc/dhcp/dhcp-release", ClientIP);
}
on expiry {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
log(info, concat("***** expiry IP " , ClientIP));
execute("/etc/dhcp/dhcp-release", ClientIP);
}
on commit {
if exists agent.remote-id {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6));
set ClientPort = binary-to-ascii(10,8,"",suffix(option agent.circuit-id,1));
set ClientSwitch = binary-to-ascii(16,8,":",substring(option agent.remote-id,2,6));
log(info, concat("***** IP: " , ClientIP, " Mac: ", ClientMac, " Port: ",ClientPort, " Switch: ",ClientSwitch));
execute("/etc/dhcp/dhcp-event", ClientIP, ClientMac, ClientPort, ClientSwitch);
}
}
option space microsoft; #не нужена нам микрософ-сеть
option microsoft.disable-netbios-over-tcpip code 1 = unsigned integer 32;
if substring(option vendor-class-identifier, 0, 4) = "MSFT" {
vendor-option-space microsoft;
}
shared-network HOSTEL {
subnet 10.160.0.0 netmask 255.255.248.0 {
range 10.160.0.1 10.160.0.100; #пул для неизвестных хостов
option routers 10.160.1.1;
option microsoft.disable-netbios-over-tcpip 2;
}
subnet a.b.c.0 netmask 255.255.252.0 {
option routers a.b.c.d;
option microsoft.disable-netbios-over-tcpip 2;
}
subnet 10.160.8.0 netmask 255.255.255.0 { #из этой подсети запросы dhcp-relay со свичей
}
}
В файле dhcp-event производится проверка agent.circuit-id, agent.remote-id, IP и MAC на валидность, и если все ок, то добавляется машрут до этого адреса через нужный интерфейс
Примитивный пример dhcp-event:
#hostel 1
if ($ARGV[3] eq '0:21:91:92:d7:55') {
system "/sbin/ip ro add $ARGV[0] dev eth1 src a.b.c.d";
}
#hostel 2
if ($ARGV[3] eq '0:21:91:92:d4:92') {
system "/sbin/ip ro add $ARGV[0] dev eth2 src a.b.c.d";
}
здесь проверяется только $ARGV[3] (то есть agent.remote-id, или МАС свича, через который получен DHCP-запрос), но можно проверять так же все остальные поля получая их валидные значения, к примеру, из базы
В итоге получаем:
1) клиент, не запросивший адрес по DHCP — не выходит дальше своего неуправляемого свича, его не пропускает IP-MAC-PORT-BINDING;
2) клиент, у которого МАС известен (есть в базе), но запрос не соответствует порту или коммутатору — получит IP привязанный к этому МАС, но маршрут до него не будет добавлен, соответственно proxy_arp «ответит», что адрес уже занят, и адрес будет тут же освобожден;
3) клиент, у которого МАС не известен, получит адрес из временного пула. С этих адресов происходит перенаправление на страничку с информацией, здесь же можно перерегистрировать свой МАС использую логин/пароль;
4) и наконец клиент, у которого МАС известен, и совпадает привяка к коммутатору и порту — получит свой адрес. Dhcp-snooping добавит динамическую привязку в таблицу impb на коммутаторе, сервер добавит маршрут до этого адреса через нужный интерфейс бывшего бриджа.
При окончании аренды или освобождении адреса вызывается скрипт /etc/dhcp/dhcp-release, содержание которого донельзя примитивно:
system "/sbin/ip ro del $ARGV[0]";
Есть небольшой изъян в безопасности, конкретно в п.2. Если использовать нестандартный dhcp-client, который не проверяет, занят ли выданный dhcp-сервером адрес, то освобождения адреса не произойдет. Конечно выхода во внешнюю сеть, дальше маршрутизатора, у пользователя не появится, так как сервер не добавит маршрут на этот адрес через нужный интерфейс, но коммутатор разблокирует эту пару MAC-IP на своем порту.
Обойти этот изъян можно, используя классы в dhcpd.conf, но это заметно усложняет конфигурационный файл, и, соответственно, увеличивает нагрузку на наш старенький сервер. Потому как для каждого абонента придется создать свой класс, с довольно сложным условием попадания в него, а потом свой пул. Хотя в планах попробовать на практике, как сильно это увеличит нагрузку.
Таким образом получилось, что за соответствием пары IP-MAC теперь «следит» DHCP при выдаче адреса, доступ с «невалидных» MAC-IP ограничивает коммутатор. С сервера теперь можно было убрать не только bridge, но и macipmap, заменив 6 сетов (из «Оптимизации 1») на один ipmap, в котром содержатся все открытые IP-адреса. А так же удалив -m physdev.
Еще интерфейсы сервера перешли из promisc mode в обычный, что тоже несколько снизило нагрузку.
А именно, вся эта процедура с разборкой бриджа снизила общую нагрузку на сервер почти в 2 раза! Softirq теперь не 50-100%, а 25-50%. При этом контроль доступа к сети стал только лучше.
Оптимизация 5
После проведения последней оптимизации, хотя нагрузка заметно упала, была замечена странность: возрос iowait. Не то чтобы сильно, с 0-0.3% до 5-7%. Это с учетом того, что на данном сервере вообще каких бы то ни было дисковых операций практически нет — он просто перекидывает пакетики.
![iowait](https://habrastorage.org/getpro/habr/post_images/2d7/016/f2a/2d7016f2adee88ecbdddb30e3b9add0f.png)
(синий юзертайм — компиляция ядра)
iostat показывал, что есть постоянная нагрузка на диск в 800-820 Blk_wrtn/s
Начал поиск процессов, которые могли бы заниматься записью. Выполнение
echo 1 > /proc/sys/vm/block_dump
дало странный результат: виновниками оказались
kjournald(483): WRITE block 76480 on md0
и
md0_raid1(481): WRITE block 154207744 on sdb2
md0_raid1(481): WRITE block 154207744 on sda3
Ext3 находится в режиме
data=writeback, noatime
, да и на диск ничего не пишется, разве что кроме логов. Но логи какие писались вчера — такие и пишутся сегодня, их объем не возрос, то есть iowait тоже не дожен был возрасти.Начал по шагам прокручивать в голове, что я делал, и что могло повлиять на iowait. В итоге остановил syslog — и iowait резко упал до ~0%
Для того, чтобы dhcp не захламял messages своими сообщениями, я отправил их в log-facility local6, а в syslog.conf вписал:
*.info;mail.none;authpriv.none;cron.none;local6.none /var/log/messages
local6.info /var/log/dhcpd.log
оказалось, что при записи через syslog для каждой строки делается sync. Запросов к dhcp-серверу довольно много, генерируется много событий, которые попадают в лог, и вызывается много sync.
Исправление на
local6.info -/var/log/dhcpd.log
снизило iowait в моем случае 10-100 раз, вместо 5-7% стало 0-0.3%
Результат оптимизации:
![Результат оптимизации](https://habrastorage.org/getpro/habr/post_images/ec0/241/62a/ec024162a5db7686827523355c9456ad.png)
К чему эта статья.
Во-первых, возможно кто-то почерпнет из нее полезные для себя решения. Америку я тут не открыл, хотя бОльшая часть описанных тут событий была в дохабровую эпоху, и «гугление рецептов» мало помогало, доходил до всего своей головой.
Во-вторых, постоянно приходится сталкиваться с тем, что вместо оптимизации кода разработчики занимаются наращиванием вычислительных мощностей, и эта статья может быть примером того, что при желании можно сколь угодно долго находить ошибки в своем, казалось бы много раз проверенном, коде и справлять их. 90% разработчиков сайтов проверяют у себя на машине работу сайта, выкладывают в продкашн. Потом все это дело тормозит под нагрузкой. Теребят админа сервера. Оптимизацией сервера в этому случае мало что можно добиться, если код написан изначально не оптимально. И покупается новый сервер, или еще один сервер и балансировщик. Потом балансировщик балансировщика и так далее. А код внутри как был неоптимальным, так и остается навеки. Я конечно понимаю, что в нынешних реалиях оптимизация кода стоит дороже, чем экстенсивное наращивание мощностей, но с появлением огромного числа «доморощенных» айтишников объемы «плохого кода» приобретют серьезные масштабы.
Из ностальгического: имею на полке старый ноутбук, Р3-866 2001гв (Panasonic CF-T1 если кому-то это что-то скажет), но сейчас на нем невозможно даже сайты смотреть, хотя смысла на этих сайтах за 10 лет больше не стало. С любовью вспоминаю интересные игрушки на ZX-Spectrum по геймплею не уступающим сегодняшним монстрам, требующим 4 ядра/4гига