K8s eBPFオブザーバビリティ:カーネルトレーシングからフルスタックモニタリングまでの5つの実践パターン

DevOps

従来のモニタリングがK8sカーネルのブラックホールに直面した時

このような経験はありませんか——Prometheusのメトリクスは正常なのに、サービスレイテンシが謎の急上昇を起こす?SidecarプロキシがCPUの15%を消費しているのに、「接続タイムアウト」としか報告しない?ログはアプリケーションレベルのエラーで溢れているのに、カーネル空間で何が起きているのか全く見えない?

これがK8sオブザーバビリティの「三重のブラインドスポット」です:従来のモニタリングはユーザー空間しか見えず、カーネル空間の出来事には全く気づけない;Sidecarインジェクションはオーバーヘッドを追加し、Istioのデータプレーンはレイテンシを2-5ms増加させる;分散トレーシングのサンプリングレートでは、重要な1%のリクエストを永遠に捕捉できない。

eBPFがこれをすべて変えます。カーネルの変更、Sidecarの注入、アプリケーションコードの変更なしに、カーネル空間で直接システムコールの全詳細をキャプチャできます。TCP再送からプロセス実行、ネットワークパケットドロップからセキュリティイベントまで——eBPFはK8sクラスタに真の「フルスタック透視能力」を与えます。

本記事では、ゼロから5つのeBPFオブザーバビリティ実践パターンを習得し、カーネルトレーシング、ネットワークモニタリング、セキュリティ監査、パフォーマンス分析の全リンクシナリオをカバーします。

コア概念リファレンステーブル

概念 フルネーム 説明
eBPF Extended Berkeley Packet Filter Linuxカーネル内のサンドボックスVM、カーネル空間でカスタムプログラムを安全に実行可能
BPF Program BPFプログラム カーネルにロードするeBPFコード、特定のフックポイントにアタッチして実行
BPF Map BPFマッピングテーブル カーネル空間とユーザー空間間のデータ共有構造、hash/array/ring等のタイプをサポート
bpftrace bpftrace 高レベルeBPFトレーシング言語、awkライクな構文、迅速なプロトタイピングに最適
Cilium Cilium eBPFベースのK8s CNIプラグイン、ネットワーク・セキュリティ・オブザーバビリティを提供
Hubble Hubble Ciliumのオブザーバビリティコンポーネント、ネットワークトラフィック可視化とサービス依存マッピングを提供
Kprobe Kernel Probe 動的カーネルプローブ、カーネル関数のエントリ/出口にアタッチ可能
Tracepoint Tracepoint カーネル開発者が事前定義した静的トレースポイント、kprobeより安定
XDP eXpress Data Path NICドライバレベルでネットワークパケットを処理するeBPFフック、超低レイテンシ
BPF Verifier BPFベリファイア カーネル内のセキュリティチェッカー、eBPFプログラムがカーネルクラッシュを引き起こさないことを保証
BTF BPF Type Format eBPF型情報フォーマット、CO-RE(一度コンパイル、どこでも実行)を実現
Perf Event Performance Event Linuxパフォーマンスイベントサブシステム、eBPFプログラムの重要なアタッチポイント

5つの課題:なぜK8s eBPFオブザーバビリティは「プラグインを入れるだけ」では済まないのか

課題1:カーネルバージョン互換性地獄

eBPF機能はカーネルバージョンの反復とともに拡張し続けています。BPF trampolineは5.5+、BTFサポートは5.2+が必要ですが、多くの企業のK8sノードはまだ4.19や5.4カーネルで動いています。慎重に作成したeBPFプログラムが異なるノードでロードできない可能性があります。

課題2:BPF Verifierの厳格な制限

BPFベリファイアは安全性を証明できないプログラムをすべて拒否します。ループは有界でなければならず、ポインタアクセスにはnullチェックが必要で、スタック空間は512バイトに制限されています。少し複雑なトレーシングロジックでも、検証を通過するために何度も調整が必要になる場合があります。

課題3:本番環境のセキュリティ懸念

eBPFプログラムはカーネル空間で実行されます。ベリファイアが安全性を保証していても、多くのセキュリティチームは「カーネルでカスタムコードを実行する」ことに慎重です。特に金融や医療などコンプライアンス要件が厳しい業界では、eBPFの導入には厳格なセキュリティ監査が必要です。

課題4:オブザーバビリティデータの爆発

eBPFはカーネルから大量のイベントをキャプチャできます——すべてのシステムコール、すべてのネットワークパケット、すべてのコンテキストスイッチ。大規模なK8sクラスタでは、フィルタリングされていないeBPFデータは毎秒数百万のイベントを生成し、ストレージと分析システムを圧倒する可能性があります。

