개발 일지/졸업 프로젝트

졸업 프로젝트 10주차

Woouuk 2026. 5. 17. 18:33

졸업 프로젝트 10주차에 들어섰다. 팀원은 세 명으로 구성되어 있고 우리는 로우레벨 인프라에 대해 더 깊게 이해하고자 k8s 환경에서의 p99지연 등을 커널 레벨에서 eBPF 기술을 활용하여 hook하고, 이에 대해 리포트를 작성해주는 프로젝트를 진행하고 있다.

 

이를 위해 1주차~9주차까지 메트릭이나 eBPF, k8s 환경에 대한 스터디를 진행하였고 미니pc 네 대와 라우터 한 대로 LAN 환경을 구성해 디버깅 환경까지 세팅해놨다. (Ubuntu 20.04 사용, 1CP + 3 Worker Nodes)

 

cilium/eBPF 라이브러리와 프로메테우스를 사용할 것이고 팀을 나누어 다른 팀에서 현재 우리가 검출하고자 하는 Rule과 메트릭 명세를 확정해놓은 상태이다. 아래의 노션에서 정리할 수 있다.

 

https://www.notion.so/35f03f6ec32b8050b256d3edcd010059

 

아래는 시나리오 초안에 대한 노션 정리 글이다.

 

https://www.notion.so/33a03f6ec32b803e893bd2223aa169fa

 

이제 다음주까지 나의 역할은 이 명세서에 따른 Hook 지점을 정하고 map을 설계하는 것과 이를 golang 기반으로 진단 룰 엔진을 만들어 프로메테우스와 연결하는 코드를 짜는 것이다.

 

잠깐 용어 정리를 하자면 Hook 지점을 정한다는 것은 패킷과 같은 신호를 eBPF 기술을 사용하여 커널 레벨에서 낚아챌 지점을 정한다는 것이고 map 설계는 커널 메모리는 한정적이고 접근 시간을 최소화하여 오버플로우를 막아야 하기 때문에 자료구조와 넣는 인덱스 형태를 설계하는 것이라고 볼 수 있다.

 

다행히도 프로메테우스 라이브러리에서 제공하는 메트릭들은 우리가 만들 eBPF 에이전트에서 hook을 걸거나 map을 설계할 필요가 없다고 한다. 앱(서비스 대상)을 개발할 때 내부에 포함된 프로메테우스 클라이언트 라이브러리가 알아서 수집한다고 한다.. 여기서 앱은 타겟 서비스들을 의미하고 누군가가 접속해서 트래픽을 쏘면 자신이 요청을 처리하는 데에 얼마나 걸렸는지 스스로 보고하게끔 짜여져 있다고 한다. 고로 향후 룰 엔진(go 기반)을 만들 때 이 수집되어있는 메트릭을 그대로 가져와서 검사하기만 하면 된다고 한다!

 

Rule 1

 

수집해야 할 타겟 메트릭

ebpf_tcp_srtt_microseconds (순수 네트워크 RTT)

ebpf_tcp_cwnd (TCP 혼잡 윈도우)

ebpf_runqlat (CPU 실행 대기열 지연 시간)

 

TCP와 관련된 메트릭들은 tcp의 tracepoint에서 hook하면 될 거고, vmlinux.h의 tcp_probe 구조체를 보았을 때 해시테이블 구조로 설계하는 것이 좋아보인다.

ebpf_tcp_srtt_microseconds & ebpf_tcp_cwnd

 

hook spot - tracepoint/tcp/tcp_probe

 

map

key - 4 Tuple (출발지 IP, 도착지 IP, 출발지 포트, 도착지 포트)

value - 2 Tuple (rtt, cwnd)

 

성격이 겹치는 두 메트릭이라서 한꺼번에 받는게 메모리 상 유리할 것이다.

 

ebpf_runqlat은 os 스케듈러가 CPU를 할당하기까지 걸리는 시간을 뱉어내야 하므로 두 개의 hook 지점과 map이 필요해보인다.

마찬가지로 해시테이블 구조로 설계하였다.

ebpf_runqlat

 

hook spot - trace/sched/sched_wakeup & trace/sched/sched_switch

 

map A

key - pid

value - timestamp

 

map B

key - pid

value - total_latency (switch time - wakeup time)

 

