30 сентября, 2015

Mikrotik WAN Failover и «повисающий» SIP

Не так давно довелось столкнуться с довольно тривиальной ситуацией, решений для которой в рунете сходу найти, к моему удивлению, не удалось. Ситуация: есть офис, раздачей интернетов в котором занимается роутер под управлением RouterOS. В офисе есть некоторое количество SIP-телефонов, а также резервный канал доступа в интернет на случай проблем с основным.

И вроде ничего не предвещало, как говорится, но вылез один очень неприятный момент: при переключении на резервный канал те самые SIP-телефоны напрочь отказывались заново устанавливать соединение с PBX (находящейся за пределами офиса), что заставляло обитателей того офиса вручную перезагружать каждый телефон, да еще и стараться побыстрее заметить, что телефоны отвалились, чтоб не пропустить какой-нибудь важный звонок.

В чем же дело? Всё довольно просто: переключение на резервный канал было реализовано с помощью двух маршрутов с destination 0.0.0.0/0 и разным значением distance в зависимости от приоритета, а также с настроенными соответствующим образом опциями check-gateway. Но переключение переключением, а вот таблицу NAT роутер при этом не очищал, т.к. его об этом никто и не просил, что приводило к «повисанию» UDP-соединений на том дефолте, который по несчастью отвалился.

Избежать подобной ситуации можно несколькими способами (мне попадались даже разного рода скриптовые ады, отключающие-включающие connection tracking, и прочие душераздирающие зрелища), но лично я решил сделать решение без ненужного скриптинга, максимально простое и лаконичное.

Итак, из руководства к RouterOS нам известно, что check-gateway проверяет доступность шлюза каждые 10 секунд, и если ответа на проверочный запрос не поступило в течение 10 секунд, то проверка считается неудачной. А после двух таких таймаутов подряд шлюз помечается как недоступный, тут-то и происходит переключение на резерв.

Для простоты будем считать, что период переключения каналов у нас равен 30 секундам. Тогда создадим netwatch, который будет пингать нашу телефонную станцию с интервалом в 15 секунд (половина от тех 30, чтоб наверняка). И да, разумеется наша телефонная станция должна в нормальной ситуации отвечать на пинг:

/tool netwatch add host=адрес_нашей_PBX interval=15s timeout=1s up-script="/ip firewall connection remove [find where dst-address~\"5060\"]" down-script="/ip firewall connection remove [find where dst-address~\"5060\"]"

Теперь, если вдруг у нас пропадет пинг до IP-АТС, из таблицы соединений будет вычищено всё, что шло на порт 5060. Но вообще я бы рекомендовал написать в up/down скрипт что-нибудь такое:

/ip firewall connection remove [find where src-address~"192.168.88."]

Под 192.168.88 подразумевается диапазон локальных офисных ip-адресов. Это сбросит все клиентские соединения (при этом не затронув те, что инициировал сам роутер, например L2TP-туннель до провайдера или еще что-нибудь), что в свою очередь сделает процесс переключения на резервный канал и обратно более «бесшовным» для пользователей, т.к. коннекты будут оборваны и пакеты точно не будут пытаться идти по умершему маршруту.

Не стоит беспокоиться насчет того, что такое решение может рвать соединения излишне часто: netwatch-скрипты выполняются по одному разу (единожды, когда пингуемый хост стал виден, и точно так же единожды, когда из виду пропал), а не циклично.
Данное решение было всячески оттестировано с пристрастием (и без), и никаких проблем выявлено не было.

14 июня, 2015

Еще одна возможная причина возникновения ошибок «Uncorrectable parity/CRC error» и борьба с ней

В интернете распространено мнение о том, что если у вас внезапно в /var/log/messages обнаружилась огромная куча ошибок вида
(ada2:ahcich2:0:0:0): WRITE_FPDMA_QUEUED. ACB: 61 00 f0 2f 9b 40 38 00 00 01 00 00
(ada2:ahcich2:0:0:0): CAM status: Uncorrectable parity/CRC error
(ada2:ahcich2:0:0:0): Retrying command
то это у вас либо диск умирает, либо питальник, либо шлейф от контроллера к диску сделан из дешевой китайской лапши. Однако, довелось столкнуться с ситуацией, когда на одном из двух абсолютно новых WD Purple у меня сразу полезло это безобразие, хотя диски явно были в порядке, шлейфы – тоже, а значения выходного напряжения у блока питания гуляли в пределах 0.03В, т.е. всё было вроде бы идеально, но ошибки продолжали сыпаться.