課題5:マルチクラスタ相関トレーシング

リクエストが複数のK8sクラスタにまたがる場合、eBPFがキャプチャしたカーネルイベントには統一された相関識別子がありません。クラスタAでTCP再送を、クラスタBでDNSタイムアウトを見つけられても、それらを同じユーザーリクエストチェーンに関連付けるのは非常に困難です。

5ステップ実践:カーネルトレーシングからフルスタックモニタリングまで

ステップ1:eBPFプログラムの基礎——bpftraceワンライナーとC BPFプログラム

bpftraceクイックトレーシング:

# すべてのTCP接続確立イベントをトレース
bpftrace -e 'kprobe:tcp_connect { printf("PID: %d, Comm: %s\n", pid, comm); }'

# TCP再送をトレース、プロセス別に集計
bpftrace -e 'kprobe:tcp_retransmit_skb { @retrans[comm] = count(); }'

# プロセス実行をトレース(セキュリティ監査)
bpftrace -e 'tracepoint:sched:sched_process_exec { printf("%s -> %s\n", comm, args->filename); }'

# VFS読み書きレイテンシ分布をトレース
bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; } kretprobe:vfs_read /@start[tid]/ { @ns = hist(nsecs - @start[tid]); delete(@start[tid]); }'

# ネットワーク接続状態変化をトレース
bpftrace -e 'kprobe:tcp_set_state { printf("state: %d -> %d, pid: %d\n", arg1, arg2, pid); }'

C言語eBPFプログラム(TCP接続トレーシング):

// tcp_connect.bpf.c
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

struct tcp_connect_event {
    u32 pid;
    u32 saddr;
    u32 daddr;
    u16 dport;
    char comm[16];
};

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} tcp_connect_events SEC(".maps");

SEC("kprobe/tcp_connect")
int BPF_KPROBE(trace_tcp_connect, struct sock *sk)
{
    struct tcp_connect_event *event;
    event = bpf_ringbuf_reserve(&tcp_connect_events, sizeof(*event), 0);
    if (!event)
        return 0;

    event->pid = bpf_get_current_pid_tgid() >> 32;
    event->saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
    event->daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
    event->dport = BPF_CORE_READ(sk, __sk_common.skc_dport);
    bpf_get_current_comm(&event->comm, sizeof(event->comm));

    bpf_ringbuf_submit(event, 0);
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

ステップ2:Go言語eBPFローダー(cilium/ebpfライブラリ)

// main.go - eBPF TCP接続トレーサー
package main

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"log"
	"net"
	"os"
	"os/signal"
	"syscall"

	"github.com/cilium/ebpf"
	"github.com/cilium/ebpf/link"
	"github.com/cilium/ebpf/ringbuf"
	"github.com/cilium/ebpf/rlimit"
)

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type tcp_connect_event bpf tcp_connect.bpf.c

type tcpConnectEvent struct {
	Pid   uint32
	Saddr uint32
	Daddr uint32
	Dport uint16
	Comm  [16]byte
}

func main() {
	if err := rlimit.RemoveMemlock(); err != nil {
		log.Fatalf("memlock制限の除去に失敗: %v", err)
	}

	objs := bpfObjects{}
	if err := loadBpfObjects(&objs, nil); err != nil {
		log.Fatalf("eBPFオブジェクトのロードに失敗: %v", err)
	}
	defer objs.Close()

	kp, err := link.Kprobe("tcp_connect", objs.TraceTcpConnect, nil)
	if err != nil {
		log.Fatalf("kprobeのアタッチに失敗: %v", err)
	}
	defer kp.Close()

	rd, err := ringbuf.NewReader(objs.TcpConnectEvents)
	if err != nil {
		log.Fatalf("ringbuf readerの作成に失敗: %v", err)
	}
	defer rd.Close()

	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

	fmt.Println("TCP接続トレーシングを開始、Ctrl+Cで終了...")
	fmt.Println("PID\tComm\t\tSrcAddr\t\tDstAddr")

	go func() {
		<-sig
		fmt.Println("\nトレーシングを停止中...")
		rd.Close()
	}()

	for {
		record, err := rd.Read()
		if err != nil {
			if errors.Is(err, ringbuf.ErrClosed) {
				fmt.Println("Ringbufが閉じられました")
				return
			}
			log.Printf("ringbufの読み取りに失敗: %v", err)
			continue
		}

		var event tcpConnectEvent
		if err := binary.Read(bytes.NewReader(record.RawSample), binary.LittleEndian, &event); err != nil {
			log.Printf("イベントの解析に失敗: %v", err)
			continue
		}

		srcIP := net.IP(uint32ToBytes(event.Saddr))
		dstIP := net.IP(uint32ToBytes(event.Daddr))
		dstPort := binary.BigEndian.Uint16([]byte{byte(event.Dport >> 8), byte(event.Dport & 0xff)})

		fmt.Printf("%d\t%s\t\t%s\t%s:%d\n",
			event.Pid,
			string(bytes.TrimRight(event.Comm[:], "\x00")),
			srcIP,
			dstIP,
			dstPort,
		)
	}
}

