시작하며


지금까지는 작은 예제를 직접 만들면서 eBPF 프로그램, 맵, bpf() 시스템 콜, Perf Buffer가 어떻게 동작하는지 살펴보았다. 5주차에는 책의 다음 장으로 넘어가는 대신, 실제 오픈 소스 프로젝트가 이 기술을 어떻게 조합하는지 분석해 보기로 했다.

이번에 선택한 프로젝트는 Cilium Hubble이다. Hubble은 Kubernetes 네트워크에서 다음과 같은 질문에 답해 주는 관측 도구다.

  • 어떤 Pod와 서비스가 서로 통신하는가?
  • 연결이 전달되었는가, 네트워크 정책 때문에 차단되었는가?
  • 패킷이 버려졌다면 그 이유는 무엇인가?
  • 어떤 DNS 질의와 HTTP 요청이 발생했는가?
  • 여러 노드의 흐름을 클러스터 단위로 어떻게 모아 볼 수 있는가?

처음에는 Hubble 저장소 안에 eBPF C 코드와 로더가 모두 있을 것이라 예상했다. 그러나 코드를 따라가 보니 Hubble을 이해하려면 cilium/hubblecilium/cilium 두 저장소를 함께 읽어야 했다.

Hubble은 독립적인 eBPF 데이터패스를 새로 만드는 프로젝트가 아니다. Cilium의 eBPF 데이터패스가 만든 관측 이벤트를 수집하고, Kubernetes 문맥을 더해 사람이 조회할 수 있는 Flow로 만드는 시스템이다.

이 글은 2026년 8월 9일을 기준으로 다음 소스 트리를 분석했다.

저장소 분석 기준 역할
cilium/hubble 75d3a68 Hubble CLI 진입점, 배포 문서와 릴리스
cilium/cilium 8c0423e eBPF 데이터패스, 이벤트 수집, Hubble Server·Relay·CLI 실제 구현

세부 파일과 구조는 이후 버전에서 달라질 수 있지만, 커널 이벤트가 Hubble Flow가 되는 큰 흐름은 동일하다.

Hubble은 무엇인가


Hubble은 Cilium 위에 만들어진 네트워크·서비스·보안 관측 플랫폼이다. 애플리케이션에 별도 계측 코드를 추가하거나 모든 Pod에 사이드카를 넣지 않아도 Cilium이 관리하는 트래픽을 관찰할 수 있다.

공식 아키텍처 그림은 Hubble을 세 가지 출력 관점으로 보여 준다.

Cilium Hubble 공식 아키텍처
Cilium의 eBPF 이벤트는 노드별 Hubble을 거쳐 CLI, UI, Prometheus 같은 소비자에게 전달된다.
구성 요소 역할
Hubble Server 각 노드의 cilium-agent 안에서 이벤트를 파싱하고 저장하며 gRPC API 제공
Hubble Relay 여러 노드의 Hubble Server에 연결해 클러스터 전체 Flow를 하나의 API로 제공
Hubble CLI gRPC API에 질의하고 Flow를 터미널에 출력
Hubble UI 서비스 맵과 네트워크 흐름을 웹 화면으로 시각화
Hubble Metrics Flow를 Prometheus 지표로 변환
Hubble Exporter Flow를 파일이나 로그 파이프라인에서 사용할 수 있는 형태로 내보냄

여기서 가장 중요한 사실은 Hubble Server가 별도의 노드 데몬이 아니라 cilium-agent 프로세스에 내장되어 있다는 점이다. 이미 eBPF 이벤트를 읽고 있는 Cilium 내부 경로에 소비자를 등록하므로, 별도의 커널 이벤트 수집 계층을 중복해서 만들지 않는다.

두 저장소를 함께 읽어야 하는 이유


cilium/hubble 저장소의 main.go는 생각보다 매우 짧다.

import "github.com/cilium/cilium/hubble/cmd"

func main() {
    if err := cmd.Execute(); err != nil {
        // 에러 출력 후 종료
    }
}

