---
title: "프로파일러를 프로파일링했더니 커널 버그가 나왔다 — eBPF map-in-map 8년 묵은 성능 버그 수정기"
published: 2025-12-13T22:04:13.000Z
canonical: https://jeff.news/article/829
---
# 프로파일러를 프로파일링했더니 커널 버그가 나왔다 — eBPF map-in-map 8년 묵은 성능 버그 수정기

CPU 프로파일러 Superluminal 팀이 자사 프로파일러로 자사 코드를 분석하다가 eBPF map-in-map 업데이트의 synchronize_rcu() 병목을 발견. synchronize_rcu_expedited로 변경해 31배 빠른 성능을 달성하고, 리눅스 6.19에 패치 반영 예정.

- CPU 프로파일러 Superluminal 개발팀이 자기네 프로파일러로 자기네 프로파일러를 프로파일링하다가 리눅스 커널의 8년 묵은 성능 버그를 발견하고 패치까지 제출한 이야기

## eBPF와 map-in-map

- Superluminal의 Linux 버전은 eBPF를 통해 성능 데이터(컨텍스트 스위치, 샘플링 이벤트 등)를 수집함. 스택 언와인딩에 필요한 `.eh_frame` 데이터를 유저스페이스에서 eBPF 프로그램으로 업로드하는데, 이 데이터를 `BPF_MAP_TYPE_ARRAY_OF_MAPS` (2D 배열과 비슷한 구조)에 저장

- 프로파일링 시작 전에 약 1,400개의 바이너리에 대한 언와인드 데이터를 프리캐싱하는데, 32코어 머신에서 31개 스레드로 병렬 처리해도 830ms나 걸림. 실제 작업량으로 치면 약 25초

## 병목 발견: "프로파일러를 프로파일링하라"

- 프로파일링 결과, 시간의 대부분이 `bpf_map_update_elem`에서 소비됨. 평균 호출당 **18ms**, 총 25초. 그런데 이 함수가 실제로 코드를 실행하는 게 아니라 **대기 상태**임

- 커널 소스를 추적해보니 원인은 `maybe_wait_bpf_programs` 함수. map-in-map 타입에 대해서만 `synchronize_rcu()`를 호출하는데, 이게 RCU(Read-Copy-Update) 시스템의 quiescent state에 도달할 때까지 블로킹하는 함수임

> [!IMPORTANT]
> 핵심: `synchronize_rcu()`가 map-in-map 업데이트를 **사실상 싱글 스레드로 직렬화**함. 31개 스레드가 동시에 업로드를 시도하면, 블로킹 대기가 순차적으로 처리되면서 병렬성이 완전히 사라짐

## 왜 이런 코드가 있었나

- 2018년에 추가된 코드로, map-in-map의 더블 버퍼링 패턴에서 발생하는 레이스 컨디션을 방지하기 위한 것. 유저스페이스가 map의 값을 교체할 때, 아직 이전 값을 사용 중인 eBPF 프로그램이 있을 수 있으니 모든 프로그램이 새 값을 쓸 때까지 기다리겠다는 의도

- 원래 메일링 리스트에서는 옵트인 플래그로 제안됐지만, 커널 메인테이너가 "비용이 작으니 기본으로 하자"며 무조건 동기화하는 방향으로 결정. **결과적으로 그 "작은 비용"이 8~20ms의 블로킹이었던 거임**

## 해결: synchronize_rcu_expedited

- 기존 동작을 바꾸면 "WE DO NOT BREAK USERSPACE" 원칙에 위배되므로 제거는 불가. 배치 업데이트(bpf_map_update_batch)도 검토했지만, 동적 라이브러리 로딩 등 배칭이 안 되는 경우가 있어 범용 해결책이 필요했음

- 커널 메일링 리스트에서 누군가 `synchronize_rcu_expedited`를 제안. 직접 커널을 컴파일해서 테스트한 결과:
  - 프리캐시 전체 시간: 830ms → **26ms** (31배 빠름)
  - `bpf_map_update_elem` 평균: 18ms → **59μs** (305배 빠름)

- 이 패치는 수락되어 **리눅스 6.19 커널에 반영 예정**. `BPF_MAP_TYPE_ARRAY_OF_MAPS`나 `BPF_MAP_TYPE_HASH_OF_MAPS`를 쓰는 모든 eBPF 프로그램이 자동으로 혜택을 받게 됨

## 왜 8년간 발견되지 않았나

- 리눅스에서 성능 분석할 때 "on-cpu" 프로파일링(실행 중인 코드 분석)이 기본이고, "off-cpu" 프로파일링(대기 중인 스레드 분석)은 별도로 설정해야 함. `perf`의 기본 설정은 샘플링만 기록하므로 이 병목은 보이지 않음

- 실제로 이 문제를 알고 난 후 perf 샘플링 모드로 같은 병목을 찾아보니, `bpf_map_update_elem`이 거의 시간을 안 쓰는 것처럼 보였음. 함수가 코드를 실행하는 게 아니라 대기하고 있었기 때문. Superluminal은 on-cpu와 off-cpu를 항상 같이 수집하기 때문에 바로 발견할 수 있었다는 거임

## 핵심 포인트

- bpf_map_update_elem이 map-in-map에서 synchronize_rcu()로 블로킹, 호출당 평균 18ms
- synchronize_rcu_expedited로 변경 시 830ms → 26ms (31배), 함수 평균 18ms → 59μs (305배)
- 2018년 도입된 코드로 off-cpu 프로파일링이 아니면 발견 불가
- 리눅스 6.19 커널에 패치 반영 예정

## 인사이트

on-cpu 프로파일링만으로는 보이지 않는 성능 문제의 전형적 사례. 프로파일링 도구의 한계가 커널 버그를 8년간 숨긴 셈.