func uint32ToBytes(v uint32) [4]byte {
	var b [4]byte
	binary.LittleEndian.PutUint32(b[:], v)
	return b
}

プロジェクトgo generate設定:

// bpf_bpfel.go - bpf2goにより自動生成(サンプル構造)
// Code generated by bpf2go; DO NOT EDIT.
package main

import "github.com/cilium/ebpf"

type bpfTcpConnectEvent struct {
	Pid   uint32
	Saddr uint32
	Daddr uint32
	Dport uint16
	Comm  [16]byte
}

type bpfPrograms struct {
	TraceTcpConnect *ebpf.Program `ebpf:"trace_tcp_connect"`
}

type bpfMaps struct {
	TcpConnectEvents *ebpf.Map `ebpf:"tcp_connect_events"`
}

type bpfObjects struct {
	Programs bpfPrograms
	Maps     bpfMaps
}

func loadBpfObjects(obj *bpfObjects, opts *ebpf.CollectionOptions) error {
	return errors.New("このファイルはbpf2goで生成されます、go generateを実行してください")
}

ステップ3:Cilium Hubbleネットワークオブザーバビリティのデプロイ

# cilium-values.yaml - Helm values for Cilium + Hubble
kubeProxyReplacement: true
hubble:
  enabled: true
  listenAddress: ":4244"
  relay:
    enabled: true
  ui:
    enabled: true
  metrics:
    enabled:
      - dns
      - drop
      - tcp
      - flow
      - icmp
      - http
    enableOpenMetrics: true
    dashboards:
      enabled: true
      namespace: monitoring
operator:
  replicas: 2
  prometheus:
    enabled: true
hostPort:
  enabled: true
ipam:
  mode: kubernetes
tunnel: vxlan
# Cilium with Hubbleのインストール
helm repo add cilium https://helm.cilium.io/
helm repo update
helm install cilium cilium/cilium --version 1.17.0 \
  --namespace kube-system \
  -f cilium-values.yaml

# Hubbleの有効化
cilium hubble port-forward&
hubble observe --since 1m --output json

# DNSクエリの表示
hubble observe --type l7-dns --since 5m

# TCP接続の表示
hubble observe --type tcp --verdict DROPPED --since 10m

# 特定サービスのトラフィック表示
hubble observe --to-service my-app.default.svc.cluster.local --since 5m

# フローログをファイルにエクスポート
hubble observe --output json --since 1h > hubble-flows.json

Hubble APIクライアント(Go):

// hubble_client.go - Hubbleフロー監視クライアント
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/cilium/hubble/api/v1/flow"
	"github.com/cilium/hubble/api/v1/observer"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	conn, err := grpc.NewClient("localhost:4245",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		log.Fatalf("Hubble gRPCへの接続に失敗: %v", err)
	}
	defer conn.Close()

	client := observer.NewObserverClient(conn)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	stream, err := client.GetFlows(ctx, &observer.GetFlowsRequest{
		Whitelist: []*flow.FlowFilter{
			{Verdict: []flow.Verdict{flow.Verdict_DROPPED}},
		},
		Since:  time.Now().Add(-5 * time.Minute).Format(time.RFC3339),
		Until:  time.Now().Add(1 * time.Hour).Format(time.RFC3339),
		Follow: true,
	})
	if err != nil {
		log.Fatalf("Hubbleフローのサブスクライブに失敗: %v", err)
	}

	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

	fmt.Println("ドロップされたネットワークトラフィックを監視中...")
	fmt.Println("時間\t\t送信元Pod\t\t宛先Pod\t\t理由")

	go func() {
		<-sig
		cancel()
	}()

	for {
		resp, err := stream.Recv()
		if err != nil {
			log.Printf("フローデータの受信に失敗: %v", err)
			return
		}

		if f := resp.GetFlow(); f != nil {
			srcPod := f.GetSource().GetPodName()
			dstPod := f.GetDestination().GetPodName()
			reason := f.GetDropReasonDesc().String()

			fmt.Printf("%s\t%s\t%s\t%s\n",
				time.Now().Format("15:04:05"),
				srcPod,
				dstPod,
				reason,
			)
		}
	}
}

