Performance matters!
1.19K subscribers
11 photos
2 files
63 links
Канал про SRE, Linux и производительность от Александра Лебедева (@alebsys).

Разбираю сбои, ускоряю системы, делюсь опытом.

🔹 Обо мне: alebedev.tech/about
🧑‍💻 Менторинг: alebedev.tech/mentoring
Download Telegram
Анализируя работу подсистемы памяти я часто обращаюсь к метрикам из /proc/vmstat - объем просканированных (pgscan) и украденных (pgsteal) страниц:

pgscan_kswapd
pgsteal_kswapd
pgscan_direct
pgsteal_direct


Показатели помогают понять, как система аллоцирует страницы при нехватки свободной памяти.

Что такое pgscan и pgsteal

* pgscan - сколько страниц было просканировано в поисках кандидатов на высвобождение;
* pgsteal - сколько из просканированных страниц удалось высвободить (украсть).
Scan/steal процессы могут быть запущены либо фоновым kswapd, либо напрямую приложением (direct).

Причем наличие kswapd в вакууме ничего плохого не подразумевает - демон трудится в бекграунде, чтобы держать определенное кол-во свободных страниц в системе.

А вот наличие direct может настораживать: похоже что kswapd не справляется и приложения явным образом требует предоставить ей памяти. Этот процесс уже происходит в foreground и чреват потерей производительности.

Как интерпретировать значения

* абсолютные значения - чем выше значения, тем чаще свободная память падает до минимума и система вынуждена принимать меры;
* соотношения pgsteal/pgscan - высокое значение (95%+) говорит, что большая часть просканированных страниц успешно освобождается (это хорошо). Низкое напротив - система тратит много ресурсов на сканирование, прежде чем найдет подходящие страницы для освобождения.

Как получить значения в grafana

node_exporter в помощь:
node_vmstat_pgscan_direct
node_vmstat_pgsteal_direct
node_vmstat_pgscan_kswapd
node_vmstat_pgsteal_kswapd


Итого

Резюмируя, если значения pgsteal, pgscan растут это сигнал, что память в системе переутилизирована и хорошо бы обратить на это внимание.

Решения проблемы в вакууме нет, все зависит от кейса: где-то будем докидывать памяти, где-то расширять swap, где-то анализировать потребление памяти приложением, а где-то хватит и лимитов на нее.
🔥11👍4
𝗚𝗶𝘁𝗟𝗮𝗯 выделяется среди других компаний своими открытыми репозиториями с множеством полезных гайдов и обсуждений 💬

В одном из таких материалов — заметке "𝗛𝗼𝘄 𝘁𝗼 𝘂𝘀𝗲 𝗳𝗹𝗮𝗺𝗲𝗴𝗿𝗮𝗽𝗵𝘀 𝗳𝗼𝗿 𝗽𝗲𝗿𝗳𝗼𝗿𝗺𝗮𝗻𝗰𝗲 𝗽𝗿𝗼𝗳𝗶𝗹𝗶𝗻𝗴" — автор объясняет, что такое 𝗳𝗹𝗮𝗺𝗲𝗴𝗿𝗮𝗽𝗵, зачем он нужен, как собирать данные и какие трудности могут возникнуть при анализе этих красно-желтых графиков.

Заметка также содержит практические примеры интерпретации 𝗳𝗹𝗮𝗺𝗲𝗴𝗿𝗮𝗽𝗵. Для сбора данных используется linux-perf: он добавляет оверхед, но подходит для старых версий ядер.

На ядрах 4.19+ лучше использовать eBPF-инструмент profile от bcc.
🔥7👍2
Когда система становится сложнее (добавляются новые технологии, версии ядер, типы дистрибутивов, машины, кластеры), необходимость в унификации и системном подходе к процессам заметно возрастает.

Профилирование приложений в таких условиях превращается в челендж: разные окружения, требования, версии и типы рантаймов усложняют задачу.

