Объём ресурсов, потребляемый контейнером, учитывается на уровне cgroup, к которой он относится.
Контейнер состоит из одного или более процессов, а процесс — из одного или более тредов.
Абстракцию можно углубить, разбивая тред на «зелёные потоки», в зависимости от рантайма это могут быть горутины (Golang), корутины (Kotlin/Python), виртуальные треды (Java) и другие.
Но о «зелёных» потоках Linux не знает, для него единицей планирования является тред (в терминах ядра — task_struct
). Именно он планируется, выполняется и вытесняется другими тредами для выполнения на CPU.
С другой стороны у каждого CPU в системе есть своя runqueue
— очередь тредов, готовых к исполнению (Runnable
).
Планировщик на основе приоритетов, квот и доли использования CPU в рамках периода, решает какой тред из runqueue
будет исполняться следующим. Таким образом, недостаточно быть в состоянии Runnable
, тред должен оказаться первым среди остальных тредов.
Для понимания насколько эффективно тред справляется с конкуренцией, полезно отслеживать время его пребывания в runqueue
.
Эксперимент
Запустим синтетическую CPU-bound нагрузку и проанализируем её с разных сторон.
$ while :; do :; done &
[1] 2676086
$ pidstat -p 2676086 -wu 1 10
[...]
Average: UID PID %usr %system %guest %wait %CPU CPU cswch/s nvcswch/s Command
Average: 0 2676086 99.90 0.00 0.00 0.00 99.90 - 0.00 2.40 bash
Ядро Linux охотно отдает все доступные ресурсы CPU под наш тред:
- минимальное количество принудительных переключений контекста (
nvcswch/s
); - потребление CPU на уровне 100% в
%usr
(нулевойcswch/s
).
$ python3 psn -p 2676086 -d 15 -a -G kstack # https://github.com/tanelpoder/0xtools
samples | avg_threads | comm | state | kstack
------------------------------------------------------------
300 | 1.00 | (bash) | Running (ON CPU) | -
Тред без переключений находится в Running
.
$ runqlat-bpfcc -p 2676086 15 1 # https://github.com/iovisor/bcc
usecs : count distribution
0 -> 1 : 0 | |
2 -> 3 : 0 | |
4 -> 7 : 1 |* |
8 -> 15 : 2 |** |
16 -> 31 : 33 |****************************|
32 -> 63 : 8 |***** |
64 -> 127 : 2 |* |
Время нахождения в runqueue
составляет от 16 до 64 микросекунд, что говорит об отсутствии каких-либо затруднений в планировании.
Теперь изменим условия и запустим ту же нагрузку, но с параметром CPUQuota=50%
(CPU Limits
на языке Kubernetes):
$ systemd-run --pty --quiet --collect --unit test.service --property CPUQuota=50% /bin/bash -c "while :; do :; done"
$ pidstat -p 2676086 -wu 1 10
[...]
Average: UID PID %usr %system %guest %wait %CPU CPU cswch/s nvcswch/s Command
Average: 0 2676086 50.00 0.00 0.00 50.00 50.00 - 0.00 10.90 bash
Теперь ядро ограничивает использование CPU:
%CPU
на уровне 50%;- растет
%wait
(CPU Wait Time); - ядро чаще приостанавливает тред —
nvcswch/s = 10.90
.
$ runqlat-bpfcc -p 2676272 15 1
Tracing run queue latency... Hit Ctrl-C to end.
usecs : count distribution
0 -> 1 : 0 | |
2 -> 3 : 0 | |
4 -> 7 : 0 | |
8 -> 15 : 2 | |
16 -> 31 : 12 |*** |
32 -> 63 : 0 | |
64 -> 127 : 0 | |
128 -> 255 : 0 | |
256 -> 511 : 0 | |
512 -> 1023 : 0 | |
1024 -> 2047 : 0 | |
2048 -> 4095 : 0 | |
4096 -> 8191 : 0 | |
8192 -> 16383 : 0 | |
16384 -> 32767 : 0 | |
32768 -> 65535 : 150 |****************************************|
Хвосты в промежутке 32768 - 65535 намекают на наличие троттлинга.
$ python3 psn -p 2676272 -d 15 -a -G kstack
[...]
samples | avg_threads | comm | state | kstack
---------------------------------------------------------------------------------------------------------------
150 | 0.50 | (bash) | Running (ON CPU) | -
150 | 0.50 | (bash) | Running (ON CPU) | irqentry_exit_to_user_mode()->exit_to_user_mode_prepare()
Подтверждаем - в половине случаев тред исполняется (Running
), а в другой половине находится в Runnable
, ожидая планирования на процессор.
Running
включает в себя состояниеRunnable
, отдельно готовность к исполнению в Linux не отслеживается.
Выводы
Хоть эксперимент максимально синтетический, но есть несколько интересных моментов:
- троттлинг на уровне процесса(треда) наблюдается как CPU Wait Time;
- тред “замораживается”, переходя в
Runnable
; - в такие моменты хвостовые задержки пребывания в
runqueue
становятся настолько большими, что говорить о производительности уже не приходится.
Последний пункт является весомым аргументом пересмотреть использование квот в пользу
cpu.shares
(cpu requests
) — особенно когда важна скорость.