Низкая скорость передачи файла почти никогда не объясняется одной причиной. Чаще узкое место лежит не в канале, а в размере буферов, в том, как ядро подбирает окно TCP и как очередь пакетов реагирует на потери. Настройки стека Linux рассчитаны на усреднённый сценарий, поэтому на быстром канале с большой задержкой они ограничивают поток. Разбираем, как найти ограничение, посчитать нужный объём буфера, изменить параметры ядра и проверить, что скорость действительно выросла.
Где на самом деле находится узкое место
Ядро Linux подбирает размер буфера сокета автоматически: наблюдает за задержкой и потерями и расширяет окно, пока позволяет максимум. Ограничение возникает там, где максимум меньше, чем нужно для конкретной связки скорости и задержки. Отдельный класс проблем связан не с объёмом данных, а с тем, куда попадают пакеты: очередь переполняется, часть теряется, алгоритм контроля перегрузки уменьшает окно, и поток проседает сильнее, чем можно ожидать от одной потери.
Прежде чем менять конфигурацию, стоит отделить канал от стека. Сравнение скорости внутри локальной сети и скорости наружу показывает, где именно теряется производительность. Если локально поток близок к номиналу канала, а наружу падает в разы, дело в задержке и потерях, а не в диске или процессоре.
Порядок диагностики выглядит так:
- Замерить скорость внутри локальной сети между двумя машинами, чтобы получить верхнюю границу, которую способно показать железо;
- Измерить задержку до удалённой стороны и долю потерь, поскольку эти два числа определяют требуемый размер буфера;
- Посмотреть счётчики ретрансмиссий и оценить, растёт ли их число во время передачи;
- Проверить загрузку процессора и распределение программных прерываний по ядрам, так как одно нагруженное ядро ограничивает поток;
- Зафиксировать базовые показатели и только после этого менять параметры, по одному за подход.
ping -c 20 remote.example.net
ss -ti dst remote.example.net
nstat -az | grep -i -E 'retrans|loss'
mpstat -P ALL 2 5
Вывод ss показывает текущие значения rtt, cwnd и retrans для конкретного соединения. Рост retrans при передаче говорит о потерях в пути, и в такой ситуации увеличение буфера даёт меньше, чем настройка очереди.
Связь размера буфера с пропускной способностью и задержкой
Объём данных, который соединение способно держать в пути, равен произведению пропускной способности на время оборота. Для канала 1 Гбит/с и задержки 50 мс это около 6 Мбайт, для 10 Гбит/с и 100 мс около 120 Мбайт. Если максимум буфера меньше этой величины, поток упрётся в окно, а не в канал, и никакие другие настройки не помогут.
При этом большой буфер не является бесплатным. Каждый сокет потребляет память, а очередь в сотни мегабайт добавляет задержку всем остальным потокам машины. Практический компромисс состоит в том, чтобы задавать максимум в два-четыре раза меньше величины, нужной одному потоку, и рассчитывать на параллельные передачи: четыре потока по 8 Мбайт дают тот же суммарный объём, что один на 32 Мбайт, но делят ресурсы честнее.
Полезно помнить про второе ограничение. Параметр tcp_mem задаёт общий объём памяти стека в страницах, и при его исчерпании ядро переводит соединения в режим давления независимо от настроек конкретного сокета. Значения по умолчанию подходят для большинства машин, и менять их стоит только после того, как счётчики покажут реальное давление.
Изменение параметров сокета через sysctl
Значение параметра читается командой sysctl с именем, меняется ключом -w на время работы системы и сохраняется через файл конфигурации. Отдельный файл в каталоге sysctl.d удобнее общего конфига: его можно удалить одним действием и не искать правки в общем списке.
sysctl net.ipv4.tcp_rmem
sudo sysctl -w net.core.rmem_max=33554432
echo 'net.core.rmem_max=33554432' | sudo tee /etc/sysctl.d/99-net-tuning.conf
sudo sysctl --system
Для канала 1 Гбит/с с задержкой до 30 мс достаточно умеренных значений, для 10 Гбит/с нужен запас в десятки мегабайт.
# 1 Гбит/с, задержка до 30 мс
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 131072 8388608
net.ipv4.tcp_wmem = 4096 65536 8388608
net.core.default_qdisc = fq
# 10 Гбит/с, задержка до 100 мс
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.ipv4.tcp_rmem = 4096 87380 33554432
net.ipv4.tcp_wmem = 4096 65536 33554432
net.ipv4.tcp_mtu_probing = 1
net.core.default_qdisc = fq
Три числа в tcp_rmem задают минимум, значение по умолчанию и максимум автоматической подстройки. Первое применяется при старте сокета, третье служит потолком, до которого окно может вырасти. Ограничение rmem_max задаёт предел для явного вызова setsockopt, поэтому его стоит поднять вместе с tcp_rmem, иначе приложение не сможет запросить большой буфер самостоятельно.
Отдельно отметим два параметра, которые иногда советуют отключить ради экономии процессора. Метки времени и выборочное подтверждение помогают восстанавливать поток после потерь, и их отключение на каналах с большой задержкой ухудшает результат сильнее, чем экономит ресурсы.
Выбор алгоритма контроля перегрузки и очереди
Список доступных алгоритмов и текущий выбор видны через sysctl. Смена выполняется той же командой с ключом -w и сохраняется в файле конфигурации.
sysctl net.ipv4.tcp_available_congestion_control
sysctl net.ipv4.tcp_congestion_control
ip route show
Алгоритм cubic реагирует на потери и строит окно по кубической зависимости, поэтому на пути без потерь он добирается до номинала канала. Алгоритм BBR оценивает пропускную способность и задержку напрямую и меньше зависит от единичных потерь, что заметно на спутниковых и перегруженных каналах. Для BBR требуется дисциплина очереди fq или fq_codel: без неё пакеты уходят пачками, и преимущество теряется.
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
Дисциплина очереди задаёт порядок отправки пакетов и поведение при переполнении. fq распределяет потоки по очередям и задаёт темп отправки, fq_codel дополнительно борется с накопленной задержкой. Для большинства серверов подходит fq_codel, для передачи крупных объёмов fq.
Полезный приём состоит в ограничении пиковой скорости, чтобы пачки пакетов не переполняли буферы коммутаторов и принимающей стороны.
sudo tc qdisc replace dev eth0 root fq maxrate 8gbit
Для машины с каналом 10 Гбит/с и четырьмя параллельными потоками темп на поток задают ниже, например 2 Гбит/с, иначе один поток забирает весь канал и теряет пакеты на переполнении.
Проверить, какой алгоритм применяет конкретное соединение, позволяет вывод ss: поле congestion control присутствует в информации о сокете. Совпадение ожидаемого и фактического значения подтверждает, что настройка применилась, а не осталась в файле.
Аппаратное ускорение, крупные кадры и прерывания
Сетевая карта умеет выполнять часть работы самостоятельно: собирать и разбирать пакеты, считать контрольные суммы, объединять несколько сегментов в один. Отдача этих операций железу снижает нагрузку на процессор и поднимает скорость на быстрых каналах.
ethtool -k eth0 | head -n 20
sudo ethtool -K eth0 tso on gso on gro on
ethtool -c eth0
Крупные кадры размером 9000 байт уменьшают число пакетов и нагрузку на процессор, но работают только при одинаковом размере на всём пути. Несовпадение приводит к тому, что часть соединений перестаёт устанавливаться, и выглядит это как плавающая недоступность сервисов.
ip link show dev eth0 | grep mtu
sudo ip link set dev eth0 mtu 9000
ping -M do -s 8972 remote.example.net
Проверка пути выполняется пакетом максимального размера с запретом фрагментации. Ответ приходит только если все устройства на пути пропускают такой кадр. Перед включением стоит убедиться, что коммутаторы и вторая сторона настроены одинаково.
Распределение прерываний влияет на скорость не меньше буферов. Если все пакеты одного потока обрачивает одно ядро, поток упирается в него, а остальные ядра простаивают. Многоочередные карты распределяют нагрузку по очередям, и каждой очереди соответствует отдельное прерывание.
ethtool -l eth0
grep -E 'eth0|virtio' /proc/interrupts
cat /proc/net/softnet_stat
Первый столбец softnet_stat в шестнадцатеричном виде показывает число обработанных пакетов, второй число потерянных из-за переполнения очереди, третий число срабатываний ограничения по бюджету. Ненулевые значения во втором и третьем столбцах означают, что очередь не успевает, и помогает увеличение лимитов netdev_max_backlog и netdev_budget вместе с распределением очередей по ядрам.
Протокол UDP и пределы для нагруженного сервера
Поток без механизма управления перегрузкой ведёт себя иначе. Клиент UDP отправляет данные с той скоростью, с которой их готовит приложение, и при переполнении буфера пакеты отбрасываются без уведомления отправителя. Поэтому для служб, которые принимают крупные датаграммы, размер буфера становится прямым ограничением: значение по умолчанию в сотни килобайт переполняется за доли секунды на быстром канале.
sysctl net.core.rmem_default net.core.rmem_max
sudo sysctl -w net.core.rmem_max=26214400
sudo sysctl -w net.core.rmem_default=1048576
cat /proc/net/snmp | grep -A1 '^Udp:'
Счётчик Rcvbuferrors в таблице Udp показывает, сколько датаграмм отброшено из-за переполнения буфера приёма. Ненулевое значение, растущее во время работы, прямой признак того, что буфер мал либо приложение не успевает разбирать очередь. Второй вариант лечится увеличением числа работников, а не размера буфера.
Для сервера с тысячами одновременных соединений ограничения возникают раньше, чем исчерпывается пропускная способность канала. Очередь принятых соединений задаётся параметром somaxconn, очередь полуоткрытых соединений параметром tcp_max_syn_backlog, а предел числа открытых файлов влияет на то, сколько сокетов вообще может существовать.
sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog
cat /proc/sys/net/ipv4/ip_local_port_range
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.ip_local_port_range = 10240 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
Диапазон локальных портов определяет, сколько исходящих соединений машина способна установить одновременно. Значение по умолчанию начинается с 32768 и даёт около 28 тысяч портов, чего не хватает для прокси и для служб, активно обращающихся наружу. Параметр tcp_tw_reuse позволяет повторно использовать порты из состояния TIME_WAIT для новых исходящих соединений и помогает именно на стороне клиента, а не сервера.
Перечисленные пределы стоит поднимать осознанно: каждое соединение потребляет память, и увеличение очередей без запаса по памяти превращает быстрый отказ в медленный.
Когда тюнинг нужен, а когда мешает
Настройки, рассчитанные на каналы от 1 Гбит/с и задержки в десятки миллисекунд, на медленном подключении дают обратный эффект. Большой буфер накапливает очередь, задержка растёт, а интерактивные приложения начинают отвечать хуже. Для домашней сети со скоростью до 100 Мбит/с изменение параметров сокета чаще вредит, чем помогает, и разумнее заняться потерями и очередью.
Признаков того, что настройка оправдана, четыре. Первый, передача крупных файлов на дальнюю сторону идёт заметно ниже номинала канала при отсутствии потерь. Второй, счётчики ретрансмиссий растут во время передачи, и снижение окна совпадает с просадкой скорости. Третий, одно ядро загружено обработкой сетевых прерываний при свободных остальных. Четвёртый, канал быстрый, задержка большая, а размер окна в выводе ss упирается в максимум буфера.
Методика простая: замерить базовую скорость, изменить один параметр, повторить замер в тех же условиях, записать результат. Изменение пяти параметров одновременно не позволяет понять, какое из них сработало. Откат выполняется удалением файла в каталоге sysctl.d и перезагрузкой настроек, поэтому риск ошибки невелик.
Отдельно стоит проверить, не упирается ли скорость в диск или в шифрование. Замер между двумя машинами в памяти, без записи на носитель, отделяет стек от накопителя, а сравнение скорости по открытому и по зашифрованному соединению показывает вклад шифрования. Без этой проверки тюнинг стека легко превращается в настройку того, что не является ограничением.