Предлагаю обменяться опытом:
* какой тулинг используете? Например, унифицированные инструменты вроде bcc (eBPF) или языкоориентированные (pprof, async-profiler);
* используете ли continuous profiling? (coroot, pyroscope, etc);
* в каких окружениях работаете (k8s, docker, чистый linux (windows?);
* как доставляете пререквизиты? доступ к symbols, frame pointer, linux-headers. Тащите все это добро проактивно или "по требованию?;
* как обходите ограничения по ИБ? Например, async-profiler требует RW-доступа к файловой системе контейнера, а требования ИБ настаивают на RO-доступе.

Будет интересно узнать, с какими сложностями сталкиваетесь и какие выбираете подходы!

——
Это же обсуждение на linkedin
🔥4
А вынесу из комментарий ответ по «проблемам» профилирования.

Унификация это то, к чему хочется стремиться. То есть одним инструментом профилировать весь зоопарк рантаймов.

На эту роль отлично подходят linux-perf или profile от BCC, но тут есть нюансы:

* многие начитавшись "вредных советов" отключают frame pointer и вырезают symbols при сборке бинарников, отчего профилирование становится бесполезным - на нем ничего не видно. Соответственно у каждого рантайма по своему включаются/отключаются эти опции;

* по умолчанию хочется использовать profile на eBPF, так как он дает сильно меньший оверхед, но для этого и linux ядра должны быть более менее свежие и скомпилированы они должны быть с соответствующими флагами, а еще на машинах должны быть подходящие версии linux-headers ;

* предыдущий пункт отчасти решает такая штука как libbpf, с помощью нее можно собрать бинарник profile, который уже в себе будет иметь все нужное(считай докер контейнер), но требования к свежей версии ядра никуда не деваются;

* далее проблемы на уровне контейнеризации, тот же libbpf profile (переносимый который) в нынешней реализации не container awareness, а значит умеет профилировать только процессы на уровне хоста, значит требуется куча зависимостей;

* а еще есть требования от ИБ, вот про тот же async-profiler и RO файловую систему. Можно деплоить Поды для профилирования на отдельные ноды, где будет все нужное, проводить тесты и деплоить обратно, но это мертворожденная история.

Вообщем проблем куча и их надо решать)
👍2🔥2
Investigation of a Cross-regional Network Performance Issue

Траблшутинг от Netflix: исследование причин низкой скорости сети между дата-центрами.

Подобные статьи люблю за то, что можно:
1. "подсмотреть", как инженеры строят гипотезы, проверяют их и находят решения;
2. узнать практические приёмы/фишки и применить в своей работе.

Из интересного

контейнерам в Netflix по умолчанию ограничивают пропускную способность сети (интересно на основе чего выбираются те или иные значения);

в окружениях за NAT пары ip:port на сервере и клиенте отличаются. Для идентификации одного TCP-стрима можно фильтровать пакеты по Sequence Numbers;

в ядре Linux 6.6+ изменился механизм расчёта TCP Receive Window:
— ранее использовался статический параметр net.ipv4.tcp_adv_win_scale.
— теперь применяются динамические расчёты на основе scaling_ratio, стартовое значение которого — 0.25.

Кроме того, упоминается поле tcp_sock->window_clamp:
Это максимальное значение окна приёма, которое может быть объявлено. Оно устанавливается как 0.25 от rcvbuf на основе начального значения scaling_ratio. Из-за этого размер окна ограничен этим значением и не может увеличиваться.


Звучит как баг, а не фича. Или это все работает немного не так;)

P.S. Подробнее вопрос Receive Window разбирается Cloudflare в Optimizing TCP for high WAN throughput while preserving low latency.

tags: #tcp #linux #kernel
🔥8
Как считается TCP Window Clamp

Ранее я упоминал статью Netflix Investigation of a Cross-regional Network Performance Issue. В ней разбирается деградация скорости TCP-соединений между дата-центрами и проблема возникла из-за изменения алгоритма расчёта TCP Receive Window в новых версиях ядра.

Как часто бывает, такие материалы оставляют больше вопросов, чем ответов — “know unknown” чистой воды.

Я покопался в исходниках Linux, чтобы лучше понять механизм подсчета TCP Window. Делюсь изысканиями:)

tags: #tcp #linux #kernel
🔥3👏2
Я отмечал ранее, что интерпретация CPU Usage — вещь не очевидная.
Высокие значения не всегда означают, что перегружен именно процессор — иногда проблема в скорости работы с памятью.

Понять во что упираемся помогает параметр — IPC (Instructions Per Cycle):
- высокий CPU Usage, высокий IPC: основная нагрузка идет на процессор;
- высокий CPU Usage, низкий IPC: узкое место в работе с памятью.

——

Окей, допустим мы поняли: надо оптимизировать память. С чего начать?

Отправной точкой может стать сборник статей Johnny's Software Lab: Memory Subsystem Optimizations.

Внутри:
- основы устройства памяти;
- советы по оптимизации кода, ОС и железа;
- техники для бенчмарков;
- и всякое другое.

Разбираемся, применяем на практике и делимся опытом;)
👍12🔥7
Breaking down CPU speed: How utilization impacts performance

Инженеры GitHub провели исследование, чтобы найти оптимальный баланс между загрузкой CPU и скоростью его работы.