ステップ4:セキュリティトレーシング——プロセス実行モニタリング

// exec_monitor.bpf.c - プロセス実行セキュリティモニタ
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

#define MAX_COMM_LEN 16
#define MAX_ARGS_LEN 128
#define MAX_FILENAME_LEN 128

struct exec_event {
    u32 pid;
    u32 ppid;
    u32 uid;
    u32 gid;
    char comm[MAX_COMM_LEN];
    char filename[MAX_FILENAME_LEN];
    char args[MAX_ARGS_LEN];
};

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} exec_events SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __type(key, u32);
    __type(value, struct exec_event);
} pending_execs SEC(".maps");

SEC("tracepoint/sched/sched_process_exec")
int trace_exec(struct trace_event_raw_sched_process_exec *ctx)
{
    struct exec_event *event;
    event = bpf_ringbuf_reserve(&exec_events, sizeof(*event), 0);
    if (!event)
        return 0;

    event->pid = bpf_get_current_pid_tgid() >> 32;
    event->uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;
    event->gid = bpf_get_current_uid_gid() >> 32;

    bpf_get_current_comm(&event->comm, sizeof(event->comm));
    bpf_probe_read_kernel_str(&event->filename, sizeof(event->filename), ctx->filename);

    struct task_struct *task = (struct task_struct *)bpf_get_current_task();
    event->ppid = BPF_CORE_READ(task, real_parent, tgid);

    bpf_ringbuf_submit(event, 0);
    return 0;
}

SEC("tracepoint/sched/sched_process_exit")
int trace_exit(struct trace_event_raw_sched_process_template *ctx)
{
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    bpf_map_delete_elem(&pending_execs, &pid);
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

セキュリティモニタリングポリシーエンジン(Go):

// security_monitor.go - プロセス実行セキュリティモニタ
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"log"
	"os"
	"os/signal"
	"strings"
	"syscall"

	"github.com/cilium/ebpf"
	"github.com/cilium/ebpf/link"
	"github.com/cilium/ebpf/ringbuf"
	"github.com/cilium/ebpf/rlimit"
)

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type exec_event bpf exec_monitor.bpf.c

type execEvent struct {
	Pid      uint32
	Ppid     uint32
	Uid      uint32
	Gid      uint32
	Comm     [16]byte
	Filename [128]byte
	Args     [128]byte
}

type SecurityRule struct {
	Name        string
	Description string
	Check       func(event execEvent) bool
}

var securityRules = []SecurityRule{
	{
		Name:        "suspicious_shell",
		Description: "不審なシェル実行を検出",
		Check: func(e execEvent) bool {
			comm := strings.TrimSpace(string(bytes.TrimRight(e.Comm[:], "\x00")))
			return comm == "bash" || comm == "sh" || comm == "zsh"
		},
	},
	{
		Name:        "privilege_escalation",
		Description: "権限昇格の可能性を検出",
		Check: func(e execEvent) bool {
			filename := strings.TrimSpace(string(bytes.TrimRight(e.Filename[:], "\x00")))
			return strings.Contains(filename, "sudo") ||
				strings.Contains(filename, "su") ||
				strings.Contains(filename, "pkexec")
		},
	},
	{
		Name:        "container_escape",
		Description: "コンテナエスケープリスクを検出",
		Check: func(e execEvent) bool {
			filename := strings.TrimSpace(string(bytes.TrimRight(e.Filename[:], "\x00")))
			return strings.Contains(filename, "nsenter") ||
				strings.Contains(filename, "docker") ||
				strings.Contains(filename, "crictl")
		},
	},
}

