Документация Engee

Инструментирование 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")));

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

Доступные зонды

Зонды сборки мусора

  1. julia:gc__begin: сборка мусора начинается в одном из потоков и инициирует приостановление выполнения программы.

  2. julia:gc__stop_the_world: все потоки достигли безопасной точки, и выполняется сборка мусора.

  3. julia:gc__mark__begin: начало фазы маркировки.

  4. julia:gc__mark_end(scanned_bytes, perm_scanned): фаза маркировки завершена.

  5. julia:gc__sweep_begin(full): начало фазы чистки.

  6. julia:gc__sweep_end: фаза чистки завершена.

  7. julia:gc__end: сборка мусора завершена, другие потоки продолжают работать.

  8. julia:gc__finalizer: в изначальном потоке сборки мусора завершено выполнение финализаторов.

Зонды среды выполнения задач

  1. julia:rt__run__task(task): переключение на задачу task в текущем потоке.

  2. julia:rt__pause__task(task): переключение с задачи task в текущем потоке.

  3. julia:rt__new__task(parent, child): задача parent создала задачу child в текущем потоке.

  4. julia:rt__start__task(task): задача task запущена впервые с новым стеком.

  5. julia:rt__finish__task(task): задача task завершилась и больше не будет выполняться.

  6. julia:rt__start__process__events(task): задача task начала обрабатывать события libuv.

  7. julia:rt__finish__process__events(task): задача task завершила обработку событий libuv.

Зонды очереди задач

  1. julia:rt__taskq__insert(ptls, task): поток ptls попытался добавить задачу task в PARTR multiq.

  2. julia:rt__taskq__get(ptls, task): поток ptls извлек задачу task из PARTR multiq.

Зонды остановки и пробуждения потоков

  1. julia:rt__sleep__check__wake(ptls, old_state): поток (PTLS ptls) пробуждается, предыдущее состояние — old_state.

  2. julia:rt__sleep__check__wakeup(ptls): поток (PTLS ptls) пробудил себя.

  3. julia:rt__sleep__check__sleep(ptls): поток (PTLS ptls) пытается перейти в неактивное состояние.

  4. julia:rt__sleep__check__taskq__wake(ptls): потоку (PTLS ptls) не удалось перейти в неактивное состояние из-за задач в PARTR multiq.

  5. julia:rt__sleep__check__task__wake(ptls): потоку (PTLS ptls) не удалось перейти в неактивное состояние из-за задач в рабочей очереди Base.

  6. julia:rt__sleep__check__uv__wake(ptls): потоку (PTLS ptls) не удалось перейти в неактивное состояние из-за пробуждения 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, но в рабочей среде может потребоваться абсолютный путь.