Для этого они зеркалили продовый трафик в отдельное окружение и замеряли latency обработки операций при разной степени утилизации процессора. Задача была с одной стороны оставаться в рамках SLA, а с другой — не позволять ресурсам простаивать.

В результате, для их типичной рабочей нагрузки (IO-bound) и используемых процессоров (Intel), оптимальная утилизация CPU оказалась в районе 61%.

Интересно, что Б. Грегг в своей книге "Systems Performance" часто упоминает порог в 60% как точку, после которой ресурс (будь то диск или CPU) может стать узким местом в системе.

Цитата:
Netflix commonly uses ASGs that target a CPU utilization of 60%, and will scale up and down with the load to maintain that target.


P.S. Теперь дело "за малым": настроить процессы resource management и capacity planning, чтобы держать утилизацию в районе этих цифр. Изи)
👍18🤨1🤝1
Scaling in the Linux Networking Stack (scaling.txt)

Документ от разработчиков ядра Linux описывает пять техник, которые помогают повысить производительность сетевого стека в многоядерных системах.

Это:
* RSS: Receive Side Scaling
* RPS: Receive Packet Steering
* RFS: Receive Flow Steering
* Accelerated Receive Flow Steering
* XPS: Transmit Packet Steering

В нашей инфраструктуре мы уже давно и успешно используем RSS.

Это аппаратная технология, суть следующая.

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

Каждая RX-очередь привязана к конкретному ядру процессора.

Таким образом:
1. Обработка трафика распределяется между несколькими CPU, что позволяет эффективно использовать ресурсы;
2. Все пакеты одного соединения попадают в одну очередь. Это исключает проблему "out of order" пакетов.

Однако бывают ситуации, когда распределить обработку трафика между ядрами не получается, и отдельное ядро или группа ядер оказывается сильно перегруженной:

- одно конкретное соединение прокачивает кратно больший объем трафика, чем другие;
- RX-очередей в системе меньше, чем доступных CPU.

Мы, например, сталкивались с такими проблемами при использовании MetalLB в L2-режиме: весь трафик шел через одну машину MetalLB (мастер) и "приклеивался" к одной RX-очереди на Ingress Controller. Остальные ядра и RX-очереди при этом простаивали.

В подобных случаях может помочь другая техника, описанная в том же документе — RPS (Receive Packet Steering).

RPS — это программная реализация RSS. Она позволяет распределить RX-очереди по конкретным ядрам, выравнивая нагрузку между ними.

Но есть и минусы:
- Программная реализация создает дополнительную нагрузку на CPU, что проявляется в увеличении числа IRQ на графике загрузки процессора;
- Снижается "локальность" данных в кэшах процессора, что может повлиять на производительность.

(на скрине RPS включили после 16:00)

———

Тема сложная, и я не уверен, что до конца понимаю, как это все работает;)
Было бы интересно узнать, какие техники масштабирования трафика используете вы и как справляетесь с подобными проблемами.

Дальнейшее чтение:
- сам документ scaling.txt
- расшифровка доклада от Одноклассников, где ребята решали похожие проблемы;
- примерно тоже самое, но забугорный доклад по перформансу сетевого стека.

tags: #network #tuning #linux #кейс
👍13
Ingress NGINX Controller vs ClusterIP (IPtables): Балансировка.

Задумывались ли вы почему балансировка через Ingress NGINX Controller показывает более ровное распределение трафика по Подам в сравнении с ClusterIP (IPtables)?

Я до недавнего времени не очень, но "сигналы с мест" требовали разобраться.

Балансировка через ClusterIP (IPtables)

ClusterIP (далее svc) это сущность Kubernetes, позволяющая приземлять трафик в Под.

В свою очередь svc это просто набор IPtables правил (или правил аналогичных инструментов), "приклеивающий" запрос к конкретному Поду.

Примерно так:
iptables -t nat -A KUBE-SVC -m random --probability 0.33 -j DNAT --to <Pod1>
iptables -t nat -A KUBE-SVC -m random --probability 0.50 -j DNAT --to <Pod2>
iptables -t nat -A KUBE-SVC -j DNAT --to <Pod3>

Что можно интерпретировать как:
* Pod1 будет выбран в качестве апстрима в 33% случаев;
* далее в 50% Pod2;
* иначе апстримом станет Pod3.

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

Теория вероятностей.


Балансировка через Ingress NGINX Controller

Зная о том как функционирует svc, я по наивности полагал, что и Ingress Controller (далее IC) оперирует в качестве апстримов сущностью svc, что будет в итоге давать тот же "уровень" баланcировки.

Факты говорили об обратном, потому я пошел перепроверять.

Под каждый объект Ingress рендерится секция server{} шаблона nginx.tmpl (пруф):

