Инструментирование 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)
# Эта заданная вручную безопасная точка необходима, иначе данный
# цикл может никогда не подвергаться сборке мусора.
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, но в рабочей среде может потребоваться абсолютный путь.