VPP 패킷 버퍼 구조 (vlib_buffer_t)

VPP vlib_buffer_t 구조체 상세: 2-캐시라인 레이아웃, 메타데이터 영역, current_data/current_length, chained buffer, 메모리 할당 패턴을 다룹니다.

선행 문서: 이 페이지는 기초와 아키텍처의 벡터 패킷 처리·그래프 노드 개념을 전제로 합니다. 내부 구현 카테고리의 다른 페이지와 함께 읽어 주세요.

vlib_buffer_t 메모리 레이아웃과 캐시라인

vlib_buffer_t는 VPP에서 패킷 데이터를 관리하는 핵심 구조체로, 커널의 sk_buff에 대응합니다. 정확히 2개의 캐시라인(128바이트)으로 설계되어 캐시 효율을 극대화합니다.

vlib_buffer_t 메모리 레이아웃 (2 캐시라인) Cache Line 0 (오프셋 0x00 ~ 0x3F, 64바이트) — 핫 필드 current_data (i16) current_length (u16) flags (u32) flow_id (u32) next_buffer (u32) current_config_index error (u16) n_add_refs (u8) buffer_pool_index opaque[10] (u32×10, 40바이트) ← 플러그인별 메타데이터 (ip, tcp 등) Cache Line 1 (오프셋 0x40 ~ 0x7F, 64바이트) — 콜드 필드 opaque2[14] (u32×14, 56바이트) ← 추가 메타데이터 (vnet, trace 등) trace_handle (u32) ← 패킷 트레이싱 연결 데이터 영역 (오프셋 0x80+) pre_data → [ headroom ] → data[0..current_length] → 구조체 크기: 128바이트 (2 캐시라인) + 패킷 데이터 (기본 2048바이트)
필드오프셋(Offset)크기용도
current_data0x00i16data[0]부터 현재 패킷 시작 위치까지의 오프셋
current_length0x02u16현재 버퍼의 유효 데이터 길이
flags0x04u32VLIB_BUFFER_TOTAL_LENGTH_VALID, IS_TRACED 등 플래그
flow_id0x08u32NIC RSS 해시(Hash) 또는 플로우 분류 ID
next_buffer0x0Cu32체인 버퍼의 다음 버퍼 인덱스 (점보 프레임)
current_config_index0x10u32Feature arc 설정 인덱스
error0x14u16노드별 에러 코드 인덱스
n_add_refs0x16u8추가 참조 카운트(Reference Count) (복제 시)
buffer_pool_index0x17u8버퍼가 속한 풀 인덱스
opaque[10]0x1840B노드별 메타데이터 (ip4_header_t *, adjacency index 등)
opaque2[14]0x4856B확장 메타데이터 (vnet 계층, 트레이스 정보)
trace_handleu32show trace 결과와 연결되는 핸들

핵심 접근 API

/* vlib_buffer_t 핵심 접근 API */

/* 버퍼 인덱스(u32)로 버퍼 포인터 획득 */
vlib_buffer_t *b = vlib_get_buffer(vm, buffer_index);

/* 현재 데이터 시작 포인터 */
void *data = vlib_buffer_get_current(b);

/* 헤더 추가/제거 (current_data 이동) */
vlib_buffer_advance(b, -sizeof(ethernet_header_t));  /* 헤더 추가 */
vlib_buffer_advance(b, sizeof(ip4_header_t));       /* 헤더 제거 */

/* 체인 버퍼의 전체 길이 */
u32 total = vlib_buffer_length_in_chain(vm, b);

/* 버퍼 복제 (멀티캐스트 등) */
u32 clone_bi;
vlib_buffer_clone(vm, buffer_index, &clone_bi, 1, CLIB_CACHE_LINE_BYTES);
코드 설명
  • 3행 vlib_get_buffer()는 32비트 인덱스를 버퍼 포인터로 변환합니다. 버퍼 풀의 시작 주소에 인덱스를 더하는 단순 산술 연산으로, O(1) 시간에 완료됩니다.
  • 6행 vlib_buffer_get_current()b->data + b->current_data를 반환합니다. 각 노드가 헤더를 파싱한 후 current_data 오프셋을 전진시키므로, 다음 노드는 자신의 헤더 시작 위치를 바로 얻습니다.
  • 9~10행 vlib_buffer_advance()는 음수 값이면 헤더를 추가(포인터 후퇴), 양수이면 헤더를 소비(포인터 전진)합니다. 데이터 복사 없이 오프셋만 조정하여 제로 카피 처리를 가능하게 합니다.
  • 16~17행 vlib_buffer_clone()은 멀티캐스트나 미러링 시 사용됩니다. 원본 데이터는 공유하고 메타데이터만 복제하여 메모리 사용과 복사 비용을 최소화합니다.

opaque 영역과 버퍼 풀 운영

/* opaque 영역 사용 예: ip4 노드가 저장하는 메타데이터 */
typedef struct {
    ip4_header_t *ip_header;
    u32 adj_index;
    u32 flow_hash;
    u32 fib_index;
} ip4_buffer_opaque_t;