go.modgithub.com/cilium/cilium 모듈을 의존성으로 가져온다. 즉, 현재 cilium/hubble 저장소는 Hubble CLI를 빌드하고 배포하는 얇은 진입점에 가깝다. 실제 observe 명령과 서버, 파서, Relay 코드는 Cilium 저장소에 있다.

처음 코드를 읽을 때는 다음과 같이 역할을 나누면 길을 잃지 않는다.

cilium/hubble
└─ main.go                         Hubble CLI 실행 진입점

cilium/cilium
├─ bpf/                            커널에서 실행되는 eBPF 데이터패스
├─ pkg/monitor/agent/              Perf Event를 읽는 Cilium Monitor
├─ pkg/hubble/monitor/             Monitor 이벤트를 Hubble로 전달
├─ pkg/hubble/parser/              바이너리 이벤트 파싱과 메타데이터 보강
├─ pkg/hubble/container/           사용자 공간 Flow 링 버퍼
├─ pkg/hubble/observer/            노드 단위 Observer gRPC API
├─ pkg/hubble/relay/               여러 노드의 Flow 집계
├─ api/v1/flow/                    Flow Protobuf 정의
├─ api/v1/observer/                Observer gRPC API 정의
└─ hubble/cmd/                     Hubble CLI 실제 구현

이 구조를 모르고 cilium/hubble 저장소에서 SEC("tc")나 BPF 맵을 계속 찾으면 Hubble이 eBPF를 사용하지 않는 것처럼 보일 수 있다. 실제 eBPF 코드는 Cilium 데이터패스에 있고, Hubble은 그 데이터패스의 관측 결과를 소비한다.

전체 데이터 흐름


L3/L4 Flow 하나가 터미널에 나타나기까지의 경로를 단순화하면 다음과 같다.

Pod의 패킷
[커널 공간]
Cilium eBPF 데이터패스
  bpf_lxc.c / bpf_host.c / bpf_overlay.c / bpf_xdp.c
    ├─ trace_notify          관측 지점과 전달 경로
    ├─ drop_notify           드롭 원인
    └─ policy_verdict_notify 정책 판정
cilium_events
BPF_MAP_TYPE_PERF_EVENT_ARRAY
    │  CPU별 Perf Event
[사용자 공간: cilium-agent]
Cilium Monitor의 perf.Reader
Hubble Monitor Consumer
    │  비동기 Go 채널
Hubble Parser + Cilium의 메타데이터
    │  IP/포트 → Pod/Namespace/Identity/Service
Hubble 사용자 공간 Ring
노드별 Hubble Observer gRPC API
    │  Unix Socket 또는 TCP 4244
Hubble Relay
    │  클러스터 API, 기본 포트 4245
    ├─ Hubble CLI
    ├─ Hubble UI
    └─ 기타 gRPC 클라이언트

이 흐름은 앞에서 공부한 Perf Buffer 예제를 실제 프로덕션 시스템으로 확장한 형태다. 다만 예제와 달리 이벤트 종류가 다양하고, 여러 단계의 버퍼와 파서, 클러스터 집계 계층이 추가되어 있다.

eBPF 프로그램은 어디에 붙어 있는가


Cilium은 Pod의 가상 이더넷 장치, 호스트 네트워크, 터널 장치 등 패킷이 지나는 여러 지점에 eBPF 프로그램을 부착한다. 대표적인 파일은 다음과 같다.

파일 관찰하는 대표 경로
bpf/bpf_lxc.c Cilium이 관리하는 Endpoint, 주로 Pod의 ingress·egress
bpf/bpf_host.c 호스트 네트워크로 들어오고 나가는 트래픽
bpf/bpf_overlay.c VXLAN·Geneve 같은 오버레이 터널 경로
bpf/bpf_xdp.c 네트워크 드라이버에 가까운 이른 ingress 경로

실제 프로그램은 TC, XDP 등 경로에 따라 서로 다른 컨텍스트에서 실행되지만, 공통 라이브러리의 알림 함수를 사용해 관측 이벤트를 만든다.