func main() {
	if err := rlimit.RemoveMemlock(); err != nil {
		log.Fatalf("memlock制限の除去に失敗: %v", err)
	}

	objs := bpfObjects{}
	if err := loadBpfObjects(&objs, nil); err != nil {
		log.Fatalf("eBPFオブジェクトのロードに失敗: %v", err)
	}
	defer objs.Close()

	tpExec, err := link.Tracepoint("sched", "sched_process_exec", objs.TraceExec, nil)
	if err != nil {
		log.Fatalf("exec tracepointのアタッチに失敗: %v", err)
	}
	defer tpExec.Close()

	tpExit, err := link.Tracepoint("sched", "sched_process_exit", objs.TraceExit, nil)
	if err != nil {
		log.Fatalf("exit tracepointのアタッチに失敗: %v", err)
	}
	defer tpExit.Close()

	rd, err := ringbuf.NewReader(objs.ExecEvents)
	if err != nil {
		log.Fatalf("ringbuf readerの作成に失敗: %v", err)
	}
	defer rd.Close()

	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

	fmt.Println("セキュリティモニタリングを開始...")

	go func() {
		<-sig
		rd.Close()
	}()

	for {
		record, err := rd.Read()
		if err != nil {
			if err == ringbuf.ErrClosed {
				return
			}
			log.Printf("イベントの読み取りに失敗: %v", err)
			continue
		}

		var event execEvent
		if err := binary.Read(bytes.NewReader(record.RawSample), binary.LittleEndian, &event); err != nil {
			log.Printf("イベントの解析に失敗: %v", err)
			continue
		}

		for _, rule := range securityRules {
			if rule.Check(event) {
				comm := string(bytes.TrimRight(event.Comm[:], "\x00"))
				filename := string(bytes.TrimRight(event.Filename[:], "\x00"))
				log.Printf("[ALERT] %s: PID=%d PPID=%d UID=%d Comm=%s File=%s",
					rule.Name, event.Pid, event.Ppid, event.Uid, comm, filename)
			}
		}
	}
}

ステップ5:eBPFパフォーマンス分析——CPUフレームグラフ

# bpftraceでCPUフレームグラフデータを生成
bpftrace -e 'profile:hz:99 /pid/ { @stacks[ustack, kstack] = count(); }' > profile.out

# BCCツールでフレームグラフを生成
profile -F 99 -a -p <pid> 60 > perf.out
flamegraph.pl perf.out > cpu_flame.svg

Go言語パフォーマンスプロファイラ:

// cpu_profiler.go - eBPF CPUパフォーマンスプロファイラ
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/cilium/ebpf"
	"github.com/cilium/ebpf/link"
	"github.com/cilium/ebpf/perf"
	"github.com/cilium/ebpf/rlimit"
)

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type stack_event bpf cpu_profiler.bpf.c

type stackEvent struct {
	Pid       uint32
	Tid       uint32
	KernelIp  [10]uint64
	UserIp    [10]uint64
	KstackLen uint32
	UstackLen uint32
}

func main() {
	if err := rlimit.RemoveMemlock(); err != nil {
		log.Fatalf("memlock制限の除去に失敗: %v", err)
	}

	objs := bpfObjects{}
	if err := loadBpfObjects(&objs, nil); err != nil {
		log.Fatalf("eBPFオブジェクトのロードに失敗: %v", err)
	}
	defer objs.Close()

	lk, err := link.AttachPerfEvent(objs.DoProfile, -1, 0, -1)
	if err != nil {
		log.Fatalf("perf eventのアタッチに失敗: %v", err)
	}
	defer lk.Close()

	rd, err := perf.NewReader(objs.ProfileEvents, os.Getpagesize()*64)
	if err != nil {
		log.Fatalf("perf readerの作成に失敗: %v", err)
	}
	defer rd.Close()

	stackCounts := make(map[string]int)
	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

	ticker := time.NewTicker(30 * time.Second)
	defer ticker.Stop()

	fmt.Println("CPUプロファイリングを開始、30秒ごとに統計を出力...")

	go func() {
		<-sig
		rd.Close()
	}()

	for {
		select {
		case <-ticker.C:
			fmt.Printf("\n=== CPU Profile at %s ===\n", time.Now().Format("15:04:05"))
			for stack, count := range stackCounts {
				if count > 10 {
					fmt.Printf("  %s: %d samples\n", stack, count)
				}
			}
			stackCounts = make(map[string]int)
		default:
			record, err := rd.Read()
			if err != nil {
				if err == perf.ErrClosed {
					return
				}
				continue
			}

			if record.LostSamples != 0 {
				log.Printf("%d サンプルを損失", record.LostSamples)
				continue
			}

			var event stackEvent
			if err := binary.Read(bytes.NewReader(record.RawSample), binary.LittleEndian, &event); err != nil {
				continue
			}

			stackKey := fmt.Sprintf("pid=%d kstack=%d ustack=%d",
				event.Pid, event.KstackLen, event.UstackLen)
			stackCounts[stackKey]++
		}
	}
}

CPU Profiler eBPF Cプログラム:

// cpu_profiler.bpf.c - CPUパフォーマンスサンプリング
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

#define MAX_STACK_DEPTH 10

struct stack_event {
    u32 pid;
    u32 tid;
    u64 kernel_ip[MAX_STACK_DEPTH];
    u64 user_ip[MAX_STACK_DEPTH];
    u32 kstack_len;
    u32 ustack_len;
};

struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(u32));
    __uint(value_size, sizeof(u32));
} profile_events SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_STACK_TRACE);
    __uint(max_entries, 10000);
    __uint(key_size, sizeof(u32));
    __uint(value_size, MAX_STACK_DEPTH * sizeof(u64));
} stacks SEC(".maps");

SEC("perf_event")
int do_profile(struct bpf_perf_event_data *ctx)
{
    struct stack_event *event;
    event = bpf_ringbuf_reserve(&profile_events, sizeof(*event), 0);
    if (!event)
        return 0;

    u64 pid_tgid = bpf_get_current_pid_tgid();
    event->pid = pid_tgid >> 32;
    event->tid = pid_tgid & 0xFFFFFFFF;

    int kstack_id = bpf_get_stackid(ctx, &stacks, 0);
    int ustack_id = bpf_get_stackid(ctx, &stacks, BPF_F_USER_STACK);

    event->kstack_len = (kstack_id >= 0) ? MAX_STACK_DEPTH : 0;
    event->ustack_len = (ustack_id >= 0) ? MAX_STACK_DEPTH : 0;

    bpf_ringbuf_submit(event, 0);
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

5つの落とし穴ガイド

落とし穴1:memlock制限を解除せずにeBPFプログラムをロード

❌ 間違ったアプローチ:

// memlockを調整せずにeBPFプログラムを直接ロード
objs := bpfObjects{}
err := loadBpfObjects(&objs, nil)
// エラー: failed to load eBPF objects: map create: operation not permitted

✅ 正しいアプローチ:

// まずmemlock制限を解除してからeBPFプログラムをロード
if err := rlimit.RemoveMemlock(); err != nil {
    log.Fatalf("memlock制限の除去に失敗: %v", err)
}
objs := bpfObjects{}
if err := loadBpfObjects(&objs, nil); err != nil {
    log.Fatalf("eBPFオブジェクトのロードに失敗: %v", err)
}

落とし穴2:eBPFプログラムで無限ループを使用

❌ 間違ったアプローチ:

// BPFベリファイアは無限ループを拒否する
SEC("kprobe/tcp_connect")
int trace_tcp(struct pt_regs *ctx) {
    while (1) {
        // ベリファイアエラー: back-edge in program
    }
    return 0;
}

✅ 正しいアプローチ:

// 有界ループを使用、ベリファイアがループの終了を証明できるようにする
SEC("kprobe/tcp_connect")
int trace_tcp(struct pt_regs *ctx) {
    #pragma unroll
    for (int i = 0; i < 10; i++) {
        // 最大10回の反復、ベリファイアはこれを受け入れ可能
    }
    return 0;
}

落とし穴3:BTF互換性を無視してCO-REが失敗

❌ 間違ったアプローチ:

# BTFサポートを確認せずにターゲットカーネルで直接実行
./ebpf-program
# エラー: CO-RE relocation failed: kernel does not support BTF

✅ 正しいアプローチ:

# まずカーネルのBTFサポートを確認
bpftool btf list
ls /sys/kernel/btf/vmlinux

# GoコードでBTF互換性チェックを追加
// BTF互換性チェック
func checkBTFSupport() error {
    if _, err := os.Stat("/sys/kernel/btf/vmlinux"); err != nil {
        return fmt.Errorf("カーネルがBTFをサポートしていません、5.2+にアップグレードするかBTFファイルをインストールしてください: %w", err)
    }
    return nil
}

落とし穴4:Ring Bufferが正しく処理されずデータ損失

❌ 間違ったアプローチ:

// 小さすぎるring bufferを使用、高負荷でデータ損失
rd, err := ringbuf.NewReader(objs.Events) // デフォルトサイズでは不十分な場合がある
// LostSamplesイベントが処理されていない

✅ 正しいアプローチ:

// eBPF Cコードで十分に大きなring bufferを設定
// __uint(max_entries, 256 * 1024); // 256KB

// Goコードでデータ損失を処理
record, err := rd.Read()
if err != nil {
    if errors.Is(err, ringbuf.ErrClosed) {
        return
    }
    log.Printf("読み取り失敗: %v", err)
    continue
}
// 注意: ringbuf.NewReaderは損失を報告しないが、perf.NewReaderは報告する

落とし穴5:Hubbleが正しく設定されずトラフィックが不可視

❌ 間違ったアプローチ:

# Hubbleを有効にしたがmetricsとrelayが設定されていない
hubble:
  enabled: true
  # relayとmetricsの設定が欠落

✅ 正しいアプローチ:

hubble:
  enabled: true
  listenAddress: ":4244"
  relay:
    enabled: true
    rollOutPods: true
  ui:
    enabled: true
  metrics:
    enabled:
      - dns
      - drop
      - tcp
      - flow
      - icmp
      - http
    enableOpenMetrics: true
  networkPolicy:
    enabled: true

エラートラブルシューティングリファレンステーブル

エラーメッセージ 原因 解決策
failed to load eBPF objects: map create: operation not permitted memlock制限が解除されていない rlimit.RemoveMemlock()を呼び出すかulimit -l unlimitedを設定
back-edge in program eBPFプログラムに無限ループが含まれる #pragma unrollと有界ループを使用
CO-RE relocation failed: kernel does not support BTF カーネルバージョンが低すぎるかBTFが欠落 5.2+カーネルにアップグレード、またはbpf-toolsでBTFを生成
map create: read-only eBPF Mapの権限不足 CAP_BPF/CAP_SYS_ADMIN権限を確認
invalid argument: couldn't find kprobe target カーネル関数が存在しない bpftool prog listで利用可能なkprobeポイントを確認
ringbuf reserve failed Ring bufferが満杯 Ring bufferサイズを増やすか、イベント頻度を下げる
Hubble agent not ready Hubbleが正しく起動していない cilium statusを確認、hubble-relay Podの稼働を確認
connection refused:4245 Hubble gRPCポートが公開されていない cilium hubble port-forwardを実行
BPF verifier: unreachable instruction デッドコードまたはベリファイアが分析できない分岐 条件ロジックを簡略化、到達不能コードを削除
failed to attach perf event: invalid argument perf eventパラメータが不正 CPU周波数とサンプリングレートパラメータを確認

3つの高度な最適化テクニック

テクニック1:eBPF Mapバッチ操作でシステムコールオーバーヘッドを削減

ユーザー空間とカーネル空間のデータやり取りで、エントリごとのMap操作は多くのシステムコールを生成します。Batch操作を使用すると複数エントリを一度に処理できます:

// eBPF Mapのバッチ更新
func batchUpdateMap(m *ebpf.Map, entries map[uint32]uint64) error {
    keys := make([]uint32, 0, len(entries))
    values := make([]uint64, 0, len(entries))
    for k, v := range entries {
        keys = append(keys, k)
        values = append(values, v)
    }

    var batchSize = uint32(64)
    var done uint32

    for done < uint32(len(keys)) {
        remaining := uint32(len(keys)) - done
        if remaining < batchSize {
            batchSize = remaining
        }

        batchKeys := keys[done : done+batchSize]
        batchValues := values[done : done+batchSize]

        err := m.UpdateBatch(batchKeys, batchValues, nil)
        if err != nil {
            return fmt.Errorf("バッチ更新に失敗(offset=%d): %w", done, err)
        }
        done += batchSize
    }
    return nil
}

テクニック2:Tail CallベースのeBPFプログラムチェーン

単一のeBPFプログラムのロジックが複雑すぎる場合、Tail Callを使用して複数のサブプログラムに分割し、ベリファイアの複雑さ制限を回避できます:

// tail_call_chain.bpf.c - Tail Callチェーン
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>

#define MAX_TAIL_CALLS 4

struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(max_entries, MAX_TAIL_CALLS);
    __type(key, __u32);
    __type(value, __u32);
} tail_call_map SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

struct event_data {
    u32 phase;
    u32 pid;
    char comm[16];
};

SEC("kprobe/tcp_connect")
int phase0(struct pt_regs *ctx)
{
    struct event_data *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;

    e->phase = 0;
    e->pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&e->comm, sizeof(e->comm));
    bpf_ringbuf_submit(e, 0);

    bpf_tail_call(ctx, &tail_call_map, 1);
    return 0;
}

SEC("kprobe/tcp_connect")
int phase1(struct pt_regs *ctx)
{
    struct event_data *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;

    e->phase = 1;
    e->pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&e->comm, sizeof(e->comm));
    bpf_ringbuf_submit(e, 0);

    bpf_tail_call(ctx, &tail_call_map, 2);
    return 0;
}

SEC("kprobe/tcp_connect")
int phase2(struct pt_regs *ctx)
{
    struct event_data *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;

    e->phase = 2;
    e->pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&e->comm, sizeof(e->comm));
    bpf_ringbuf_submit(e, 0);

    return 0;
}

char LICENSE[] SEC("license") = "GPL";
// Tail Callサブプログラムの登録
progArray := objs.TailCallMap
if err := progArray.Update(uint32(1), objs.Phase1.ProgramFD(), ebpf.UpdateAny); err != nil {
    log.Fatalf("tail call phase1の登録に失敗: %v", err)
}
if err := progArray.Update(uint32(2), objs.Phase2.ProgramFD(), ebpf.UpdateAny); err != nil {
    log.Fatalf("tail call phase2の登録に失敗: %v", err)
}

