sysmerge IT

15 сент. 2017 г.

nginxconf 2017: Оптимизация веб серверов под большие пропускные способности и низкие задержки. Часть 1.

Перевод доклада Алексея Иванова на nginxConf 2017 на тему "Оптимизация веб серверов под большие пропускные способности и низкие задержки"(Optimizing web servers for high throughput and low latency). Оригинал тут. Перевод вольный, не дословный. Некоторые части(вроде вступлений) опущены.

Часть 1. Железо.

Процессор.
Для высоких показателей ассиметричного RSA / EC (ec - elliptic curve, криптография на эллиптических кривых) необходимо обратить внимание на процессоры с поддержкой технологии AVX2(avx2 в /proc/cpuinfo) а так же расчеты с длинной арифметикой (adx и bmi). В случае с симметричным шифрованием обратите внимание на AES-NI для шифрования AES и AVX512 в случае с ChaCha+Poly. У Intel есть сравнение производительности различных поколений их железа на OpenSSL 1.0.2, которое демонстрирует эффект от выбора того или иного процессора.


Чувствительные к задержкам задачи, как роутинг, к примеру, получат прирост к производительности в случае использования меньшего количество NUMA нод и отключением HT(Hyper-Threading). Задачи, требующие большой пропускной способности, в свою очередь потребуют большего количества ядер и включенного HT, причем зависимость от NUMA минимальна.

Если вы используете железо от Intel, то необходимо выбирать как минимум Haswell/Broadwell, а в идеале  Skylake процессоры. Для AMD идеальным выбором будет EPYC.

NIC(сетевая карта)
Тут вы должны выбирать как минимум 10G,а  лучше даже 25G карточки. Если необходимы еще большие объемы на 1 сервер, то тюнинг, описанный тут, будет не особо эффективен и придется тюнить на уровне ядра. С точки зрения ПО - выбирайте открытые драйверы с активными рассылками и большим комьюнити. Это пригодится вам если (а точнее "когда") вы будете дебажить проблемы на уровне драйверов.

Память
Тут правило простое - если ваша задача чувствительна к задержкам, то нужна быстрая память, а если задача чувствительная к пропускной способности, то памяти нужно побольше.

Диск
Выбор зависит от требований к буферизации/кешированию, но если вы намереваетесь кешировать большие объемы данных, то стоить выбирать хранилища на основе флеш-памяти.  Некоторые заходят настолько далеко, что используют файловые системы, заточенные под флеш-хранилища, правда, в основном, они не дают производительности лучшей чем чистый ext4/xfs. В любом случае, главное не убейте свой флеш из-за того, что не включили TRIM или забыли обновить прошивку.



Часть 2. Операционная система(низкоуровневая настройка)

Прошивка.
Используйте актуальные прошивки во избежание довольно болезненного и продолжительного дебага. Это касается прошивок микрокода ЦП, материнской платы, сетевой карты и SSD. Это не значит, что нужно обновляться на каждую новую прошивку,  тут действует простое правило - используйте вторую по свежести прошивку, кроме случаев, когда самая последняя содержит фиксы критических уязвимостей. Не спешите поперед батька в пекло.

Драйвера.
Правила использование драйверов аналогичны правилам прошивок  - поближе к самой свежей. Но предупрежу, что стоит разделять обновления ядра от обновления драйверов, по возможности. К примеру вы можете использовать DKMS или прекомпиленные драйверы для всех версий ядра, которые будут использоваться. В таком случае при обновлении ядра будет меньше мороки в случае если что-то не заработает.

Процессор.
Лучшим другом тут для вас станут утилиты, поставляемые с ядром. В  Ubuntu/Debian вы можете установить linux-tools c кучей утилит, правда, понадобятся нам только cpupower, turbostat  и x86_energy_perf_policy.
Для проверки всевозможных оптимизаций, связанных с вашим ЦП, можно воспользоваться стресс-тестами, к примеру Яндекc делает это с помощью Yandex.Tank. По ссылке можно скачать  доклад с прошлого NginxConf о тестировании нагрузок на nginx.