중요한 설계는 전달과 정책 집행을 수행한 바로 그 eBPF 프로그램이 관측 이벤트도 만든다는 것이다. 외부에서 패킷만 캡처하면 “패킷이 사라졌다”는 사실은 알 수 있어도 Cilium 정책의 어느 판단 때문에 버려졌는지 정확히 알기 어렵다. 반면 정책을 집행하는 지점에서 이벤트를 만들면 verdict와 drop reason을 함께 기록할 수 있다.

eBPF가 만드는 세 가지 핵심 이벤트


Hubble의 L3/L4 Flow를 이해할 때는 Trace, Drop, Policy Verdict 알림부터 보면 좋다.

Trace 알림

Trace 알림은 패킷이 데이터패스의 어느 관측 지점을 통과했는지 나타낸다. 이벤트에는 다음과 같은 정보가 포함된다.

  • 출발지와 목적지 Security Identity
  • 목적지 Endpoint ID
  • ingress·egress 인터페이스 인덱스
  • 관측 지점을 나타내는 reason과 flags
  • 원본 IP와 IP trace ID
  • 설정에 따라 캡처한 패킷 헤더 일부

따라서 Hubble 출력의 to-endpoint, from-endpoint, to-stack, to-overlay 같은 observation point를 구성할 수 있다.

Drop 알림

Drop 알림은 패킷이 버려진 위치에서 생성된다. 출발지·목적지 Identity뿐 아니라 drop reason, 확장 오류 코드, 인터페이스, 소스 파일과 라인 정보 등을 담을 수 있다.

Hubble이 단순히 DROPPED만 보여 주지 않고 Policy denied, Invalid packet, No route처럼 원인을 설명할 수 있는 이유다.

Policy Verdict 알림

Policy Verdict 알림은 정책 조회 결과를 표현한다. 원격 Security Identity, ingress·egress 방향, L4 프로토콜과 목적지 포트, 허용·거부 verdict, 어떤 종류의 정책 항목과 일치했는지 등의 정보를 담는다.

패킷 헤더 + Endpoint의 Security Identity
         eBPF 정책 조회
        ┌───────┴────────┐
        │                │
      허용             거부
        │                │
Policy Verdict      Policy Verdict
Trace Notify        Drop Notify

여기서 Security Identity는 IP 주소만으로는 얻기 어려운 Cilium의 핵심 문맥이다. Pod가 다시 생성되어 IP가 달라져도 Kubernetes label 기반 Identity를 사용하면 “어떤 워크로드가 어떤 워크로드와 통신했는가”라는 질문으로 Flow를 해석할 수 있다.

cilium_events: 커널과 사용자 공간 사이의 다리


이벤트는 bpf/lib/events.h에 정의된 cilium_events 맵으로 전달된다. 핵심 정의를 줄이면 다음과 같다.

struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(__u32));
    __uint(value_size, sizeof(__u32));
    __uint(pinning, LIBBPF_PIN_BY_NAME);
} cilium_events __section_maps_btf;

Trace, Drop, Policy Verdict 알림 함수는 최종적으로 다음과 같은 형태로 이 맵에 이벤트를 출력한다.

ctx_event_output(ctx, &cilium_events,
                 (capture_len << 32) | BPF_F_CURRENT_CPU,
                 &message, sizeof(message));

이 코드는 4장에서 살펴본 Perf Buffer 구조와 연결된다.

  1. BPF_MAP_TYPE_PERF_EVENT_ARRAY 맵은 CPU별 Perf Event와 연결된다.
  2. eBPF 프로그램은 현재 CPU의 버퍼에 작은 바이너리 이벤트를 기록한다.
  3. 사용자 공간은 CPU별 링 버퍼를 mmap()하고 새 레코드를 읽는다.
  4. capture_len이 있다면 고정 메타데이터 뒤에 패킷의 일부도 함께 전달된다.

