Админские байки: в погоне за фрагментацией туннелей в оверлейной сети
Когда администраторы сталкиваются с неожиданной проблемой (раньше работало, и, вдруг, после обновления, перестало), у них существует два возможных алгоритма поведения: fight or flight. То есть либо разбиратся в проблеме до победного конца, либо убежать от проблемы не вникая в её суть. В контексте обновления ПО — откатиться назад.
Откатиться после неудачного апгрейда — это, можно сказать, печальная best practice. Существуют целые руководства как готовиться к откату, как их проводить, и что делать, если откатиться не удалось. Целая индустрия трусливого поведения.
Альтернативный путь — разбираться до последнего. Это очень тяжёлый путь, в котором никто не обещает успеха, объём затраченных усилий будет несравним с результатом, а на выходе будет лишь чуть большее понимание произошедшего.
Облако «Instant Servers» Webzillа. Рутинное обновление хоста nova-compute. Новый live image (у нас используется PXE-загрузка), отработавший шеф. Всё хорошо. Внезапно, жалоба от клиента: «одна из виртуалок странно работает, вроде работает, но как начинается реальная нагрузка, так всё замирает». Инстансы клиента переносим на другую ноду, проблема клиента решена. Начинается наша проблема. Запускаем инстанс на этой ноде. Картинка: логин по ssh на Cirros успешен, на Ubuntu — зависает. ssh -v показывает, что всё останавливается на этапе «debug1: SSH2_MSG_KEXINIT sent».Все возможные внешние методы отладки работают — метаданные получаются, DHCP-аренда инстансом обновляется. Возникает подозрение, что инстанс не получает опцию DHCP с MTU. Tcpdump показывает, что опция отправляется, но не известно, принимает ли её инстанс.
Нам очень хочется попасть на инстанс, но на Cirros, куда мы можем попасть, MTU правильный, а на Ubuntu, в отношении которой есть подозрение о проблеме MTU, мы как раз попасть не можем. Но очень хотим.
Если это проблема с MTU, то у нас есть внезапный помощник. Это IPv6. При том, что «белые» IPv6 мы не выделяем (извините, оно пока что не production-ready в openstack), link-local IPv6 работают.Открываем две консоли. Одна на сетевую ноду. Проникаем в network namespace:
sudo stdbuf -o0 -e0 ip net exec qrouter-271cf1ec-7f94–4d0a-b4cd-048ab80b53dc /bin/bash (stdbuf позволяет отключить буфферизацию у ip net, благодаря чему вывод на экран появляется в реальном времени, а не с задержкой, ip net exec выполняет код в заданном network namespace, bash даёт нам шелл).На второй консоли открываем compute-node, цепляемся tcpdump к tap’у нашей ubuntu: tcpdump -ni tap87fd85b5–65.
Изнутри namespace делаем запрос на all-nodes link-local мультикаст (эта статья не про ipv6, но суть происходящего вкратце: каждый узел имеет автоматически сгенерированный ipv6 адрес, начинающийся с FE80::, кроме того, каждый узел слушает на мультикаст-адресах и отвечает на запросы по ним. В зависимости от роли узла список мультикастов разный, но каждый узел, хотя бы, отвечает на all-nodes, то есть на адрес FF02::1). Итак, делаем мультикаст-пинг:
ping6 -I qr-bda2b276–72 ff02::1 PING ff02::1(ff02::1) from fe80:: f816:3eff: fe0a: c6a8 qr-bda2b276–72: 56 data bytes 64 bytes from fe80:: f816:3eff: fe0a: c6a8: icmp_seq=1 ttl=64 time=0.040 ms 64 bytes from fe80:: f816:3eff: fe10:35e7: icmp_seq=1 ttl=64 time=0.923 ms (DUP!) 64 bytes from fe80:: f816:3eff: fe4a:8bca: icmp_seq=1 ttl=64 time=1.23 ms (DUP!) 64 bytes from fe80::54e3:5eff: fe87:8637: icmp_seq=1 ttl=64 time=1.29 ms (DUP!) 64 bytes from fe80:: f816:3eff: feec:3eb: icmp_seq=1 ttl=255 time=1.43 ms (DUP!) 64 bytes from fe80:: f816:3eff: fe42:8927: icmp_seq=1 ttl=64 time=1.90 ms (DUP!) 64 bytes from fe80:: f816:3eff: fe62: e6b9: icmp_seq=1 ttl=64 time=2.01 ms (DUP!) 64 bytes from fe80:: f816:3eff: fe4d:53af: icmp_seq=1 ttl=64 time=3.66 ms (DUP!) Возникает вопрос — кто тут кто? По очереди пробовать зайти неудобно и долго.Рядом у нас в соседнем окне tcpdump, слушающий интерфейс интересующего нас инстанса. И мы видим в нём ответ только от одного IP — интересующего нас IP. Это оказывается fe80:: f816:3eff: feec:3eb.
Теперь мы хотим подключиться по ssh к этой ноде. Но любого, попробовавшего команду ssh fe80:: f816:3eff: feec:3eb ждёт сюрприз — » Invalid argument».
Причина в том, что link-local адреса не могут быть использованы «просто так», они имеют смысл только в пределах линка (интерфейса). Но ssh нет опции «использовать такой-то исходящий IP/интерфейс такой-то»! К счастью, есть опция по указанию имени интерфейса в IP-адресе.
Мы делаем ssh fe80:: f816:3eff: feec:3eb% qr-bda2b276–72 — и оказываемся на виртуалке. Да, да, я понимаю ваше возмущение и недоумение (если у вас его нет — вы ненастоящий гик, либо у вас много лет работы с IPv6). «fe80:: f816:3eff: feec:3eb% qr-bda2b276–72» — это такой «ИП-адрес». У меня не хватает языка передать степень сарказма в этих кавычках. IP-адрес, с процентами и именем интерфейса. Интересно, что будет, если кто-то загрузит себе аватаркой что-то вроде http://[fe80:: f816:3eff: feec:3eb%eth1]/secret.file с сервера в локалке веб-сервера на каком-нибудь сайте…
… И мы оказываемся на виртуалке. Почему? Потому что IPv6 лучше, чем IPv4 умеет обрабатывать ситуации плохого MTU, благодаря обязательному PMTUD. Итак, мы на виртуалке.
Я ожидаю увидеть неправильное значение MTU, идти в логи cloud-init’а и разбираться почему так. Но вот сюрприз — MTU правильный. Упс.
Внезапно, проблема из локальной и понятной становится совсем не понятной. MTU правильный, а пакеты дропаются.… Если же подумать внимательно, то и с самого начала проблема была не такой уж простой — миграция инстанса не должна была поменять ему MTU.Начинается мучительная отладка. Вооружившись tcpdump’ом, ping’ом и двумя инстансами (плюс network namespace на сетевой ноде), разбираемся:
Локально два инстанса на одном compute друг друга пингуют с пингом максимального размера. Инстанс с сетевой ноды не не пингуется (здесь и дальше — с пингом максимального размера) Сетевая нода инстансы на других компьютах пингует. Пристальное внимание к tcpdump’у внутри инстанса показывает, что когда сетевая нода пингует инстанс, он пинги видит и отвечает. Упс. Большой пакет доходит, но теряется по дороге обратно. Я бы сказал, asymmetric routing, но какой там к чёрту роутинг, когда они в соседних портах коммутатора? Пристальное внимание на ответ: ответ виден на инстансе. Ответ виден на tap’е. Но ответ не виден в network namespace. А как у нас обстоят дела с mtu и пакетами между сетевой нодой и компьютом? (внутренне я уже торжествую, мол, нашёл проблему). Рраз — и (большие) пинги ходят.
Чо? (и длинная недоумевающая пауза).
Что дальше делать не понятно. Возвращаюсь к оригинальной проблеме. MTU плохой. А какой MTU хороший? Начинаем экспериментировать. Бисекция: минус 14 байт от предыдущего значения. Минус четырнадцать байт. С какой стати? После апгрейда софта? Делаю vimdiff на список пакетов, обнаруживаю приятную перспективу разбираться с примерно 80 обновившимися пакетами, включая ядро, ovs, libc, и ещё кучу либ. Итак, два пути отступления: понизить MTU на 14 байт, либо откатиться назад и дрожать над любым апдейтом.
Напомню, что о проблеме сообщил клиент, а не мониторинг. Так как MTU — это клиентская настройка, то «непрохождение больших пакетов с флагом DF» — это не совсем проблема инфраструктуры. То есть совсем не проблема инфраструктуры. То есть если оно вызвано не апгрейдом, а предстоящим солнечным затмением и вчерашним дождём, то мы даже не узнаем о возврате проблемы, пока кто-нибудь не пожалуется. Дрожать над апдейтом и опасаться неведомого о чём не узнаешь заранее? Спасибо, перспектива, о которой я мечтал всю свою профессиональную жизнь. А даже если мы понизим MTU, то почему четырнадцать байт? А если завтра станет двадцать? Или нефть подешевеет до 45? Как с этим жить?
Однако, проверяем. Действительно, MTU чуть ниже в опциях DHCP, и перезагрузившийся инстанс отлично работает. Но это не выход. ПОЧЕМУ?
Начинаем всё с начала. Возвращаем старый MTU, трейсим пакет tcpdump’ом ещё раз: ответ виден на интерфейсе инстанса, на tap’е… Смотрим tcpdump на сетевом интерфейсе ноды. Куча мелкого раздражающего флуда, но с помощью grep’а мы видим, что запросы приходят (внутри GRE), но ответы не уходят обратно.
АГА!
По крайней мере видно, что оно теряется где-то в процессе. Но где? Я решаю сравнить поведение с живой нодой. Но вот беда, на «живой» ноде tcpdump показывает нам пакеты. Тысячи их. В милисекунду. Добро пожаловать в эру tengigabitethernet. Grep позволяет из этого флуда наловить что-то, но вот нормального дампа получить уже не удастся, да и производительность такой конструкции вызывает вопросы.
Фокусируемся на проблеме: я не знаю, как фильтровать трафик с помощью tcpdump’а. Я знаю, как отфильтровать по source, dest, port, proto и т.д., но как отфильтровать пакет по IP-адресу внутри GRE — я совершенно не знаю. Более того, это довольно плохо знает и гугль.
До определённого момента я этот вопрос игнорировал, считая, что чинить важнее, но нехватка знания начала очень больно кусать. Коллега (kevit, которого я привлёк к вопросу, занялся им. Прислал ссылку tcpdump -i eth1 'proto gre and (ip[58:4] = 0×0a050505 or ip[62:4] = 0×0a050505)'.
Ух. Хардкорный 0xhex в моих вебдванольных облачных сингулярностях. Ну, что ж. Жить можно.
К сожалению, правило срабатывало неправильно или не срабатывало вовосе. Ухватив идею, методом brute force я поймал искомые смещения: 54 и 58 для source и dest IP-адресов. Хотя kevit показал откуда он взял смещения — и это выглядело чертовски убедительно. IP-заголовок, GRE, IP-заголовок.
Важное достижение: у меня появился инструмент для прецизионного разглядывания единичных пакетиков в многогигабитном флуде. Разглядываем пакеты… Всё равно ничего не понятно.
Tcpdump наш друг, но wireshark удобнее. (Я знаю про tshark, но он тоже неудобный). Делаем дамп пакетов (tcpdump -w dump, теперь мы его можем сделать), утаскиваем к себе на машину и начинаем разбираться. Я решил для себя разобраться с смещениями (из общей въедливости). Открываем в wireshark’е и видим…
Смотрим на размеры заголовков и убеждаемся, что правильное смещение начала IP-пакета — 42, а не 46. Списав эту ошибку на чью-то невнимательность, я решил продолжить разбираться дальше на следующий день, и пошёл домой.
Уже где-то совсем рядом с домом меня осенило. Если исходные предположения про структуру заголовков неверны, то это означает, что оверхед от GRE при туннелировании другой.
Ethernet-заголовок, vlan, IP-заголовок, GRE-заголовок, энкапсулированный IP-пакет…
Стоп. Но на картинке совсем другой заголовок. GRE в neutron’е энкапуслирует вовсе не IP-пакеты, а ethernet-фреймы. Другими словами, начальные предположения о том, какую часть MTU на себя отъедает GRE неверны. GRE «берёт» на 14 байт больше, чем мы рассчитывали.
Нейтрон строит overlay network поверх IP с помощью GRE, и это L2 сеть. Разумеется, там должны быть энкапуслированные ethernet-заголовки.
То есть MTU и должен быть на 14 байт меньше. С самого начала. Когда мы планировали сеть, предположения про cнижение MTU из-за GRE, мы сделали ошибку. Довольно серьёзную, так как это вызывало фрагментацию пакетов.
Ладно, с ошибкой понятно. Но почему после обновления оно переставало работать? По предыдущим изысканиям стало понятно, что проблема связана с MTU, неверным учётом заголовка GRE и фрагментацией GRE-пакетов. Почему фрагментированные пакеты перестали проходить?
Внимательный и пристальный tcpdump показал ответ: GRE стал отсылаться с DNF (do not fragment) флагом. Флаг появлялся только на GRE-пакетах, которые энкапсулировали IP-пакеты с DNF флагом внутри, то есть флаг копировался на GRE из его полезной нагрузки.
Для пущей уверенности я посмотрел на старые ноды — они фрагментировали GRE. Шёл основной пакет, и хвостик с 14 байтами полезной нагрузки. Вот так ляп…
Осталось выяснить, почему это началось после апгрейда.
Самыми подозрительными на регрессию пакетами были Linux и Openvswitch. Readme/changelog/news не прояснило ничего особенного, а вот инспекция git’а (вот и ответ, зачем нам нужен открытый исходый код — чтобы иметь доступ к Документации) открыла что-то крайне любопытное:
commit bf82d5560e38403b8b33a1a846b2fbf4ab891af8
Author: Pravin B Shelar
datapath: compat: Fix compilation 3.11 Kernel 3.11 is only kernel where GRE APIs are available but not vxlan. Add check for vxlan xmit to detect this case. Сам патч ничего интересного из себя не представляет и к сути дела не относится, но зато даёт подсказку: GRE API в ядре. А у нас апгрейд с 3.8 до 3.13 как раз происходил. Гуглим в бинге… Находим патч в openvswitch (datapath module), в ядре: git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/? id=aa310701e787087dbfbccf1409982a96e16c57a6. Другими словами, как только у нас ядро начинает предоставлять сервисы GRE, ядерный модуль openvswitch передаёт обработку gre в модуль ядра ip_gre. Изучаем код ip_gre.c, спасибо за комментарии в нём, да, мы все «любим» циску.
Вот заветная строчка:
static int ipgre_fill_info (struct sk_buff *skb, const struct net_device *dev) { struct ip_tunnel *t = netdev_priv (dev); struct ip_tunnel_parm *p = &t→parms;
if (nla_put_u32(skb, IFLA_GRE_LINK, p→link) || … nla_put_u8(skb, IFLA_GRE_PMTUDISC, !(p→iph.frag_off & htons (IP_DF))))
Другими словами, ядро копирует IP_DF из заголовка энкапсулируемого пакета.
(Внезапный интересный оффтопик: Linux копирует из оригинального пакета так же и TTL, то есть GRE-туннель «наследует» TTL у энкапсулируемого пакета)
Самолёт упал, потому что по направлению полёта оказалась Земля.Во время начальной настройки инсталляции мы выставили MTU для виртуальных машин в рамках ошибочного предположения. Из-за механизма фрагментации мы отделались незначительной деградацией производительности. После апгрейда ядра с 3.8 на 3.13, OVS переключился на ядерный модуль ip_gre.c, который копирует флаг do not fragment из исходного IP-пакета. Большие пакеты, которые не «влазили» в MTU после дописывания к ним заголовка, больше не фрагментировались, а дропались. Из-за того, что дропался GRE, а не вложенный в него пакет, ни одна из сторон TCP-сесии (посылающей пакеты) не получала ICMP-оповещений о «непроходимости», то есть не могла адаптироваться к меньшему MTU. IPv6 в свою же очередь, не рассчитывал на наличие фрагментации (её нет в IPv6) и обрабатывал потерю больших пакетов правильным образом — уменьшая размер пакета.
Виноваты мы — ошибочно выставили MTU. Едва заметное поведение в ПО привело к тому, что ошибка начала нарушать работу IPv4.Что делать? Мы поправили MTU в настройках dnsmasq-neutron.conf (опция dhcp-option-force=26,), дали клиентам «отстояться» (обновить аренду адреса по DHCP, вместе с опцией), проблема полностью устранена.
Можно ли такое обнаружить мониторингом упреждающе? Сказать честно, разумных вариантов я не вижу — слишком тонкая и сложная диагностика, требующая крайней кооперации со стороны клиентских инстансов (на это мы полагаться не можем — вдруг, кто-то, по собственным нуждам, пропишет что-нибудь странное с помощью iptables?).
Вместо того, чтобы трусливо откатиться на предыдущю версию ПО и занять позицию «работает — не трогай», «я не знаю, что поменяется, если мы обновимся, так что обновляться мы больше никогда не будем», было потрачено примерно 2 человекодня на отладку, но была решена не только локальная (видимая) регрессия, но и найдена и устранена ошибка в существующей конфигурации, повышающая оверхед от работы сети. Помимо устранения проблемы ещё значительно улучшилось понимание используемых технологий, была разработана методика отладки сетевых проблем (фильтрация трафика в tcpdump по полям внутри GRE).