После долгих поисков виновника всё-таки удалось отыскать. Им оказался spread spectrum, включенный в настройках биоса материнской платы. Стоило отключить эту опцию, и ошибки исчезли, а волосы мои стали мягкими и шелковистыми.

20 мая, 2015

«Два конца»

Интересная и не совсем тривиальная задача подвернулась. Ситуация: в некоторой организации локальная сеть имеет адрес, скажем, 192.168.0.0/24, и нужно пробросить некоторый порт (пусть будет tcp/8080) для кофеварки, адрес которой 192.168.0.50.

Казалось бы, что в этом такого? Так уж сложилось, что у провайдера, через которого мы получаем доступ в интернет по L2TP, адрес VPN-сервера тоже 192.168.0.50. Соответственно, сделать так, чтобы роутер подключался к VPN через сеть провайдера, у нас есть маршрут для 192.168.0.50/32, но если попытаться пробросить порт на этот адрес, пакеты закономерно пойдут на VPN-сервер провайдера, а не на нашу кофеварку, что нам совсем не нужно.

Что же делать?

Сначала создадим маршрут:

/ip route add dst-address=192.168.0.50/32 gateway=bridge-local routing_mark=coffee_mark

Обратите внимание на routing_mark.
Теперь создадим правило в mangle:
/ip firewall mangle add chain=prerouting in-interface=WAN protocol=tcp dst-port=8080 \
action=mark-routing new-routing-mark=coffee_mark

Ну и dst-nat правило, с ним ничего необычного:
/ip firewall nat add chain=dstnat in-interface=WAN protocol=tcp dst-port=8080 action=dst-nat to-addresses=192.168.0.50

Вот и все. Теперь соединениям, идущим на нашу кофеварку по указанному порту, будет присваиваться routing_mark, с помощью которого наш роутер поймет, где искать ту кофеварку.

07 апреля, 2015

Организация a/b тестирования веб-проектов средставами nginx

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

Определимся с условиями нашей задачи:
  1.  На новую версию сервиса мы должны направить ≈20% всех пользователей
  2.  Есть ip-адреса клиентов, которые должны всенепременно попадать на старую версию
  3.  То же самое, но – всенепременно на новую.
Далее я приведу конфигурационный файл целиком, а после опишу принцип его действия.

geo $mytest_participant {
  default              1;
  10.1.1.1             0;
  192.168.45.2         2;
}

map $mytest_participant $resulting_upstream_name {
  0     "old_version_upstream";
  1     "distributed_upstream";
  2     "new_version_upstream";
}

upstream distributed_upstream {
  ip_hash;
  server 1.1.1.1 weight=8; # Old version server, 80% of clients
  server 2.2.2.2; # New version server
}

upstream old_version_upstream {
  server 1.1.1.1;
}

upstream new_version_upstream {
  server 2.2.2.2;
}

server {
  listen 80;

  server_name example.com;

  location / {
    proxy_pass http://$resulting_upstream_name;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For  $proxy_add_x_forwarded_for;
  }

}

Итак, первый блок (geo) описывает как раз распределение пользователей по апстримам.
ip-адреса (можно использовать CIDR-нотацию, разумеется), для которых выставлено значение 0, будут направляться на старую версию принудительно, 1 - распределяться по вышеуказанному алгоритму (80% – на старую, 20% – на новую версию), ну и 2 - принудительно на новую.

Именно в зависимости от этого значения и присваивается имя апстрима в следующем блоке (map).

Далее все просто: описываются апстримы для старой и новых версий, а также наш распределенный апстрим. Для последнего используется алгоритм балансировки ip_hash, благодаря которому одни и те же ip-адреса будут попадать строго на тот апстрим, на который попали впервые. Если нам нужно изменить процентное соотношение распределения по старой и новым версиям, то правим соответствующим образом параметр weight для старого апстрима.

Вот, собственно, и всё.

Нужно, однако, помнить, что ip_hash использует только первые три октета ip-адреса клиента, т.е. на ту или иную версию попадет сразу весь /24 сегмент. Однако обычно это не является чем-то критичным, тем более, что в блоке geo можно подкорректировать ситуацию для отдельных ip-адресов, при наличии необходимости.