cpupower - использование этой консольной утилиты куда проще, чем искать что-то в /proc/. Чтобы посмотреть информацию о вашем процессора и частотных характеристиках можно выполнить
$ cpupower frequency-info
...
  driver: intel_pstate
  ...
  available cpufreq governors: performance powersave
  ...           
  The governor "performance" may decide which speed to use
  ...
  boost state support:
    Supported: yes
    Active: yes
Убедитесь, что Turbo Boost включен, для процессоров Intel важно чтобы был активен intel_pstate, а не acpi-cpufreq или pcc-cpufreq. Если же вы все еще используете acpi-cpufreq, то стоит задуматься об обновлении ядра, или, если это не представляется возможным, использовать режим performance(производительность). При использовании intel_pstate даже режим powersave показывает хорошие результаты, но это следует проверять для каждого частного случая.

Кстати говоря, с помощью turbostat можно напрямую следить за состоянием регистров а так же информацией о питании, частоте и нагрузке на ядра.

# turbostat --debug -P
... Avg_MHz Busy% ... CPU%c1 CPU%c3 CPU%c6 ... Pkg%pc2 Pkg%pc3 Pkg%pc6 ...

Тут можно посмотреть актуальную частоту ЦП(да, /proc/cpuinfo врет) и IDLE состояния ядер/пакетов.

Даже с драйвером intel_pstate цп тратит впустую гораздо больше времени, чем вам кажется, потому мы можем:
  • Сменить режим на "производительность"
  • Сменить x86_energy_perf_policy на производительность
или же в случае с необходимостью сократить задержки
Вы можете глубже ознакомиться с балансировкой производительности и режимами производительность процессоров Intel тут.

CPU Affinity (суть технологии в назначении выполнения чего-либо на конкретном ядре).
Вы можете дополнительно сократить задержки с помощью технологии CPU Affinity на каждый процесс/поток, к примеру nginx обладает директивой worker_cpu_affinity, которая автоматически биндит каждый процесс веб-сервера на отдельное ядро. Таким образом мы избавляемся от процессорной миграции, сокращаем кеш-мисы и немного увеличиваем количество выполненных инструкций за цикл. Все это можно легко проверить с помощью perf stat. Увы, но данная технология может и негативно сказаться на производительности, так как может возрасти время ожидания свободного процессора. Это можно отследить с помощью runglat запущенном на одном из PID'ов воркеров nginx:
usecs               : count     distribution
    0 -> 1          : 819      |                                        |
    2 -> 3          : 58888    |******************************          |
    4 -> 7          : 77984    |****************************************|
    8 -> 15         : 10529    |*****                                   |
   16 -> 31         : 4853     |**                                      |
   ...
 4096 -> 8191       : 34       |                                        |
 8192 -> 16383      : 39       |                                        |
16384 -> 32767      : 17       |                                        |

Если в выводе большое количество (count) событий в районе многомиллисекундых задержек, то, вероятно, кроме процессов nginx сервер нагружен кучей других задач и в этом случае технология Affinity лишь увеличит задержки.

Память
Весь тюнинг весьма специфичен и зависит от конкретных условий и задач, но есть парочка полезных правил:
 Современные CPU - это по сути отдельные процессоры, объединенные очень быстрыми каналами и использующие различные общие ресурсы, начиная от L1 кеша на hyper threading ядрах, заканчивая L3 кешем, памятью и PCIe линками через сокеты. Это по сути демонстрирует суть NUMA - несколько модулей для выполнения и хранения кода связанные очень быстрыми каналами связи.

Для более комплексного обзора технологии NUMA вы можете ознакомиться со статьями "Погружение в NUMA" Франка Деннемана.
Короче говоря выбор у вас следующий:
  • Забить на это отключив в BIOS или запуская ПО с помощью numactl --interleave=all, вы получите посредственную, но стабильную производительность.
  • Отрицать и использовать серверы на основе 1 ноды, как к примеру делает Facebook со своей платформой OCP Yosemite
  • Принять это оптимизируя работу цп/памяти  в пространстве юзера и ядра.