{{ range $tcpServer := .TCPBackends }}
server {...}
{{ end }}


В ней директива proxy_pass указывает на некий upstream_balancer:

### Attention!!!
# We no longer create "upstream" section for every backend.
# Backends are handled dynamically using Lua....
...

balancer_by_lua_block {
balancer.balance()
}
...


В balancer.balance() и живет логика выбора апстрима, где peer == endpoint (считай Pod):

...
local peer = balancer:balance()
if not peer then
ngx.log(ngx.WARN, "no peer was returned, balancer: " .. balancer.name)
return
end
...


——
Да и как иначе IC сможет обеспечивать разные типы балансировки? А по умолчанию используется round-robin.
👍10👏31😐1
Сетевой анализ с eBPF: измеряем Round Trip Time

Длительность (latency) — ключевой показатель производительности системы. На первый взгляд всё просто: рост задержки — признак деградации. Сложность в деталях...


Под катом:
* мои рассуждения о сложности интерпретации latency в современных системах;
* небольшой гайд по eBPF - напишем по шагам инструмент, который поможет отвечать на вопрос: "причина замедления в приложении или в инфраструктуре?".

Полезного чтения!

tags: #eBPF #Linux #SRE #TCP
👍19
Алгоритмы управления потоком (Flow Control) в TCP служат для предотвращения перегрузки сети и потерь данных.

Исследования в этой области не прекращаются и на сегодня нам доступно множество вариантов:

* Reno (1986)
* New Reno (1999)
* CUBIC (2004)
* FAST TCP (2005)
* BBRv1 (2016)
* BBRv2 (2019)
* BBRv3 (2023)
* ...

По умолчанию в Linux используется CUBIC. Однако создатели BBR (Google) выкладывают любопытные исследования, где резюмируют:

BBR enables big throughput improvements on high-speed, long-haul links...
BBR enables significant reductions in latency in last-mile networks that connect users to the internet...


Так может нам просто переехать на новые рельсы?

Хотя кажется правильнее поставить вопрос по другому: в каких случаях какой алгоритм может быть предпочтительнее?

————

Алгоритмы Flow Control можно условно разделить на два типа:
1. Loss-based (ориентированы на потери пакетов): Reno, NewReno, CUBIC
2. Delay-based (ориентированы на изменения RTT): FAST TCP, BBRv*

Основная цель любой реализации Flow Control — максимально эффективно использовать пропускную способность канала, сохраняя баланс между скоростью передачи данных и предотвращением перегрузок.

Скорость регулируется через Congestion Window (окно перегрузки) — сколько данных можно отправить без получения подтверждения.

Разница между подходами к контролю перегрузки заключается в методах её определения.

Loss-based (CUBIC)

Алгоритмы этого типа оценивают перегрузку по потерям пакетов.

Пришел дублирующий ACK или сработал Retransmission Timeout (RTO)? Значит есть потери и следовательно канал перегружен - снижаем скорость.
Затем ориентируясь на поступающие ACK, скорость увеличивается, пока не обнаружатся новые потери.

Такой подход может забивать очереди в канале до предела, что и будет приводить к потерям. Реакция носит реактивный характер: перегрузка фиксируется только после её возникновения.

Delay-based (BBR)

В Delay-based алгоритмах, таких как BBR, перегрузка оценивается на основе изменения задержек:
* минимальный RTT (RTT_min) принимается за эталон;
* если текущий RTT (RTT_now) превышает RTT_min, алгоритм предполагает, что канал перегружен, и снижает скорость передачи данных.

Таким образом, BBR стремится избегать заполнения очередей, что позволяет сократить задержки.
Его подход более превентивный: предотвращение перегрузки до её появления.

————

CUBIC проигрывает BBR в сетях с высоким RTT, например, в интернете. Это происходит из-за медленного роста скорости после обнаружения потерь: ACK приходят с задержкой.

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

BBR же в таких сетях может не дать преимуществ. При всплесках трафика он снижает скорость, чтобы избежать заполнения очередей, из-за чего канал используется не полностью. Кроме того, возможны конфликты между алгоритмами, когда та или иная реализация будет захватывать пропусную способность, вытесняя другие. Настоящие войны)

Вообщем как обычно надо быть осторожее!

Почитать:
- https://blog.apnic.net/2017/05/09/bbr-new-kid-tcp-block/
- https://book.systemsapproach.org/congestion.html
- https://tcpcc.systemsapproach.org/

tags: #network #tcp
🔥20👍3🤝1
На Hacker News происходит множество обсуждений разной степени интересности и иногда попадаются настоящие самородки, где умные дядьки делятся с общественностью своими знаниями.