/* opaque 접근 매크로 */
#define vnet_buffer(b) ((vnet_buffer_opaque_t *)(b)->opaque)
#define vnet_buffer2(b) ((vnet_buffer_opaque2_t *)(b)->opaque2)
버퍼 풀 고갈 증상: show buffers에서 free 카운트가 0에 가까워지면, 패킷이 error-drop 노드로 전달되며 show errorsno-buffer 에러가 증가합니다. buffers-per-numa 값을 늘리거나 hugepage를 추가 할당하세요.
🆕 VPP 26.02 — selog 플러그인과 trace_handle: vlib_buffer_t.trace_handle이 연결하는 이벤트 로거(elog_main)를 SSVM 공유 메모리로 노출하는 selog 플러그인(src/plugins/selog/)이 26.02에서 추가되었습니다(실험적). 외부 모니터링 도구가 selog_get_shm API를 통해 파일 디스크립터를 받아 라이브 이벤트 로그를 읽을 수 있습니다. 기존 show trace / vlib_add_trace API는 변경 없이 유지됩니다.

체이닝된 버퍼 순회 패턴

점보 프레임이나 TSO(TCP Segmentation Offload) 시나리오에서 하나의 패킷은 여러 vlib_buffer_t가 단방향 링크드 리스트로 연결된 체인(chain) 형태로 표현됩니다. 각 버퍼의 next_buffer 필드가 다음 세그먼트의 버퍼 인덱스를 담고 있으며, 체인의 마지막 버퍼에는 VLIB_BUFFER_NEXT_PRESENT 플래그가 설정되지 않습니다.

단일 세그먼트 여부 확인

/* 체인 버퍼인지 단일 버퍼인지 확인 */
if (b->flags & VLIB_BUFFER_NEXT_PRESENT) {
    /* 다음 세그먼트가 존재 — 체이닝된 버퍼 */
    handle_chained_buffer(vm, b);
} else {
    /* 단일 세그먼트 — current_length 가 전체 패킷 길이 */
    handle_single_buffer(vm, b);
}

체인 순회 루프

/* 체인의 모든 세그먼트를 순서대로 순회하는 패턴 */
vlib_buffer_t *seg = b;
while (1) {
    void  *data   = vlib_buffer_get_current(seg);
    uword  len    = seg->current_length;

    /* 현재 세그먼트의 데이터 처리 */
    process_segment(data, len);

    if (!(seg->flags & VLIB_BUFFER_NEXT_PRESENT))
        break; /* 마지막 세그먼트 */

    /* 다음 세그먼트로 이동 */
    seg = vlib_get_buffer(vm, seg->next_buffer);
}

체인 순회 시 주의점: 순회 중 버퍼를 수정하거나 vlib_buffer_free를 호출하면 안 됩니다. 수정이 필요하다면 먼저 전체 체인을 파악한 뒤 처리하세요.

vlib_buffer_length_in_chain 활용

/* 체인 전체 길이(바이트) 획득 — 순회 없이 단번에 */
u32 total_len = vlib_buffer_length_in_chain(vm, b);

/* 내부 구현: VLIB_BUFFER_TOTAL_LENGTH_VALID 플래그가 세팅되어 있으면
   b->total_length_not_including_first_buffer + b->current_length 반환.
   플래그가 없으면 체인을 직접 순회하여 합산하고 플래그를 세팅함. */

/* 노드에서 total_length_not_including_first_buffer 를 직접 설정할 때 */
b->total_length_not_including_first_buffer = total_len - b->current_length;
b->flags |= VLIB_BUFFER_TOTAL_LENGTH_VALID;

체인 구성: vlib_buffer_chain_append_data

/* 새 버퍼를 할당하고 체인에 데이터를 추가하는 패턴 */
u32 n_alloc, new_bi;
u16 written;

/* 테일 버퍼(마지막 세그먼트)의 포인터 추적 */
vlib_buffer_t *tail = b;

/* vlib_buffer_chain_append_data: 공간이 부족하면 새 버퍼를 자동 할당하여 체인에 연결 */
written = vlib_buffer_chain_append_data(vm,
                                         tail,     /* 현재 테일 버퍼 */
                                         data_ptr, /* 복사할 데이터 */
                                         data_len  /* 복사할 길이 */
                                         );
if (written != data_len) {
    /* 버퍼 풀 고갈 — 에러 처리 */
    vlib_buffer_free_one(vm, vlib_get_buffer_index(vm, b));
    return;
}
코드 설명
  • VLIB_BUFFER_NEXT_PRESENT 이 플래그가 설정된 버퍼는 next_buffer 인덱스가 유효하며, 다음 세그먼트가 존재함을 나타냅니다. 마지막 세그먼트에는 이 플래그가 없습니다.
  • vlib_buffer_length_in_chain 처음 호출 시에는 체인 전체를 순회하지만, VLIB_BUFFER_TOTAL_LENGTH_VALID 플래그 설정 이후에는 캐시된 값을 바로 반환합니다. 핫 패스에서 반복 호출 비용을 최소화합니다.
  • vlib_buffer_chain_append_data 내부적으로 현재 버퍼의 남은 공간을 먼저 채우고, 부족하면 vlib_buffer_alloc으로 새 버퍼를 할당해 체인 끝에 연결합니다. 반환값이 요청 길이보다 작으면 버퍼 풀 고갈입니다.

