26S05z3

QK·2일 전

대규모 베어메탈 환경에서 Dual-Socket 이상 Intel Xeon(Sapphire Rapids/Emerald Rapids 등) 프로세서를 기반으로 Cilium Native Routing, 고성능 분산 스토리지(MinIO AIStor), 그리고 연산 엔진(StarRocks, Spark)을 구동할 때 UPI(Ultra Path Interconnect) 버스 트래픽 경합과 크로스 소켓 메모리 접근(Remote Memory Access)은 P99 지연 시간과 최대 Throughput을 갉아먹는 주범입니다.

OS, K8s 플랫폼, 그리고 레이어별 워크로드(스토리지, 쿼리 엔진, CNI) 관점에서 NUMA 친화성(NUMA Locality)을 극대화하는 엔드투엔드 전략입니다.


1. 하드웨어/BIOS 및 RHEL 10.2 OS 베이스라인 설정

K8s 상위 설정 이전에 하드웨어 및 커널 레벨에서 NUMA 도메인을 노출하고 인터럽트 경로를 정렬해야 합니다.

  • BIOS / Sub-NUMA Clustering (SNC) 활성화:
  • 소켓당 코어 수가 많은 최신 Xeon(Gen4/Gen5)은 BIOS에서 SNC-2 또는 SNC-4(Sub-NUMA Clustering)를 활성화합니다. 소켓 1개를 물리적 2~4개 NUMA 도메인으로 쪼개어 L3 캐시 지역성과 로컬 메모리 컨트롤러 접근 지연시간을 최소화합니다.
  • 커널 부트 파라미터 (GRUB):
  • numa_balancing=0: Linux 커널의 자동 NUMA 밸런싱(Background page scanning/migration)은 대규모 DB/스토리지 구동 시 무작위 레이턴시 스파이크를 유발하므로 끕니다. K8s 레벨에서 정적으로 바인딩하는 것이 유리합니다.
  • transparent_hugepage=never (또는 madvise): MinIO, StarRocks, PostgreSQL 구동 시 메모리 압축(Compaction)으로 인한 락 경합 방지.
  • NIC IRQ & NUMA 정렬 (irqbalance 비활성화 또는 격리):
  • bond1(내부망 Intel E810)이 물리적으로 체결된 PCIe 슬롯의 NUMA 노드를 식별합니다.
cat /sys/class/net/bond1/device/numa_node
# 또는 물리 슬롯 인터페이스 확인: cat /sys/class/net/<ethX>/device/numa_node
  • NIC 링 버퍼 인터럽트가 원격 소켓의 CPU 코어로 분산되지 않도록 해당 인터페이스의 드라이버(ice) 인터럽트 CPU 마스크를 해당 NUMA 노드의 코어로 강제 제한합니다.

2. K8s (Kubespray) 노드 레벨 아키텍처 구성

K8s 스케줄러와 Kubelet이 컨테이너에 CPU와 로컬 메모리를 동일 NUMA 도메인에서 단일 단위로 할당하도록 유도합니다.

Kubelet Topology Manager 및 CPU Manager 활성화

Kubespray 인벤토리(group_vars/k8s_cluster/k8s-cluster.yml)에 다음 kubelet 플래그를 주입합니다.

# kubespray group_vars 설정
kubelet_custom_flags:
  - "--cpu-manager-policy=static"
  - "--cpu-manager-policy-options=full-pcpus-only=true"
  - "--topology-manager-policy=single-numa-node"
  - "--topology-manager-scope=container"
  - "--reserved-cpus=0-3,64-67" # OS/Cilium/Kubelet 시스템 데몬 전용 격리 코어 (소켓 0/1 분할)
  • topology-manager-policy: single-numa-node:
  • Pod가 요청한 CPU, HugePages, (해당되는 경우 SR-IOV/PCIe 장치)가 반드시 단 하나의 NUMA 도메인 내에서 모두 충족될 때만 파드를 스케줄링하고 승인(Admission)합니다. 멀티 소켓 분산 할당을 원천 차단합니다.
  • cpu-manager-policy: static + `Guaranteed QoS`:
  • 컨테이너 스펙에서 limits.cpu == requests.cpu (정수 단위) 및 limits.memory == requests.memory를 설정하면 Kubelet이 cgroups cpuset.cpuscpuset.mems를 로컬 NUMA 도메인에 완전히 하드 바인딩합니다.

