🪒

BPFを使ったリアルタイムプロセス可視化システムの構築と実験

に公開

はじめに

システム上で実行されているプロセスの動作はどのように可視化できるのでしょうか?今回はBPF(Berkeley Packet Filter)とbpftraceを使用してリアルタイムプロセス監視システムを構築し、実際に実験を行ってその有用性を確認してみます。
また、今回だけでなく次回以降も様々なシステム監視やデバッグ手法について探求していきたいと思います。

マシンスペック

MacBook Air M2 arm64
Docker上で実施

準備

ディレクトリ構成

daemon/
├── Dockerfile
├── docker-compose.yml
├── scripts/
│   ├── trace.bt          
│   ├── render.py         
│   ├── cgroup_lookup.py  
└── output/              

Dockerfile

FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive \
    LANG=C.UTF-8

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        bpftrace linux-tools-common linux-tools-generic \
        clang llvm gcc make git curl ca-certificates \
        graphviz python3 python3-venv python3-pip jq \
        sudo less && \
    rm -rf /var/lib/apt/lists/*

RUN git clone --depth=1 https://github.com/brendangregg/FlameGraph.git /opt/FlameGraph
ENV PATH="/opt/FlameGraph:${PATH}"

RUN python3 -m venv /opt/venv && \
    /opt/venv/bin/pip install --no-cache-dir graphviz livereload psutil docker

ENV PATH="/opt/venv/bin:${PATH}"

RUN useradd -m tracer && echo "tracer ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/tracer
USER tracer
WORKDIR /home/tracer

COPY --chown=tracer:tracer scripts/ ./scripts/

RUN chmod +x /home/tracer/scripts/trace.bt /home/tracer/scripts/*.py

CMD ["bash"]

docker‑compose.yml

services:
  procviz:
    build: .
    privileged: true
    pid: "host"
    network_mode: "host"
    volumes:
      - /sys:/sys:ro
      - /proc:/proc:ro
      - ./output:/home/tracer/output
    working_dir: /home/tracer
    command: >
      bash -c '
        # 1) FIFO が残っていても安全に再利用
        [ -p /tmp/proc_fifo ] || mkfifo /tmp/proc_fifo

        # 2) BPF トレーサ起動(バックグラウンド)
        sudo /home/tracer/scripts/trace.bt > /tmp/proc_fifo &

        # 3) Graphviz レンダラ起動
        python /home/tracer/scripts/render.py &

        # 4) LiveReload サーバー起動
        livereload /home/tracer/output --port 8000 --host 0.0.0.0
      '

scripts/trace.bt

#!/usr/bin/env bpftrace

tracepoint:sched:sched_process_fork
{
  printf("F %d %d %s\n", args->parent_pid, args->child_pid, str(args->child_comm));
}

tracepoint:sched:sched_process_exec
{
  printf("X %d %s\n", pid, str(args->filename));
}

tracepoint:sched:sched_process_exit
{
  printf("E %d\n", pid);
}

scripts/cgroup_lookup.py

import os, re

def container_id(pid: str) -> str | None:
    try:
        with open(f"/proc/{pid}/cgroup") as f:
            for line in f:
                # パス末尾が /docker/<CONTAINER_ID>.scope 形式
                m = re.search(r"/docker[-/](?P<id>[0-9a-f]{12,64})", line)
                if m:
                    return m.group("id")[:12]
    except FileNotFoundError:
        pass
    return None

scripts/render.py

#!/usr/bin/env python3
import graphviz, time, threading, collections, os, sys
from cgroup_lookup import container_id

FIFO = "/tmp/proc_fifo"
SVG_OUT = "output/proc.svg"

edges   = set()
labels  = {}
colors  = collections.defaultdict(lambda: "#9ecae1")
containers = {}  # pid -> containerID

def tail_fifo():
    with open(FIFO) as f:
        for line in f:
            try:
                t, *rest = line.strip().split()
                if t == "F":       # fork
                    p, c, comm = rest
                    edges.add((p, c))
                    labels.setdefault(p, comm)
                    labels[c] = comm
                    containers[c] = container_id(c)
                elif t == "X":     # exec
                    pid, cmd = rest
                    labels[pid] = os.path.basename(cmd)
                elif t == "E":     # exit
                    pid, = rest
                    edges.difference_update({e for e in list(edges) if e[1] == pid})
                    labels.pop(pid, None)
                    containers.pop(pid, None)
                render()
            except Exception:
                pass

def render():
    g = graphviz.Digraph("proc", node_attr={"shape":"box","fontname":"monospace","penwidth":"1"})
    # サブグラフ per container
    subgraphs = {}
    for (p,c) in edges:
        for n in (p,c):
            labels.setdefault(n, n)
    for pid,label in labels.items():
        cid = containers.get(pid)
        color = colors[pid]
        if cid:
            sg = subgraphs.setdefault(cid, graphviz.Digraph(name=f"cluster_{cid}",
                        graph_attr={"label":f"docker:{cid}","color":"#6baed6"}))
            sg.node(pid, f"{pid}:{label}", style="filled", fillcolor=color, fontcolor="white")
        else:
            g.node(pid, f"{pid}:{label}", style="filled", fillcolor=color)
    for cid,sg in subgraphs.items():
        g.subgraph(sg)
    for p,c in edges:
        g.edge(p,c)
    g.render(SVG_OUT, format="svg", overwrite_source=True, quiet=True)

if __name__=="__main__":
    if not os.path.exists("output"): os.mkdir("output")
    threading.Thread(target=tail_fifo, daemon=True).start()
    print("[*] Rendering…  press Ctrl+C to stop")
    try:
        while True: time.sleep(1)
    except KeyboardInterrupt:
        sys.exit(0)

実験

システム起動

docker compose build
docker compose up -d

簡易プロセステスト

# 10個のsleepプロセスを生成
for i in {1..10}; do sleep 15 & done

複雑なプロセス階層テスト

# 3層階層のプロセス構造
bash -c '
  for i in {1..3}; do
    bash -c "
      for j in {1..2}; do
        sleep 8 &
      done
      wait
    " &
  done
'

コンテナ内プロセス監視

# テスト用コンテナの起動
docker run --rm -d --name test-container alpine:latest sh -c '
  while true; do
    for i in 1 2 3; do
      sleep 15 &
    done
    sleep 20
  done
'

結果

実験1: 簡易プロセス生成テスト

$ for i in {1..10}; do sleep 15 & done
$ ps aux | grep "sleep 15" | grep -v grep
kazuki-k  43168  sleep 15
kazuki-k  43177  sleep 15
kazuki-k  43176  sleep 15
kazuki-k  43175  sleep 15
kazuki-k  43174  sleep 15
kazuki-k  43173  sleep 15
kazuki-k  43172  sleep 15
kazuki-k  43171  sleep 15
kazuki-k  43170  sleep 15
kazuki-k  43169  sleep 15

SVGファイル変化: 116,629 bytes → 124,005 bytes (+7,376 bytes)
結果: プロセス生成と同時にSVGファイルが更新

実験2: 複雑プロセス階層テスト

親プロセス: 43892
中間プロセス1: 43892  
中間プロセス2: 43892
中間プロセス3: 43892
       9  # 生成されたsleepプロセス数

SVGファイル変化: 135,324 bytes → 138,219 bytes (+2,895 bytes)
結果: 階層的なプロセス関係が正確に記録

実験3: コンテナ内プロセス監視

$ docker exec test-container ps aux
PID   USER     COMMAND
1     root     sh -c (メインプロセス)
6     root     sleep 15
7     root     sleep 15  
8     root     sleep 15
10    root     sleep 20

SVGファイル変化: 139,602 bytes → 145,043 bytes (+5,441 bytes)
結果: コンテナ内プロセス検出成功、グループ化表示は要確認

システムパフォーマンス統計

項目 実験開始時 実験終了時 増加量
監視プロセス数 1,949 2,148 +199
プロセス関係数 1,012 1,075 +63
SVGファイルサイズ 116KB 148KB +32KB
$ docker stats daemon-procviz-1
CONTAINER        CPU %    MEM USAGE / LIMIT    
daemon-procviz-1 2.34%    45.6MiB / 7.77GiB    

リソース消費: CPU 2.34% (低負荷), メモリ 45.6MB (軽量)

まとめ

機能 状態 詳細
BPFプロセス監視 ⭕️ fork/exec/exitイベント正常捕捉、2,148個のプロセス安定監視
リアルタイム更新 ⭕️ プロセス生成と同時のSVG更新、動的変化追跡
プロセス階層表示 ⭕️ 親子関係正常記録、3層階層構造の正確な追跡
グラフ生成 ⭕️ Graphviz正常レンダリング、視覚的に分かりやすい表現
コンテナ検出 🔺 プロセス検出OK、グループ化表示要確認
スケーラビリティ 14分間連続稼働で安定、低リソース消費

活用場面

  • システム管理者のプロセス監視ツール
  • DevOpsエンジニアのデバッグ支援
  • セキュリティ分析での異常検出
  • 教育目的でのシステム理解促進

最後に

今回はBPFを使用したリアルタイムプロセス可視化システムを深掘りしてみました。実験により、システム全体のプロセス動作を直感的に理解できる実用的なツールであることが確認できました。皆様のシステム監視・デバッグ作業のお役に立てると幸いです。

Discussion