Одно из таких - Ask HN: How can I learn about performance optimization?

Народ из разных сфер разработки — обработки видео, геймдева, HFT и классической продуктовой — делится опытом и помогает выработать правильный майндсет: о чем думать и куда смотреть, когда речь заходит о производительности.

Отдельно стоит выделить совет:
cамый простой способ ускорить работу — делать меньше работы или не делать вовсе


Проще некуда, да? 🙂

Интересно, что в книге Systems Performance: Enterprise and the Cloud Брэндан Грегг, формулируя свои "мантры о производительности", начинает именно с этого принципа:
1. Don’t do it.
2. Do it, but don’t do it again.
3. Do it less.
4. Do it later.
5. Do it when they’re not looking.
6. Do it concurrently.
7. Do it more cheaply
👍11🔥7🫡2💯1
photo_2025-01-20_08-36-01.jpg
60.5 KB
Ранее я писал о баге Haproxy: после рестарта треды не завершались, что приводило к их накоплению, память иссякала и приходил OOM Killer.

Проблему решали костылем — директива hard-stop-after принудительно завершает треды после рестарта.

Но Haproxy не сдается и наносит ответный удар!

Причины еще предстоит выяснить, поэтому это скорее "заметка с полей"


Симптомы схожи: утечка памяти.

Но сбой наступает когда (это гипотеза) заканчивается память для TCP-буферов (net.ipv4.tcp_mem) - ядро с переменным успехом пытается освободить память для новых / существующих соединений, что приводит к затруднению в сетевых взаимодействиях.

На скрине такой период отмечен красным прямоугольником.

# sysctl net.ipv4.tcp_mem
net.ipv4.tcp_mem = 90435 120582 180870


Где 180870 - максимальное значение (в страницах памяти) под все TCP сокеты в системе, что равно ~ 706MB.

Оказалось, что система насыщается "повисшими" соединениями, чьи буферы сокетов содержат данные:
# ss -ntOai | awk '{for(i=1;i<=NF;i++)if($i~/^lastsnd:/){split($i,a,":");print a[2], $2, $4, $5}}' | sort -n | tail 

#lastsnd # Recv-Q #Src #Dst
234423668 157355 10.11.12.4:57354 10.11.6.123:80
235316436 302417 10.11.12.4:56232 10.11.6.124:80
238200680 301585 10.11.12.4:37940 10.11.6.124:80
238726828 300103 10.11.12.4:58944 10.11.6.124:80
243816724 297015 10.11.12.4:51700 10.11.6.125:80
251456440 302959 10.11.12.4:52324 10.11.6.125:80
252237780 302464 10.11.12.4:47786 10.11.6.123:80
257868244 163453 10.11.12.4:41568 10.11.6.125:80
259905196 300433 10.11.12.4:40202 10.11.6.123:80
261307944 214022 10.11.12.4:54888 10.11.6.123:80 # это ~ 72 часа

где:
* lastsnd - время с последней отправки данных, в милисекундах;
* Recv-Q - объем не прочитанных данных, в байтах.

А раз есть не прочитанные данные, значит таймер TCP keepalive не взводится:
static void tcp_keepalive_timer (struct timer_list *t)
{
...
/* It is alive without keepalive 8) */
if (tp->packets_out || !tcp_write_queue_empty(sk))
goto resched;

...

resched:
inet_csk_reset_keepalive_timer (sk, elapsed);
goto out;

...

out:
bh_unlock_sock(sk);
sock_put(sk);
}

Был бы повод, а костыль найдется!

Ребята из CloudFlare писали в свое время статью When TCP sockets refuse to die, где в виде решения предлагалось использовать опцию сокета TCP_USER_TIMEOUT:
...it specifies the maximum amount of time in milliseconds that transmitted data may remain unacknowledged, or buffered data may remain untransmitted (due to zero window size) before TCP will forcibly close the corresponding connection and return **ETIMEDOUT** to the application...


В свою очередь Haproxy поддерживает ее через tcp-ut.

Посмотрим, как себя покажет.

tags: #tcp #linux #kernel #troubleshooting #кейс
👍21🔥2
Я тут понял, что большинство контента в канале про прикладной уровень. Разбавим немного концептуальным.
———
The long way towards resilience — серия статей (аж 9 частей) про устойчивость как свойство IT-систем.

Автор разбирает понятие resilience: оно объединяет надёжность (учёт предсказуемых и непредсказуемых факторов) и способность системы развиваться под внешним воздействием.

Описываются шаги (или уровни зрелости), через которые может пройти организация, чтобы все таки назвать себя устойчивой:

- Фокус на разработке и запуске бизнес-функционала.
- Внедрение базовой надёжности через избыточность.
- Осознание, что сбои неизбежны — включение стратегий их смягчения.
- Подготовка к неожиданным ситуациям и неизвестным рискам.
- Достижение уровня, когда система не только выдерживает сбои, но и становится сильнее благодаря им.

Вдумчивого чтения на неделю.
👍11🔥2
Гид по #TCP (собрание материалов о TCP из канала)

📦 Алгоритмы контроля перегрузки

- TCP Congestion Control в разных окружениях — как алгоритмы перегрузки работают при потере пакетов. Практические наблюдения.
- BBR vs CUBIC — выбираем подходящий под наше окружение алгоритм.
- Мониторинг TCP: метрики Zero Window — что такое Zero Window, как он сигнализирует о перегрузке получателя, и почему это важно мониторить.
- Как считается TCP Window Clamp — копаемся в исходниках Linux и разбираемся в механизмах TCP Window;

📈 Ретрансмиты

- Мое выступление на Perf Conf №10 — влияние потерь пакетов на производительность приложений.
- TCP Retransmission May Be Misleading — классификация типов ретрансмитов и их мониторинг
- TCP ретрансмиты и их направления — пишем eBPF код для визуализации направления ретрансмитов в Grafana;

🔗 TCP соединения

- Сетевой анализ с eBPF: измеряем Round Trip Time — пишем eBPF код для мониторинга Round Trip Time в Grafana;
- TCP Puzzlers — интерпретируй закрытие соединений правильно.
- A Complete Guide of 'ss' Output Metrics — полный разбор метрик утилиты ss.

📚 Разное

- Зависшие соединения в Haproxy и механизм работы TCP keepalive — читаем код ядра и устраняем проблемы зависших соединений.
- Рассуждения о размерах очередей и какие трейдофы у больших значений;
- Investigation of a Cross-regional Network Performance Issue — траблшутинг медленной сети между дата-центрами после обновления ядра Linux
👍21❤‍🔥1
Performance matters! pinned «Гид по #TCP (собрание материалов о TCP из канала) 📦 Алгоритмы контроля перегрузки - TCP Congestion Control в разных окружениях — как алгоритмы перегрузки работают при потере пакетов. Практические наблюдения. - BBR vs CUBIC — выбираем подходящий под наше…»
Ранее я публиковал заметку Мониторинг TCP: метрики Zero Window, где обсуждал, что такое Zero Window и как его отслеживать в Linux.

А на днях, анализируя сетевые метрики одной машины, обнаружил, что мое представление о счетчиках TCP*ZeroWindowAdv оказалось неверным.

Сегодня будем исправляться.

⚙️ Итак, TCP перед отправкой сегмента добавляет заголовок, в котором рассчитывается размер окна.

Напомню, размер окна определяет, сколько данных можно отправить получателю без подтверждения — так называемые in-flight данные, "в полете" то есть.


tcp_select_window(sk) — функция ядра, которая определяет размер окна и отвечает за увеличение счетчиков TCP*ZeroWindowAdv:

static u16 tcp_select_window(struct sock *sk)
{
...
u32 old_win = tp->rcv_wnd;
u32 cur_win = tcp_receive_window(tp);
u32 new_win = __tcp_select_window(sk);

if (new_win < cur_win) {
...
if (new_win == 0)
NET_INC_STATS(sock_net(sk),
LINUX_MIB_TCPWANTZEROWINDOWADV);
new_win = ALIGN(cur_win, 1 << tp->rx_opt.rcv_wscale);
}
...

/* If we advertise zero window, disable fast path. */
if (new_win == 0) {
if (old_win)
NET_INC_STATS(sock_net(sk),
LINUX_MIB_TCPTOZEROWINDOWADV);
} else if (old_win == 0) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPFROMZEROWINDOWADV);
}
...
}


Видно, что:

1. Рост TCP*ZeroWindowAdv зависят только от состояния локального буфера приёма, то есть от того, сколько в нём свободного места.

Ранее я ошибочно полагал, что TCPToZeroWindowAdv растёт, если мы получаем уведомление о Zero Window от удаленной стороны, но это не так.


2. Рост TCPWantZeroWindowAdv возможен без увеличения TCPToZeroWindowAdv, так как последний требует дополнительной проверки размера окна через выравнивание (ALIGN)

📍 Переформулируем, что это за показатели и для чего они нужны.

TCPWantZeroWindowAdv увеличивается, когда локальная сторона не успевает вычитывать данные, и размер окна стремится к нулю. Однако Zero Window не обязательно будет направлен удалённой стороне.