Давайте обсудим третий пункт, так как для первых двух особых оптимизаций не нужно. Чтобы правильно использовать NUMA необходимо обрабатывать каждую ноду NUMA как отдельный сервер, для этого необходимо перво-наперво исследовать топологию, это делается так   numactl --hardware:
$ numactl --hardware
available: 4 nodes (0-3)
node 0 cpus: 0 1 2 3 16 17 18 19
node 0 size: 32149 MB
node 1 cpus: 4 5 6 7 20 21 22 23
node 1 size: 32213 MB
node 2 cpus: 8 9 10 11 24 25 26 27
node 2 size: 0 MB
node 3 cpus: 12 13 14 15 28 29 30 31
node 3 size: 0 MB
node distances:
node   0   1   2   3
  0:  10  16  16  16
  1:  16  10  16  16
  2:  16  16  10  16
  3:  16  16  16  10
 

На что обратить внимание:
  • количество нод
  • объем памяти для каждой из нод
  • количество процессоров на каждую ноду
  • расстояние между нодами
 На самом деле пример выше не такой удачный, так как мы имеем 4 ноды, часть которых без памяти. Невозможно рассмотреть каждую ноду как отдельный сервер не жертвуя половиной ядер в системе. Мы это можно проверить с помощью numastat:
$ numastat -n -c
                  Node 0   Node 1 Node 2 Node 3    Total
                -------- -------- ------ ------ --------
Numa_Hit        26833500 11885723      0      0 38719223
Numa_Miss          18672  8561876      0      0  8580548
Numa_Foreign     8561876    18672      0      0  8580548
Interleave_Hit    392066   553771      0      0   945836
Local_Node       8222745 11507968      0      0 19730712
Other_Node      18629427  8939632      0      0 27569060

Можно так же проверить использование памяти каждой из нод в формате /proc/meminfo
$ numastat -m -c
                 Node 0 Node 1 Node 2 Node 3 Total
                 ------ ------ ------ ------ -----
MemTotal          32150  32214      0      0 64363
MemFree             462   5793      0      0  6255
MemUsed           31688  26421      0      0 58109
Active            16021   8588      0      0 24608
Inactive          13436  16121      0      0 29557
Active(anon)       1193    970      0      0  2163
Inactive(anon)      121    108      0      0   229
Active(file)      14828   7618      0      0 22446
Inactive(file)    13315  16013      0      0 29327
...
FilePages         28498  23957      0      0 52454
Mapped              131    130      0      0   261
AnonPages           962    757      0      0  1718
Shmem               355    323      0      0   678
KernelStack          10      5      0      0    16
Давайте рассмотрим более простую топологию:
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 46967 MB
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
node 1 size: 48355 MB
Так как ноды у нас практически симметричны мы можем запустить на них экземпляр приложения на каждой из нод с помощью numactl --cpunodebind=X, после чего повесить их на разные порты, таким образом получив большую пропускную способность путем использования сразу 2 нод и более низкие задержки, при этом сохраняя локальность памяти.
Можно легко проверить эффективность использования NUMA по задержкам на операциях с памятью, к примеру с помощью утилиты funclatency пакета bcc, используемой для измерения задержек на требовательных к памяти задачах, например memmove. На стороне ядра эффективность можно проверить к примеру так
# perf stat -e sched:sched_stick_numa,sched:sched_move_numa,sched:sched_swap_numa,migrate:mm_migrate_pages,minor-faults -p PID
...
                 1      sched:sched_stick_numa
                 3      sched:sched_move_numa
                41      sched:sched_swap_numa
             5,239      migrate:mm_migrate_pages
            50,161      minor-faults
И последний момент оптимизаций связанный с NUMA исходит из того факта, что используются сетевые карты PCIe и каждая карта выделена под собственную NUMA ноду, в таком случае некоторые процессоры будут иметь довольно низкие задержки при работе с сетью. Мы еще вернемся к этой теме в разделе NIC→CPU affinity, а пока давайте перейдем к PCI-Express.

PCIe
Обычно нет необходимости в глубоком анализе проблем с PCIe, разве что вы столкнетесь с неисправностями в работе оборудования. Тем не менее стоит потратить толику времени на создание уведомлений о состояниях "link speed", "link width" и, скорее всего RxErr/BadTLP для PCIe устройства. Эту информацию можно получить с помощью lspci:
 # lspci -s 0a:00.0 -vvv