커널의 eBPF 프로그램이 gRPC를 호출하거나 JSON을 만드는 것은 아니다. 커널에서는 검증기, 스택 크기, 실행 시간의 제약을 받으므로 꼭 필요한 사실만 빠르게 기록하고, 해석이 비싼 작업은 사용자 공간으로 넘긴다.

Cilium의 이 경로는 BPF_MAP_TYPE_RINGBUF가 아니라 Perf Event Array를 사용한다. “eBPF에서 사용자 공간으로 이벤트를 보낸다”는 목적은 같지만 맵 타입과 읽는 API는 다르다.

Cilium Monitor가 Perf Event를 읽는다


사용자 공간에서는 pkg/monitor/agent/agent.go의 Monitor Agent가 BPFFS에 핀된 cilium_events 맵을 연다. 내부적으로 cilium/ebpf 라이브러리의 perf.NewReader()를 만들고 레코드를 계속 읽는다.

흐름을 의사 코드로 표현하면 다음과 같다.

eventsMap := ebpf.LoadPinnedMap("cilium_events", nil)
reader, _ := perf.NewReader(eventsMap, bufferSize)

for {
    record, _ := reader.Read()
    if record.LostSamples > 0 {
        notifyLostEvents(record.LostSamples)
        continue
    }
    notifyConsumers(record.RawSample, record.CPU)
}

실제 코드는 오류 처리와 생명주기 관리가 더 복잡하지만 핵심은 같다. Cilium Monitor는 하나의 커널 이벤트 스트림을 읽고, 등록된 내부 Consumer들에게 원시 바이트와 CPU 번호를 전달한다.

Hubble은 여기서 별도의 BPF 맵을 다시 여는 대신 MonitorConsumer로 등록된다. pkg/hubble/cell/hubbleintegration.go를 보면 Hubble Observer를 시작한 후 대략 다음 관계를 만든다.

Cilium Monitor Agent
        │ RegisterNewConsumer
Hubble Monitor Consumer
Local Observer Server

이 구조 덕분에 Cilium의 기존 Monitor 출력과 Hubble이 같은 데이터 소스를 공유할 수 있다.

원시 이벤트를 Hubble Flow로 바꾸기


커널에서 올라온 데이터는 아직 사람이 읽을 수 있는 Flow가 아니다. 첫 바이트는 이벤트 타입을 나타내고, 그 뒤에는 Trace·Drop·Policy Verdict 구조체와 선택적으로 캡처된 패킷 바이트가 이어진다.

pkg/hubble/parser/parser.goDecode()는 이벤트 종류를 확인하고 알맞은 하위 파서로 분기한다.

MonitorEvent
    ├─ PerfEvent  ──→ L3/L4 Parser
    │                  ├─ Drop
    │                  ├─ Trace
    │                  ├─ Policy Verdict
    │                  └─ Packet Capture
    ├─ AgentEvent ──→ L7 Parser 또는 Agent Event Parser
    └─ DebugEvent ──→ Debug Parser

L3/L4 파서는 알림 구조체를 해석하고, 함께 전달된 패킷 바이트를 Ethernet, IPv4·IPv6, TCP, UDP, SCTP, ICMP, VXLAN, Geneve 등으로 디코딩한다. 그 후 Cilium Agent가 이미 유지하고 있는 Resolver와 캐시를 이용해 정보를 보강한다.

커널에서 얻은 값 사용자 공간에서 보강되는 값
출발지·목적지 IP Pod 이름과 Namespace
Security Identity Kubernetes label과 워크로드 정보
IP·포트 Kubernetes Service 이름
L4 프로토콜 TCP flags, 서비스 포트 문맥
verdict와 reason 읽을 수 있는 Verdict·Drop Reason 문자열
IP·Identity 관찰된 DNS 이름

최종 결과는 api/v1/flow/flow.proto에 정의된 Flow Protobuf 메시지가 된다. 이 메시지에는 IP와 L4 헤더뿐 아니라 Endpoint, Identity, Pod, Namespace, Service, DNS, L7, Verdict, Traffic Direction, Observation Point 등이 함께 들어간다.