То есть сетевой стек планирует (Want) объявить Zero Window;

TCPToZeroWindowAdv увеличивается, если после пересчёта размер окна остаётся нулевым, и тогда флаг добавляется в заголовок, что приводит к приостановке передачи данных. И это скверная ситуация.

То есть мы "идем" в нулевое окно - ToZero.

TCPFromZeroWindowAdv увеличивается, если окно ранее было равно нулю (old_win == 0), но теперь его размер стал больше, что позволяет возобновить передачу данных.

FromZero звучит как "уходим от нулевого окна". Ну или мне так проще запомнить)

_________________________________________________________________

Все таки прав был Мюллер: "Верить в наше время нельзя никому. Исходникам можно."
🔥9👍3
(Не) очевидные особенности настроек TCP сокетов. Часть 1.

Задача: затюнить размеры сокетов у Nginx, чтобы без потерь переживать всплески трафика.

Уточним размер буфера чтения:
# ss -ntlmO | grep ':80 '
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* skmem:(r0,rb131072,t0,tb16384,f0,w0,o0,bl0,d0)

Нас интересует значение rb - 131072 байт.

Окей, допустим мы хотим сделать его равным 6291456 байт (~6мб).

В Nginx за размер буфер приема отвечает параметр rcvbuf, см. док .

Вносим изменения в конфиг и перезапускаем Nginx:
# grep listen /etc/nginx/nginx.conf
listen 80 rcvbuf=6291456;
# systemctl restart nginx


Проверяем:
# ss -ntlmO | grep ':80 '
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* skmem:(r0,rb425984,t0,tb16384,f0,w0,o0,bl0,d0)


425984 не похоже на 6291456 ;)

———

# sysctl -a | grep rmem
net.core.rmem_default = 212992
net.core.rmem_max = 212992
net.ipv4.tcp_rmem = 4096 131072 18874368
...


Среднее значение tcp_rmem соответствует размеру буфера до изменений, а новое (425984) не совпадает ни с чем.

Документация к rcvbuf говорит, что настройка соответствует опции сокета SO_RCVBUF:
# man 7 socket

SO_RCVBUF
Sets or gets the maximum socket receive buffer in bytes. The kernel doubles this value (to allow space for bookkeeping overhead) when it is set using setsockopt(2), and this doubled value is returned by getsockopt(2). The default value is set by the /proc/sys/net/core/rmem_default file, and the maximum allowed value is set by the /proc/sys/net/core/rmem_max file...


1. ядро будет удваивать переданное значение (что и увидим в`rb`)
2. дефолтное (начальное) равно rmem_default;
3. максимум задается через rmem_max.

Причины удвоения (хотя там могут быть разные варианты) стоит искать в "man 7 tcp" и в "net.ipv4.tcp_adv_win_scale". Или в подробной статье от CloudFlare.


Окей, произведем расчеты: 425984 / 2 = 212992.

Похоже, что мы уперлись в net.core.rmem_max:
# sysctl net.core.rmem_max=6291456
net.core.rmem_max = 6291456
# nginx -s reload
# ss -ntlmO | grep '0.0.0.0:80 '
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* skmem:(r0,rb12582912,t0,tb16384,f0,w0,o0,bl0,d0)


Получаем искомое rb в 12582912 (6mb * 2).

Промежуточный итог: если мы выставляем руками размеры буферов на уровне приложение (опция SO_RCVBUF), следует учесть это и в net.core.rmem_max.

Следующая часть.

tags: #tcp #linux #kernel
🔥15
(Не) очевидные особенности настроек TCP сокетов. Часть 2.

Начало тут.

Есть мнение, что механизмы autotuning TCP в Linux не просто так придумали и в норме стоит пользоваться именно ими, а хардкод стоит избегать.

Потому разберемся с настройкой net.ipv4.tcp_rmem и узнаем оказывает ли на него влияние net.core.rmem_max:
# man 7 tcp
...
tcp_rmem (since Linux 2.4)
This is a vector of 3 integers: [min, default, max]. These parameters are used by TCP to regulate receive buffer sizes. ...

min minimum size of the receive buffer used by each TCP socket. The default value is the system page size. (On Linux 2.4, the default value is 4 kB, lowered to PAGE_SIZE bytes in low-memory systems.)...

default the default size of the receive buffer for a TCP socket. This value overwrites the initial default buffer size from the generic global net.core.rmem_default defined for all protocols...

max the maximum size of the receive buffer used by each TCP socket. This value does not override the global net.core.rmem_max. This is not used to limit the size of the receive buffer declared using SO_RCVBUF on a socket...