이미 exporter에서 수집 중인 메트릭들은 전부 건너뛰겠다. 수집한 메트릭들로 어떤 룰 엔진을 만들 건지는 향후 작성 예정이고, 당장은 위의 노션을 참고하면 된다.

 

추가로 설계가 필요한 metric들

룰4 (DNS 패킷 왕복 시간)

ebpf_dns_query_latency

 

Hook spot - kprobe/udp_sendmsg + kprobe/udp_recvmsg

 

map A

key - 4 Tuple (출발지 IP, 도착지 IP, 출발지 포트, 53번 포트(아마?))

value - timestamp (요청을 보낸 커널 시간)

 

Map B

key - coredns_ip (어떤 DNS 서버와 통신했는지)

value - 2 Tuple (total_latency, query_count)

 

DNS는 주로 UDP를 사용(53번 포트)하므로 fin 항목이 없어 왕복 시간을 구하기 위해 두 개의 map이 필요하다. (UDP 패킷의 출발 시간과 도착 시간)

map B의 value에선 Go agent에서 편하게 받아와서 연산하기 위해 구조체 형태로 value 값을 받게 설계하였다.

 

룰 5(UDP 세션 커널 생존 기간의 추적)

ebpf_udp_session_duration

 

Hook spot - kprobe/__nf_conntrack_alloc + kprobe/nf_conntrack_free

 

map A (해시 테이블)

key - Conntrack ID (or 4 tuple)

value - start time

 

map B (ringbuf)

value - 3 Tuple ( struct{src_ip, dst_ip, duration_seconds} )

 

룰 5의 udp 세선 커널 생존 기간을 추적하려면 리눅스 커널의 연결 추적(Conntrack) 테이블의 생성/삭제 함수를 감지해야 한다.(alloc / free 함수)

앞서 언급한 것처럼 UDP는 TCP와 달리 fin에 대한 명시가 없어 한 번 생성된 UDP conntrack 엔트리는 기본 30초 동안 사라지지 않는다.

pod들이 dns 질의를 쏟아내면 순식간에 테이블이 꽉 차는 장애를 감지하기 위한 설계다.

 

링 버퍼 사용 이유 - 어떤 IP의 UDP 세션이 정확히 몇 초 만에 죽었다는 상세 정보가 룰 분석에 필요하므로 집계보다는 이벤트 스트리밍이 유리할 것(hash table보다는 ringbuf가 유리)

 

좀 더 구체적으로는 go agent는 보통 1초에 한 번 커널 맵을 읽어가는데 Conntrack ID가 계속 쌓일거고, 이를 매 초 전부 스캔해서 새로운 ID가 있는지 등을 조회하는 것은 엄청난 cpu 낭비이기 때문에 링 버퍼가 유리할 것이다.

C agent가 세션이 죽을 때마다 링 버퍼에 메트릭을 올리면 올라올때만 Go agent가 버퍼를 읽어가고 프로메테우스의 히스토그램 지표에 업데이트 하는 것이 효율적인 방식임!

 

작동 방식 - alloc 에서 map A에 시작 시간 저장 / free 호출 시 duration을 구해 링 버퍼에 전송

 

룰 7

ebpf_tcp_connections_active / closed (커넥션 재사용 상태)

 

Hook spot - tracepoint/sock/inet_sock_set_state

 

map

key - src_ip (어떤 파드로부터 훅인지 식별)

value - 2 Tuple (struct{ active_count, closed_count })

 

앱에서 API를 호출할 때마다 매번 TCP 3-Way Handshaking 오버헤드가 발생하고 있는지 파악하기 위한 설계

짧은 시간 동안 닫히는(Closed) 커넥션이 기형적으로 많은지 추적한다

 

* C - agent에 약간의 로직을 구현하는데 이는 TCP 소켓의 상태가 변할 때마다 훅이 호출되어 여기서 걸러내는게 부하가 덜 할 것이라는 판단..!

훅이 호출될 때 넘어오는 newstate를 oldstate와 비교하여

  • newstate == TCP_Established → active_count++
  • newstate == TCP_Closed → active_count— & closed_count++

로직을 추가해준다.

 

여기까지가 메트릭들 중 훅 지점과 맵 구조를 잡아야하는 부분들이고 이제 이 구현이 완료되면 다음 글에서 go agent에서 가져오는 부분을 구현하겠다