AnyMath 文档

用DTrace和bpftrace仪器Julia

DTrace和bpftrace是实现流程轻量级检测的工具。 您可以在进程运行时打开和关闭仪器,并且关闭仪器的开销是最小的。

兼容性

Julia1.8中增加了对探测的支持

请注意,本文档是从Linux的角度编写的,其中大部分应该适用于Mac OS/Darwin和FreeBSD。

启用支持

在Linux上安装 系统图 有一个版本的软件包 dtrace 并创建一个 做吧。用户 文件包含

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/uprobe。d. 生成的头文件包含在 src/julia_internal.h 如果你添加了探测,你应该在那里提供一个noop实现。

标头将包含一个信号量 *已启用 以及对探测器的实际调用。 如果探测参数的计算成本很高,则应首先检查探测是否已启用,然后计算参数并调用探测。

  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")));

而探针本身是一个noop sled,它将被修补到探针处理程序的蹦床上。

可用探针

GC探针

  1. 朱莉娅:gc__开始:GC开始在一个线程上运行,并触发stop-the-world。

  2. 朱莉娅:gc__stop_the_world:所有线程都到达安全点,GC运行。

  3. julia:gc__mark__begin:开始标记阶段

  4. julia:gc__mark_end(scanned_bytes,perm_scanned):标记阶段结束

  5. julia:gc__sweep_begin(已满):开始扫荡

  6. 朱莉娅:gc__sweep_end:扫荡阶段结束

  7. 朱莉娅:gc__end:GC完成,其他线程继续工作

  8. 朱莉娅:gc__终结器:Initial GC thread has finished running finalizers

任务运行时探测

  1. julia:rt__run__task(任务):切换到任务 任务 在当前线程上。

  2. julia:rt__pause__task(任务):从任务切换 任务 在当前线程上。

  3. julia:rt__new__task(parent,child):任务 家长/家长 创建的任务 儿童 在当前线程上。

  4. julia:rt__start__task(任务):任务 任务 第一次用一个新的堆栈开始。

  5. julia:rt__finish__task(任务):任务 任务 完成,将不再执行。

  6. julia:rt__start__process__events(任务):任务 任务 开始处理libuv事件。

  7. julia:rt__finish__process__events(任务):任务 任务 完成处理libuv事件。

任务队列探测

  1. julia:rt__taskq__insert(ptls,task):线程 [医]ptls 试图插入 任务 成PARTR multiq。

  2. julia:rt__taskq__get(ptls,task):线程 [医]ptls 弹出 任务 来自PARTR multiq。

线程睡眠/唤醒探针

  1. julia:rt__sleep__check__wake(ptls,old_state):螺纹(PTLS [医]ptls)醒来,以前处于状态 旧状态.

  2. julia:rt__sleep__check__wakeup(ptls):螺纹(PTLS [医]ptls)自己醒了。

  3. 朱莉娅:rt__睡眠__检查__睡眠(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 workqueue中的任务导致睡眠失败。

  6. julia:rt__sleep__check__uv__wake(ptls):螺纹(PTLS) [医]ptls)由于libuv唤醒而无法入睡。

探针使用示例

GC停止世界延迟

一个例子 bpftrace 脚本在给出 contrib/gc_stop_the_world_latency.bt 它创建了所有线程到达安全点的延迟直方图。

运行这个Julia代码, 朱莉娅-t2

using Base.Threads

fib(x) = x <= 1 ? 1 : fib(x-1) + fib(x-2)

beaver = @spawn begin
    while true
        fib(30)
        # A manual safepoint is necessary since otherwise this loop
        # may never yield to GC.
        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进程中看到stop-the-world阶段的延迟分布。

任务生成监视器

知道某个任务何时生成其他任务有时很有用。 这很容易看到 rt__new__任务. 探头的第一个参数, 家长/家长,是正在创建新任务的现有任务。 这意味着,如果您知道要监视的任务的地址,则可以轻松地查看该特定任务产生的任务。 让我们看看如何做到这一点;首先让我们开始一个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__任务 对于_only_这个父母: sudo bpftrace-p997825-e’usdt:usr/lib/libjulia-internal.so:julia:rt__new__task/arg0==0x00007f524d088010/{printf("Task:%x\n",arg0);}'

(注意,在上面, arg0 是第一个参数, 家长/家长).

如果我们产生一个任务:

线程。@产卵1+1

我们看到这个任务正在创建:

任务:4d088010

但是,如果我们从新产生的任务中产生了一堆任务:

Threads.@spawn for i in 1:10
   Threads.@spawn 1+1
end

我们仍然只看到一个任务 bpftrace:

任务:4d088010

这仍然是我们监控的任务! 当然,我们可以删除这个过滤器来查看_all_新创建的任务: sudo bpftrace-p997825-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

我们可以看到我们的根任务,以及新产生的任务作为十个更新的任务的父级。

雷鸣般的牛群探测

任务运行时通常会受到"雷鸣般的群众性"问题的影响:当一些工作被添加到安静的任务运行时时,所有线程都可能从沉睡中被唤醒,即使没有足够的工作供每个线程处理。 这可能会导致额外的延迟和CPU周期,而所有线程唤醒(并同时回到睡眠状态,没有找到任何工作要执行)。

我们可以看到这个问题说明与 bpftrace 很容易。 首先,在一个终端中,我们用多个线程(本例中为6)启动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__睡眠__检查__唤醒 钩,钩: sudo bpftrace-p997825-e’usdt:usr/lib/libjulia-internal.so:julia:rt__sleep__check__wake { printf("Thread wake up! %x\n", arg0); }'

现在,我们在Julia中创建并执行单个任务:

线程。@产卵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. 有一个演示可用于监视任务运行时、GC和线程睡眠/唤醒转换https://github.com/jpsamaroo/BPFnative.jl/blob/master/examples/task-runtime.jl[这里]。

使用须知 bpftrace

Bpftrace格式的示例探针如下所示:

usdt:usr/lib/libjulia-internal.so:julia:gc__begin
{
  @start[pid] = nsecs;
}

探测声明采用的是 ust/ust,然后是库的路径或PID,提供者名称 朱莉娅 和探针名称 gc__开始. 请注意,我正在使用一个相对路径到 libjulia-internal.so,但这可能需要是生产系统上的绝对路径。