3. 주요 워크로드별 최적화 전략 및 설정

[NUMA Node 0 (Socket 0)]                 [NUMA Node 1 (Socket 1)]
┌──────────────────────────────┐        ┌──────────────────────────────┐
│  NIC (E810 bond1)            │        │  NVMe Controller Pool B      │
│  Cilium eBPF Routing Stack   │        │                              │
│  MinIO Server Pod 1          │        │  StarRocks BE Pod 1          │
│  (Direct NVMe Pool A)        │        │  (In-Memory Query Compute)   │
└──────────────────────────────┘        └──────────────────────────────┘
               ▲                                       ▲
               └───────── UPI Link (경합 최소화) ───────┘

Case 1. MinIO AIStor (초고속 S3 Throughput / Line Rate 달성)

MinIO는 NVMe I/O와 E810 NIC 네트워크 I/O의 처리량이 균형을 이뤄야 합니다. NIC와 드라이브 버스가 연결된 NUMA 도메인에 파드를 바인딩해야 합니다.

  • 전략:
  • NVMe 드라이브들이 꽂힌 PCIe 스위치가 속한 NUMA 노드와 E810 NIC가 속한 NUMA 노드가 일치하도록 물리적 배치.
  • 단일 대형 MinIO 컨테이너를 소켓 전체에 걸쳐 띄우지 않고, 소켓(또는 SNC 도메인)당 1개의 MinIO 인스턴스(Pod)로 분할하여 드라이브 풀을 반씩 맵핑.
  • 설정 (Guaranteed QoS & Pod YAML):
resources:
  limits:
    cpu: "16"          # 소켓 단일 NUMA 노드 내 코어 수 이하 정수
    memory: "64Gi"
  requests:
    cpu: "16"
    memory: "64Gi"
  • MinIO 기동 옵션/환경 변수:
  • GOMAXPROCS=16 명시 (cpuset 경계를 넘어 Go 런타임이 다른 소켓 코어로 고루틴을 훔쳐가는(Work-stealing) 오버헤드 방지).
  • Go GC 튜닝: GOGC=100, GOMEMLIMIT=58GiB 설정으로 불필요한 크로스 NUMA 메모리 스왑 방지.

Case 2. StarRocks BE (In-Memory 고속 벡터 연산)

StarRocks Backend(BE)는 대규모 분산 Hash Join 및 집계 시 L3 캐시 및 로컬 메모리 대역폭(Memory Bandwidth)이 쿼리 병목의 80% 이상을 차지합니다.

  • 전략:
  • 소켓 2개를 합쳐서 단일 대형 BE를 띄우면 크로스 소켓 트래픽으로 인해 L3 캐시 히트율이 급감하고 UPI 버스가 포화됩니다.
  • 노드당 2개의 BE Pod(Multi-BE per Node)를 띄워 각각 NUMA 0과 NUMA 1에 단일 바인딩합니다.
  • Pod 설정 및 Kubelet 토폴로지 적용:
  • resources.requests/limits를 소켓 1개의 여유 코어 수(예: 32코어/128GiB)로 정확히 분할.
  • StarRocks be.conf 최적화:
# 백엔드 코어 수에 맞춘 파이프라인 엔진 스레드 격리
pipeline_exec_thread_pool_size = 32
# 원격 메모리 할당 회피
chunk_reserved_bytes_limit_enable = true
  • Jemalloc NUMA Aware 바인딩:
  • StarRocks 실행 래퍼에 jemalloc의 per-CPU 아레나 옵션 활성화:
    MALLOC_CONF="dirty_decay_ms:2000,muzzy_decay_ms:2000,narenas:32"

Case 3. Apache Spark on K8s (대규모 ETL / Data Shuffle)

Spark 워크로드는 드라이버와 익스큐터 간 Shuffle 단계에서 대량의 메모리 직렬화/역직렬화 및 디스크 I/O가 발생합니다.

  • 전략 (Thin vs Fat Executor):
  • 대형 익스큐터(예: 1개 익스큐터에 32코어)를 지양하고, NUMA 노드 경계를 넘지 않는 중간 크기(4~8 코어, 32GB)의 익스큐터 여러 개를 생성.
  • SparkConf 설정:
# 익스큐터당 코어 및 메모리를 단일 NUMA 노드 크기로 제약
spark.executor.cores=8
spark.executor.memory=28g
spark.executor.memoryOverhead=4g

# K8s Guaranteed QoS 트리거를 위한 명시적 오버헤드 반영
spark.kubernetes.executor.request.cores=8
spark.kubernetes.executor.limit.cores=8

# Off-heap 메모리 할당 시 크로스 소켓 바인딩 억제
spark.memory.offHeap.enabled=true
spark.memory.offHeap.size=4g

Case 4. CloudNativePG (CNPG / PostgreSQL)

OLTP 성격의 마스터/레플리카 DB는 Commit 트랜잭션 지연 시간과 Buffer Pool 히트가 핵심입니다.

  • 전략:
  • single-numa-node 정책 하에 단일 소켓에 완전 격리.
  • Pod 스펙 및 HugePages 적용:
  • 메모리 접근 시 TLB(Translation Lookaside Buffer) 미스를 줄이기 위해 2MB HugePages를 NUMA 로컬에서 직접 할당받도록 Pod에 구성:
resources:
  limits:
    cpu: "8"
    memory: "32Gi"
    hugepages-2Mi: "16Gi"
  requests:
    cpu: "8"
    memory: "32Gi"
    hugepages-2Mi: "16Gi"
  • PostgreSQL 엔진 옵션 (postgresql.conf):
  • shared_buffers = '14GB' (할당된 로컬 HugePages 대역과 일치)
  • huge_pages = on

Case 5. Cilium CNI (Native Routing + BGP + ClusterMesh)

Cilium의 eBPF 맵(BPF Maps)과 XDP/tc 훅은 커널 공간 메모리를 공유하므로, 네트워크 패킷이 들어오는 NIC의 로컬 CPU에서 처리되지 않으면 패킷마다 Inter-Socket 인터럽트가 발생합니다.

  • Cilium Agent (daemonset) Daemon CPU 바인딩:
  • Kubelet의 --reserved-cpus 영역 중, E810 NIC가 위치한 NUMA 소켓의 시스템 코어에 Cilium Agent와 BGP Control Plane이 스케줄링되도록 nodeAffinity 또는 데몬셋 설정을 최적화합니다.
  • Cilium Helm Values 핵심 파라미터:
bpf:
  # 맵 크기를 적절히 제한하여 과도한 메모리 분산 방지
  mapDynamicSizeRatio: 0.005
  preallocateMaps: true # 동적 맵 할당으로 인한 비로컬 메모리 참조 방지
routingMode: native
autoDirectNodeRoutes: true
loadBalancer:
  mode: dsr # DSR(Direct Server Return) 적용 시 인그레스 트래픽의 불필요한 홉과 소켓 간 릴레이 방지

4. 설정 무결성 점검 체크리스트

배포 후 실제 Pod들이 NUMA 경계 내에 갇혀있는지 확인하는 실측 검증 명령어입니다.

  • Pod의 cpuset/mems 바인딩 상태 확인 (Worker Node 터미널):
# 특정 StarRocks 또는 MinIO 컨테이너의 PID 확인
CONTAINER_ID=$(crictl ps --name starrocks -q)
PID=$(crictl inspect $CONTAINER_ID | jq .info.pid)

# 바인딩된 CPU 코어와 메모리 노드 확인
cat /proc/$PID/status | grep -E "Cpus_allowed_list|Mems_allowed"
# 출력 예: Mems_allowed: 0 (반드시 0 또는 1처럼 단일 NUMA 노드만 찍혀야 정상)
  • NUMA 원격 메모리 미스 실시간 모니터링:
# numa_miss 및 foreign 카운터가 급증하지 않는지 확인
numastat -c starrocks_be
# 또는 시스템 전체 검사
watch -n 1 'numastat -m'
  • Perf를 활용한 UPI 크로스 소켓 트래픽 프로파일링:
# Intel Uncore 카운터를 통해 UPI 대역폭 모니터링 (Gen4/Gen5 Xeon)
perf stat -a -e uncore_upi_0/event=0x01,umask=0x01/ -I 2000
profile
engineer

0개의 댓글