이것이 Hubble의 핵심 가치다. eBPF가 정확한 커널 관측 지점과 정책 결과를 제공하고, 사용자 공간이 이를 Kubernetes의 언어로 번역한다.

같은 이름처럼 보이는 두 개의 링 버퍼


코드를 읽을 때 가장 헷갈렸던 부분은 “ring”이라는 단어가 서로 다른 계층에 등장한다는 점이었다.

계층 구현 저장하는 것 가득 찼을 때
커널 → 사용자 공간 Perf Event의 CPU별 링 버퍼 eBPF가 만든 바이너리 알림 LostSamples가 발생할 수 있음
Hubble 내부 전달 Go 채널 Event Queue 타임스탬프가 붙은 Monitor Event 비차단 전송이 실패하면 lost event 기록
Hubble 조회 저장소 pkg/hubble/container.Ring 파싱된 Flow와 상태 이벤트 가장 오래된 이벤트를 덮어씀

pkg/hubble/container/ring.go의 Ring은 BPF 맵도 아니고 BPF_MAP_TYPE_RINGBUF도 아니다. Go로 구현한 고정 크기 사용자 공간 순환 버퍼다. Hubble은 최근 Flow를 이 메모리에 보관하고, hubble observe 요청이 오면 여기서 과거 이벤트를 읽거나 새 이벤트를 계속 따라간다.

따라서 hubble statusCurrent/Max Flows는 커널 BPF 맵의 원소 개수가 아니라 Hubble 사용자 공간 Ring의 사용량을 의미한다.

Hubble Observer API


노드별 Hubble Server는 Observer gRPC 서비스를 제공한다. 주요 RPC는 다음과 같다.

RPC 역할
GetFlows 조건에 맞는 Flow를 스트리밍
GetAgentEvents Cilium Agent 상태·정책 변경 이벤트 스트리밍
GetDebugEvents 데이터패스 디버그 이벤트 스트리밍
GetNodes Hubble 인스턴스와 노드 상태 조회
GetNamespaces 최근 Flow에서 관찰한 Namespace 조회
ServerStatus Flow 수, 초당 Flow, 노드 상태 요약

노드 내부에서는 기본적으로 /var/run/cilium/hubble.sock Unix Domain Socket을 사용한다. Relay가 접근할 때는 노드별 TCP API를 사용하며 기본 포트는 4244다.

GetFlowsRequest에는 최근 몇 개를 읽을지, 계속 따라갈지, 시작·종료 시간, 포함·제외할 Flow Filter 등을 담을 수 있다. Local Observer는 요청을 받으면 사용자 공간 Ring Reader를 만들고 서버 스트리밍으로 결과를 보낸다.

hubble observe --verdict DROPPED --follow
CLI가 GetFlowsRequest 생성
Observer가 Ring에서 조건에 맞는 Flow 선택
GetFlowsResponse 스트림
CLI가 compact/json/table 형식으로 출력

여기서 필터는 Observer의 사용자 공간 Flow에 적용된다. 예를 들어 --verdict DROPPED를 실행한다고 해서 새로운 eBPF 프로그램을 로드하거나 커널의 관측 조건을 다시 작성하는 것은 아니다.

Hubble Relay가 클러스터 전체를 보는 방법


각 Hubble Server는 기본적으로 자기 노드에서 발생한 Flow만 알고 있다. Kubernetes 클러스터 전체를 조회하려면 Hubble Relay가 필요하다.

Node A: cilium-agent + Hubble Server ─┐
Node B: cilium-agent + Hubble Server ─┼─→ Hubble Relay ─→ CLI / UI
Node C: cilium-agent + Hubble Server ─┘

Relay는 Peer 서비스를 통해 Hubble 인스턴스의 추가·변경·삭제를 감지하고 각 노드의 API에 연결을 유지한다. 기본 구성에서는 노드의 TCP API가 mTLS로 보호되어 Relay만 안전하게 접근할 수 있다.

클라이언트가 Relay의 GetFlows를 호출하면 Relay는 요청을 여러 노드에 전달하고 응답을 하나의 스트림으로 합친다. 이때 priority queue를 사용해 타임스탬프 기준으로 오래된 Flow부터 내보낸다.

다만 여러 노드의 시계와 네트워크 지연이 있으므로 이것을 완벽한 전역 순서라고 생각해서는 안 된다. Relay는 분산된 스트림을 시간 기준으로 합리적으로 병합하는 계층이다.

Hubble CLI가 로컬에서 기본적으로 접속하는 Relay 주소는 localhost:4245다. 보통 -P 옵션으로 Kubernetes의 hubble-relay 서비스에 port-forward한 후 사용한다.

L7 정보도 모두 eBPF가 파싱할까


Hubble 출력에는 HTTP method, URL, response code, DNS query 같은 L7 정보도 나타난다. 이를 보고 eBPF 프로그램이 커널에서 HTTP 전체를 파싱한다고 오해하기 쉽다.

실제 L7 경로는 L3/L4 Perf Event 경로와 다르다.

L7 visibility 또는 L7 policy가 적용된 트래픽
Cilium eBPF 데이터패스가 프록시로 리다이렉트
Node-local Envoy / Cilium L7 Proxy
        │  HTTP·DNS·Kafka 등을 해석
Access Log (AgentEvent: MessageTypeAccessLog)
Hubble L7 Parser
동일한 Hubble Flow 모델과 Ring으로 합류

즉, 역할은 다음과 같이 나뉜다.

  • eBPF는 패킷을 빠르게 전달·차단하고, 필요한 경우 L7 프록시로 리다이렉트한다.
  • Envoy 같은 사용자 공간 프록시는 가변 길이 애플리케이션 프로토콜을 해석하고 Access Log를 만든다.
  • Hubble은 L3/L4 eBPF 이벤트와 L7 Access Log를 같은 Flow 모델로 보여 준다.

이 분리는 합리적이다. HTTP 헤더처럼 복잡하고 길이가 가변적인 데이터를 커널 eBPF에서 모두 처리하면 검증기와 메모리 제약이 커진다. 빠른 패킷 경로는 eBPF가 맡고, 복잡한 프로토콜 해석은 사용자 공간 프록시가 맡는다.

또한 L7 Flow에는 URL query, user info, HTTP header 같은 민감한 정보가 포함될 수 있다. 운영 환경에서는 Hubble의 redaction 설정도 함께 검토해야 한다.

차단된 요청 하나를 끝까지 따라가 보기


client Pod가 네트워크 정책상 허용되지 않은 server:8080으로 TCP 연결을 시도한다고 가정해 보자.

  1. 패킷이 client Endpoint의 TC eBPF 프로그램에 도착한다.
  2. eBPF 프로그램은 목적지 Identity, 포트, 프로토콜을 이용해 정책 맵을 조회한다.
  3. 정책이 거부되면 실제 패킷을 버리고 Policy Verdict와 Drop 알림을 만든다.
  4. 알림은 현재 CPU의 cilium_events Perf Buffer에 기록된다.
  5. Cilium Monitor의 perf.Reader가 원시 레코드를 읽어 Hubble Consumer에 전달한다.
  6. Hubble Parser가 IP/TCP 헤더, verdict, drop reason을 해석한다.
  7. Cilium의 Endpoint·Identity·Service Resolver가 IP를 default/client, default/server 같은 Kubernetes 문맥으로 바꾼다.
  8. 완성된 Flow가 노드의 Hubble Ring에 저장된다.
  9. Relay가 노드 Hubble Server의 스트림을 받아 클라이언트에 전달한다.
  10. CLI가 다음과 비슷한 한 줄을 출력한다.
default/client:43618 -> default/server:8080
Policy denied (L3/L4) DROPPED (TCP Flags: SYN)