버퍼 할당 전략과 재사용

VPP의 버퍼 관리는 NUMA 토폴로지를 인식하는 풀(pool) 기반으로 동작합니다. 올바른 할당 API 선택과 재사용 패턴이 성능에 직접 영향을 미칩니다.

vlib_buffer_alloc vs vlib_buffer_alloc_from_free_list

/* 방법 1: 기본 할당 — 현재 워커의 NUMA 노드 기본 풀에서 할당 */
u32  bis[VLIB_FRAME_SIZE];
u32  n_alloc;
n_alloc = vlib_buffer_alloc(vm, bis, 32);
if (n_alloc < 32) {
    /* 요청한 개수보다 적게 할당됨 — 풀 고갈 가능성 */
    vlib_buffer_free(vm, bis, n_alloc);
    goto drop;
}

/* 방법 2: 특정 free-list(풀 인덱스)에서 할당 — 고정 크기 버퍼가 필요할 때 */
u8 pool_index = vlib_buffer_pool_get_default_for_numa(vm, numa_node);
n_alloc = vlib_buffer_alloc_from_free_list(vm, bis, 32, pool_index);

vlib_buffer_alloc은 대부분의 노드에서 충분하며, 특정 NUMA 노드나 버퍼 크기를 명시해야 할 때만 vlib_buffer_alloc_from_free_list를 사용합니다.

풀 할당: buffers-per-numa 설정

startup.confbuffers 섹션에서 NUMA 노드별 버퍼 풀 크기를 제어합니다.

## startup.conf 예시 — buffers 섹션
## 기본값: 16384 (16K 버퍼, 약 32MB @ 2K/버퍼)
buffers {
  buffers-per-numa 65536    ## NUMA 노드당 버퍼 수 (2의 거듭제곱 권장)
  default-data-size 2048    ## 버퍼당 데이터 영역 크기 (기본 2048)
  page-size default-hugepage ## 2MB hugepage 사용 (DPDK 연동 시 필수)
}

버퍼 풀 현황은 show buffers CLI로 확인할 수 있습니다. free 컬럼이 전체의 20% 미만으로 떨어지면 buffers-per-numa를 늘려야 합니다.

이그레스 후 버퍼 재사용: vlib_buffer_free

/* TX 완료 또는 drop 시 버퍼 반환 */

/* 단일 버퍼 해제 */
vlib_buffer_free_one(vm, buffer_index);

/* 여러 버퍼 일괄 해제 (프레임 단위) */
vlib_buffer_free(vm, buffer_indices, n_buffers);

/* 체인 버퍼 전체 해제 — next_buffer 링크를 따라 모두 반환 */
vlib_buffer_free_one(vm, head_buffer_index);  /* 체인 헤드만 전달하면 됨 */

/* n_add_refs 가 0이 될 때까지 실제 해제가 지연됨 (복제 버퍼 공유 해제) */
/* vlib_buffer_clone() 으로 복제된 버퍼는 참조 카운트가 1씩 증가됨 */

VLIB_BUFFER_REPL_FAIL 플래그

VLIB_BUFFER_REPL_FAIL은 버퍼 복제(replicate) 시도가 실패했음을 나타내는 플래그입니다. 멀티캐스트 복제 또는 vlib_buffer_clone 호출에서 버퍼 풀이 고갈되어 원하는 수만큼 복제하지 못했을 때, VPP 내부에서 이 플래그를 설정합니다.

/* 복제 실패 여부 확인 패턴 */
u32 n_clones;
n_clones = vlib_buffer_clone(vm, src_bi, clone_bis,
                              n_wanted, CLIB_CACHE_LINE_BYTES);
if (n_clones < n_wanted) {
    /* 일부 복제 실패 — VLIB_BUFFER_REPL_FAIL 가 원본 버퍼에 세팅될 수 있음 */
    /* 성공한 n_clones 개의 클론만 사용하고 나머지 전달 경로는 drop 처리 */
    b->flags |= VLIB_BUFFER_REPL_FAIL;
}

/* 상위 노드(multicast-midchain 등)에서 이 플래그를 확인하여 에러 카운터 증가 */
if (b->flags & VLIB_BUFFER_REPL_FAIL)
    vlib_node_increment_counter(vm, node->node_index,
                                 MCAST_REPL_FAIL_ERROR, 1);
버퍼 재사용 시 주의: vlib_buffer_free 후 해당 버퍼 인덱스를 계속 참조하면 use-after-free 버그가 발생합니다. 해제 직후 인덱스 배열을 0으로 초기화하거나 포인터를 NULL로 설정하는 방어적 코딩을 권장합니다. 또한 n_add_refs > 0인 버퍼를 조기에 해제하면 다른 복제본이 손상됩니다.