네. 이 환경에서는 “NUMA를 잘 쓰는 서버 설정”보다 “NUMA를 Kubernetes resource allocation → NIC/Storage locality → application process/thread locality까지 일관되게 연결하는 것”이 핵심입니다.
특히 현재 환경처럼 Xeon + 다중 NUMA + K8s + Cilium native/BGP/ECMP + AIStor + CNPG + OpenEBS + StarRocks/Spark가 한 플랫폼에 있으면, 모든 Pod를 무조건 single-numa-node로 묶는 방식은 오히려 좋지 않습니다.
제가 권하는 방향은:
① NUMA topology를 정확히 파악 → ② K8s CPU/Memory/Topology Manager 활성화 → ③ workload별 NUMA policy 분리 → ④ NIC/IRQ와 NUMA 연결 → ⑤ Storage와 CPU locality 연결 → ⑥ 애플리케이션별 thread/memory tuning
입니다.
전체 구조를 이렇게 가져가는 것이 좋습니다.
Xeon Server
┌─────────────────────────────────────────────────────────────┐
│ │
│ NUMA 0 NUMA 1 │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ CPU 0-31 │ │ CPU 32-63 │ │
│ │ Memory 256GB │ │ Memory 256GB │ │
│ │ NIC Queue │ │ NIC Queue │ │
│ │ NVMe/Disk │ │ NVMe/Disk │ │
│ └───────────────┘ └───────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ CPU/Memory CPU/Memory │
│ locality locality │
│ ▲ ▲ │
│ └────────── Kubernetes ─────────┘ │
│ │
│ CPU Manager static │
│ Memory Manager static │
│ Topology Manager │
│ Guaranteed QoS │
│ │
└─────────────────────────────────────────────────────────────┘
그리고 workload를 크게 5종류로 나눕니다.
| Workload | NUMA 전략 |
|---|---|
| AIStor | NIC + Disk + CPU locality 최우선 |
| CNPG | CPU + memory + WAL/storage locality 최우선 |
| StarRocks | CPU + memory + local disk locality 최우선 |
| Spark | NUMA-aware executor sizing |
| 일반 K8s/Polaris/OPA | NUMA 최적화보다는 packing/availability 우선 |
각 Xeon 서버에서 반드시 다음을 수집하세요.
lscpu
lscpu -e=CPU,NODE,SOCKET,CORE
numactl --hardware
numactl --show
cat /sys/devices/system/node/node*/cpulist
cat /sys/devices/system/node/node*/meminfo
그리고 PCIe topology:
lspci -tv
for d in /sys/class/net/*; do
echo "==== $d ===="
readlink -f "$d/device/numa_node"
done
NIC:
ethtool -i bond0
ethtool -i bond1
ethtool -l <nic>
ethtool -x <nic>
Disk:
lsblk -o NAME,KNAME,SIZE,MODEL,TYPE,MOUNTPOINT
for d in /sys/block/*; do
echo "$d NUMA=$(cat $d/device/numa_node 2>/dev/null)"
done
여기서 NUMA topology map을 먼저 만들어야 합니다.
예:
NUMA 0
├─ CPU 0-31
├─ Memory 256 GB
├─ bond1 NIC port 0
├─ NVMe 0
└─ NVMe 1
NUMA 1
├─ CPU 32-63
├─ Memory 256 GB
├─ bond1 NIC port 1
├─ NVMe 2
└─ NVMe 3
이게 만들어져야 이후 최적화가 의미가 있습니다.
현재 K8s 1.33.x라면 CPU Manager + Memory Manager + Topology Manager를 중심으로 구성하는 것을 권합니다.
Kubernetes CPU Manager의 static policy는 Guaranteed Pod의 정수 CPU 요청에 exclusive CPU allocation을 제공하고, 1.33부터 full-pcpus-only, distribute-cpus-across-numa 등의 policy option을 사용할 수 있습니다. (Kubernetes)
Memory Manager의 Static policy는 NUMA별 memory allocation hint를 Topology Manager에 제공하고 Guaranteed Pod의 메모리를 가능한 적은 NUMA node에 배치합니다. 이 기능은 Kubernetes 1.32부터 stable입니다. (Kubernetes)
가장 먼저:
cpuManagerPolicy: static
을 권합니다.
그리고 production에서는:
cpuManagerPolicyOptions:
full-pcpus-only: "true"
를 우선 검토합니다.
즉:
CPU 0/1 = SMT sibling
CPU 2/3 = SMT sibling
CPU 4/5 = SMT sibling
이라면 애플리케이션에 가능한 한:
2 physical cores
4 physical cores
8 physical cores
...
단위로 배정하는 것입니다.
AIStor, StarRocks, CNPG 같은 latency-sensitive workload라면:
Housekeeping CPU
↓
kubelet
containerd
Cilium
IRQ
systemd
kernel threads
Workload CPU
↓
AIStor
StarRocks
CNPG
Spark
로 분리하는 것이 좋습니다.
RHEL 10의 cpu-partitioning TuneD profile은 low-latency workload에서 housekeeping CPU와 isolated CPU를 분리하도록 설계되어 있습니다. (레드햇 문서)
예를 들어 64 physical cores라면 단순화해서:
CPU 0-3 housekeeping
CPU 4-31 NUMA 0 workload
CPU 32-35 housekeeping
CPU 36-63 NUMA 1 workload
같은 구조를 고려할 수 있습니다.
다만 Cilium + NIC IRQ를 housekeeping CPU에 전부 몰아버리는 것도 좋지 않습니다.
특히 25/100Gbps NIC라면 IRQ 처리량이 상당하기 때문입니다.
AIStor/StarRocks/CNPG처럼 NUMA locality가 중요한 workload에는:
topologyManagerPolicy: single-numa-node
를 적용할 수 있습니다.
그러면:
Pod 요구사항
CPU : NUMA 0
Memory : NUMA 0
Device : NUMA 0
↓
Admission = PASS
반대로:
CPU : NUMA 0
Memory : NUMA 1
Device : NUMA 0
↓
Admission = FAIL
이런 식으로 remote NUMA allocation을 예방할 수 있습니다.
Kubernetes는 single-numa-node 외에도 restricted, best-effort 등을 제공하며, topology manager는 CPU/memory/device plugin 등의 topology hints를 조합합니다. (Kubernetes)
모든 Pod에 single-numa-node를 적용하지 마세요.
예를 들어:
Polaris
OPA
Keycloak
Kafka
small CNPG
small Spark driver
monitoring
같은 workload는 NUMA 때문에 scheduling failure가 발생하는 것보다 전체 cluster utilization이 더 중요할 수 있습니다.
그래서 저는 node pool별 또는 workload별 전략을 추천합니다.
제가 현재 환경이라면 대략 이렇게 나눕니다.
| Workload | CPU Manager | Memory Manager | Topology |
|---|---|---|---|
| AIStor | static | static | single-numa |
| StarRocks BE | static | static | single-numa |
| CNPG | static | static | single-numa |
| 대형 Spark executor | static | static | single-numa |
| 일반 Spark executor | static | static | restricted/best-effort |
| Polaris | 일반 | 일반 | best-effort |
| OPA | 일반 | 일반 | best-effort |
| monitoring | 일반 | 일반 | best-effort |
NUMA locality가 중요한 Pod는:
resources:
requests:
cpu: "16"
memory: "64Gi"
limits:
cpu: "16"
memory: "64Gi"
처럼 request = limit로 만드세요.
그러면:
QoS = Guaranteed
가 되고 CPU Manager/Memory Manager/Topology Manager가 제대로 작동할 수 있습니다.
kubelet:
memoryManagerPolicy: Static
을 권합니다.
그리고 NUMA별 reserved memory를 명확하게 잡습니다.
예:
reservedMemory:
- numaNode: 0
limits:
memory: "4Gi"
- numaNode: 1
limits:
memory: "4Gi"
실제 값은 서버의 RAM과 system workload에 맞춰 잡아야 합니다.
Kubernetes 문서도 Static Memory Manager 사용 시 reservedMemory를 NUMA node별로 구성하도록 설명합니다. (Kubernetes)
두 번째로 중요한 것이 HugePages입니다.
특히:
에서는 workload별로 테스트할 가치가 있습니다.
RHEL:
grep Huge /proc/meminfo
2MB hugepage:
vm.nr_hugepages=...
또는 Kubernetes에서:
resources:
requests:
hugepages-2Mi: 4Gi
limits:
hugepages-2Mi: 4Gi
형태로 별도 resource로 관리합니다.
다만 모든 workload에 HugePages를 강제하지는 마세요.
PostgreSQL은 huge_pages=on으로 explicit HugeTLB를 강제할 수 있고, 큰 shared memory 영역에서 page-table overhead를 줄일 수 있습니다. (PostgreSQL)
현재 환경에서는 AIStor가 NUMA optimization의 1순위라고 봅니다.
왜냐하면:
Application
↓
AIStor
↓
Network
↓
Disk
이 모두 NUMA 영향을 받기 때문입니다.
AIStor 자체도 production hardware tuning에서 CPU governor를 performance로 설정하고, AVX2/SSE4.2와 충분한 physical core를 권장합니다. (MinIO AIStor Documentation)
예:
NUMA 0
├─ CPU 0-31
├─ Memory 256G
├─ NIC Queue 0
└─ NVMe 0/1
↓
AIStor Pod
CPU 0-31
Memory NUMA0
↓
NIC 0
↓
Disk 0/1
가능하면:
NIC NUMA == CPU NUMA == Disk NUMA
로 맞춥니다.
여기서 Kubernetes보다 오히려 Linux tuning이 중요합니다.
확인:
cat /proc/interrupts | grep -Ei 'ice|eth|bond'
그리고:
ethtool -l <E810>
ethtool -x <E810>
NIC RSS queue를 NUMA-local CPU에 배치합니다.
예:
NUMA 0:
RX queue 0-15 → CPU 4-19
NUMA 1:
RX queue 16-31 → CPU 36-51
같은 구조입니다.
이렇게 해야:
NIC packet
↓
IRQ
↓
CPU
↓
AIStor thread
가 NUMA-local해집니다.
현재 Cilium이:
native routing
BGP
ECMP
ClusterMesh
이므로 상당히 좋은 출발점입니다.
Native routing은 encapsulation을 거치지 않고 Linux routing subsystem을 사용하기 때문에 overlay 방식보다 network overhead를 줄일 수 있습니다. (Cilium Documentation)
하지만:
Cilium native mode = NUMA 최적화 완료
는 아닙니다.
특히 bond1이 내부 cluster traffic의 핵심이라면:
NUMA 0
CPU
↑
Cilium
↑
bond1
↑
NIC
NUMA 1
CPU
↑
Cilium
↑
bond1
↑
NIC
처럼 맞춰주는 것이 좋습니다.
Cilium의 BPF host routing은 host stack의 일부 경로를 우회하여 network overhead를 줄이는 데 도움이 됩니다. Cilium 문서에서도 BPF host routing이 legacy routing 대비 성능상 이점을 목표로 한다고 설명합니다. (Cilium Documentation)
현재:
Pod
↓
Cilium
↓
bond1
↓
BGP
↓
ECMP
구조라면 NUMA보다 ECMP traffic distribution이 먼저 병목이 될 수도 있습니다.
따라서:
ethtool -S <nic>
cat /proc/interrupts
ss -s
nstat
와 함께:
NIC utilization
RX/TX queue distribution
CPU utilization per NUMA
softirq
TCP retransmit
Cilium drops
를 같이 봐야 합니다.
Cilium Bandwidth Manager를 사용하는 경우 BBR을 검토할 수 있지만, 현재 환경에서는 무조건 켜지는 않는 것을 권합니다.
Cilium은 Bandwidth Manager + BPF host routing을 통해 BBR을 사용할 수 있지만, BBR은 CUBIC보다 더 공격적일 수 있고 retransmission 증가 가능성도 문서에 명시되어 있습니다. (Cilium Documentation)
현재처럼:
AIStor
+
Spark
+
StarRocks
+
Cilium
+
25Gbps bond
가 같이 움직이는 환경이라면 먼저:
CUBIC baseline
을 잡고 BBR을 A/B test 하는 것이 좋습니다.
CNPG는 NUMA locality가 꽤 중요합니다.
예:
NUMA 0
├─ CPU 0-15
├─ Memory
└─ NVMe/WAL
↓
PostgreSQL
가능하면:
PostgreSQL CPU
=
PostgreSQL memory
=
WAL disk
를 같은 NUMA에 맞춥니다.
특히 WAL disk가 NVMe라면 PCIe topology를 확인하세요.
lspci -tv
cat /sys/block/nvme0n1/device/numa_node
예:
resources:
requests:
cpu: "8"
memory: "32Gi"
limits:
cpu: "8"
memory: "32Gi"
그리고:
CPU Manager static
Memory Manager static
Topology Manager single-numa-node
조합을 추천합니다.
기본적으로:
shared_buffers
work_mem
maintenance_work_mem
max_connections
max_worker_processes
max_parallel_workers
max_parallel_workers_per_gather
를 NUMA/CPU 수와 함께 봐야 합니다.
예를 들어 NUMA 2개 × 32 physical core라면:
max_parallel_workers
를 무작정 64로 잡는 것보다 workload benchmark로 결정해야 합니다.
그리고 PostgreSQL에는 NUMA별 shared-memory allocation을 확인할 수 있는 pg_shmem_allocations_numa view도 있습니다. (PostgreSQL)
StarRocks BE는:
CPU-heavy
+
memory-heavy
+
local disk
+
network
성격이라 NUMA-aware configuration의 효과가 큰 편입니다.
특히 현재 workload가:
StarRocks
↓
Iceberg
↓
AIStor
구조라면:
StarRocks CPU
↓
Memory
↓
NIC
↓
AIStor
경로 전체를 봐야 합니다.
StarRocks에는 resource group에서 exclusive_cpu_cores를 사용하여 CPU isolation을 구성할 수 있습니다. 최근 버전에서는 hard CPU limit도 지원합니다. (StarRocks Docs)
예를 들어:
NUMA0
├─ CPU 4-15
│ └─ StarRocks query
│
└─ CPU 16-23
└─ compaction
NUMA1
├─ CPU 36-47
│ └─ StarRocks query
│
└─ CPU 48-55
└─ compaction
같은 식으로 분리하는 것이 이상적입니다.
예:
Pod CPU request = 4
Pod limit = 32
같은 Burstable 형태로 만들어놓고
"NUMA 최적화를 했다"
라고 생각하면 안 됩니다.
중요 BE는:
requests:
cpu: "16"
memory: "64Gi"
limits:
cpu: "16"
memory: "64Gi"
처럼 명확하게 reservation하는 것이 좋습니다.
Spark는 AIStor/CNPG와 달리 무조건 single NUMA로 가면 오히려 손해일 수 있습니다.
예를 들어:
1 executor
64 cores
256GB
를 하나의 NUMA에 넣으려고 하면:
NUMA 0 capacity = 32 cores / 256GB
때문에 불가능합니다.
그보다는:
NUMA0
Executor 1
16 cores
64GB
NUMA1
Executor 2
16 cores
64GB
처럼 executor를 NUMA 단위로 쪼개는 전략을 추천합니다.
예를 들어 2 NUMA / 32 physical core라고 하면:
BAD
1 executor
32 cores
128 GB
보다:
GOOD
executor 1
16 cores
64 GB
executor 2
16 cores
64 GB
가 NUMA locality 측면에서는 더 유리한 경우가 많습니다.
특히 shuffle-heavy workload에서는 이 차이가 커질 수 있습니다.
Spark:
spark.executor.cores
spark.executor.memory
spark.executor.memoryOverhead
를 K8s:
resources.requests.cpu
resources.limits.cpu
resources.requests.memory
resources.limits.memory
와 일치시키는 방향으로 설계합니다.
예:
Spark executor
16 cores
64G heap
8G overhead
K8s
cpu request/limit = 16
memory request/limit = 72G
처럼요.
이렇게 해야 Kubernetes가 해당 executor를 명확하게 하나의 NUMA domain에 배치하기 쉬워집니다.
OpenEBS에서는 Storage device topology가 핵심입니다.
특히 LocalPV를 사용한다면:
Pod
↓
LocalPV
↓
/dev/nvme0n1
↓
PCIe root complex
↓
NUMA 0
를 확인해야 합니다.
즉:
Pod NUMA 0
+
Disk NUMA 0
로 맞추세요.
현재 환경에서 제가 가장 중요하게 보는 조합입니다.
CNPG Pod
│
├── CPU NUMA 0
├── Memory NUMA 0
│
▼
OpenEBS LocalPV
│
▼
NVMe NUMA 0
이렇게 되면:
PostgreSQL
↓
shared_buffers
↓
WAL
↓
NVMe
경로가 local NUMA가 됩니다.
반대로:
PostgreSQL CPU NUMA0
↓
Memory NUMA0
↓
NVMe NUMA1
이면 remote PCIe/NUMA access가 생길 수 있습니다.
이쪽은 NUMA 최적화 우선순위가 낮습니다.
예:
Polaris
OPA
Keycloak
API
controller
operator
는:
NUMA locality
보다:
HA
availability
scheduling
resource efficiency
가 더 중요합니다.
따라서 일반 Burstable QoS를 허용해도 됩니다.
AIStor
StarRocks BE
CNPG
large Spark executor
정책:
CPUManager static
MemoryManager static
TopologyManager single-numa-node
Guaranteed QoS
CPU exclusive
NUMA-local storage
NUMA-local NIC
Spark executor
Kafka
Redis
heavy Trino
정책:
CPUManager static
MemoryManager static
TopologyManager restricted
Guaranteed/Burstable
Polaris
OPA
Keycloak
operators
monitoring
controllers
정책:
normal scheduling
Burstable
best-effort
현재 플랫폼 규모를 생각하면 저는:
general-worker
storage-worker
database-worker
analytics-worker
정도로 논리적인 node pool을 만드는 것을 권합니다.
예:
storage-worker
├─ AIStor
└─ storage-related
database-worker
└─ CNPG
analytics-worker
├─ StarRocks
└─ Spark
general-worker
├─ Polaris
├─ OPA
└─ controllers
이렇게 하면 NUMA tuning이 훨씬 쉬워집니다.
예를 들어 64 physical core 서버라면:
CPU 0-3
housekeeping
CPU 4-7
NIC IRQ / kernel networking
CPU 8-31
NUMA0 application
CPU 32-35
housekeeping
CPU 36-39
NIC IRQ / kernel networking
CPU 40-63
NUMA1 application
같은 구조를 생각할 수 있습니다.
다만 이 숫자는 예시이고 실제 Xeon의:
socket
NUMA
SMT
NIC PCIe
disk PCIe
구조를 보고 정해야 합니다.
이 부분은 지금 환경에서 상당히 중요합니다.
현재:
Intel E810
bond1
25Gbps × 2
Cilium
AIStor
K8s internal traffic
이므로 다음을 측정해야 합니다.
cat /proc/interrupts
mpstat -P ALL 1
sar -n DEV 1
ethtool -S <nic>
그리고:
cat /proc/softirqs
에서 NET_RX, NET_TX도 확인합니다.
CPU NUMA0
Memory NUMA1
NIC NUMA1
이면 효과가 제한됩니다.
CPU가 다른 NUMA에서 실행되면 remote access가 발생합니다.
특히 NVMe는 PCIe root complex 때문에 NUMA locality가 중요합니다.
AIStor/StarRocks 같은 network-heavy workload에서는 상당히 큰 차이를 만들 수 있습니다.
작은 Pod가 scheduling failure를 일으키고 cluster fragmentation이 심해집니다.
Kubernetes Node
┌─────────────────────────────────────────────────────┐
│ │
│ NUMA 0 NUMA 1 │
│ │
│ CPU 0-3 housekeeping CPU 32-35 │
│ CPU 4-7 NIC/IRQ CPU 36-39 │
│ CPU 8-31 workload CPU 40-63 │
│ │
│ Memory 0 Memory 1 │
│ │
│ NIC 0 ──────────────┐ ┌──────── NIC 1 │
│ │ │ │
│ Disk 0/1 ───────────┘ └────── Disk 2/3 │
│ │
└─────────────────────────────────────────────────────┘
그리고:
AIStor
CPU NUMA0
Memory NUMA0
NIC NUMA0
Disk NUMA0
CNPG
CPU NUMA1
Memory NUMA1
WAL Disk NUMA1
StarRocks
CPU NUMA0
Memory NUMA0
Local cache NUMA0
Spark Executor
Executor A → NUMA0
Executor B → NUMA1
Polaris/OPA
어디든
처럼 locality를 workload 단위로 유지합니다.
현재 환경에서는 저는 다음 순서로 작업하겠습니다.
| 순위 | 항목 | 중요도 |
|---|---|---|
| 1 | BIOS/NUMA/PCIe topology 확인 | ★★★★★ |
| 2 | CPU Manager static | ★★★★★ |
| 3 | Memory Manager static | ★★★★★ |
| 4 | Topology Manager | ★★★★★ |
| 5 | Guaranteed QoS | ★★★★★ |
| 6 | NIC IRQ/RSS NUMA affinity | ★★★★★ |
| 7 | OpenEBS disk NUMA affinity | ★★★★★ |
| 8 | AIStor CPU/NIC/Disk locality | ★★★★★ |
| 9 | CNPG CPU/Memory/WAL locality | ★★★★☆ |
| 10 | StarRocks CPU/memory/cache locality | ★★★★☆ |
| 11 | Spark executor NUMA sizing | ★★★★☆ |
| 12 | HugePages | ★★★☆☆ |
| 13 | CPU isolation/TuneD | ★★★★☆ |
| 14 | Cilium BPF/network tuning | ★★★★☆ |
| 15 | Polaris/OPA NUMA tuning | ★☆☆☆☆ |
지금처럼 Intel Xeon + RHEL 10.2 + Cilium + AIStor + CNPG + OpenEBS + StarRocks + Spark가 있는 환경이라면, 단순히 kubelet 설정만 하는 것보다 “NUMA topology profile”을 노드 표준으로 만드는 것이 좋습니다.
예를 들어 각 노드마다 다음을 자동 수집해서:
node-numa-profile
NUMA0:
CPU: 0-31
MEM: 256GB
NIC: E810 port0
DISK: nvme0,nvme1
NUMA1:
CPU: 32-63
MEM: 256GB
NIC: E810 port1
DISK: nvme2,nvme3
이 정보를 기준으로
Kubelet
↓
CPUManager
MemoryManager
TopologyManager
↓
Pod
↓
OpenEBS
↓
Disk
Pod
↓
Cilium
↓
NIC Queue/IRQ
까지 연결하는 방식입니다.
특히 사용자 환경에서는 AIStor + StarRocks + CNPG + Spark를 같은 NUMA 정책으로 취급하면 안 됩니다. AIStor/CNPG는 locality를 강하게 잡고, StarRocks는 resource group까지 활용하고, Spark는 executor 크기를 NUMA domain에 맞춰 쪼개는 방식이 가장 합리적입니다.
원하시면 다음 단계로는 현재 사용 중인 RHEL 10.2 + K8s 1.33.4 + Cilium 1.18.4 기준으로 실제 적용 가능한 KubeletConfiguration + TuneD + GRUB/kernel parameter + systemd + IRQ/RSS + Pod YAML + CNPG + StarRocks + Spark 전체 설정 템플릿을 하나의 표준안으로 만들어 드리는 게 좋습니다. 특히 64/96/128 physical-core Xeon 서버 각각에 대해 NUMA0/NUMA1 CPU 번호를 어떻게 나누고, 몇 개를 housekeeping/NIC IRQ/AIStor/StarRocks 등에 예약할지까지 산정할 수 있습니다.