이 한 줄 뒤에는 커널의 정책 집행, CPU별 Perf Buffer, 사용자 공간 파싱, Kubernetes 메타데이터 보강, gRPC 스트리밍이라는 긴 경로가 숨어 있다.

모든 패킷이 반드시 하나의 Flow가 되는 것은 아니다


Hubble을 패킷 캡처 도구와 완전히 같다고 생각해서는 안 된다.

Cilium 데이터패스는 모니터 aggregation 설정에 따라 동일한 흐름의 모든 패킷 대신 대표 이벤트만 만들 수 있다. 이벤트 출력에는 rate limit도 적용할 수 있다. 이후에도 커널 Perf Buffer, Hubble Event Queue, 사용자 공간 Ring은 모두 크기가 유한하다.

손실 가능 지점은 다음과 같다.

지점 원인 Hubble의 대응
eBPF 이벤트 출력 전 aggregation·rate limit 설정 필요한 수준으로 관측 설정 조정
Perf Buffer 사용자가 읽는 속도보다 이벤트 생성이 빠름 LostSamples를 lost event로 보고
Hubble Event Queue Go 채널이 가득 참 non-blocking drop 수를 집계해 보고
사용자 공간 Ring 보관 용량 초과 오래된 Flow를 덮어쓰고 ring-buffer lost event 제공

이는 결함이라기보다 데이터패스를 보호하기 위한 설계 선택이다. 관측 때문에 실제 네트워크 패킷 처리가 멈추어서는 안 되므로, 생산자 경로를 block하지 않고 손실 자체를 관측 가능하게 만든다.

따라서 중요한 장애를 분석할 때는 Flow 내용뿐 아니라 hubble status의 Flow 사용량, lost event, 노드 연결 상태도 함께 봐야 한다.

직접 확인해 볼 명령


Hubble Relay에 port-forward할 수 있는 환경이라면 다음 명령으로 각 계층의 결과를 확인할 수 있다.

# Relay와 노드 연결 상태
hubble status -P
hubble list nodes -P

# 최근 Flow와 이후 Flow를 계속 관찰
hubble observe -P --follow

# 정책이나 데이터패스에서 드롭된 Flow만 조회
hubble observe -P --verdict DROPPED --follow

# 두 Pod 사이의 Flow만 조회
hubble observe -P \
  --from-pod default/client \
  --to-pod default/server \
  --follow

# L7 visibility가 설정된 트래픽의 L7 이벤트 조회
hubble observe -P --type l7 --follow

같은 Flow를 JSON으로 보면 터미널의 한 줄 출력에서 생략된 필드를 확인할 수 있다.

hubble observe -P --verdict DROPPED -o json | jq

node_name, source, destination, IP, l4, verdict, drop_reason_desc, traffic_direction, event_type을 찾아보면 Flow Protobuf의 구조와 실제 출력이 연결된다.

소스 코드를 읽은 순서


Hubble처럼 큰 프로젝트는 처음부터 파일을 순서대로 읽기보다, 이벤트 하나의 이동 경로를 따라가는 편이 이해하기 쉬웠다.

  1. cilium/hubble/main.go에서 실제 CLI 구현이 어디에 있는지 확인한다.
  2. bpf/lib/events.h에서 이벤트 맵 타입을 확인한다.
  3. bpf/lib/trace.h, drop.h, policy_log.h에서 어떤 데이터를 보내는지 본다.
  4. pkg/monitor/agent/agent.go에서 Perf Event를 누가 읽는지 찾는다.
  5. pkg/hubble/monitor/consumer.go에서 Hubble로 넘어가는 경계를 찾는다.
  6. pkg/hubble/parser/parser.gothreefour/parser.go에서 Flow 변환 과정을 본다.
  7. pkg/hubble/observer/local_observer.go에서 Ring 저장과 GetFlows를 연결한다.
  8. pkg/hubble/relay/observer/server.go에서 여러 노드의 스트림을 어떻게 합치는지 본다.
  9. hubble/cmd/observe/flows.go에서 CLI 요청과 출력까지 닫힌 고리를 완성한다.

