Инструментирование Julia с помощью DTrace и bpftrace
Инструменты DTrace и bpftrace обеспечивают легковесное инструментирование процессов. Инструментирование можно включать и отключать в ходе выполнения процесса, причем его издержки минимальны.
Совместимость: Julia 1.8
Поддержка зондов была добавлена в Julia 1.8. |
Данный документ предназначен для Linux, но содержимое в основном должно быть верно и для Mac OS/Darwin и FreeBSD. |
Включение поддержки
В Linux установите пакет systemtap
с версией dtrace
и создайте файл Make.user
со следующим содержимым:
WITH_DTRACE=1
чтобы включить зонды USDT.
Проверка
> readelf -n usr/lib/libjulia-internal.so.1 Displaying notes found in: .note.gnu.build-id Owner Data size Description GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring) Build ID: 57161002f35548772a87418d2385c284ceb3ead8 Displaying notes found in: .note.stapsdt Owner Data size Description stapsdt 0x00000029 NT_STAPSDT (SystemTap probe descriptors) Provider: julia Name: gc__begin Location: 0x000000000013213e, Base: 0x00000000002bb4da, Semaphore: 0x0000000000346cac Arguments: stapsdt 0x00000032 NT_STAPSDT (SystemTap probe descriptors) Provider: julia Name: gc__stop_the_world Location: 0x0000000000132144, Base: 0x00000000002bb4da, Semaphore: 0x0000000000346cae Arguments: stapsdt 0x00000027 NT_STAPSDT (SystemTap probe descriptors) Provider: julia Name: gc__end Location: 0x000000000013214a, Base: 0x00000000002bb4da, Semaphore: 0x0000000000346cb0 Arguments: stapsdt 0x0000002d NT_STAPSDT (SystemTap probe descriptors) Provider: julia Name: gc__finalizer Location: 0x0000000000132150, Base: 0x00000000002bb4da, Semaphore: 0x0000000000346cb2 Arguments:
Добавление зондов в libjulia
Зонды объявляются в формате dtraces в файле src/uprobes.d
. Генерируемый файл заголовков включается в файл src/julia_internal.h
: при добавлении зондов необходимо предоставить в нем холостую реализацию.
Заголовок будет содержать семафор *_ENABLED
и фактический вызов зонда. Если для вычисления аргументов зонда требуется много ресурсов, сначала следует проверить, включен ли зонд, а затем вычислить аргументы и вызвать зонд.
if (JL_PROBE_{PROBE}_ENABLED())
auto expensive_arg = ...;
JL_PROBE_{PROBE}(expensive_arg);
Если у зонда нет аргументов, лучше не включать проверку семафора. При включении зондов USDT семафор создает нагрузку на память, независимо от того, включен ли зонд.
#define JL_PROBE_GC_BEGIN_ENABLED() __builtin_expect (julia_gc__begin_semaphore, 0)
__extension__ extern unsigned short julia_gc__begin_semaphore __attribute__ ((unused)) __attribute__ ((section (".probes")));
Так как зонд сам по себе является холостой операцией, произойдет передача управления обработчику зонда.
Доступные зонды
Зонды сборки мусора
-
julia:gc__begin
: сборка мусора начинается в одном из потоков и инициирует приостановление выполнения программы. -
julia:gc__stop_the_world
: все потоки достигли безопасной точки, и выполняется сборка мусора. -
julia:gc__mark__begin
: начало фазы маркировки. -
julia:gc__mark_end(scanned_bytes, perm_scanned)
: фаза маркировки завершена. -
julia:gc__sweep_begin(full)
: начало фазы чистки. -
julia:gc__sweep_end
: фаза чистки завершена. -
julia:gc__end
: сборка мусора завершена, другие потоки продолжают работать. -
julia:gc__finalizer
: в изначальном потоке сборки мусора завершено выполнение финализаторов.
Зонды среды выполнения задач
-
julia:rt__run__task(task)
: переключение на задачуtask
в текущем потоке. -
julia:rt__pause__task(task)
: переключение с задачиtask
в текущем потоке. -
julia:rt__new__task(parent, child)
: задачаparent
создала задачуchild
в текущем потоке. -
julia:rt__start__task(task)
: задачаtask
запущена впервые с новым стеком. -
julia:rt__finish__task(task)
: задачаtask
завершилась и больше не будет выполняться. -
julia:rt__start__process__events(task)
: задачаtask
начала обрабатывать события libuv. -
julia:rt__finish__process__events(task)
: задачаtask
завершила обработку событий libuv.
Зонды очереди задач
-
julia:rt__taskq__insert(ptls, task)
: потокptls
попытался добавить задачуtask
в PARTR multiq. -
julia:rt__taskq__get(ptls, task)
: потокptls
извлек задачуtask
из PARTR multiq.
Зонды остановки и пробуждения потоков
-
julia:rt__sleep__check__wake(ptls, old_state)
: поток (PTLSptls
) пробуждается, предыдущее состояние —old_state
. -
julia:rt__sleep__check__wakeup(ptls)
: поток (PTLSptls
) пробудил себя. -
julia:rt__sleep__check__sleep(ptls)
: поток (PTLSptls
) пытается перейти в неактивное состояние. -
julia:rt__sleep__check__taskq__wake(ptls)
: потоку (PTLSptls
) не удалось перейти в неактивное состояние из-за задач в PARTR multiq. -
julia:rt__sleep__check__task__wake(ptls)
: потоку (PTLSptls
) не удалось перейти в неактивное состояние из-за задач в рабочей очереди Base. -
julia:rt__sleep__check__uv__wake(ptls)
: потоку (PTLSptls
) не удалось перейти в неактивное состояние из-за пробуждения libuv.
Примеры использования зондов
Задержка приостановления выполнения программы при сборке мусора
В contrib/gc_stop_the_world_latency.bt
приводится пример скрипта bpftrace
, который создает гистограмму задержки достижения безопасной точки для всех потоков.
Запустите этот код Julia с параметром julia -t 2
.
using Base.Threads fib(x) = x <= 1 ? 1 : fib(x-1) + fib(x-2) beaver = @spawn begin while true fib(30) # Эта безопасная точка необходима до #41616, иначе данный # цикл никогда не будет подвергнут сборке мусора. GC.safepoint() end end allocator = @spawn begin while true zeros(1024) end end wait(allocator)
Во втором терминале:
> sudo contrib/bpftrace/gc_stop_the_world_latency.bt Attaching 4 probes... Tracing Julia GC Stop-The-World Latency... Hit Ctrl-C to end. ^C @usecs[1743412]: [4, 8) 971 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@| [8, 16) 837 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | [16, 32) 129 |@@@@@@ | [32, 64) 10 | | [64, 128) 1 | |
Мы можем увидеть распределение задержки в фазе приостановления выполнения программы для процесса Julia.
Монитор порождения задач
Иногда может быть полезно знать, когда задача порождает другие задачи. Это легко делается с помощью rt__new__task
. Первый аргумент зонда, parent
, — это существующая задача, создающая другую задачу. Таким образом, если вам известен адрес задачи, которую нужно отслеживать, вы легко можете узнать, какие задачи она породила. Давайте посмотрим, как это делается. Сначала запустим сеанс Julia и получим PID и адрес задачи REPL.
> julia _ _ _ _(_)_ | Documentation: https://docs.julialang.org (_) | (_) (_) | _ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help. | | | | | | |/ _` | | | | |_| | | | (_| | | Version 1.6.2 (2021-07-14) _/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release |__/ | 1> getpid() 997825 2> current_task() Task (runnable) @0x00007f524d088010
Теперь мы можем запустить bpftrace
и сделать так, чтобы зонд rt__new__task
отслеживал только родительскую задачу.
sudo bpftrace -p 997825 -e 'usdt:usr/lib/libjulia-internal.so:julia:rt__new__task /arg0==0x00007f524d088010/{ printf("Task: %x\n", arg0); }'
(Обратите внимание, что в приведенном выше коде первым аргументом, то есть parent
, является arg0
.)
Если мы породим одну задачу:
@async 1+1
то увидим, что она создана:
Task: 4d088010
Однако, если мы породим еще несколько задач из этой новой задачи:
@async for i in 1:10
@async 1+1
end
с помощью bpftrace
мы увидим только одну задачу:
Task: 4d088010
И это все та же задача, которую мы отслеживаем! Конечно, мы можем легко снять этот фильтр, чтобы увидеть все созданные задачи.
sudo bpftrace -p 997825 -e 'usdt:usr/lib/libjulia-internal.so:julia:rt__new__task { printf("Task: %x\n", arg0); }'
Task: 4d088010 Task: 4dc4e290 Task: 4dc4e290 Task: 4dc4e290 Task: 4dc4e290 Task: 4dc4e290 Task: 4dc4e290 Task: 4dc4e290 Task: 4dc4e290 Task: 4dc4e290 Task: 4dc4e290
Мы видим нашу корневую задачу и новую порожденную задачу в качестве родительской для еще десяти новых задач.
Обнаружение массового пробуждения
Иногда задачи сталкиваются с проблемой массового пробуждения (thundering herd): когда в неактивную среду выполнения задач поступает запрос на выполнение какой-либо работы, пробудиться могут все потоки, даже если достаточно было бы лишь их части. Из-за этого может возникать дополнительная задержка, так как на пробуждение всех потоков (и их останов из-за отсутствия достаточного объема работы) расходуются ресурсы ЦП.
С помощью bpftrace
можно легко проиллюстрировать эту проблему. Сначала в одном терминале мы запускаем Julia с несколькими потоками (шестью в данном примере) и получаем PID процесса.
> julia -t 6 _ _ _ _(_)_ | Documentation: https://docs.julialang.org (_) | (_) (_) | _ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help. | | | | | | |/ _` | | | | |_| | | | (_| | | Version 1.6.2 (2021-07-14) _/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release |__/ | 1> getpid() 997825
В другом терминале мы запускаем bpftrace
для мониторинга процесса, в частности с помощью зонда rt__sleep__check__wake
.
sudo bpftrace -p 997825 -e 'usdt:usr/lib/libjulia-internal.so:julia:rt__sleep__check__wake { printf("Thread wake up! %x\n", arg0); }'
Теперь мы создадим и выполним в Julia одну задачу.
Threads.@spawn 1+1
В bpftrace
вывод будет примерно следующим.
Thread wake up! 3f926100 Thread wake up! 3ebd5140 Thread wake up! 3f876130 Thread wake up! 3e2711a0 Thread wake up! 3e312190
Хотя мы породили лишь одну задачу (которую одновременно может обрабатывать только один поток), пробудились все потоки! В будущем, возможно, эта проблема будет решена и среда выполнения будет пробуждать только один поток (или вообще не будет пробуждать их, если для выполнения задачи достаточно порождающего потока).
Мониторинг задач с помощью BPFnative.jl
BPFnative.jl можно подключать к точкам зондирования USDT точно так же, как bpftrace
. Здесь можно найти демонстрацию мониторинга среды выполнения задач, сборки мусора, а также перехода потоков в спящий режим и их пробуждения.
Примечания об использовании bpftrace
Пример зонда в формате bpftrace выглядит следующим образом.
usdt:usr/lib/libjulia-internal.so:julia:gc__begin { @start[pid] = nsecs; }
Объявление зонда принимает тип usdt
, а затем либо путь к библиотеке, либо PID, имя поставщика julia
и имя зонда gc__begin
. Обратите внимание, что здесь используется относительный путь к libjulia-internal.so
, но в рабочей среде может потребоваться абсолютный путь.