...
LnkCap: Port #0, Speed 8GT/s, Width x8, ASPM L1, Exit Latency L0s <2us, L1 <16us
LnkSta: Speed 8GT/s, Width x8, TrErr- Train- SlotClk+ DLActive- BWMgmt- ABWMgmt-
...
Capabilities: [100 v2] Advanced Error Reporting
UESta:  DLP- SDES- TLP- FCP- CmpltTO- CmpltAbrt- ...
UEMsk:  DLP- SDES- TLP- FCP- CmpltTO- CmpltAbrt- ...
UESvrt: DLP+ SDES+ TLP- FCP+ CmpltTO- CmpltAbrt- ...
CESta:  RxErr- BadTLP- BadDLLP- Rollover- Timeout- NonFatalErr-
CEMsk:  RxErr- BadTLP- BadDLLP- Rollover- Timeout- NonFatalErr+
PCIe вполне может стать узким местом в системах, где быстрые устройства работают в комбинации с быстрой сетью, тут вам может понадобиться физически выделить PCIe устройства для каждого из CPU для получения максимальной пропускной способности.


Рекомендую к ознакомлению так же статью "Understanding PCIe Configuration for Maximum Performance", в которой более углубленно изучается настройка PCIe устройств, что будет полезно, если наблюдаются потери пакетов между картой и ОС. Так же Intel предполагает, что порой управление энергопотреблением карт PCIe (ASPM) может привести к повышения задержек и даже повышению потери пакетов. Можно отключить это командой в pcie_aspm=off  в командной строке ядра.

NIC(сетевая карта)
Прежде чем мы начнем хочу сказать, что и у Intel и Mellanox есть свои статьи по тюнингу карт и вне зависимости от выбора производителя рекомендую вам ознакомиться с ними. К тому же драйвера к картам обычно поставляются с README , в котором как правило расписаны некоторые полезные нюансы и утилиты.

Далее неплохо было бы ознакомиться с мануалами от операционных систем, в частности, к примеру, гайд от RHEL, который объясняет многие из оптимизаций, что будут нами разобраны ниже. У Cloudflare тоже есть своя неплохая статья в блоге, правда она больше направлена на системы с низкими задержками.

Лучшим другом для настроек станет утилита ethtool .
Кстати, если вы используете довольно свежее ядро(а вы должны), то неплохо бы еще обновить и другие компоненты системы, в частности ethtool, iproute2 и, скорее всего, iptables/nftables.
Получить полезную статистику интерфейса можно командой  ethtool -S
$ ethtool -S eth0 | egrep 'miss|over|drop|lost|fifo'
     rx_dropped: 0
     tx_dropped: 0
     port.rx_dropped: 0
     port.tx_dropped_link_down: 0
     port.rx_oversize: 0
     port.arq_overflows: 0
Более детальное описание параметров можно узнать у  разработчиков карты, к примеру Mellanox предоставляет подобное описание тут
Со стороны ядра вам будут интересны параметры /proc/interrupts, /proc/softirqs и  /proc/net/softnet_stat. Тут пригодятся полезные команды из пакета bcc hardirqs и softirqs . Суть всех оптимизаций в том, чтобы свести использование процессора к минимуму при отсутствии потерь пакетов.

Прерывания IRQ Affinity
Настройка начинается с распределения прерываний по процессорам. Как именно вы это сделаете зависит от задач
  • Для достижения максимальной пропускной способности вы можете распределить прерывания по всем нодам NUMA в системе
  • Для минимизации задержек вы можете ограничить прерывания одной нодой NUMA. Для этого вам возможно понадобится сократить количество очередей, поступающих на ноду(это, как правило,  значит, что нужно сократить их количество в 2 раза с помощью ethtool -L)
Производителя обычно предоставляют собственные команды для этого, к примеру set_irq_affinity у Intel.