テクニック3:eBPFイベント集約とサンプリングでデータ量を削減

高トラフィックシナリオでは、カーネル空間での集約とサンプリングにより、ユーザー空間が処理するイベント量を大幅に削減できます:

// aggregate.bpf.c - カーネル空間イベント集約
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>

struct flow_key {
    u32 saddr;
    u32 daddr;
    u16 dport;
    u8 protocol;
};

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 65536);
    __type(key, struct flow_key);
    __type(value, u64);
} flow_counter SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 65536);
    __type(key, struct flow_key);
    __type(value, u64);
} flow_latency SEC(".maps");

SEC("kprobe/tcp_sendmsg")
int count_sendmsg(struct pt_regs *ctx)
{
    struct flow_key key = {};
    struct sock *sk = (struct sock *)PT_REGS_PARM1(ctx);

    key.saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
    key.daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
    key.dport = BPF_CORE_READ(sk, __sk_common.skc_dport);
    key.protocol = IPPROTO_TCP;

    u64 *count = bpf_map_lookup_elem(&flow_counter, &key);
    if (count) {
        __sync_fetch_and_add(count, 1);
    } else {
        u64 init = 1;
        bpf_map_update_elem(&flow_counter, &key, &init, BPF_ANY);
    }

    return 0;
}

char LICENSE[] SEC("license") = "GPL";
// ユーザー空間で集約データを定期読み取り
func pollAggregatedMap(m *ebpf.Map, interval time.Duration) {
    ticker := time.NewTicker(interval)
    defer ticker.Stop()

    for range ticker.C {
        var key flowKey
        var value uint64
        iter := m.Iterate()

        fmt.Printf("\n=== Flow Stats at %s ===\n", time.Now().Format("15:04:05"))

        for iter.Next(&key, &value) {
            if value > 100 {
                srcIP := intToIP(key.Saddr)
                dstIP := intToIP(key.Daddr)
                fmt.Printf("  %s -> %s:%d: %d requests\n",
                    srcIP, dstIP, key.Dport, value)
            }
        }

        if err := iter.Err(); err != nil {
            log.Printf("Mapの反復に失敗: %v", err)
        }
    }
}

オブザーバビリティソリューション比較分析

次元 eBPF Prometheus OpenTelemetry Istio Datadog
データソース カーネル空間 アプリ/Exporter アプリSDK Sidecarプロキシ Agent+SDK
パフォーマンスオーバーヘッド 非常に低い(<1%) 低い 中程度(SDKオーバーヘッド) 中高(Sidecar) 中程度
コード侵入性 ゼロ Exporterが必要 SDKが必要 Sidecarが必要 Agentが必要
カーネル可視性 完全 なし なし なし 一部
ネットワーク可視性 L3-L7 L7メトリクス L7トレーシング L4-L7 L3-L7
セキュリティ監査 ネイティブサポート 追加ツールが必要 追加ツールが必要 ポリシーログ ネイティブサポート
リアルタイム性 マイクロ秒 秒 ミリ秒 ミリ秒 秒
学習曲線 急勾配 緩やか 中程度 中程度 緩やか
マルチクラスタサポート カスタム構築が必要 フェデレーション ネイティブ マルチクラスタMesh ネイティブ
コスト オープンソース無料 オープンソース無料 オープンソース無料 オープンソース無料 商用有料
ユースケース 深いカーネルトレーシング メトリクスモニタリング 分散トレーシング サービスメッシュ 統合モニタリング

まとめ

eBPFはオブザーバビリティの銀の弾丸ではありませんが、カーネル空間モニタリングのギャップを埋める唯一のソリューションです。K8sオブザーバビリティスタックにおいて、eBPFは最下層のデータソースとして、PrometheusのメトリクスとOpenTelemetryのトレーシングを補完するべきです——eBPFは「カーネルで何が起きたか」を教え、Prometheusは「システムがどうパフォーマンスしているか」を教え、OpenTelemetryは「リクエストが何を経験したか」を教えます。この3つの組み合わせこそが、真のフルスタックオブザーバビリティです。

おすすめツール

#Kubernetes#eBPF#可观测性#Cilium#内核追踪#2026#DevOps