![image](https://habrastorage.org/getpro/habr/post_images/ff4/f8a/000/ff4f8a000a92a27f8f91112bd1d0c7cc.jpg)
До недавнего времени в Одноклассниках в качестве основного Linux-дистрибутива использовался частично обновлённый OpenSuSE 10.2. Однако, поддерживать его становилось всё труднее, поэтому с прошлого года мы перешли к активной миграции на CentOS 7. На подготовительном этапе перехода для CentOS были отработаны все внутренние процедуры, подготовлены конфиги и политики настройки (мы используем CFEngine). Поэтому сейчас во многих случаях миграция с одного дистрибутива на другой заключается в установке ОС через kickstart и развёртывании приложения с помощью системы деплоя нашей разработки — всё остальное осуществляется без участия человека. Так происходит во многих случаях, хотя и не во всех.
Но с самыми большими проблемами мы столкнулись при миграции серверов раздачи видео. На их решение у нас ушло полгода.
Вкратце об их конфигурации:
- 4 x 10 Гбит к пользователям
- 2 x 10 Гбит к хранилищу
- 256 Гбайт RAM — кэш в памяти
- 22 х 480 Гбайт SSD — кэша на SSD
- 2 х E5-2690 v2
Несколько метрик, характеризующих раздачу видео:
- 600 Гбит/сек. — общая пиковая скорость отдачи видео
- 220М просмотров видео в день
- 600К одновременно просматриваемых видео
Проблема 1 — сильный рост CPU system time
![image](https://habrastorage.org/getpro/habr/post_images/836/f96/55a/836f9655a3602d87c35f57407fda6431.jpg)
Вскоре после запуска первого сервера начала сильно расти нагрузка на процессор по system time.
В это же время в top было видно множество migration и ksoftirqd процессов. Сначала мы попробовали крутить настройки ядра. Что нам не помогло:
- увеличение sched_migration_cost
- отключение transparent_hugepage
- увеличение vfs_cache_pressure
- отключение NUMA
При очередном росте нагрузки perf top показал 50% нагрузки на isolate_freepages_block. Само по себе название вызова нам, к сожалению, ничего не говорит. Но вот слово freepages несколько смутило, т.к. свободной памяти на сервере было около 45 Гбайт. Из своего опыта мы уже знали, что если на сервере много свободной памяти, а ядро всё равно жалуется на его нехватку (иногда это выражается в запуске OOM killer), то, скорее всего, проблема во фрагментации. Сброс дискового кеша (
echo 3 > /proc/sys/vm/drop_caches
) моментально исцелял сервер, что только подтверждало наши предположения.Фрагментация памяти — нередкая проблема (и характерна не только для Linux), и в ядро регулярно вносятся изменения для борьбы с ней. Одним из виновников фрагментации является само ядро, точнее дисковый кеш, который нельзя ни отключить, ни ограничить в объёме. Это не значит, что фрагментация в нашем случае была вызвана именно дисковым кешем, но точные причины были не так важны. Важнее было решение — дефрагментация. Такой механизм есть в ядре, но очевидно, что он не справлялся (либо вместо него запускалось высвобождение памяти — global reclaim). Дефрагментация запускается только тогда, когда свободная память опускается ниже определённой отметки (zone watermark), и в нашем случае это происходило слишком поздно. Единственный способ заставить её запускаться раньше — это повысить min_free_kbytes через sysctl. Данный параметр говорит ядру стараться держать часть памяти свободной, а чтобы удовлетворить это требование, ему приходится запускать дефрагментацию раньше. В нашем случае хватило значения в 1 Гбайт.
Проблема 2 — уход в swap
Для начала стоит упомянуть, что мы используем vm.swappiness=0, так как точно знаем, что для нас своп — это зло. Итак, на сервере около 45 Гбайт свободной памяти и всё же он периодически уходит в своп. Как такое возможно? Для начала стоит напомнить, что память в Linux — это не один большой кусок.
- Память делится на ноды: один физический процессор — одна нода.
- Каждая нода делится на зоны (представление для 64-битных систем) — ZONE_DMA (0-16 Мбайт), ZONE_DMA32 (0-4 Гбайт), ZONE_NORMAL (4+ Гбайт).
- Каждая зона делится на области памяти размером степеней двойки (order of 2), т.е. 20*PAGE_SIZE, 21*PAGE_SIZE...210*PAGE_SIZE (текущее распределение можно посмотреть в /proc/buddyinfo). Отсутствие свободных областей большого размера — это и есть фрагментация, о которой мы говорили в предыдущем разделе.
Сначала мы опять винили фрагментацию, но дальнейшее увеличение min_free_kbytes, а также увеличение vfs_cache_pressure нам не помогло. Пришлось познакомиться с утилитой numastat (
numastat -m ).
Но сначала ещё одно отступление про работу приложения раздачи видео. На сервере замонтирован tmpfs (занимает почти всю память), на котором приложение создаёт 1 файл и использует его в качестве кеша.
Что же мы увидели в выводе numastat? А то, что различные типы памяти распределены между нодами крайне неравномерно:
Per-node process memory usage (in MBs) for PID 7781 (java)
Node 0
Node 1
Total
Huge
0
0
0
Heap
0.20
0.14
0.34
Stack
118.82
137.87
256.70
Private
80200.73
123323.81
203524.55
Total
80319.76
123461.82
203781.58
Per-node system memory usage (in MBs):
Node 0
Node 1
Total
MemTotal
131032.75
131072.00
262104.75
MemFree
1228.93
639.84
1868.78
MemUsed
129803.82
130432.16
260235.98
Active
23224.13
121073.42
144297.55
Inactive
101138.88
3753.98
104892.85
Active(anon)
1690.50
120997.86
122688.36
Inactive(anon)
79528.66
3560.95
83089.61
Active(file)
21533.63
75.57
21609.20
Inactive(file)
21610.21
193.03
21803.24
Unevictable
0
0
0
Mlocked
0
0
0
Dirty
0.11
0.02
0.13
Writeback
0
0
0
FilePages
122397.46
124295.47
246692.93
Mapped
78436.03
122947.26
201383.29
AnonPages
1966.62
532.02
2498.64
Shmem
79251.21
123964.70
203215.90
KernelStack
2.44
2.57
5.01
PageTables
158.62
252.29
410.91
NFS_Unstable
0
0
0
Bounce
0
0
0
WritebackTmp
0
0
0
Slab
1801.95
1932.29
3734.23
SReclaimable
1653.13
1818.79
3471.92
SUnreclaim
148.82
113.49
262.31
AnonHugePages
1856.00
498.00
2354.00
HugePages_Total
0
0
0
HugePages_Free
0
0
0
HugePages_Surp
0
0
0
За распределение памяти между нодами отвечает технология NUMA, и она не может равномерно распределить 1 файл в tmpfs между нодами. Запуск приложения в режиме interleave (numactl —interleave=all, равномерное распределение памяти приложения между нодами) решило нашу проблему.
Проблема 3 — неравномерное распределение нагрузки по ядрам
Пожалуй, все, кто работал с большим трафиком, сталкивались с проблемой распределения прерываний между ядрами процессоров. Чаще всего проблема исследуется и решается так (здесь приведены все шаги, но не обязательно, что вы столкнётесь с каждым):
- CPU0 нагружен больше остальных (особенно много softirq). Проблема в том, что у сетевого адаптера только одна очередь, она работает на одном прерывании, которое работает на одном ядре. Настраиваем очереди сетевой карты (раньше это делалось через опции драйвера, сейчас через ethtool -l/-L). Например, для 1 Гбит карт Intel очередей может быть 8, для 10 Гбит карт — до 128.
- Нагрузка не перераспределилась. Очередей много, прерываний много, но все на CPU0. Включаем демон irq_balancer.
- Нагрузка распределяется не равномерно. Ну что же, автоматика не идеальна, нужен ручной режим. Находим в сети популярный set_irq_affinity (или его аналог) и запускаем — так мы настроили RSS (receive side scaling) — первую технологию из увлекательного документа от разработчиков ядра https://www.kernel.org/doc/Documentation/networking/scaling.txt.
- Нагрузка ложится только на 8 первых ядер, если у нас всего 8 очередей, или нагрузка ложится только на 16 ядер, даже если очередей больше.
Эээ?.. Об этом расскажу подробнее.
Для начала разберёмся с ограничением в 16 ядер и с тем, почему Intel позволяет делать много очередей (на 10 Гбит), а загружает, в основном, первые 16.
Волшебная цифра 16 — это максимальное количество очередей, на которое может распределить трафик RSS. Длина хеша RSS-индекса нормальная, но используются только последние 4 бита, поэтому максимальное количество очередей, которое он может выдать — 16. Это ограничение самой технологии и с ним ничего сделать нельзя.
Что касается конкретно карт Intel, то тут всё несколько интереснее. Во-первых, у карт есть возможность статически привязывать трафик на основе классификаторов к определённым очередям (причём классификация работает на стороне карты, без задействования ресурсов процессора). Мы не тестировали эту технологию, т.к. статическая конфигурация нам, в данном случае, не подходит.
Во-вторых, у карт есть Flow director.
Сразу оговорюсь, что в коде драйверов, в документации драйверов, в документации карт и в документации утилит часто используются разные термины для одних и тех же вещей, что порождает много путаницы. Надеюсь, нам удалось распутать этот клубок правильно.
Flow director
Flow director, судя по доке Intel, работает в 2 режимах — Signature Filter (он же ATR — Application Targeted Receive) и Perfect filter. Flow director не воспринимает не-IP пакеты, туннелированные пакеты и фрагментированные пакеты. Можно посмотреть статистику попавших/не попавших пакетов в flow director: ethtool -S ethN|grep fdir
Perfect filter (ntuple)
Может содержать не более 8 000 правил. Классификатор пакетов Perfect filter просматривается/настраивается через ethtool -u/U flow-type
.
Signature Filter
Может содержать не более 32 000 правил. Согласно интернетам, ATR запоминает, в какую очередь (и на каком процессоре) _ушёл_ SYN пакет и направляет приходящие пакеты этого потока через ту же очередь.
During transmission of a packet (every 20 packets by default), a hash is calculated based on the 5-tuple. The (up to) 15-bit hash result is used as an index in a hash lookup table to store the TX queue. When a packet is received, a similar hash is calculated and used to look up an associated Receive Queue. For uni-directional incoming flows, the hash lookup tables will not be initialized and Flow Director will not work. For bidirectional flows, the core handling the interrupt will be the same as the core running the process handling the network flow.
[1]
Т.е., для входящего пакета будет использоваться та же очередь, в которую был помещён первый исходящий SYN, или каждый 20 пакет (конфигурация этого числа в новом драйвере отсутствует, а в старых называлась AtrSampleRate). Если пакет не попадает в ATR, то он классифицируется по RSS (т.е. на первые 16 ядер). Последний факт и навёл нас на правильный путь. Посмотрев статистику flow director через ethtool -S ethN | grep fdir
, мы увидели большое количество fdir_miss. Т.е. пакеты не обрабатывались в ATR, а проваливались до RSS. В результате большая часть нагрузки ложилась на первые 16 ядер. Разработчики драйвера ответили (https://sourceforge.net/p/e1000/bugs/464/), что у нас слишком много потоков, поэтому нам лучше использовать софтверный аналог этой технологии — RPS.
Раз оно нам не помогает, то лучше отключить совсем — для этого делаем ethtool -k ethN ntuple on
. Эта команда не только выключает ATR, но и включает Perfect filter, который, однако, без правил совсем никак не помогает, но и не мешает.
RPS (receive packet steering)
Чисто софтверный механизм. Технология номер 2 (после RSS) в уже упомянутом документе от разработчиков ядра — scaling.txt.
Прерывания не трогает вообще (можно распределить статически или через irq_balancer).
Изначально этот механизм был создан для распределения нагрузки по ядрам на картах с одной очередью. Однако, как мы теперь понимаем, он также хорош, когда количество очередей и ядер больше 16. Суть этого механизма в том, что независимо от того, в какую очередь пришёл пакет, обрабатываться он будет на ядре, выбранном по хешу. Хеш RPS, в отличие от RSS, может выдать результат больше 16 (т.к. использует его целиком), поэтому задействуются все ядра (если быть точнее, то в файле rps_cpus можно указать, какие конкретно ядра разрешено использовать).
Т.к. эта технология использует ресурсы процессора, то, конечно, есть некоторый оверхед, но он незначительный, т.к. самую тяжёлую операцию — расчёт хеша — всё равно делает карта. Не останавливаясь на достигнутом, продолжаем читать уже полюбившийся scaling.txt и находим там RFS.
RFS (receive flow steering)
Что же нам даёт замена одного слова в названии? При использовании этого механизма пакеты распределяются между ядрами не только на основе хеша, но и на основе того, на каком ядре висит тред приложения, ожидающий эти пакеты. Т.е. пакет отправляется не на фиксированное ядро, а туда, где его ждут, что ускоряет его обработку.
Разница хорошо видна на диаграммах[2]:
RPS
RFS
![RPS](https://habrastorage.org/r/w1560/getpro/habr/post_images/337/157/e27/337157e278de30ebd46e60fb3ca30001.png)
![RFS](https://habrastorage.org/r/w1560/getpro/habr/post_images/cca/309/c70/cca309c709b4c105c0d98796aae9e634.png)
Accelerated RFS
Чтобы закончить разбор технологий из scaling.txt, стоит также рассказать и про Accelerated RFS. Кто и что тут ускоряет? В случае с RFS работу по распределению пакетов по нужным ядрам в зависимости от хеша пакета выполняет центральный процессор. А в случае с Accelerated RFS этим занимается сетевая карта, снимая нагрузку с процессора. Но из известных нам сетевых адаптеров эту технологию на сегодняшний день поддерживает только Mellanox.
По счастливому совпадению, часть раздачи видео работает как раз на картах этого производителя. По несчастливому совпадению, каждая попытка включить этот режим заканчивается kernel panic.
В последних версиях драйвера проблема с падениями решена, но в этом режиме пропускная способность в нашей конфигурации оказалась значительно ниже. Похоже, что драйверы или firmware карт ещё не готовы к таким нагрузкам и режимам.
Проблема 4 — большое количество broken pipe
Регулярно появление ошибок broken pipe наблюдалось в приложении ещё на OpenSuSE, но с переходом на CentOS было принято решение положить этому конец. Тогда мы ещё не знали, сколько времени это займёт, ведь “пощупать” эти ошибки очень сложно.
Broken pipe — это ошибка, которая возникает, когда приложение не может записать данные в pipe. На одном конце pipe находится приложение, на другом — файл или соединение. Если пропадёт диск, на котором находится файл, или компьютер, с которым установлено соединение, то попытка записи в pipe завершится ошибкой broken pipe. В ходе изучения дампов трафика было замечено, что клиенты не просто пропадают, а закрывают соединения в ускоренном режиме (half-duplex tcp close sequence), что само по себе не является чем-то некорректным. Так почему же некоторые наши клиенты закрывают соединения не получив все данные и даже не пытаясь нормально закрыть соединение? Откуда такое безразличие к запрошенным данным и спешка? Дальнейшее расследование происходило исключительно эмпирическим путём, т.к. поймать такие эти ошибки на стороне клиентов и понять, что у них происходит, было бы слишком трудоёмким занятием. Поэтому поиск решения занял много времени.
Наиболее вероятная причина — это "packet reordering" (http://en.wikipedia.org/wiki/Out-of-order_delivery), хотя доказательств нет. Т.к. некоторые проблемы, описанные в данной статье, решались параллельно, то оказалось, что включение RFS резко снижает количество broken pipe.
![image](https://habrastorage.org/r/w780q1/getpro/habr/post_images/e7c/753/a76/e7c753a767af4707f152b373b9f3a123.jpg)
Второй фактор, который повлиял на снижение количества ошибок — это interrupt coalescing. Что это за зверь, рассказывается в следующей главе.
Проблема 5 — большое количество прерываний
Трафик бывает разный, и если фанаты Counter Strike крайне болезненно относятся к высокому latency, то для любителей торрентов (и любых других приложений, работающих с большим количеством трафика) куда важнее пропускная способность. Что касается раздачи видео, то это очень много трафика (десятки гигабит с сервера) и очень много пакетов (миллионы), а значит очень много процессорной нагрузки типа softirq. Во время тестов при пиковой нагрузке на сервер softirq мог достигать 50% от всей нагрузки на процессор.
Настройка, которая уменьшает нагрузку на CPU, — мечта любого админа. В данном случае нам помог interrupt coalescing, который настраивается через ethtool -c/-C
. При увеличении параметров interrupt coalescing прерывания генерируются реже — не на каждый пакет, а сразу на пачку.
![image](https://habrastorage.org/r/w780q1/getpro/habr/post_images/cde/a25/e1d/cdea25e1d694858868d63d012edf8225.jpg)
Стоит отметить, что нагрузка softirq растёт нелинейно, так что чем больше трафик, тем сильнее эффект.
Описание параметров:
- [rx|tx]-usecs — максимальное время в микросекундах между получением пакета и генерированием прерывания
- [rx|tx]-frames — количество пакетов, после получения которого генерируется прерывание
- *-irq — то же самое для обновления статуса при отключенных прерываниях
- *-[low|high] — то же самое для адаптивного режима
Итоги
В результате проделанных работ мы получили:
- дистрибутив, поддерживаемый до 2024 года [3]
- снижение количества ошибок broken pipe в 40 раз
- повышение производительности серверов (один сервер способен обработать 50 Гбит в секунду)
- опыт работы с различными новыми технологиями (а также политики CFEngine для их автоматического конфигурирования)
А всё это вместе позволит нам в будущем отдать с одного сервера ещё больше трафика.
Постскриптум
В заключение хочется сказать спасибо нашим разработчикам, которые помогали в работе и оптимизировали приложение, а также коллегам-администраторам, благодаря которым у меня было время на все эти изыскания. Самые сложные задачи дают больше всего знаний, опыта и удовлетворения. Хотя и треплют нервы.
Сноски