Буфер Ring
Сетевая карта обменивается информацией с ядром, как правила это происходит с помощью канала, который называют "ring" (кольцо). Текущие(и максимальные) значения этого кольца можно посмотреть командой ethtool -g:
$ ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX:                4096
TX:                4096
Current hardware settings:
RX:                4096
TX:                4096
Эти значения можно увеличить, для этого существует ключ -G. Правило простое - чем больше тем лучше(особенно если вы используете объединение прерываний(interrupt coalescing)) , потому что это дает необходимый запас против резких скачков или проблем со стороны процессора, а потому количество дропнутых пакетов по причине нехватки буфера или пропущенного прерывания будет минимализироваться. Но и тут есть несколько "но":
  •  На старых ядрах или драйверах без поддержки BQL(byte queue limits) может привести к повышенной буферизации на TX стороне(отправляющая)
  • Большой буфер так же повышает нагрузку на кеш, а потому можно попробовать уменьшать буфер
Объединение прерываний(Coalescing)

Объединение прерываний позволяет откладывать уведомления ядра о новых событиях путем сбора нескольких событий в одно прерывание. Текущие настройки можно посмотреть так ethtool -c:
$ ethtool -c eth0
Coalesce parameters for eth0:
...
rx-usecs: 50
tx-usecs: 50
 Вы можете использовать как и жестко заданные значения максимального количества прерываний в секунду на 1 ядро, так и позволить железу автоматически изменять показатели количества прерываний на основе пропускной способности. Активация объединения (с ключем -C) поднимет задержки и, возможно, приведет к потере пакетов, потому вам стоит избегать этого для систем чувствительных к задержкам. С другой стороны, отключение сoalescing может в итоге привести к троттлингу(пропуску) прерываний и это значительно снизит производительность.


Разгрузка ядра (Offloads)
Современные карточки относительно развиты и позволяют разгрузить процессор на довольно солидную часть с помощью железа или эмулировать эту разгрузку драйверами.
Все доступные типы разгрузок можно получить командой ethtool -k:
$ ethtool -k eth0
Features for eth0:
...
tcp-segmentation-offload: on
generic-segmentation-offload: on
generic-receive-offload: on
large-receive-offload: off [fixed]
Разгрузки, которые не могут быть настроены в выводе отмечаются как [fixed]. Можно много говорить о них, но ниже представим несколько простых правил:
  • Не включайте LRO, вместо него используйте GRO
  • Осторожнее с TSO, так как его работа очень сильно зависит от качества драйверов и прошивок
  • Не включайте TSO/GSO на старых ядрах, так как это приведет к слишком высокой буферизации.
Packet Steering
Все современные карточки оптимизированы для работы с многоядерными системами,  потому они распределяют пакеты в виртуальные очереди, как правило на каждый из процессоров. Когда это распределение выполняется на железе, то мы называем это RSS(Receive Side Scaling), а когда за это отвечает ОС - RPS (receive packet steering) (с "обратной" техологией TX-стороны XPS). Когда ОС тоже пытается быть умной и распределяет потоки на те процессоры, на которых работает текущее приложение, ожидающее пакет, то технология называется RFS(receive flow steering). А когда то же самое делает железо - accelerated RFS (aRFS).
Ниже несколько полезностей:
  • Если вы используете новое железо 25G+, то вероятно вам достаточно очередей для использования RSS по всем ядрам. Старые карточки были ограничены в использовании только 16 процессоров.
  • Активировать RPS имеет смысл если:
    •  процессоров больше, чем потоков на железе и вы желаете пожертвовать задержками для большей пропускной способности
    • вы используете внутреннее туннелирования(GRE/IPinIP), которое сетевая карта не сможет RSS'ить
  • Не используйте RPS если у вас старый процессор и он не поддерживает x2APIC 
  • Назначить каждый процесор на собственную TX очередь с помощью XPS в принципе довольно неплохая идея
  • Эффективность RFS сильно зависит от задач и использования CPU affiniti
Flow Director и ATR
flow director(или fdir  на терминалогии Intel) используется по умолчанию в ATR(application targeting routing), который реализует aRFS путем сбора пакетов и перенаправления их в ядро, где они обрабатываются. Эта статистика так же доступна через ethtool -S.
Хотя Intel утверждает, что fdir в некоторых случаях дает прирост в производительности, независимые исследования показали, что fdir еще и повышает на 1% изменение порядка следования пакетов, что скажется не лучшим образом для производительности TCP. Хотя, попробуйте потестировать сами и проверьте, будет ли FD полезен для ваших задач, и не забывайте следить за счетчиком TCPOFOQueue .