Перефразирую:
- дефолтное значение tcp_rmem перезаписывает net.core.rmem_default — это мы заметили в самом начале, до использования rcvbuf;
- максимальное значение tcp_rmem НЕ перезаписывает net.core.rmem_max и НЕ используется при выставлении SO_RCVBUF.

Делаем промежуточные вывод, что:
1. net.core.rmem_max задает жесткий лимит на размер TCP буфера;
2. tcp_rmem не участвует в игре при выставлении SO_RCVBUF.

Проверим!

Начнем с конца:
# sysctl -a | grep rmem
net.core.rmem_default = 212992
net.core.rmem_max = 6291456
net.ipv4.tcp_rmem = 4096 131072 18874368

### Выставляем максимальное значение tcp_rmem ниже rmem_max
# sysctl net.ipv4.tcp_rmem="4096 131072 851968"
net.ipv4.tcp_rmem = 4096 131072 851968

# grep listen /etc/nginx/nginx.conf
listen 80 rcvbuf=6291456;

# systemctl restart nginx

# ss -ntlmO | grep '0.0.0.0:80 '
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* skmem:(r0,rb12582912,t0,tb16384,f0,w0,o0,bl0,d0)


Действительно, максимальный tcp_rmem не сыграл.

Теперь проверим, что net.core.rmem_max задает жесткий лимит над размером TCP сокетов:
#  sysctl -a | grep rmem
net.core.rmem_default = 212992
net.core.rmem_max = 6291456
net.ipv4.tcp_rmem = 4096 131072 851968

### Делаем net.core.rmem_max ниже чем дефолтный tcp_rmem, значение которого поднимем

# sysctl net.core.rmem_max=212992
net.core.rmem_max = 212992
# sysctl net.ipv4.tcp_rmem="4096 524288 851968"
net.ipv4.tcp_rmem = 4096 524288 851968

### Убираем директиву rcvbuf
# grep listen /etc/nginx/nginx.conf
listen 80;

# systemctl restart nginx
# ss -ntlmO | grep '0.0.0.0:80 '
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* skmem:(r0,rb524288,t0,tb16384,f0,w0,o0,bl0,d0)


Размер буфера выставляется равным дефолтному tcp_rmem (кстати без удвоений), который больше net.core.rmem_max.

——
Особо пытливым для перепроверки можно обратиться к исходникам:
1. net.core.rmem_max участвует либо в обработке опции SO_RCVBUF:
...
case SO_RCVBUF:
...
__sock_set_rcvbuf(sk, min_t(u32, val, READ_ONCE(sysctl_rmem_max)));


2. либо в определении начального TCP окна:
...
space = max_t(u32, space, READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_rmem[2]));
space = max_t(u32, space, READ_ONCE(sysctl_rmem_max));
space = min_t(u32, space, *window_clamp);
*rcv_wscale = clamp_t(int, ilog2(space) - 15,
0, TCP_MAX_WSCALE);


что все таки не бьется с документацией.

Выходит обманывают нас разработчики или я так интерпретирую тексты :)

Окончательные выводы:
1. net.core.rmem_max играет только при ручном выставлении размеров сокетов (SO_RCVBUF);
2. net.ipv4.tcp_rmem напротив, не участвует в SO_RCVBUF, зато позволяет использовать автоподстройку, что в большинстве случаев будет более гибким решением.

В итоге решение задачи может быть следующим:
# sysctl net.ipv4.tcp_rmem="4096 1048576 12582912"


tags: #tcp #linux #kernel
👍23
В главе When to Stop Analysis книги Systems Performance обсуждается, как определить момент, когда дальнейший анализ проблемы становится неэффективным из-за низкого соотношения затрат к выгоде (ROI).

Аппетит приходит во время игры, ага:)

Автор предлагает три критерия, когда стоит остановиться:

1. Если найденное решение покрывает основную часть проблемы (66+ % от деградации);
2. Если затраты на анализ превышают потенциальную выгоду (отрицательный ROI);
3. Если есть другие задачи с более высоким ROI.

На мой взгляд, удобнее смотреть на схему по другому — перевернуть с ног на голову и сверяться с ней на каждом шаге:

1. Выбираем наиболее приоритетную проблему — хорошо когда можем сделать количественную оценку (деньги, время);
2. Погружаемся в суть и ищем решение, которое закроет хотя бы 66% проблемы;
3. GOTO 1 — проводим переоценку: точно ли оставшиеся 34 % всё ещё важнее других задач?

Звучит просто, но сколько раз я залипал на часы, разбирая то, что к делу почти не относится — подсчёту не поддаётся:)

P.S. есть мнение, что подход будет работать далеко за пределами траблшутинга и IT.
👍21