K8s eBPFオブザーバビリティ:カーネルトレーシングからフルスタックモニタリングまでの5つの実践パターン
従来のモニタリングが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つの組み合わせこそが、真のフルスタックオブザーバビリティです。
おすすめツール
- JSONフォーマッター - eBPF MapのJSON出力をフォーマット
- Base64エンコーダー - eBPFプログラム設定と証明書をエンコード
- ハッシュ計算ツール - eBPFプログラムのフィンガープリントとチェックサムを計算
ブラウザローカルツールを無料で試す →