검색할 때는 함수 이름보다 이벤트가 통과하는 자료형과 맵 이름이 좋은 이정표가 되었다.

rg "cilium_events" bpf pkg
rg "NotifyPerfEvent" pkg
rg "MessageTypeAccessLog" pkg/hubble
rg "GetFlows" pkg/hubble hubble/cmd api/v1

분석하며 바로잡은 오해


처음의 생각 코드를 읽고 확인한 내용
Hubble 저장소 안에 자체 eBPF 프로그램이 있다 eBPF 데이터패스는 Cilium 저장소에 있고 Hubble은 그 이벤트를 소비한다
hubble CLI가 BPF 맵을 직접 읽는다 CLI는 Observer gRPC API에 GetFlowsRequest를 보낸다
Hubble의 Ring은 BPF Ring Buffer다 최근 Flow를 저장하는 Go 사용자 공간 순환 버퍼다
HTTP를 eBPF 프로그램이 모두 파싱한다 eBPF가 프록시 경로를 만들고 Envoy Access Log가 Hubble L7 Parser로 들어온다
모든 패킷이 빠짐없이 Flow 하나가 된다 aggregation, rate limit, 유한한 버퍼 때문에 패킷 캡처와 의미가 다르다
Relay가 Flow를 생성한다 Flow는 각 노드에서 만들어지고 Relay는 분산 스트림을 발견·병합한다

Hubble이 eBPF를 잘 활용하는 방식


소스 코드를 따라가며 Hubble이 eBPF를 사용하는 방식을 네 가지로 정리할 수 있었다.

첫째, 정책 집행 지점에서 관측한다. 별도의 센서가 결과를 추측하지 않고, 패킷을 전달하거나 버린 eBPF 프로그램이 verdict와 reason을 직접 남긴다.

둘째, 커널에서는 최소한의 사실만 수집한다. 이벤트 구조체와 패킷 헤더 일부만 Perf Buffer로 보내고, Protobuf 생성과 Kubernetes 메타데이터 결합은 사용자 공간에서 수행한다.

셋째, 노드 단위로 분산 처리한다.cilium-agent가 자기 노드의 이벤트를 파싱하고 보관하므로 중앙 수집기가 모든 원시 이벤트를 처음부터 처리하지 않아도 된다. Relay는 필요할 때 여러 노드의 API를 하나로 합친다.

넷째, L3/L4와 L7에 맞는 도구를 나눈다. 빠른 네트워크 경로와 정책은 eBPF가 맡고, 복잡한 애플리케이션 프로토콜은 Envoy가 해석한다. Hubble은 두 결과를 공통 Flow 모델로 통합한다.

마치며


Hubble을 분석하기 전에는 “eBPF 기반 관측 도구”라는 말을 하나의 거대한 eBPF 프로그램으로 상상했다. 실제 구조는 훨씬 계층적이었다.

eBPF는 커널에서 사실을 기록하고,
Cilium Monitor는 그 사실을 안전하게 꺼내며,
Hubble은 Kubernetes 문맥을 더해 Flow로 만들고,
Relay는 여러 노드의 Flow를 하나의 관점으로 합친다.

특히 4장에서 공부한 Perf Event Array가 실제 대규모 프로젝트에서도 커널과 사용자 공간 사이의 경계로 사용된다는 점이 인상적이었다. 동시에 프로덕션 시스템에서는 Perf Buffer 하나로 끝나지 않고, 손실 처리, 사용자 공간 Queue와 Ring, 파서, gRPC, 분산 집계까지 함께 설계해야 한다는 것도 확인했다.

Hubble의 핵심은 eBPF 자체를 보여 주는 데 있지 않다. eBPF가 남긴 낮은 수준의 이벤트를 default/client가 default/server로 보낸 요청이 정책 때문에 차단되었다는 운영자의 언어로 바꾸는 데 있다. 이것이 작은 eBPF 예제와 실제 관측 제품 사이의 가장 큰 차이였다.

참고 자료


>> Home