커널에서의 필터링, 링 버퍼, 사용자 공간의 윈도잉(시간 창 집계)을 이용해 수많은 eBPF 이벤트를 의미 있는 알림으로 압축하면서 오버헤드를 최소화하는 3단계 퍼널 아키텍처를 설명합니다.
eBPF는 리눅스 커널이 하는 일을 모두 관찰할 수 있게 해줍니다.
문제는 사용자 공간으로 모든 이벤트를 내보내면 모니터링이 곧 장애가 된다는 점입니다.
바쁜 서버에서 커널은 초당 수백만 개의 이벤트를 만들어낼 수 있습니다: 파일 열기, 네트워크 패킷, 프로세스 포크… 모든 것.
이걸 전부 데이터베이스나 로그 시스템으로 보내려고 하면 두 가지가 벌어집니다:
저는 잘못된 곳에서 debug를 켰다는 이유만으로 꽤 큰 로그 비용이 발생하는 걸 본 적이 있습니다.
그래서 질문은 이렇게 바뀝니다:
10,000개 이상의 원시 이벤트를
1개의 유용한 알림으로,
CPU를 태우지 않고 줄이려면 어떻게 해야 할까?
저에게 답은 깔때기(funnel) 같은 아키텍처입니다.
모든 이벤트를 사용자 공간으로 보낼 수는 없습니다. 경계(커널 → 사용자)를 넘는 건 공짜가 아닙니다.
제가 쓰는 패턴은 3단계 퍼널입니다:
이게 바로 제가 지금 Rust + eBPF로 제 에이전트를 만들면서 적용하고 있는 핵심 아이디어입니다.
eBPF를 로그 파이프라인처럼 취급하면 망합니다.
비용 대부분은 “eBPF” 자체가 아니라, 커널 → 사용자 공간을 너무 자주 넘게 해서 강제되는 작업들입니다:
원하는 건 깔때기입니다. 지루한 것들은 초기에 버리고, 흥미로운 꼬리(tail)만 내보내세요.
가장 빠른 코드는 애초에 실행되지 않는 코드입니다.
가장 싼 이벤트는 아예 보내지 않는 이벤트입니다.
순진한 접근(naive approach)
(end - start)를 계산하고 > 500ms인지 확인합니다.결과: 거의 대부분이 정상임을 확인하기 위해 초당 수천 개의 이벤트를 보냅니다.
eBPF 접근
커널 내 로직:
<= 500ms → 엔트리 삭제, 아무것도 안 함> 500ms → 사용자 공간으로 단 하나의 이벤트 전송그래서 “건강한” 요청의 99%는 커널 경계를 넘지 않습니다. 추가 wakeup도 없고, 에이전트에서 추가 할당도 없고, 아무것도 없습니다.
같은 아이디어는 fork 폭풍, 수명이 짧은 잡 등에도 적용됩니다. 커널에서 싼 체크를 하고, “흥미로운” 케이스만 내보내세요.
(예: fork-bomb 패턴 같은) “나쁜” 이벤트를 찾았으면, 이제 그걸 사용자 공간으로 보내야 합니다.
하지만 원하지 않는 것들:
둘 다 너무 느리고 오버헤드를 추가합니다.
대신 perf 링 버퍼를 사용하세요:
커널이 쓰는 속도가 읽는 속도보다 빠르면, 버퍼가 래핑되면서 오래된 데이터를 덮어씁니다. 즉 이벤트가 드롭됩니다.
이 위험을 줄이려면 이벤트를 하나씩 읽고 동기적으로 처리하지 마세요. 제가 쓰는 패턴은 다음과 같습니다:
링 버퍼는 가능한 한 비어 있게 유지하세요. 커널을 행복하게 유지하세요.
필터링을 거친 뒤에도 원시 이벤트는 알림이 아닙니다.
예시 스트림:
이건 실행 가능한(actionable) 정보가 아닙니다. 그냥 목록일 뿐입니다.
이걸 유용한 것으로 바꾸려면 사용자 공간에서 시간 윈도우(time window)를 사용하세요.
아주 단순화한 의사코드 예시:
// 각 fork 이벤트마다
if event.type == FORK {
process_stats[pid].fork_count += 1
}
// 매 1초마다(틱)
for pid in process_stats {
if process_stats[pid].fork_count > 50 {
trigger_alert("fork_bomb_suspected", pid)
}
process_stats[pid].fork_count = 0
}
이제 메트릭이 생깁니다: PID별 초당 fork 횟수.
알림은 이렇게 됩니다:
“PID 1234가 지난 1초 동안 fork를 57번 호출했습니다.”
단일 fork 이벤트 벽을 멍하니 바라보는 것보다 훨씬 유용합니다.
같은 아이디어를 다른 패턴에도 적용할 수 있습니다:
좋은 필터링과 윈도잉이 있어도, 도구들은 종종 “왜?”라는 질문에서 실패합니다.
“PID 555의 CPU가 높다”라는 메시지를 받습니다.
그리고 묻죠: “PID 555는 실제로 뭘 하고 있었지?”
프로세스가 이미 사라졌다면 나중에 검사할 수 없습니다.
그래서 저는 이벤트 순간에 컨텍스트를 붙이려고 합니다:
이 데이터를 이벤트에 최대한 가깝게(eBPF 프로그램 내부 또는 사용자 공간에 도달하자마자) 가져와서, 알림과 함께 보냅니다.
그래서 알림이 더 이상:
“PID 555 CPU 높음”
이 아니라, 이렇게 됩니다:
“컨테이너 X에서 CPU 높음, 프로세스 /usr/bin/worker, 함수 handle_batch(), 부모 PID 42”
이제 숫자만 바라보는 게 아니라, 실제 문제를 고칠 기회가 생깁니다.
이 모든 아이디어는 저에게 이론이 아닙니다. 저는 Linnix라고 부르는 작은 Rust + eBPF 에이전트에 이걸 녹여 만들고 있습니다:
이 원칙을 따르면:
초당 수백만 번의 작업을 하는 시스템을 관찰하면서도 “관측성(observability)” 계층이 문제가 되는 일을 피할 수 있습니다.
다음에는 자동화된 remediation(자동 조치)에 대해 이야기하고 싶습니다 — 이런 시그널에 안전하게 행동하는 방법(예: 폭주 프로세스를 죽이기)을 어떻게 새 장애 클래스 없이 할지에 대해서요.
코드 쪽이 궁금하다면, 여기에서 천천히 오픈소스로 공개하고 있습니다: