<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>youngkyoo_kim.log</title>
        <link>https://velog.io/</link>
        <description>engineer</description>
        <lastBuildDate>Fri, 04 Sep 2026 20:29:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>youngkyoo_kim.log</title>
            <url>https://velog.velcdn.com/images/youngkyoo_kim/profile/7ee4ca13-1034-49bb-b6ab-736c3065a14a/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. youngkyoo_kim.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/youngkyoo_kim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[26S05z3]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S05z3</link>
            <guid>https://velog.io/@youngkyoo_kim/26S05z3</guid>
            <pubDate>Fri, 04 Sep 2026 20:29:00 GMT</pubDate>
            <description><![CDATA[<p>대규모 베어메탈 환경에서 Dual-Socket 이상 Intel Xeon(Sapphire Rapids/Emerald Rapids 등) 프로세서를 기반으로 Cilium Native Routing, 고성능 분산 스토리지(MinIO AIStor), 그리고 연산 엔진(StarRocks, Spark)을 구동할 때 UPI(Ultra Path Interconnect) 버스 트래픽 경합과 크로스 소켓 메모리 접근(Remote Memory Access)은 P99 지연 시간과 최대 Throughput을 갉아먹는 주범입니다.</p>
<p>OS, K8s 플랫폼, 그리고 레이어별 워크로드(스토리지, 쿼리 엔진, CNI) 관점에서 NUMA 친화성(NUMA Locality)을 극대화하는 엔드투엔드 전략입니다.</p>
<hr>
<h3 id="1-하드웨어bios-및-rhel-102-os-베이스라인-설정">1. 하드웨어/BIOS 및 RHEL 10.2 OS 베이스라인 설정</h3>
<p>K8s 상위 설정 이전에 하드웨어 및 커널 레벨에서 NUMA 도메인을 노출하고 인터럽트 경로를 정렬해야 합니다.</p>
<ul>
<li><strong>BIOS / Sub-NUMA Clustering (SNC) 활성화</strong>:</li>
<li>소켓당 코어 수가 많은 최신 Xeon(Gen4/Gen5)은 BIOS에서 <strong>SNC-2</strong> 또는 <strong>SNC-4</strong>(Sub-NUMA Clustering)를 활성화합니다. 소켓 1개를 물리적 2~4개 NUMA 도메인으로 쪼개어 L3 캐시 지역성과 로컬 메모리 컨트롤러 접근 지연시간을 최소화합니다.</li>
</ul>
<ul>
<li><strong>커널 부트 파라미터 (GRUB)</strong>:</li>
<li><code>numa_balancing=0</code>: Linux 커널의 자동 NUMA 밸런싱(Background page scanning/migration)은 대규모 DB/스토리지 구동 시 무작위 레이턴시 스파이크를 유발하므로 끕니다. K8s 레벨에서 정적으로 바인딩하는 것이 유리합니다.</li>
<li><code>transparent_hugepage=never</code> (또는 <code>madvise</code>): MinIO, StarRocks, PostgreSQL 구동 시 메모리 압축(Compaction)으로 인한 락 경합 방지.</li>
</ul>
<ul>
<li><strong>NIC IRQ &amp; NUMA 정렬 (<code>irqbalance</code> 비활성화 또는 격리)</strong>:</li>
<li><code>bond1</code>(내부망 Intel E810)이 물리적으로 체결된 PCIe 슬롯의 NUMA 노드를 식별합니다.<pre><code class="language-bash">cat /sys/class/net/bond1/device/numa_node
# 또는 물리 슬롯 인터페이스 확인: cat /sys/class/net/&lt;ethX&gt;/device/numa_node
</code></pre>
</li>
</ul>
<pre><code>

* 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 플래그를 주입합니다.

```yaml
# kubespray group_vars 설정
kubelet_custom_flags:
  - &quot;--cpu-manager-policy=static&quot;
  - &quot;--cpu-manager-policy-options=full-pcpus-only=true&quot;
  - &quot;--topology-manager-policy=single-numa-node&quot;
  - &quot;--topology-manager-scope=container&quot;
  - &quot;--reserved-cpus=0-3,64-67&quot; # OS/Cilium/Kubelet 시스템 데몬 전용 격리 코어 (소켓 0/1 분할)
</code></pre><ul>
<li><strong><code>topology-manager-policy: single-numa-node</code></strong>:</li>
<li>Pod가 요청한 CPU, HugePages, (해당되는 경우 SR-IOV/PCIe 장치)가 <strong>반드시 단 하나의 NUMA 도메인 내에서 모두 충족</strong>될 때만 파드를 스케줄링하고 승인(Admission)합니다. 멀티 소켓 분산 할당을 원천 차단합니다.</li>
</ul>
<ul>
<li><strong><code>cpu-manager-policy: static</code> + `Guaranteed QoS</strong>`:</li>
<li>컨테이너 스펙에서 <code>limits.cpu == requests.cpu</code> (정수 단위) 및 <code>limits.memory == requests.memory</code>를 설정하면 Kubelet이 cgroups <code>cpuset.cpus</code> 및 <code>cpuset.mems</code>를 로컬 NUMA 도메인에 완전히 하드 바인딩합니다.</li>
</ul>
<hr>
<h3 id="3-주요-워크로드별-최적화-전략-및-설정">3. 주요 워크로드별 최적화 전략 및 설정</h3>
<pre><code>[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 (경합 최소화) ───────┘
</code></pre><h4 id="case-1-minio-aistor-초고속-s3-throughput--line-rate-달성">Case 1. MinIO AIStor (초고속 S3 Throughput / Line Rate 달성)</h4>
<p>MinIO는 NVMe I/O와 E810 NIC 네트워크 I/O의 처리량이 균형을 이뤄야 합니다. NIC와 드라이브 버스가 연결된 NUMA 도메인에 파드를 바인딩해야 합니다.</p>
<ul>
<li><strong>전략</strong>:</li>
<li>NVMe 드라이브들이 꽂힌 PCIe 스위치가 속한 NUMA 노드와 E810 NIC가 속한 NUMA 노드가 일치하도록 물리적 배치.</li>
<li>단일 대형 MinIO 컨테이너를 소켓 전체에 걸쳐 띄우지 않고, 소켓(또는 SNC 도메인)당 1개의 MinIO 인스턴스(Pod)로 분할하여 드라이브 풀을 반씩 맵핑.</li>
</ul>
<ul>
<li><strong>설정 (Guaranteed QoS &amp; Pod YAML)</strong>:<pre><code class="language-yaml">resources:
limits:
  cpu: &quot;16&quot;          # 소켓 단일 NUMA 노드 내 코어 수 이하 정수
  memory: &quot;64Gi&quot;
requests:
  cpu: &quot;16&quot;
  memory: &quot;64Gi&quot;
</code></pre>
</li>
</ul>
<pre><code>

* **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` 최적화**:
```properties
# 백엔드 코어 수에 맞춘 파이프라인 엔진 스레드 격리
pipeline_exec_thread_pool_size = 32
# 원격 메모리 할당 회피
chunk_reserved_bytes_limit_enable = true
</code></pre><ul>
<li><strong>Jemalloc NUMA Aware 바인딩</strong>:</li>
<li>StarRocks 실행 래퍼에 jemalloc의 per-CPU 아레나 옵션 활성화:
<code>MALLOC_CONF=&quot;dirty_decay_ms:2000,muzzy_decay_ms:2000,narenas:32&quot;</code></li>
</ul>
<h4 id="case-3-apache-spark-on-k8s-대규모-etl--data-shuffle">Case 3. Apache Spark on K8s (대규모 ETL / Data Shuffle)</h4>
<p>Spark 워크로드는 드라이버와 익스큐터 간 Shuffle 단계에서 대량의 메모리 직렬화/역직렬화 및 디스크 I/O가 발생합니다.</p>
<ul>
<li><strong>전략 (Thin vs Fat Executor)</strong>:</li>
<li>대형 익스큐터(예: 1개 익스큐터에 32코어)를 지양하고, <strong>NUMA 노드 경계를 넘지 않는 중간 크기(4~8 코어, 32GB)의 익스큐터 여러 개</strong>를 생성.</li>
</ul>
<ul>
<li><strong>SparkConf 설정</strong>:<pre><code class="language-properties"># 익스큐터당 코어 및 메모리를 단일 NUMA 노드 크기로 제약
spark.executor.cores=8
spark.executor.memory=28g
spark.executor.memoryOverhead=4g
</code></pre>
</li>
</ul>
<h1 id="k8s-guaranteed-qos-트리거를-위한-명시적-오버헤드-반영">K8s Guaranteed QoS 트리거를 위한 명시적 오버헤드 반영</h1>
<p>spark.kubernetes.executor.request.cores=8
spark.kubernetes.executor.limit.cores=8</p>
<h1 id="off-heap-메모리-할당-시-크로스-소켓-바인딩-억제">Off-heap 메모리 할당 시 크로스 소켓 바인딩 억제</h1>
<p>spark.memory.offHeap.enabled=true
spark.memory.offHeap.size=4g</p>
<pre><code>


#### Case 4. CloudNativePG (CNPG / PostgreSQL)

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

* **전략**:
* `single-numa-node` 정책 하에 단일 소켓에 완전 격리.


* **Pod 스펙 및 HugePages 적용**:
* 메모리 접근 시 TLB(Translation Lookaside Buffer) 미스를 줄이기 위해 2MB HugePages를 NUMA 로컬에서 직접 할당받도록 Pod에 구성:


```yaml
resources:
  limits:
    cpu: &quot;8&quot;
    memory: &quot;32Gi&quot;
    hugepages-2Mi: &quot;16Gi&quot;
  requests:
    cpu: &quot;8&quot;
    memory: &quot;32Gi&quot;
    hugepages-2Mi: &quot;16Gi&quot;
</code></pre><ul>
<li><strong>PostgreSQL 엔진 옵션 (<code>postgresql.conf</code>)</strong>:</li>
<li><code>shared_buffers = &#39;14GB&#39;</code> (할당된 로컬 HugePages 대역과 일치)</li>
<li><code>huge_pages = on</code></li>
</ul>
<h4 id="case-5-cilium-cni-native-routing--bgp--clustermesh">Case 5. Cilium CNI (Native Routing + BGP + ClusterMesh)</h4>
<p>Cilium의 eBPF 맵(BPF Maps)과 XDP/tc 훅은 커널 공간 메모리를 공유하므로, 네트워크 패킷이 들어오는 NIC의 로컬 CPU에서 처리되지 않으면 패킷마다 Inter-Socket 인터럽트가 발생합니다.</p>
<ul>
<li><strong>Cilium Agent (<code>daemonset</code>) Daemon CPU 바인딩</strong>:</li>
<li>Kubelet의 <code>--reserved-cpus</code> 영역 중, <strong>E810 NIC가 위치한 NUMA 소켓의 시스템 코어</strong>에 Cilium Agent와 BGP Control Plane이 스케줄링되도록 <code>nodeAffinity</code> 또는 데몬셋 설정을 최적화합니다.</li>
</ul>
<ul>
<li><strong>Cilium Helm Values 핵심 파라미터</strong>:<pre><code class="language-yaml">bpf:
# 맵 크기를 적절히 제한하여 과도한 메모리 분산 방지
mapDynamicSizeRatio: 0.005
preallocateMaps: true # 동적 맵 할당으로 인한 비로컬 메모리 참조 방지
routingMode: native
autoDirectNodeRoutes: true
loadBalancer:
mode: dsr # DSR(Direct Server Return) 적용 시 인그레스 트래픽의 불필요한 홉과 소켓 간 릴레이 방지
</code></pre>
</li>
</ul>
<pre><code>


---

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

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

* **Pod의 cpuset/mems 바인딩 상태 확인 (Worker Node 터미널)**:
```bash
# 특정 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 &quot;Cpus_allowed_list|Mems_allowed&quot;
# 출력 예: Mems_allowed: 0 (반드시 0 또는 1처럼 단일 NUMA 노드만 찍혀야 정상)
</code></pre><ul>
<li><strong>NUMA 원격 메모리 미스 실시간 모니터링</strong>:<pre><code class="language-bash"># numa_miss 및 foreign 카운터가 급증하지 않는지 확인
numastat -c starrocks_be
# 또는 시스템 전체 검사
watch -n 1 &#39;numastat -m&#39;
</code></pre>
</li>
</ul>
<pre><code>

* **Perf를 활용한 UPI 크로스 소켓 트래픽 프로파일링**:
```bash
# Intel Uncore 카운터를 통해 UPI 대역폭 모니터링 (Gen4/Gen5 Xeon)
perf stat -a -e uncore_upi_0/event=0x01,umask=0x01/ -I 2000
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[26S05z2]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S05z2</link>
            <guid>https://velog.io/@youngkyoo_kim/26S05z2</guid>
            <pubDate>Fri, 04 Sep 2026 20:28:12 GMT</pubDate>
            <description><![CDATA[<p>네. 이 환경에서는 <strong>“NUMA를 잘 쓰는 서버 설정”보다 “NUMA를 Kubernetes resource allocation → NIC/Storage locality → application process/thread locality까지 일관되게 연결하는 것”</strong>이 핵심입니다.</p>
<p>특히 현재 환경처럼 <strong>Xeon + 다중 NUMA + K8s + Cilium native/BGP/ECMP + AIStor + CNPG + OpenEBS + StarRocks/Spark</strong>가 한 플랫폼에 있으면, 모든 Pod를 무조건 <code>single-numa-node</code>로 묶는 방식은 오히려 좋지 않습니다.</p>
<p>제가 권하는 방향은:</p>
<blockquote>
<p><strong>① NUMA topology를 정확히 파악 → ② K8s CPU/Memory/Topology Manager 활성화 → ③ workload별 NUMA policy 분리 → ④ NIC/IRQ와 NUMA 연결 → ⑤ Storage와 CPU locality 연결 → ⑥ 애플리케이션별 thread/memory tuning</strong></p>
</blockquote>
<p>입니다.</p>
<hr>
<h1 id="1-먼저-전체-전략">1. 먼저 전체 전략</h1>
<p>전체 구조를 이렇게 가져가는 것이 좋습니다.</p>
<pre><code class="language-text">                         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                                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘</code></pre>
<p>그리고 workload를 크게 5종류로 나눕니다.</p>
<table>
<thead>
<tr>
<th>Workload</th>
<th>NUMA 전략</th>
</tr>
</thead>
<tbody><tr>
<td>AIStor</td>
<td><strong>NIC + Disk + CPU locality 최우선</strong></td>
</tr>
<tr>
<td>CNPG</td>
<td><strong>CPU + memory + WAL/storage locality 최우선</strong></td>
</tr>
<tr>
<td>StarRocks</td>
<td><strong>CPU + memory + local disk locality 최우선</strong></td>
</tr>
<tr>
<td>Spark</td>
<td><strong>NUMA-aware executor sizing</strong></td>
</tr>
<tr>
<td>일반 K8s/Polaris/OPA</td>
<td>NUMA 최적화보다는 packing/availability 우선</td>
</tr>
</tbody></table>
<hr>
<h1 id="2-가장-먼저-해야-할-것-서버-numa-topology-조사">2. 가장 먼저 해야 할 것: 서버 NUMA topology 조사</h1>
<p>각 Xeon 서버에서 반드시 다음을 수집하세요.</p>
<pre><code class="language-bash">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</code></pre>
<p>그리고 PCIe topology:</p>
<pre><code class="language-bash">lspci -tv

for d in /sys/class/net/*; do
  echo &quot;==== $d ====&quot;
  readlink -f &quot;$d/device/numa_node&quot;
done</code></pre>
<p>NIC:</p>
<pre><code class="language-bash">ethtool -i bond0
ethtool -i bond1

ethtool -l &lt;nic&gt;
ethtool -x &lt;nic&gt;</code></pre>
<p>Disk:</p>
<pre><code class="language-bash">lsblk -o NAME,KNAME,SIZE,MODEL,TYPE,MOUNTPOINT

for d in /sys/block/*; do
    echo &quot;$d NUMA=$(cat $d/device/numa_node 2&gt;/dev/null)&quot;
done</code></pre>
<p>여기서 <strong>NUMA topology map을 먼저 만들어야 합니다.</strong></p>
<p>예:</p>
<pre><code class="language-text">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</code></pre>
<p>이게 만들어져야 이후 최적화가 의미가 있습니다.</p>
<hr>
<h1 id="3-kubernetes에서-가장-중요한-3개">3. Kubernetes에서 가장 중요한 3개</h1>
<p>현재 K8s 1.33.x라면 <strong>CPU Manager + Memory Manager + Topology Manager</strong>를 중심으로 구성하는 것을 권합니다.</p>
<p>Kubernetes CPU Manager의 <code>static</code> policy는 Guaranteed Pod의 정수 CPU 요청에 exclusive CPU allocation을 제공하고, 1.33부터 <code>full-pcpus-only</code>, <code>distribute-cpus-across-numa</code> 등의 policy option을 사용할 수 있습니다. (<a href="https://kubernetes.io/docs/tasks/administer-cluster/cpu-management-policies/?utm_source=chatgpt.com" title="Control CPU Management Policies on the Node | Kubernetes">Kubernetes</a>)</p>
<p>Memory Manager의 <code>Static</code> policy는 NUMA별 memory allocation hint를 Topology Manager에 제공하고 Guaranteed Pod의 메모리를 가능한 적은 NUMA node에 배치합니다. 이 기능은 Kubernetes 1.32부터 stable입니다. (<a href="https://kubernetes.io/docs/tasks/administer-cluster/memory-manager/?utm_source=chatgpt.com" title="Control Memory Management Policies on a Node | Kubernetes">Kubernetes</a>)</p>
<hr>
<h1 id="4-cpu-manager">4. CPU Manager</h1>
<p>가장 먼저:</p>
<pre><code class="language-yaml">cpuManagerPolicy: static</code></pre>
<p>을 권합니다.</p>
<p>그리고 production에서는:</p>
<pre><code class="language-yaml">cpuManagerPolicyOptions:
  full-pcpus-only: &quot;true&quot;</code></pre>
<p>를 우선 검토합니다.</p>
<p>즉:</p>
<pre><code class="language-text">CPU 0/1 = SMT sibling
CPU 2/3 = SMT sibling
CPU 4/5 = SMT sibling</code></pre>
<p>이라면 애플리케이션에 가능한 한:</p>
<pre><code class="language-text">2 physical cores
4 physical cores
8 physical cores
...</code></pre>
<p>단위로 배정하는 것입니다.</p>
<hr>
<h1 id="5-cpu-isolation도-같이-가져가야-합니다">5. CPU isolation도 같이 가져가야 합니다</h1>
<p>AIStor, StarRocks, CNPG 같은 latency-sensitive workload라면:</p>
<pre><code class="language-text">Housekeeping CPU
   ↓
kubelet
containerd
Cilium
IRQ
systemd
kernel threads

Workload CPU
   ↓
AIStor
StarRocks
CNPG
Spark</code></pre>
<p>로 분리하는 것이 좋습니다.</p>
<p>RHEL 10의 <code>cpu-partitioning</code> TuneD profile은 low-latency workload에서 housekeeping CPU와 isolated CPU를 분리하도록 설계되어 있습니다. (<a href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/pdf/monitoring_and_managing_system_status_and_performance/Red_Hat_Enterprise_Linux-10-Monitoring_and_managing_system_status_and_performance-en-US.pdf?utm_source=chatgpt.com" title="Red Hat Enterprise Linux 10 Monitoring and managing system status and performance">레드햇 문서</a>)</p>
<p>예를 들어 64 physical cores라면 단순화해서:</p>
<pre><code class="language-text">CPU 0-3       housekeeping
CPU 4-31      NUMA 0 workload
CPU 32-35     housekeeping
CPU 36-63     NUMA 1 workload</code></pre>
<p>같은 구조를 고려할 수 있습니다.</p>
<p>다만 <strong>Cilium + NIC IRQ를 housekeeping CPU에 전부 몰아버리는 것도 좋지 않습니다.</strong></p>
<p>특히 25/100Gbps NIC라면 IRQ 처리량이 상당하기 때문입니다.</p>
<hr>
<h1 id="6-topology-manager">6. Topology Manager</h1>
<p>AIStor/StarRocks/CNPG처럼 NUMA locality가 중요한 workload에는:</p>
<pre><code class="language-yaml">topologyManagerPolicy: single-numa-node</code></pre>
<p>를 적용할 수 있습니다.</p>
<p>그러면:</p>
<pre><code class="language-text">Pod 요구사항

CPU : NUMA 0
Memory : NUMA 0
Device : NUMA 0

       ↓

Admission = PASS</code></pre>
<p>반대로:</p>
<pre><code class="language-text">CPU : NUMA 0
Memory : NUMA 1
Device : NUMA 0

       ↓

Admission = FAIL</code></pre>
<p>이런 식으로 <strong>remote NUMA allocation을 예방</strong>할 수 있습니다.</p>
<p>Kubernetes는 <code>single-numa-node</code> 외에도 <code>restricted</code>, <code>best-effort</code> 등을 제공하며, topology manager는 CPU/memory/device plugin 등의 topology hints를 조합합니다. (<a href="https://kubernetes.io/docs/tasks/administer-cluster/topology-manager/?utm_source=chatgpt.com" title="Control Topology Management Policies on a node | Kubernetes">Kubernetes</a>)</p>
<h3 id="그런데-여기서-중요한-점">그런데 여기서 중요한 점</h3>
<p><strong>모든 Pod에 single-numa-node를 적용하지 마세요.</strong></p>
<p>예를 들어:</p>
<pre><code class="language-text">Polaris
OPA
Keycloak
Kafka
small CNPG
small Spark driver
monitoring</code></pre>
<p>같은 workload는 NUMA 때문에 scheduling failure가 발생하는 것보다 전체 cluster utilization이 더 중요할 수 있습니다.</p>
<p>그래서 저는 <strong>node pool별 또는 workload별 전략</strong>을 추천합니다.</p>
<hr>
<h1 id="7-권장-kubernetes-정책">7. 권장 Kubernetes 정책</h1>
<p>제가 현재 환경이라면 대략 이렇게 나눕니다.</p>
<table>
<thead>
<tr>
<th>Workload</th>
<th>CPU Manager</th>
<th>Memory Manager</th>
<th>Topology</th>
</tr>
</thead>
<tbody><tr>
<td>AIStor</td>
<td>static</td>
<td>static</td>
<td>single-numa</td>
</tr>
<tr>
<td>StarRocks BE</td>
<td>static</td>
<td>static</td>
<td>single-numa</td>
</tr>
<tr>
<td>CNPG</td>
<td>static</td>
<td>static</td>
<td>single-numa</td>
</tr>
<tr>
<td>대형 Spark executor</td>
<td>static</td>
<td>static</td>
<td>single-numa</td>
</tr>
<tr>
<td>일반 Spark executor</td>
<td>static</td>
<td>static</td>
<td>restricted/best-effort</td>
</tr>
<tr>
<td>Polaris</td>
<td>일반</td>
<td>일반</td>
<td>best-effort</td>
</tr>
<tr>
<td>OPA</td>
<td>일반</td>
<td>일반</td>
<td>best-effort</td>
</tr>
<tr>
<td>monitoring</td>
<td>일반</td>
<td>일반</td>
<td>best-effort</td>
</tr>
</tbody></table>
<hr>
<h1 id="8-pod는-반드시-guaranteed-qos를-적극-활용">8. Pod는 반드시 Guaranteed QoS를 적극 활용</h1>
<p>NUMA locality가 중요한 Pod는:</p>
<pre><code class="language-yaml">resources:
  requests:
    cpu: &quot;16&quot;
    memory: &quot;64Gi&quot;
  limits:
    cpu: &quot;16&quot;
    memory: &quot;64Gi&quot;</code></pre>
<p>처럼 <strong>request = limit</strong>로 만드세요.</p>
<p>그러면:</p>
<pre><code class="language-text">QoS = Guaranteed</code></pre>
<p>가 되고 CPU Manager/Memory Manager/Topology Manager가 제대로 작동할 수 있습니다.</p>
<hr>
<h1 id="9-memory-manager">9. Memory Manager</h1>
<p>kubelet:</p>
<pre><code class="language-yaml">memoryManagerPolicy: Static</code></pre>
<p>을 권합니다.</p>
<p>그리고 NUMA별 reserved memory를 명확하게 잡습니다.</p>
<p>예:</p>
<pre><code class="language-yaml">reservedMemory:
- numaNode: 0
  limits:
    memory: &quot;4Gi&quot;
- numaNode: 1
  limits:
    memory: &quot;4Gi&quot;</code></pre>
<p>실제 값은 서버의 RAM과 system workload에 맞춰 잡아야 합니다.</p>
<p>Kubernetes 문서도 <code>Static</code> Memory Manager 사용 시 <code>reservedMemory</code>를 NUMA node별로 구성하도록 설명합니다. (<a href="https://kubernetes.io/docs/tasks/administer-cluster/memory-manager/?utm_source=chatgpt.com" title="Control Memory Management Policies on a Node | Kubernetes">Kubernetes</a>)</p>
<hr>
<h1 id="10-hugepages">10. HugePages</h1>
<p>두 번째로 중요한 것이 HugePages입니다.</p>
<p>특히:</p>
<ul>
<li>StarRocks</li>
<li>CNPG/PostgreSQL</li>
<li>Spark</li>
<li>고메모리 workload</li>
</ul>
<p>에서는 workload별로 테스트할 가치가 있습니다.</p>
<p>RHEL:</p>
<pre><code class="language-bash">grep Huge /proc/meminfo</code></pre>
<p>2MB hugepage:</p>
<pre><code class="language-bash">vm.nr_hugepages=...</code></pre>
<p>또는 Kubernetes에서:</p>
<pre><code class="language-yaml">resources:
  requests:
    hugepages-2Mi: 4Gi
  limits:
    hugepages-2Mi: 4Gi</code></pre>
<p>형태로 별도 resource로 관리합니다.</p>
<p>다만 <strong>모든 workload에 HugePages를 강제하지는 마세요.</strong></p>
<p>PostgreSQL은 <code>huge_pages=on</code>으로 explicit HugeTLB를 강제할 수 있고, 큰 shared memory 영역에서 page-table overhead를 줄일 수 있습니다. (<a href="https://www.postgresql.org/docs/current/runtime-config-resource.html?utm_source=chatgpt.com" title="PostgreSQL: Documentation: 18: 19.4. Resource Consumption">PostgreSQL</a>)</p>
<hr>
<h1 id="11-aistor가-가장-중요한-케이스">11. AIStor가 가장 중요한 케이스</h1>
<p>현재 환경에서는 AIStor가 NUMA optimization의 <strong>1순위</strong>라고 봅니다.</p>
<p>왜냐하면:</p>
<pre><code class="language-text">Application
    ↓
AIStor
    ↓
Network
    ↓
Disk</code></pre>
<p>이 모두 NUMA 영향을 받기 때문입니다.</p>
<p>AIStor 자체도 production hardware tuning에서 CPU governor를 <code>performance</code>로 설정하고, AVX2/SSE4.2와 충분한 physical core를 권장합니다. (<a href="https://docs.min.io/aistor/installation/checklists/hardware-tuning/?utm_source=chatgpt.com" title="System tuning | MinIO AIStor Documentation">MinIO AIStor Documentation</a>)</p>
<hr>
<h2 id="aistor의-이상적인-구조">AIStor의 이상적인 구조</h2>
<p>예:</p>
<pre><code class="language-text">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</code></pre>
<p>가능하면:</p>
<pre><code class="language-text">NIC NUMA == CPU NUMA == Disk NUMA</code></pre>
<p>로 맞춥니다.</p>
<hr>
<h1 id="12-aistor에서-특히-중요한-것은-nic-irq">12. AIStor에서 특히 중요한 것은 NIC IRQ</h1>
<p>여기서 Kubernetes보다 오히려 Linux tuning이 중요합니다.</p>
<p>확인:</p>
<pre><code class="language-bash">cat /proc/interrupts | grep -Ei &#39;ice|eth|bond&#39;</code></pre>
<p>그리고:</p>
<pre><code class="language-bash">ethtool -l &lt;E810&gt;
ethtool -x &lt;E810&gt;</code></pre>
<p>NIC RSS queue를 NUMA-local CPU에 배치합니다.</p>
<p>예:</p>
<pre><code class="language-text">NUMA 0:
  RX queue 0-15 → CPU 4-19

NUMA 1:
  RX queue 16-31 → CPU 36-51</code></pre>
<p>같은 구조입니다.</p>
<p>이렇게 해야:</p>
<pre><code class="language-text">NIC packet
   ↓
IRQ
   ↓
CPU
   ↓
AIStor thread</code></pre>
<p>가 NUMA-local해집니다.</p>
<hr>
<h1 id="13-cilium도-numa-관점에서-봐야-합니다">13. Cilium도 NUMA 관점에서 봐야 합니다</h1>
<p>현재 Cilium이:</p>
<pre><code class="language-text">native routing
BGP
ECMP
ClusterMesh</code></pre>
<p>이므로 상당히 좋은 출발점입니다.</p>
<p>Native routing은 encapsulation을 거치지 않고 Linux routing subsystem을 사용하기 때문에 overlay 방식보다 network overhead를 줄일 수 있습니다. (<a href="https://docs.cilium.io/en/stable/network/concepts/routing/?utm_source=chatgpt.com" title="Routing — Cilium 1.20.1 documentation">Cilium Documentation</a>)</p>
<p>하지만:</p>
<blockquote>
<p>Cilium native mode = NUMA 최적화 완료</p>
</blockquote>
<p>는 아닙니다.</p>
<hr>
<h1 id="14-cilium--nic-numa">14. Cilium + NIC NUMA</h1>
<p>특히 bond1이 내부 cluster traffic의 핵심이라면:</p>
<pre><code class="language-text">NUMA 0
  CPU
   ↑
 Cilium
   ↑
 bond1
   ↑
 NIC

NUMA 1
  CPU
   ↑
 Cilium
   ↑
 bond1
   ↑
 NIC</code></pre>
<p>처럼 맞춰주는 것이 좋습니다.</p>
<p>Cilium의 BPF host routing은 host stack의 일부 경로를 우회하여 network overhead를 줄이는 데 도움이 됩니다. Cilium 문서에서도 BPF host routing이 legacy routing 대비 성능상 이점을 목표로 한다고 설명합니다. (<a href="https://docs.cilium.io/en/latest/operations/performance/tuning/?utm_source=chatgpt.com" title="Tuning Guide — Cilium 1.21.0-dev documentation">Cilium Documentation</a>)</p>
<hr>
<h1 id="15-cilium-bgpecmp에서-주의할-것">15. Cilium BGP/ECMP에서 주의할 것</h1>
<p>현재:</p>
<pre><code class="language-text">Pod
 ↓
Cilium
 ↓
bond1
 ↓
BGP
 ↓
ECMP</code></pre>
<p>구조라면 NUMA보다 <strong>ECMP traffic distribution</strong>이 먼저 병목이 될 수도 있습니다.</p>
<p>따라서:</p>
<pre><code class="language-bash">ethtool -S &lt;nic&gt;
cat /proc/interrupts
ss -s
nstat</code></pre>
<p>와 함께:</p>
<pre><code class="language-text">NIC utilization
RX/TX queue distribution
CPU utilization per NUMA
softirq
TCP retransmit
Cilium drops</code></pre>
<p>를 같이 봐야 합니다.</p>
<hr>
<h1 id="16-bbr은-신중하게">16. BBR은 신중하게</h1>
<p>Cilium Bandwidth Manager를 사용하는 경우 BBR을 검토할 수 있지만, 현재 환경에서는 <strong>무조건 켜지는 않는 것</strong>을 권합니다.</p>
<p>Cilium은 Bandwidth Manager + BPF host routing을 통해 BBR을 사용할 수 있지만, BBR은 CUBIC보다 더 공격적일 수 있고 retransmission 증가 가능성도 문서에 명시되어 있습니다. (<a href="https://docs.cilium.io/en/latest/operations/performance/tuning/?utm_source=chatgpt.com" title="Tuning Guide — Cilium 1.21.0-dev documentation">Cilium Documentation</a>)</p>
<p>현재처럼:</p>
<pre><code class="language-text">AIStor
+
Spark
+
StarRocks
+
Cilium
+
25Gbps bond</code></pre>
<p>가 같이 움직이는 환경이라면 먼저:</p>
<pre><code class="language-text">CUBIC baseline</code></pre>
<p>을 잡고 BBR을 A/B test 하는 것이 좋습니다.</p>
<hr>
<h1 id="17-cnpgpostgresql">17. CNPG/PostgreSQL</h1>
<p>CNPG는 <strong>NUMA locality가 꽤 중요합니다.</strong></p>
<p>예:</p>
<pre><code class="language-text">NUMA 0
 ├─ CPU 0-15
 ├─ Memory
 └─ NVMe/WAL

      ↓

   PostgreSQL</code></pre>
<p>가능하면:</p>
<pre><code class="language-text">PostgreSQL CPU
=
PostgreSQL memory
=
WAL disk</code></pre>
<p>를 같은 NUMA에 맞춥니다.</p>
<p>특히 WAL disk가 NVMe라면 PCIe topology를 확인하세요.</p>
<pre><code class="language-bash">lspci -tv
cat /sys/block/nvme0n1/device/numa_node</code></pre>
<hr>
<h1 id="18-cnpg-pod-설정">18. CNPG Pod 설정</h1>
<p>예:</p>
<pre><code class="language-yaml">resources:
  requests:
    cpu: &quot;8&quot;
    memory: &quot;32Gi&quot;
  limits:
    cpu: &quot;8&quot;
    memory: &quot;32Gi&quot;</code></pre>
<p>그리고:</p>
<pre><code class="language-text">CPU Manager static
Memory Manager static
Topology Manager single-numa-node</code></pre>
<p>조합을 추천합니다.</p>
<hr>
<h1 id="19-postgresql-자체-tuning">19. PostgreSQL 자체 tuning</h1>
<p>기본적으로:</p>
<pre><code class="language-text">shared_buffers
work_mem
maintenance_work_mem
max_connections
max_worker_processes
max_parallel_workers
max_parallel_workers_per_gather</code></pre>
<p>를 NUMA/CPU 수와 함께 봐야 합니다.</p>
<p>예를 들어 NUMA 2개 × 32 physical core라면:</p>
<pre><code class="language-text">max_parallel_workers</code></pre>
<p>를 무작정 64로 잡는 것보다 workload benchmark로 결정해야 합니다.</p>
<p>그리고 PostgreSQL에는 NUMA별 shared-memory allocation을 확인할 수 있는 <code>pg_shmem_allocations_numa</code> view도 있습니다. (<a href="https://www.postgresql.org/docs/19/view-pg-shmem-allocations-numa.html?utm_source=chatgpt.com" title="PostgreSQL: Documentation: 19: 53.30. pg_shmem_allocations_numa">PostgreSQL</a>)</p>
<hr>
<h1 id="20-starrocks는-numa-최적화-효과가-상당히-클-수-있음">20. StarRocks는 NUMA 최적화 효과가 상당히 클 수 있음</h1>
<p>StarRocks BE는:</p>
<pre><code class="language-text">CPU-heavy
+
memory-heavy
+
local disk
+
network</code></pre>
<p>성격이라 NUMA-aware configuration의 효과가 큰 편입니다.</p>
<p>특히 현재 workload가:</p>
<pre><code class="language-text">StarRocks
  ↓
Iceberg
  ↓
AIStor</code></pre>
<p>구조라면:</p>
<pre><code class="language-text">StarRocks CPU
      ↓
Memory
      ↓
NIC
      ↓
AIStor</code></pre>
<p>경로 전체를 봐야 합니다.</p>
<hr>
<h1 id="21-starrocks-cpu-resource-group">21. StarRocks CPU resource group</h1>
<p>StarRocks에는 resource group에서 <code>exclusive_cpu_cores</code>를 사용하여 CPU isolation을 구성할 수 있습니다. 최근 버전에서는 hard CPU limit도 지원합니다. (<a href="https://docs.starrocks.io/docs/faq/resource_isolation_faq/?utm_source=chatgpt.com" title="Troubleshooting Resource Isolation | StarRocks">StarRocks Docs</a>)</p>
<p>예를 들어:</p>
<pre><code class="language-text">NUMA0
 ├─ CPU 4-15
 │    └─ StarRocks query
 │
 └─ CPU 16-23
      └─ compaction

NUMA1
 ├─ CPU 36-47
 │    └─ StarRocks query
 │
 └─ CPU 48-55
      └─ compaction</code></pre>
<p>같은 식으로 분리하는 것이 이상적입니다.</p>
<hr>
<h1 id="22-starrocks에서-특히-피해야-하는-것">22. StarRocks에서 특히 피해야 하는 것</h1>
<p>예:</p>
<pre><code class="language-text">Pod CPU request = 4
Pod limit = 32</code></pre>
<p>같은 Burstable 형태로 만들어놓고</p>
<blockquote>
<p>&quot;NUMA 최적화를 했다&quot;</p>
</blockquote>
<p>라고 생각하면 안 됩니다.</p>
<p>중요 BE는:</p>
<pre><code class="language-yaml">requests:
  cpu: &quot;16&quot;
  memory: &quot;64Gi&quot;
limits:
  cpu: &quot;16&quot;
  memory: &quot;64Gi&quot;</code></pre>
<p>처럼 명확하게 reservation하는 것이 좋습니다.</p>
<hr>
<h1 id="23-spark는-조금-다른-전략">23. Spark는 조금 다른 전략</h1>
<p>Spark는 AIStor/CNPG와 달리 <strong>무조건 single NUMA</strong>로 가면 오히려 손해일 수 있습니다.</p>
<p>예를 들어:</p>
<pre><code class="language-text">1 executor
64 cores
256GB</code></pre>
<p>를 하나의 NUMA에 넣으려고 하면:</p>
<pre><code class="language-text">NUMA 0 capacity = 32 cores / 256GB</code></pre>
<p>때문에 불가능합니다.</p>
<p>그보다는:</p>
<pre><code class="language-text">NUMA0
  Executor 1
  16 cores
  64GB

NUMA1
  Executor 2
  16 cores
  64GB</code></pre>
<p>처럼 <strong>executor를 NUMA 단위로 쪼개는 전략</strong>을 추천합니다.</p>
<hr>
<h1 id="24-spark-executor-sizing">24. Spark executor sizing</h1>
<p>예를 들어 2 NUMA / 32 physical core라고 하면:</p>
<pre><code class="language-text">BAD

1 executor
32 cores
128 GB</code></pre>
<p>보다:</p>
<pre><code class="language-text">GOOD

executor 1
16 cores
64 GB

executor 2
16 cores
64 GB</code></pre>
<p>가 NUMA locality 측면에서는 더 유리한 경우가 많습니다.</p>
<p>특히 shuffle-heavy workload에서는 이 차이가 커질 수 있습니다.</p>
<hr>
<h1 id="25-spark--kubernetes">25. Spark + Kubernetes</h1>
<p>Spark:</p>
<pre><code class="language-text">spark.executor.cores
spark.executor.memory
spark.executor.memoryOverhead</code></pre>
<p>를 K8s:</p>
<pre><code class="language-text">resources.requests.cpu
resources.limits.cpu
resources.requests.memory
resources.limits.memory</code></pre>
<p>와 일치시키는 방향으로 설계합니다.</p>
<p>예:</p>
<pre><code class="language-text">Spark executor
16 cores
64G heap
8G overhead

K8s
cpu request/limit = 16
memory request/limit = 72G</code></pre>
<p>처럼요.</p>
<p>이렇게 해야 Kubernetes가 해당 executor를 명확하게 하나의 NUMA domain에 배치하기 쉬워집니다.</p>
<hr>
<h1 id="26-openebs">26. OpenEBS</h1>
<p>OpenEBS에서는 <strong>Storage device topology</strong>가 핵심입니다.</p>
<p>특히 LocalPV를 사용한다면:</p>
<pre><code class="language-text">Pod
 ↓
LocalPV
 ↓
/dev/nvme0n1
 ↓
PCIe root complex
 ↓
NUMA 0</code></pre>
<p>를 확인해야 합니다.</p>
<p>즉:</p>
<pre><code class="language-text">Pod NUMA 0
+
Disk NUMA 0</code></pre>
<p>로 맞추세요.</p>
<hr>
<h1 id="27-cnpg--openebs-조합">27. CNPG + OpenEBS 조합</h1>
<p>현재 환경에서 제가 가장 중요하게 보는 조합입니다.</p>
<pre><code class="language-text">CNPG Pod
   │
   ├── CPU NUMA 0
   ├── Memory NUMA 0
   │
   ▼
OpenEBS LocalPV
   │
   ▼
NVMe NUMA 0</code></pre>
<p>이렇게 되면:</p>
<pre><code class="language-text">PostgreSQL
   ↓
shared_buffers
   ↓
WAL
   ↓
NVMe</code></pre>
<p>경로가 local NUMA가 됩니다.</p>
<p>반대로:</p>
<pre><code class="language-text">PostgreSQL CPU NUMA0
       ↓
Memory NUMA0
       ↓
NVMe NUMA1</code></pre>
<p>이면 remote PCIe/NUMA access가 생길 수 있습니다.</p>
<hr>
<h1 id="28-polaris--opa">28. Polaris + OPA</h1>
<p>이쪽은 NUMA 최적화 우선순위가 낮습니다.</p>
<p>예:</p>
<pre><code class="language-text">Polaris
OPA
Keycloak
API
controller
operator</code></pre>
<p>는:</p>
<pre><code class="language-text">NUMA locality</code></pre>
<p>보다:</p>
<pre><code class="language-text">HA
availability
scheduling
resource efficiency</code></pre>
<p>가 더 중요합니다.</p>
<p>따라서 일반 Burstable QoS를 허용해도 됩니다.</p>
<hr>
<h1 id="29-전체적으로-workload를-3개-tier로-나누는-것을-추천">29. 전체적으로 workload를 3개 tier로 나누는 것을 추천</h1>
<h3 id="tier-1--numa-critical">Tier 1 — NUMA critical</h3>
<pre><code class="language-text">AIStor
StarRocks BE
CNPG
large Spark executor</code></pre>
<p>정책:</p>
<pre><code class="language-text">CPUManager static
MemoryManager static
TopologyManager single-numa-node
Guaranteed QoS
CPU exclusive
NUMA-local storage
NUMA-local NIC</code></pre>
<hr>
<h3 id="tier-2--numa-preferred">Tier 2 — NUMA preferred</h3>
<pre><code class="language-text">Spark executor
Kafka
Redis
heavy Trino</code></pre>
<p>정책:</p>
<pre><code class="language-text">CPUManager static
MemoryManager static
TopologyManager restricted
Guaranteed/Burstable</code></pre>
<hr>
<h3 id="tier-3--numa-agnostic">Tier 3 — NUMA agnostic</h3>
<pre><code class="language-text">Polaris
OPA
Keycloak
operators
monitoring
controllers</code></pre>
<p>정책:</p>
<pre><code class="language-text">normal scheduling
Burstable
best-effort</code></pre>
<hr>
<h1 id="30-그리고-node-pool도-분리하는-것을-강하게-추천">30. 그리고 Node Pool도 분리하는 것을 강하게 추천</h1>
<p>현재 플랫폼 규모를 생각하면 저는:</p>
<pre><code class="language-text">general-worker
storage-worker
database-worker
analytics-worker</code></pre>
<p>정도로 논리적인 node pool을 만드는 것을 권합니다.</p>
<p>예:</p>
<pre><code class="language-text">storage-worker
  ├─ AIStor
  └─ storage-related

database-worker
  └─ CNPG

analytics-worker
  ├─ StarRocks
  └─ Spark

general-worker
  ├─ Polaris
  ├─ OPA
  └─ controllers</code></pre>
<p>이렇게 하면 NUMA tuning이 훨씬 쉬워집니다.</p>
<hr>
<h1 id="31-cpu-reserved-영역도-workload별로-다르게">31. CPU reserved 영역도 workload별로 다르게</h1>
<p>예를 들어 64 physical core 서버라면:</p>
<pre><code class="language-text">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</code></pre>
<p>같은 구조를 생각할 수 있습니다.</p>
<p>다만 이 숫자는 예시이고 실제 Xeon의:</p>
<pre><code class="language-text">socket
NUMA
SMT
NIC PCIe
disk PCIe</code></pre>
<p>구조를 보고 정해야 합니다.</p>
<hr>
<h1 id="32-nic-irq와-cpu를-반드시-함께-튜닝">32. NIC IRQ와 CPU를 반드시 함께 튜닝</h1>
<p>이 부분은 지금 환경에서 상당히 중요합니다.</p>
<p>현재:</p>
<pre><code class="language-text">Intel E810
bond1
25Gbps × 2
Cilium
AIStor
K8s internal traffic</code></pre>
<p>이므로 다음을 측정해야 합니다.</p>
<pre><code class="language-bash">cat /proc/interrupts
mpstat -P ALL 1
sar -n DEV 1
ethtool -S &lt;nic&gt;</code></pre>
<p>그리고:</p>
<pre><code class="language-bash">cat /proc/softirqs</code></pre>
<p>에서 <code>NET_RX</code>, <code>NET_TX</code>도 확인합니다.</p>
<hr>
<h1 id="33-numa-tuning에서-가장-많이-하는-실수">33. NUMA tuning에서 가장 많이 하는 실수</h1>
<h3 id="❌-cpu만-pinning">❌ CPU만 pinning</h3>
<pre><code class="language-text">CPU NUMA0
Memory NUMA1
NIC NUMA1</code></pre>
<p>이면 효과가 제한됩니다.</p>
<h3 id="❌-memory만-numa-binding">❌ Memory만 NUMA binding</h3>
<p>CPU가 다른 NUMA에서 실행되면 remote access가 발생합니다.</p>
<h3 id="❌-disk-topology-무시">❌ Disk topology 무시</h3>
<p>특히 NVMe는 PCIe root complex 때문에 NUMA locality가 중요합니다.</p>
<h3 id="❌-nic-irq-무시">❌ NIC IRQ 무시</h3>
<p>AIStor/StarRocks 같은 network-heavy workload에서는 상당히 큰 차이를 만들 수 있습니다.</p>
<h3 id="❌-모든-pod를-single-numa-node">❌ 모든 Pod를 single-numa-node</h3>
<p>작은 Pod가 scheduling failure를 일으키고 cluster fragmentation이 심해집니다.</p>
<hr>
<h1 id="34-제가-가장-추천하는-최종-구조">34. 제가 가장 추천하는 최종 구조</h1>
<pre><code class="language-text">                   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     │
│                                                     │
└─────────────────────────────────────────────────────┘</code></pre>
<p>그리고:</p>
<pre><code class="language-text">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
  어디든</code></pre>
<p>처럼 <strong>locality를 workload 단위로 유지</strong>합니다.</p>
<hr>
<h1 id="35-우선순위를-정한다면">35. 우선순위를 정한다면</h1>
<p>현재 환경에서는 저는 다음 순서로 작업하겠습니다.</p>
<table>
<thead>
<tr>
<th align="right">순위</th>
<th>항목</th>
<th>중요도</th>
</tr>
</thead>
<tbody><tr>
<td align="right">1</td>
<td>BIOS/NUMA/PCIe topology 확인</td>
<td>★★★★★</td>
</tr>
<tr>
<td align="right">2</td>
<td>CPU Manager static</td>
<td>★★★★★</td>
</tr>
<tr>
<td align="right">3</td>
<td>Memory Manager static</td>
<td>★★★★★</td>
</tr>
<tr>
<td align="right">4</td>
<td>Topology Manager</td>
<td>★★★★★</td>
</tr>
<tr>
<td align="right">5</td>
<td>Guaranteed QoS</td>
<td>★★★★★</td>
</tr>
<tr>
<td align="right">6</td>
<td>NIC IRQ/RSS NUMA affinity</td>
<td>★★★★★</td>
</tr>
<tr>
<td align="right">7</td>
<td>OpenEBS disk NUMA affinity</td>
<td>★★★★★</td>
</tr>
<tr>
<td align="right">8</td>
<td>AIStor CPU/NIC/Disk locality</td>
<td>★★★★★</td>
</tr>
<tr>
<td align="right">9</td>
<td>CNPG CPU/Memory/WAL locality</td>
<td>★★★★☆</td>
</tr>
<tr>
<td align="right">10</td>
<td>StarRocks CPU/memory/cache locality</td>
<td>★★★★☆</td>
</tr>
<tr>
<td align="right">11</td>
<td>Spark executor NUMA sizing</td>
<td>★★★★☆</td>
</tr>
<tr>
<td align="right">12</td>
<td>HugePages</td>
<td>★★★☆☆</td>
</tr>
<tr>
<td align="right">13</td>
<td>CPU isolation/TuneD</td>
<td>★★★★☆</td>
</tr>
<tr>
<td align="right">14</td>
<td>Cilium BPF/network tuning</td>
<td>★★★★☆</td>
</tr>
<tr>
<td align="right">15</td>
<td>Polaris/OPA NUMA tuning</td>
<td>★☆☆☆☆</td>
</tr>
</tbody></table>
<hr>
<h2 id="특히-현재-환경에서는-한-단계-더-가는-것을-추천">특히 현재 환경에서는 한 단계 더 가는 것을 추천</h2>
<p>지금처럼 <strong>Intel Xeon + RHEL 10.2 + Cilium + AIStor + CNPG + OpenEBS + StarRocks + Spark</strong>가 있는 환경이라면, 단순히 kubelet 설정만 하는 것보다 <strong>“NUMA topology profile”을 노드 표준으로 만드는 것</strong>이 좋습니다.</p>
<p>예를 들어 각 노드마다 다음을 자동 수집해서:</p>
<pre><code class="language-text">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</code></pre>
<p>이 정보를 기준으로</p>
<pre><code class="language-text">Kubelet
  ↓
CPUManager
MemoryManager
TopologyManager
  ↓
Pod
  ↓
OpenEBS
  ↓
Disk

Pod
  ↓
Cilium
  ↓
NIC Queue/IRQ</code></pre>
<p>까지 연결하는 방식입니다.</p>
<p><strong>특히 사용자 환경에서는 AIStor + StarRocks + CNPG + Spark를 같은 NUMA 정책으로 취급하면 안 됩니다.</strong> AIStor/CNPG는 locality를 강하게 잡고, StarRocks는 resource group까지 활용하고, Spark는 executor 크기를 NUMA domain에 맞춰 쪼개는 방식이 가장 합리적입니다.</p>
<p>원하시면 다음 단계로는 <strong>현재 사용 중인 RHEL 10.2 + K8s 1.33.4 + Cilium 1.18.4 기준으로 실제 적용 가능한 <code>KubeletConfiguration + TuneD + GRUB/kernel parameter + systemd + IRQ/RSS + Pod YAML + CNPG + StarRocks + Spark</code> 전체 설정 템플릿</strong>을 하나의 표준안으로 만들어 드리는 게 좋습니다. 특히 <strong>64/96/128 physical-core Xeon 서버 각각에 대해 NUMA0/NUMA1 CPU 번호를 어떻게 나누고, 몇 개를 housekeeping/NIC IRQ/AIStor/StarRocks 등에 예약할지</strong>까지 산정할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S05z1]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S05z1</link>
            <guid>https://velog.io/@youngkyoo_kim/26S05z1</guid>
            <pubDate>Fri, 04 Sep 2026 20:27:17 GMT</pubDate>
            <description><![CDATA[<h1 id="rhel-102--kubespray-k8s-클러스터-numa-최적화-전략">RHEL 10.2 + Kubespray K8s 클러스터 NUMA 최적화 전략</h1>
<h2 id="대상-환경-요약">대상 환경 요약</h2>
<ul>
<li>OS: RHEL 10.2 (Intel Xeon, 멀티소켓 = 멀티 NUMA)</li>
<li>배포: Kubespray</li>
<li>CNI: Cilium (Native Routing, BGP, ECMP, ClusterMesh)</li>
<li>Storage: MinIO AIStor, OpenEBS</li>
<li>DB/Runtime: CNPG(PostgreSQL), StarRocks, Spark</li>
<li>Catalog/Policy: Polaris, OPA</li>
</ul>
<p>핵심 원칙은 하나입니다: <strong>&quot;CPU 코어 - 메모리 - PCIe 디바이스(NIC/NVMe)가 같은 NUMA 노드 안에서 다뤄지도록 정렬한다.&quot;</strong> 이게 깨지면(cross-NUMA access) QPI/UPI를 건너는 메모리 접근이 발생해 지연시간·대역폭이 눈에 띄게 나빠집니다.</p>
<hr>
<h2 id="0-사전-진단--하드웨어-토폴로지-파악">0. 사전 진단 — 하드웨어 토폴로지 파악</h2>
<p>모든 튜닝의 출발점입니다. 이 결과를 바탕으로 이후 모든 설정값(코어 범위, 포트 매핑 등)이 정해집니다.</p>
<pre><code class="language-bash">lscpu                     # 소켓/코어/스레드, NUMA node별 CPU 범위
numactl --hardware        # NUMA node별 메모리 용량, node간 거리(distance)
lstopo --of txt           # (hwloc-gui 패키지) CPU-PCIe-NIC-NVMe 물리 연결 트리
lspci -vvv -t             # PCIe 슬롯의 NUMA 소속 확인
for d in /sys/class/net/*; do echo $d: $(cat /sys/class/net/$d/device/numa_node 2&gt;/dev/null); done
for d in /sys/class/nvme/*; do echo $d: $(cat /sys/class/nvme/$d/device/numa_node 2&gt;/dev/null); done</code></pre>
<p>이 단계에서 <strong>NIC(ConnectX-6/E810류)와 NVMe가 NUMA node 0/1 중 어디 붙어있는지</strong>를 노드별로 표로 정리해두세요. 서버 벤더/슬롯마다 다를 수 있어 전수 조사가 필요합니다.</p>
<hr>
<h2 id="1-rhel-102-os-레벨-설정">1. RHEL 10.2 OS 레벨 설정</h2>
<h3 id="1-1-tuned-프로파일">1-1. tuned 프로파일</h3>
<pre><code class="language-bash">sudo dnf install -y tuned tuned-profiles-cpu-partitioning
sudo tuned-adm profile throughput-performance
# 워크로드 격리(CPU pinning)까지 필요하면:
sudo tuned-adm profile cpu-partitioning</code></pre>
<ul>
<li><code>cpu-partitioning</code>은 <code>isolated_cores=</code> 설정으로 커널 스케줄러/interrupt에서 특정 코어를 제외시켜 K8s CPU Manager의 static 정책과 조합했을 때 지연시간 편차(jitter)를 크게 줄여줍니다. StarRocks BE, Spark executor처럼 CPU-heavy 워크로드가 도는 노드에 우선 적용을 권장합니다.</li>
</ul>
<h3 id="1-2-hugepages-사전-할당">1-2. Hugepages 사전 할당</h3>
<pre><code class="language-bash"># /etc/sysctl.d/99-hugepages.conf
vm.nr_hugepages = &lt;NUMA당 필요량 합산&gt;</code></pre>
<p>NUMA-aware하게 노드별로 나누고 싶다면 부팅 파라미터로:</p>
<pre><code>hugepagesz=1G hugepages=&lt;N&gt; default_hugepagesz=1G</code></pre><p>CNPG(PostgreSQL)의 shared_buffers, StarRocks의 벡터화 엔진, Spark의 off-heap 메모리가 hugepage 사용 시 TLB miss가 줄어 이득이 큽니다.</p>
<h3 id="1-3-nicnvme-irq-affinity를-numa에-정렬">1-3. NIC/NVMe IRQ affinity를 NUMA에 정렬</h3>
<pre><code class="language-bash"># irqbalance는 기본적으로 cross-NUMA 분산도 허용하므로, 트래픽이 많은 NIC는
# irqbalance 대상에서 제외하고 수동 pinning을 권장
systemctl status irqbalance

# NIC가 물려있는 NUMA node의 CPU 목록만 사용해 큐/IRQ를 정렬 (예시)
ethtool -l &lt;iface&gt;                       # 큐 개수 확인
ethtool -L &lt;iface&gt; combined &lt;N&gt;          # 해당 NUMA의 코어 수에 맞춤
ethtool -N &lt;iface&gt; rx-flow-hash tcp4 sdfn
# 각 큐의 IRQ를 해당 NUMA CPU에 개별 pinning (smp_affinity_list)</code></pre>
<p>NVMe도 동일한 원리로, <code>/sys/block/nvme*/mq/*/cpu_list</code>가 해당 디스크의 NUMA 코어와 정렬되어 있는지 확인합니다 (보통 커널이 자동 정렬하지만 멀티 컨트롤러/멀티 네임스페이스 구성에서는 어긋나는 경우가 있어 확인 필요).</p>
<h3 id="1-4-커널-numa-balancing-정책">1-4. 커널 NUMA balancing 정책</h3>
<pre><code class="language-bash"># 지연시간에 민감한 DB/분석 워크로드는 자동 밸런싱이 오히려 스레드를 이리저리
# 옮기며 성능 편차를 만들 수 있어, 명시적 pinning(2장 이후)을 쓸 거라면 끄는 걸 권장
echo 0 | sudo tee /proc/sys/kernel/numa_balancing</code></pre>
<p>반대로 애플리케이션 레벨 pinning을 전혀 하지 않는 범용 워크로드 노드라면 켜두는 편이 낫습니다 — <strong>노드 역할별로 다르게 적용</strong>하세요.</p>
<hr>
<h2 id="2-kuberneteskubelet-레벨--topology-manager--cpu-manager--memory-manager">2. Kubernetes(kubelet) 레벨 — Topology Manager / CPU Manager / Memory Manager</h2>
<p>Kubespray inventory(<code>group_vars/k8s_cluster/k8s-cluster.yml</code> 등)에 다음을 노드 그룹별로 다르게 지정할 수 있습니다.</p>
<pre><code class="language-yaml"># StarRocks BE, Spark executor, CNPG처럼 NUMA pinning이 이득인 노드 그룹
kubelet_cpu_manager_policy: static
kubelet_topology_manager_policy: single-numa-node   # 가장 엄격, 리소스가 한 NUMA node에 다 들어가야 Admit
kubelet_topology_manager_scope: pod                  # 컨테이너 단위가 아니라 pod 전체 기준 정렬
kubelet_memory_manager_policy: Static
kubelet_reserved_cpus: &quot;0,1,32,33&quot;   # NUMA별 system-reserved 코어를 균등 배분 (예시)
kubelet_config_extra_args:
  reservedMemory:
    - numaNode: 0
      limits:
        memory: 4Gi
    - numaNode: 1
      limits:
        memory: 4Gi</code></pre>
<p><strong>주의할 점</strong></p>
<ul>
<li><code>single-numa-node</code> 정책은 Pod가 요청한 CPU/메모리(+hugepages)가 <strong>하나의 NUMA node 용량을 넘으면 Admit 자체가 실패</strong>합니다. 따라서 StarRocks BE 등은 &quot;노드 전체 자원을 다 쓰는 거대한 Pod 1개&quot;가 아니라, <strong>NUMA node 1개당 Pod 1개씩(듀얼 소켓이면 노드당 2개 Pod)</strong>로 사이징하는 것이 정석입니다.</li>
<li>static CPU Manager는 <strong>Guaranteed QoS</strong>(requests == limits, CPU는 정수 단위) Pod에만 적용됩니다. Burstable/BestEffort Pod는 예외 없이 공유 코어 풀에서 동작합니다.</li>
<li>Cilium, OPA, Polaris 같은 컨트롤플레인성 서비스까지 static 정책 대상 노드에 함께 몰아넣으면 &quot;공유 코어 풀&quot;이 줄어들어 오히려 비효율적입니다 → <strong>워크로드 성격별로 노드 그룹(Node Pool)을 분리</strong>하는 것을 권장합니다(아래 11장 표 참고).</li>
</ul>
<h3 id="2-1-클러스터-전역-numa-aware-스케줄링-중요--놓치기-쉬운-부분">2-1. 클러스터 전역 NUMA-aware 스케줄링 (중요 — 놓치기 쉬운 부분)</h3>
<p>kubelet의 Topology Manager는 <strong>이미 선택된 노드 내부</strong>에서만 정렬을 결정합니다. kube-scheduler 자체는 기본적으로 NUMA 토폴로지를 모르기 때문에, &quot;이 Pod가 들어갈 수 있는 NUMA 여유가 있는 노드&quot;를 먼저 골라주지 못하고 Admit 실패 후 재시도만 반복될 수 있습니다.</p>
<p>이를 해결하려면 <code>kubernetes-sigs/scheduler-plugins</code>의 <strong>NodeResourceTopology(NRT)</strong> 플러그인을 추가로 배포하세요:</p>
<ul>
<li>각 노드에서 NUMA 토폴로지를 수집해 <code>NodeResourceTopology</code> CRD로 리포트하는 daemon(topology-updater) 설치</li>
<li>kube-scheduler에 <code>NodeResourceTopologyMatch</code> Filter/Score 플러그인 활성화</li>
<li>이렇게 하면 스케줄러가 애초에 &quot;이 Pod가 들어갈 NUMA 여유가 있는 노드&quot;만 골라 보내므로 Admit 실패로 인한 스케줄링 재시도/지연이 줄어듭니다.</li>
</ul>
<hr>
<h2 id="3-cilium-native-routing-bgp-ecmp-clustermesh-numa-최적화">3. Cilium (Native Routing, BGP, ECMP, ClusterMesh) NUMA 최적화</h2>
<ul>
<li><strong>NIC 큐/IRQ를 NUMA에 정렬</strong>하는 것이 Cilium 최적화의 8할입니다. eBPF datapath 자체는 패킷이 도착한 CPU(정확히는 NIC 큐가 매핑된 CPU)에서 실행되므로, 큐가 엉뚱한 NUMA에 있으면 매 패킷마다 cross-NUMA 메모리 접근이 발생합니다(1-3장 참고).<pre><code class="language-bash">helm upgrade cilium cilium/cilium --namespace kube-system \
--set routingMode=native \
--set bpf.masquerade=true \
--set kubeProxyReplacement=true \
--set bandwidthManager.enabled=true \
--set bpf.distributedLRU.enabled=true</code></pre>
</li>
<li><code>bandwidthManager</code>(BBR 등)와 <code>distributedLRU</code>(per-CPU 분산 LRU맵)는 멀티코어/멀티NUMA 환경에서 conntrack/정책 맵 접근의 lock contention을 줄여줍니다.</li>
<li><strong>BGP/ECMP</strong>: ECMP 자체는 라우팅 레벨 이슈라 NUMA와 직접적 연관은 적지만, ECMP로 여러 경로에 분산된 흐름이 서로 다른 NIC 큐/코어로 들어오면서 RSS 해시가 고르게 분산되는지(<code>ethtool -N ... rx-flow-hash tcp4 sdfn</code>) 함께 점검하세요. 분산이 고르지 않으면 특정 NUMA/코어만 과부하됩니다.</li>
<li><strong>ClusterMesh</strong>: 클러스터 간 트래픽은 결국 로컬 NIC를 다시 타므로 위 NIC 튜닝의 효과를 그대로 받습니다. 다만 ClusterMesh 자체(etcd 기반 상태 동기화)는 latency-sensitive한 컨트롤플레인이므로, 이 트래픽이 몰리는 노드(주로 컨트롤플레인 노드)는 static CPU Manager 대상에서 제외하고 공유 코어 풀에 남겨 스케줄링 유연성을 확보하는 편이 낫습니다.</li>
</ul>
<hr>
<h2 id="4-openebs--nvme-로컬-스토리지-numa">4. OpenEBS + NVMe 로컬 스토리지 NUMA</h2>
<p>Kubernetes의 device plugin 기반 Topology Manager는 GPU 등 device-plugin이 NUMA 힌트를 리포트하는 리소스에만 작동합니다. <strong>OpenEBS의 local PV(hostpath, LVM, ZFS 등)는 기본적으로 NUMA를 인식하지 못합니다</strong> — 여기가 수작업이 가장 많이 필요한 지점입니다.</p>
<p><strong>전략</strong></p>
<ol>
<li>노드별 NVMe 디스크를 0-1단계에서 파악한 NUMA 소속대로 그룹핑하고, 각 그룹을 별도의 OpenEBS StorageClass/PoolConfig로 분리 (예: <code>openebs-lvm-numa0</code>, <code>openebs-lvm-numa1</code>)</li>
<li>노드에 커스텀 라벨 부여:<pre><code class="language-bash">kubectl label node &lt;node&gt; numa0-storage=true
kubectl label node &lt;node&gt; numa1-storage=true</code></pre>
</li>
<li>StatefulSet/Pod의 <code>nodeAffinity</code> + <code>volumeBindingMode: WaitForFirstConsumer</code>를 조합해, <strong>CPU가 pinning될 NUMA와 같은 NUMA의 디스크를 쓰는 PVC</strong>가 매칭되도록 강제</li>
<li>StarRocks BE, Spark shuffle 등 로컬 스토리지 I/O가 많은 워크로드는 이 매칭이 특히 중요합니다 — cross-NUMA로 디스크 I/O가 발생하면 CPU pinning 효과가 상당 부분 상쇄됩니다.</li>
</ol>
<hr>
<h2 id="5-minio-aistor-numa">5. MinIO AIStor NUMA</h2>
<ul>
<li>AIStor(MinIO 계열)는 프로세스 1개가 여러 드라이브를 관리하는 구조이므로, <strong>드라이브 풀을 NUMA 단위로 나누고, 서버 프로세스/Pod도 NUMA당 1개씩 배치</strong>하는 것이 정석입니다(멀티소켓 서버 1대에 AIStor 인스턴스 2개를 각각 NUMA0/NUMA1 전용으로 pinning).</li>
<li>Pod 배치 시 CPU Manager static + <code>single-numa-node</code> 정책을 적용하고, 해당 Pod가 사용하는 드라이브(OpenEBS local PV 또는 direct hostPath)도 동일 NUMA 소속인지 4장 방식으로 강제하세요.</li>
<li>네트워크 측면에서는 대용량 오브젝트 업/다운로드가 몰리므로, AIStor Pod가 배치된 NUMA와 <strong>외부 통신에 쓰이는 NIC의 NUMA가 일치</strong>하는지가 성능에 크게 영향을 줍니다 (3장의 NIC IRQ 정렬과 직결).</li>
<li>멀티 인스턴스로 분리할 경우 erasure coding set 구성이 인스턴스 경계와 어떻게 맞물리는지 AIStor 자체 문서 기준으로 별도 검증이 필요합니다 (드라이브 풀 분리가 EC set/장애 도메인 설계에 영향을 줄 수 있음).</li>
</ul>
<hr>
<h2 id="6-cnpg-postgresql-numa">6. CNPG (PostgreSQL) NUMA</h2>
<ul>
<li>CNPG가 생성하는 PostgreSQL Pod에 <strong>Guaranteed QoS</strong>(CPU/메모리 requests=limits)를 명시적으로 설정 → kubelet static CPU/Memory Manager가 자동으로 단일 NUMA에 pinning 시도<pre><code class="language-yaml">resources:
requests: { cpu: &quot;8&quot;, memory: &quot;32Gi&quot; }
limits:   { cpu: &quot;8&quot;, memory: &quot;32Gi&quot; }</code></pre>
</li>
<li><code>shared_buffers</code>, <code>effective_cache_size</code>는 이 Pod가 정렬될 NUMA node의 메모리 용량을 넘지 않게 설계 (넘으면 Admit 실패 또는 실제로는 cross-NUMA 스필오버 발생)</li>
<li>Hugepages 사용 권장 (<code>postgresql.parameters.huge_pages: try</code> 또는 <code>on</code>) — PostgreSQL 공유메모리 세그먼트가 커질수록 TLB miss 감소 효과가 커집니다.</li>
<li>커널 <code>numa_balancing</code>은 위 1-4에서 언급한 대로 이런 pinned DB 워크로드에는 끄는 것을 권장합니다.</li>
<li>WAL/데이터 디스크가 OpenEBS local PV라면 4장 원칙에 따라 같은 NUMA의 NVMe를 쓰도록 정렬하세요.</li>
</ul>
<hr>
<h2 id="7-starrocks-분석-엔진-numa">7. StarRocks (분석 엔진) NUMA</h2>
<ul>
<li>BE(Backend)는 CPU/메모리 집약적 벡터화 엔진이라 NUMA 효과가 가장 크게 나타나는 컴포넌트입니다.</li>
<li><strong>권장 배치</strong>: 듀얼소켓 노드 1대당 BE Pod 2개(NUMA0용, NUMA1용)로 나눠 배치. 거대한 BE Pod 1개로 노드 전체 자원을 요청하면 <code>single-numa-node</code> 정책에서 Admit이 실패하거나(정책 미적용 시) 내부적으로 cross-NUMA 접근이 발생합니다.</li>
<li>CPU/메모리 requests=limits(Guaranteed) + hugepages + local NVMe(4장 매칭) 조합이 기본 세트입니다.</li>
<li>FE(Frontend, 메타데이터/쿼리플래너)는 상대적으로 NUMA 민감도가 낮아 static 정책 대상에서 제외하고 공유 코어 풀에 둬도 무방합니다.</li>
<li>데이터 이동(compaction, shuffle)이 많은 워크로드 특성상, BE가 쓰는 로컬 디스크와 CPU NUMA 정합성이 어긋나면 성능 저하가 특히 두드러지니 4장 스토리지 정렬을 꼭 함께 적용하세요.</li>
</ul>
<hr>
<h2 id="8-spark-on-kubernetes-numa">8. Spark on Kubernetes NUMA</h2>
<ul>
<li>Executor 사이징 원칙: <strong><code>spark.executor.cores</code>가 노드의 NUMA당 코어 수를 넘지 않도록</strong> 설계 (예: NUMA당 32코어면 executor는 최대 28~30코어 정도로, 나머지는 kube-reserved/system-reserved에)</li>
<li>Executor Pod에 Guaranteed QoS 부여 → static CPU Manager가 단일 NUMA에 pinning</li>
<li>Off-heap 메모리(<code>spark.memory.offHeap.enabled=true</code>) 사용 시 hugepages와 결합하면 GC/TLB 오버헤드 감소</li>
<li>Shuffle 데이터가 로컬 NVMe(OpenEBS)에 쓰인다면, executor가 pinning된 NUMA와 셔플 디스크의 NUMA를 일치시키는 것이 4장과 동일한 원리로 중요합니다.</li>
<li>Driver Pod는 상대적으로 가볍고 latency-sensitive 하지 않은 경우가 많아 static 정책 미적용 노드(공유 코어 풀)에 둬도 무방합니다 — executor만 NUMA-aware 노드 그룹에 스케줄되도록 <code>nodeSelector</code>/taint-toleration을 분리하세요.</li>
</ul>
<hr>
<h2 id="9-polaris카탈로그-opa--낮은-우선순위">9. Polaris(카탈로그), OPA — 낮은 우선순위</h2>
<ul>
<li>두 컴포넌트 모두 경량 컨트롤플레인 서비스(메타데이터 서빙, 정책 평가)로 처리량이 CPU-bound 대용량 연산이 아니라 <strong>NUMA pinning 이득이 크지 않습니다.</strong></li>
<li>오히려 이런 서비스를 static CPU Manager 대상 노드에 함께 배치하면 공유 코어 풀을 잠식해 다른 워크로드의 유연성을 해칩니다 → <strong>별도의 일반 노드 그룹(공유 코어 풀 유지)에 배치</strong>하는 것을 권장합니다.</li>
<li>다만 Polaris가 대량의 메타데이터 조회 트래픽(예: Spark/StarRocks에서 매우 빈번한 카탈로그 조회)을 받는 규모라면, 최소한 <strong>네트워크 NIC NUMA 정렬(3장)</strong> 정도는 적용해볼 가치가 있습니다.</li>
</ul>
<hr>
<h2 id="10-워크로드-성격별-노드-그룹-분리-전략-요약">10. 워크로드 성격별 노드 그룹 분리 전략 (요약)</h2>
<table>
<thead>
<tr>
<th>노드 그룹</th>
<th>대상 워크로드</th>
<th>CPU Manager</th>
<th>Topology Manager</th>
<th>Hugepages</th>
<th>로컬 NVMe 정렬</th>
</tr>
</thead>
<tbody><tr>
<td>numa-pinned-compute</td>
<td>StarRocks BE, Spark executor</td>
<td>static</td>
<td>single-numa-node</td>
<td>필요</td>
<td>필수(4장)</td>
</tr>
<tr>
<td>numa-pinned-storage</td>
<td>MinIO AIStor, CNPG</td>
<td>static</td>
<td>single-numa-node</td>
<td>권장</td>
<td>필수(4장)</td>
</tr>
<tr>
<td>general-shared</td>
<td>Cilium(agent 자체), Polaris, OPA, StarRocks FE, Spark driver</td>
<td>none(기본)</td>
<td>none</td>
<td>불필요</td>
<td>불필요</td>
</tr>
<tr>
<td>control-plane</td>
<td>kube-apiserver, etcd, ClusterMesh 관련</td>
<td>none</td>
<td>none</td>
<td>불필요</td>
<td>불필요</td>
</tr>
</tbody></table>
<p>노드 그룹은 kubespray inventory의 host group으로 나누고, 그룹별로 <code>kubelet_cpu_manager_policy</code> 등을 다르게 지정하면 됩니다. 동일 물리 서버 스펙이라도 <strong>라벨/taint로 워크로드를 분리</strong>해서 &quot;NUMA pinning이 필요한 워크로드 전용 노드&quot;와 &quot;범용 노드&quot;를 명확히 나누는 것이 운영 복잡도 대비 효과가 가장 좋습니다.</p>
<hr>
<h2 id="11-검증-및-모니터링">11. 검증 및 모니터링</h2>
<pre><code class="language-bash"># Pod에 실제로 할당된 cpuset이 기대한 NUMA와 일치하는지 확인
crictl inspect &lt;container-id&gt; | grep -A5 cpuset
cat /sys/fs/cgroup/kubepods.slice/.../cpuset.cpus

# NUMA 노드 간 메모리 접근/미스 통계
numastat -p &lt;pid&gt;
numastat -m

# cross-NUMA 메모리 접근이 실제로 성능에 영향을 주는지 (perf 설치 필요)
perf stat -e node-loads,node-load-misses -p &lt;pid&gt;

# NIC IRQ가 기대한 CPU/NUMA에 정렬되어 있는지
cat /proc/interrupts | grep &lt;iface&gt;</code></pre>
<ul>
<li>Node Exporter의 <code>node_memory_numa_*</code> 계열 지표, DCGM(GPU 있는 경우), StarRocks/Spark 자체 메트릭(쿼리 지연, GC 시간)을 함께 대시보드화해 튜닝 전후 비교 기준선을 반드시 남겨두세요.</li>
<li><code>NodeResourceTopology</code> CRD를 배포했다면 <code>kubectl get noderesourcetopology -o yaml</code>로 클러스터가 인식한 실제 NUMA 여유 자원도 주기적으로 확인하세요.</li>
</ul>
<hr>
<h2 id="12-고려사항-및-리스크">12. 고려사항 및 리스크</h2>
<ol>
<li><strong>자원 효율 vs 성능의 트레드오프</strong>: static CPU Manager + Guaranteed QoS는 필연적으로 코어를 통째로 예약하므로 <strong>클러스터 전체 CPU 활용률(bin-packing 효율)이 떨어집니다.</strong> 모든 워크로드에 무분별하게 적용하지 말고, 실측으로 이득이 확인된 컴포넌트(StarRocks BE, CNPG, AIStor)에 한정하세요.</li>
<li><strong>Admit 실패 리스크</strong>: <code>single-numa-node</code> 정책은 요구 자원이 한 NUMA를 넘으면 Pod가 아예 뜨지 않습니다. 초기 사이징을 넉넉히 잡았다가 실패를 겪는 경우가 흔하니, NUMA당 실제 가용 코어/메모리를 정확히 계산해 Pod spec을 설계하세요 (10장 표와 0장 진단 결과 활용).</li>
<li><strong>kubespray 기본값은 NUMA 최적화가 꺼져 있음</strong>: 위 설정들은 대부분 inventory에서 명시적으로 켜야 합니다. 클러스터 배포 후 재설정 시 kubelet 재시작이 필요하고, 이미 떠 있는 Pod들은 재스케줄되어야 새 정책이 적용됩니다 — <strong>초기 클러스터 구축 단계에서 미리 반영</strong>하는 것이 롤아웃 리스크가 훨씬 적습니다.</li>
<li><strong>OpenEBS local PV의 NUMA 비인식</strong>: 4장에서 언급했듯 OpenEBS 자체는 NUMA를 모르므로, 라벨/nodeAffinity로 강제하는 수작업 설계가 어긋나면 &quot;CPU는 NUMA0인데 디스크는 NUMA1&quot; 같은 상황이 조용히 발생할 수 있습니다. 배포 후 정기적으로 실제 매칭 여부를 점검하는 자동화(스크립트/알림)를 갖추길 권장합니다.</li>
<li><strong>하이퍼스레딩(SMT) 처리</strong>: static CPU Manager는 기본적으로 sibling 스레드를 함께 할당하는 방식이 아니므로, 지연시간에 매우 민감한 워크로드라면 <code>cpu-manager-policy-options</code>의 <code>full-pcpus-only=true</code> 옵션을 검토하세요(물리 코어 단위로만 pinning되어 SMT 간섭을 줄임 — 단, 가용 코어 수 계산이 더 보수적이 됨).</li>
<li><strong>ClusterMesh/BGP의 노드 간 지연</strong>: NUMA 튜닝은 단일 노드 내부 최적화이고, 멀티클러스터/BGP 피어링 구간의 네트워크 지연은 별개 문제입니다. 두 최적화를 혼동해 &quot;NUMA 튜닝했는데 왜 크로스 클러스터 쿼리가 느리지&quot;라는 식으로 원인을 잘못 짚지 않도록, 문제 발생 시 로컬 NUMA 이슈인지 네트워크 경로 이슈인지 먼저 구분하는 진단 순서를 세워두세요.</li>
<li><strong>버전 호환성</strong>: <code>scheduler-plugins</code>의 NodeResourceTopology, Cilium의 <code>distributedLRU</code>/<code>bpf.datapathMode=netkit</code> 등은 비교적 최근 기능이라 사용 중인 Kubernetes/Cilium 버전에서 실제 지원되는지 배포 전 반드시 릴리스 노트로 재확인하세요.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S05y]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S05y</link>
            <guid>https://velog.io/@youngkyoo_kim/26S05y</guid>
            <pubDate>Fri, 04 Sep 2026 20:25:35 GMT</pubDate>
            <description><![CDATA[<h1 id="sftpgo-pod-배포--aistors3-연동-구성-가이드">SFTPGo Pod 배포 &amp; AIStor(S3) 연동 구성 가이드</h1>
<h2 id="0-아키텍처-개요-및-핵심-결정사항">0. 아키텍처 개요 및 핵심 결정사항</h2>
<pre><code>[클라이언트] --FTP(21/2121)+Passive Data Port--&gt; [SFTPGo Pod] --S3 API--&gt; [AIStor]
                                                        |
                                              [메타데이터 DB: SQLite/PostgreSQL]</code></pre><p><strong>먼저 결정해야 할 것 3가지</strong></p>
<ol>
<li><strong>FTP passive mode 노출 방식</strong>: FTP는 제어 채널(21) 외에 데이터 채널을 매번 별도 포트로 협상합니다(passive mode). 쿠버네티스에서는 이 포트 range를 그대로 열어줘야 하므로, 일반 <code>ClusterIP</code> Service로는 동작하지 않습니다. → <strong>NodePort(범위 지정) 또는 hostNetwork/hostPort</strong> 방식이 필요합니다. (가능하다면 SFTP가 NAT/방화벽 친화적이라 더 낫지만, 요구사항이 FTP이므로 이 가이드는 FTP 기준으로 작성합니다.)</li>
<li><strong>메타데이터 저장소(data provider)</strong>: 기본은 SQLite(파일 기반)라 Pod가 1개(replica=1)로 제한되고 PVC가 필요합니다. 이중화/스케일 아웃이 필요하면 PostgreSQL 등 외부 DB를 권장합니다. 이 가이드는 단일 Pod + SQLite(PVC) 기준으로 작성하고, 외부 DB 전환 방법도 함께 안내합니다.</li>
<li><strong>S3 자격증명 관리</strong>: AIStor access key/secret은 반드시 Kubernetes <code>Secret</code>으로 관리하고, ConfigMap이나 평문 env로 노출하지 않습니다.</li>
</ol>
<hr>
<h2 id="1-namespace-및-secret-생성">1. Namespace 및 Secret 생성</h2>
<pre><code class="language-bash">kubectl create namespace sftpgo</code></pre>
<p><strong>AIStor 접속 정보 Secret</strong></p>
<pre><code class="language-bash">kubectl -n sftpgo create secret generic aistor-s3-credentials \
  --from-literal=access_key=&#39;&lt;AISTOR_ACCESS_KEY&gt;&#39; \
  --from-literal=access_secret=&#39;&lt;AISTOR_SECRET_KEY&gt;&#39;</code></pre>
<p><strong>SFTPGo 초기 관리자 계정 Secret</strong></p>
<pre><code class="language-bash">kubectl -n sftpgo create secret generic sftpgo-admin-credentials \
  --from-literal=admin_user=&#39;admin&#39; \
  --from-literal=admin_password=&#39;&lt;강력한-비밀번호&gt;&#39;</code></pre>
<hr>
<h2 id="2-configmap-sftpgo-설정-오버라이드">2. ConfigMap: SFTPGo 설정 오버라이드</h2>
<p>SFTPGo는 기본적으로 env var(<code>SFTPGO_섹션__키</code> 형태)로 대부분 설정을 오버라이드할 수 있습니다. FTP 활성화 + passive IP 설정이 핵심입니다.</p>
<pre><code class="language-yaml"># configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sftpgo-config
  namespace: sftpgo
data:
  SFTPGO_FTPD__BINDINGS__0__PORT: &quot;2121&quot;
  SFTPGO_FTPD__BINDINGS__0__FORCE_PASSIVE_IP: &quot;&lt;노드의 외부 접근 가능 IP 또는 LB IP&gt;&quot;
  SFTPGO_FTPD__PASSIVE_PORT_RANGE__START: &quot;50000&quot;
  SFTPGO_FTPD__PASSIVE_PORT_RANGE__END: &quot;50100&quot;
  # 데이터 제공자 (기본 SQLite 유지 시 아래 생략 가능)
  SFTPGO_DATA_PROVIDER__DRIVER: &quot;sqlite&quot;
  SFTPGO_DATA_PROVIDER__NAME: &quot;/srv/sftpgo/data/sftpgo.db&quot;
  # 최초 admin 자동 생성 (선택)
  SFTPGO_DEFAULT_ADMIN__USERNAME: &quot;admin&quot;</code></pre>
<blockquote>
<p><code>FORCE_PASSIVE_IP</code>는 FTP passive mode의 핵심입니다. 클라이언트가 데이터 채널을 열 때 서버가 &quot;이 IP로 접속해&quot;라고 알려주는 값인데, Pod 내부 IP를 그대로 주면 외부에서 접근이 불가능합니다. <strong>NodePort를 쓴다면 해당 Node의 외부 IP</strong>, LoadBalancer를 쓴다면 <strong>그 LB의 IP</strong>를 지정해야 합니다.</p>
</blockquote>
<hr>
<h2 id="3-deployment">3. Deployment</h2>
<pre><code class="language-yaml"># deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sftpgo
  namespace: sftpgo
spec:
  replicas: 1               # SQLite 사용 시 반드시 1 유지
  strategy:
    type: Recreate           # PVC 단일 마운트이므로 롤링 업데이트 대신 Recreate
  selector:
    matchLabels:
      app: sftpgo
  template:
    metadata:
      labels:
        app: sftpgo
    spec:
      containers:
        - name: sftpgo
          image: drakkan/sftpgo:v2.6.7   # 배포 시점 최신 안정 버전으로 확인 후 지정 권장
          envFrom:
            - configMapRef:
                name: sftpgo-config
          env:
            - name: SFTPGO_DEFAULT_ADMIN__PASSWORD
              valueFrom:
                secretKeyRef:
                  name: sftpgo-admin-credentials
                  key: admin_password
          ports:
            - containerPort: 8080   # Web/REST API
            - containerPort: 2022   # SFTP (필요 없으면 제거 가능)
            - containerPort: 2121   # FTP 제어 채널
            - containerPort: 50000  # Passive 포트 range 시작
              # K8s ports는 단일 포트만 명시 가능하므로 range는 Service/방화벽에서 처리
          volumeMounts:
            - name: sftpgo-data
              mountPath: /srv/sftpgo/data
          resources:
            requests:
              cpu: &quot;250m&quot;
              memory: &quot;256Mi&quot;
            limits:
              cpu: &quot;1&quot;
              memory: &quot;1Gi&quot;
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 15
      volumes:
        - name: sftpgo-data
          persistentVolumeClaim:
            claimName: sftpgo-data-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sftpgo-data-pvc
  namespace: sftpgo
spec:
  accessModes: [&quot;ReadWriteOnce&quot;]
  resources:
    requests:
      storage: 5Gi</code></pre>
<hr>
<h2 id="4-service--ftp-passive-port-노출-가장-까다로운-부분">4. Service — FTP Passive Port 노출 (가장 까다로운 부분)</h2>
<h3 id="옵션-a-nodeport-권장-온프레미스-환경">옵션 A. NodePort (권장, 온프레미스 환경)</h3>
<pre><code class="language-yaml"># service.yaml
apiVersion: v1
kind: Service
metadata:
  name: sftpgo
  namespace: sftpgo
spec:
  type: NodePort
  selector:
    app: sftpgo
  ports:
    - name: web
      port: 8080
      targetPort: 8080
    - name: ftp-control
      port: 2121
      targetPort: 2121
      nodePort: 30121
    - name: passive-50000
      port: 50000
      targetPort: 50000
      nodePort: 30000
    # NodePort Service는 포트를 하나씩 나열해야 함 — range 전체(50000~50100)를
    # 매핑하려면 101개 port 항목이 필요하거나, 아래 &quot;옵션 B(hostNetwork)&quot;가 현실적입니다.</code></pre>
<blockquote>
<p><strong>중요</strong>: 표준 K8s Service는 포트 range를 한 번에 지정할 수 없습니다(각 포트를 개별 선언해야 함). Passive port를 50~100개 단위로 열려면 YAML이 매우 길어집니다. 실무에서는 아래 <strong>옵션 B(hostNetwork)</strong>를 더 많이 사용합니다.</p>
</blockquote>
<h3 id="옵션-b-hostnetwork--hostport-실무-권장">옵션 B. hostNetwork + hostPort (실무 권장)</h3>
<pre><code class="language-yaml"># deployment.yaml 의 spec.template.spec 에 추가
spec:
  hostNetwork: true
  dnsPolicy: ClusterFirstWithHostNet
  containers:
    - name: sftpgo
      ports:
        - containerPort: 2121
          hostPort: 2121
        - containerPort: 8080
          hostPort: 8080
      # passive range는 컨테이너 내부 -e 로 전달한 50000-50100과
      # 노드 자체의 방화벽 정책만 맞으면 hostNetwork 특성상 그대로 통과됩니다.</code></pre>
<ul>
<li><code>hostNetwork: true</code>를 쓰면 Pod가 노드의 네트워크 스택을 직접 사용하므로 passive port range가 별도 매핑 없이 그대로 열립니다. 다만 <strong>해당 노드에는 SFTPGo Pod가 1개만 뜰 수 있고(포트 충돌), 노드 방화벽에서 50000-50100/tcp를 직접 열어야</strong> 합니다.</li>
<li>이 경우 <code>FORCE_PASSIVE_IP</code>는 그 <strong>노드의 IP</strong>로 지정합니다.</li>
<li><code>nodeSelector</code>로 특정 노드에 고정 배치하는 것을 권장합니다.</li>
</ul>
<pre><code class="language-yaml">      nodeSelector:
        sftpgo-node: &quot;true&quot;</code></pre>
<hr>
<h2 id="5-sftpgo-사용자-생성--s3aistor-백엔드-연결-초기화-job">5. SFTPGo 사용자 생성 + S3(AIStor) 백엔드 연결 (초기화 Job)</h2>
<p>관리자 계정이 뜬 뒤, REST API로 &quot;AIStor를 백엔드로 쓰는 FTP 업로드 전용 사용자&quot;를 자동 생성하는 Job입니다.</p>
<pre><code class="language-yaml"># init-user-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: sftpgo-init-user
  namespace: sftpgo
spec:
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: init-user
          image: curlimages/curl:8.10.1
          env:
            - name: ADMIN_USER
              valueFrom: { secretKeyRef: { name: sftpgo-admin-credentials, key: admin_user } }
            - name: ADMIN_PASS
              valueFrom: { secretKeyRef: { name: sftpgo-admin-credentials, key: admin_password } }
            - name: S3_ACCESS_KEY
              valueFrom: { secretKeyRef: { name: aistor-s3-credentials, key: access_key } }
            - name: S3_ACCESS_SECRET
              valueFrom: { secretKeyRef: { name: aistor-s3-credentials, key: access_secret } }
          command: [&quot;/bin/sh&quot;, &quot;-c&quot;]
          args:
            - |
              set -e
              BASE=http://sftpgo.sftpgo.svc.cluster.local:8080/api/v2

              # 1) 관리자 토큰 발급
              TOKEN=$(curl -s -u &quot;$ADMIN_USER:$ADMIN_PASS&quot; &quot;$BASE/token&quot; | sed -n &#39;s/.*&quot;access_token&quot;:&quot;\([^&quot;]*\)&quot;.*/\1/p&#39;)

              # 2) FTP 업로드용 사용자 생성 (S3 필터시스템, AIStor 엔드포인트 연결)
              curl -s -X POST &quot;$BASE/users&quot; \
                -H &quot;Authorization: Bearer $TOKEN&quot; \
                -H &quot;Content-Type: application/json&quot; \
                -d &#39;{
                  &quot;username&quot;: &quot;gpu-uploader&quot;,
                  &quot;password&quot;: &quot;&#39;&quot;&lt;업로드-계정-비밀번호&gt;&quot;&#39;&quot;,
                  &quot;status&quot;: 1,
                  &quot;permissions&quot;: { &quot;/&quot;: [&quot;*&quot;] },
                  &quot;filesystem&quot;: {
                    &quot;provider&quot;: 1,
                    &quot;s3config&quot;: {
                      &quot;bucket&quot;: &quot;&lt;AISTOR_BUCKET_NAME&gt;&quot;,
                      &quot;region&quot;: &quot;us-east-1&quot;,
                      &quot;access_key&quot;: &quot;&#39;&quot;$S3_ACCESS_KEY&quot;&#39;&quot;,
                      &quot;access_secret&quot;: &quot;&#39;&quot;$S3_ACCESS_SECRET&quot;&#39;&quot;,
                      &quot;endpoint&quot;: &quot;https://&lt;AISTOR_ENDPOINT_HOST&gt;:&lt;PORT&gt;&quot;,
                      &quot;force_path_style&quot;: true,
                      &quot;key_prefix&quot;: &quot;gpu-node-uploads/&quot;
                    }
                  }
                }&#39;</code></pre>
<p><strong>S3 config에서 특히 신경 써야 할 값</strong></p>
<ul>
<li><code>endpoint</code>: AIStor의 S3 API 엔드포인트 (예: <code>https://aistor.internal.example.com:9000</code>). AWS S3가 아니므로 반드시 명시.</li>
<li><code>force_path_style: true</code>: AIStor/MinIO 계열은 대부분 virtual-hosted style이 아닌 <strong>path-style</strong>(<code>https://endpoint/bucket/key</code>)을 요구합니다. 빠뜨리면 버킷 인식 실패가 흔한 오류입니다.</li>
<li><code>region</code>: AIStor가 특정 region 문자열을 요구하지 않더라도 SDK가 빈 값을 거부하는 경우가 있어 <code>us-east-1</code> 등 임의 값을 넣는 것이 안전합니다.</li>
<li><code>key_prefix</code>: 이 사용자가 버킷 내 특정 경로 하위에서만 동작하게 제한하고 싶을 때 사용 (일종의 chroot).</li>
</ul>
<hr>
<h2 id="6-보안tls-고려사항">6. 보안/TLS 고려사항</h2>
<ul>
<li>FTP는 기본적으로 <strong>평문 프로토콜</strong>입니다. 인증정보와 파일 내용이 그대로 노출되므로, 가능하면 <strong>FTPS(TLS)</strong>를 활성화하세요:<pre><code class="language-yaml">SFTPGO_FTPD__BINDINGS__0__TLS_MODE: &quot;1&quot;   # 1=explicit, 2=implicit</code></pre>
이 경우 인증서/키를 Secret으로 마운트하고 <code>SFTPGO_FTPD__CERTIFICATE_FILE</code>, <code>SFTPGO_FTPD__CERTIFICATE_KEY_FILE</code> 환경변수로 경로를 지정합니다.</li>
<li>GPU node → SFTPGo Pod 구간이 아직 private망이 아니라면(이전 논의 참고), 이 FTP 트래픽도 임시 경로(외부망)를 탈 가능성이 있습니다 — TLS 미적용 시 자격증명 평문 노출 리스크가 커지므로, 임시 구간에서는 <strong>FTPS 필수 적용을 권장</strong>합니다.</li>
<li>AIStor access key는 버킷 단위로 최소 권한(해당 업로드 버킷/prefix에만 <code>PutObject</code>, <code>ListBucket</code> 등)으로 발급하는 것을 권장합니다.</li>
</ul>
<hr>
<h2 id="7-배포-및-검증-절차">7. 배포 및 검증 절차</h2>
<pre><code class="language-bash">kubectl apply -f configmap.yaml
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl -n sftpgo rollout status deployment/sftpgo
kubectl apply -f init-user-job.yaml
kubectl -n sftpgo logs job/sftpgo-init-user</code></pre>
<p><strong>테스트 체크리스트</strong></p>
<ul>
<li><input disabled="" type="checkbox"> <code>kubectl -n sftpgo get pods</code> — Pod Running 상태 확인</li>
<li><input disabled="" type="checkbox"> Web Admin 접속 확인 (<code>http://&lt;node-ip&gt;:8080/web/admin</code>)</li>
<li><input disabled="" type="checkbox"> FTP 클라이언트(lftp, FileZilla 등)로 접속 테스트:<pre><code class="language-bash">lftp -u gpu-uploader,&lt;비밀번호&gt; ftp://&lt;FORCE_PASSIVE_IP&gt;:2121
put testfile.bin</code></pre>
</li>
<li><input disabled="" type="checkbox"> 업로드한 파일이 <strong>AIStor 버킷에서 실제로 확인되는지</strong> (AIStor 콘솔 또는 <code>mc ls</code>/<code>aws s3 ls</code>로 직접 검증)</li>
<li><input disabled="" type="checkbox"> 대용량 파일(모델 체크포인트 크기 급) 업로드 시 passive port timeout 없이 완료되는지 확인</li>
<li><input disabled="" type="checkbox"> Pod 재시작 후에도 SQLite 데이터(PVC)와 사용자 설정이 유지되는지 확인</li>
<li><input disabled="" type="checkbox"> (TLS 적용 시) FTPS 접속이 인증서 오류 없이 되는지 확인</li>
</ul>
<hr>
<h2 id="8-향후-확장-시-고려사항">8. 향후 확장 시 고려사항</h2>
<ul>
<li><strong>다중 replica가 필요해지면</strong>: SQLite → PostgreSQL로 data provider 전환 (<code>SFTPGO_DATA_PROVIDER__DRIVER=postgresql</code> 등), StatefulSet 대신 다수의 stateless Deployment 운용 가능.</li>
<li><strong>hostNetwork 대신 순수 K8s networking을 쓰고 싶다면</strong>: FTP 대신 SFTP(단일 포트 2022)로 전환하는 것을 재고해볼 가치가 있습니다 — GPU node에서 AIStor로의 업로드가 자동화 스크립트/파이프라인이라면 SFTP나 S3 API 직접 호출이 K8s 운영 관점에서 훨씬 단순합니다.</li>
<li><strong>모니터링</strong>: SFTPGo는 Prometheus <code>/metrics</code> 엔드포인트를 제공하므로 업로드 처리량/실패율을 모니터링 스택에 편입해두면 이후 Phase 4/5(Storage 연동 테스트, 성능 테스트)에서 유용합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S05x]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S05x</link>
            <guid>https://velog.io/@youngkyoo_kim/26S05x</guid>
            <pubDate>Fri, 04 Sep 2026 20:07:57 GMT</pubDate>
            <description><![CDATA[<p>네. <strong>SFTPGo를 MinIO AIStor 앞단의 FTP/FTPS/SFTP Gateway로 두는 방식은 꽤 괜찮은 아키텍처</strong>라고 봅니다. 특히 지금 말씀하신 것처럼 <strong>기존 클라이언트가 S3 API가 아니라 FTP 계열로 파일을 올려야 하는 요구</strong>라면, AIStor 자체에 FTP 기능을 억지로 붙이는 것보다 SFTPGo를 별도 계층으로 두는 게 훨씬 깔끔합니다.</p>
<p>SFTPGo는 S3-compatible backend를 지원하고, 공식 문서에도 <strong>AIStor/MinIO를 S3 backend로 직접 연결하는 설정</strong>이 명시되어 있습니다. (<a href="https://docs.sftpgo.com/enterprise/s3-compatible-services/?utm_source=chatgpt.com" title="S3-compatible services - SFTPGo docs">SFTPGo Docs</a>)</p>
<h3 id="제가-추천하는-구조">제가 추천하는 구조</h3>
<pre><code class="language-text">                   External / Legacy Client
                           |
                    FTP / FTPS / SFTP
                           |
                           v
                +----------------------+
                |       SFTPGo         |
                |                      |
                | FTP/FTPS             |
                | SFTP                 |
                | User/Auth/ACL        |
                | Quota/Logging        |
                +----------+-----------+
                           |
                         S3 API
                           |
                           v
                +----------------------+
                |      AIStor          |
                |      MinIO           |
                |                      |
                | Bucket               |
                |   └── project-A/     |
                |   └── project-B/     |
                |   └── ...            |
                +----------------------+</code></pre>
<p>SFTPGo가 <strong>filesystem을 제공하는 게 아니라 S3 backend를 직접 storage로 사용</strong>할 수 있기 때문에, 중간에 별도의 NFS/PV 같은 staging filesystem을 둘 필요가 없습니다. (<a href="https://docs.sftpgo.com/enterprise/documentation-map/?utm_source=chatgpt.com" title="Documentation Map - SFTPGo docs">SFTPGo Docs</a>)</p>
<hr>
<h2 id="특히-지금-환경에서는-장점이-큽니다">특히 지금 환경에서는 장점이 큽니다</h2>
<h3 id="1-aistor와-ftp를-분리할-수-있음">1. AIStor와 FTP를 분리할 수 있음</h3>
<p>제가 보기엔 이게 가장 큰 장점입니다.</p>
<pre><code class="language-text">AIStor
 └─ S3 API
     ↑
     |
   SFTPGo
     ↑
     |
 FTP/FTPS/SFTP clients</code></pre>
<p>AIStor는 원래 잘하는 <strong>S3/Object Storage 역할</strong>에 집중시키고,</p>
<p>SFTPGo는</p>
<ul>
<li>FTP</li>
<li>FTPS</li>
<li>SFTP</li>
<li>사용자 관리</li>
<li>접근제어</li>
<li>connection 제한</li>
<li>audit/logging</li>
<li>필요하면 antivirus/DLP</li>
</ul>
<p>등을 담당하게 합니다.</p>
<p>SFTPGo 자체가 여러 protocol과 storage backend를 추상화하는 MFT 제품이라 이 용도에 상당히 잘 맞습니다. (<a href="https://docs.sftpgo.com/?utm_source=chatgpt.com" title="SFTPGo docs">SFTPGo Docs</a>)</p>
<hr>
<h2 id="2-bucketprefix별-사용자-격리가-가능">2. Bucket/prefix별 사용자 격리가 가능</h2>
<p>예를 들어 AIStor가</p>
<pre><code class="language-text">bucket: customer-data

customer-data/
 ├── companyA/
 ├── companyB/
 ├── companyC/
 └── companyD/</code></pre>
<p>라면 SFTPGo에서</p>
<pre><code class="language-text">FTP user: companyA
        ↓
S3 bucket: customer-data
S3 key prefix: companyA/

FTP user: companyB
        ↓
S3 bucket: customer-data
S3 key prefix: companyB/</code></pre>
<p>처럼 구성할 수 있습니다.</p>
<p>SFTPGo의 <strong>Key Prefix</strong>가 특정 사용자를 bucket 내부의 특정 path로 제한하는 용도로 제공됩니다. (<a href="https://docs.sftpgo.com/enterprise/initial-configuration/?utm_source=chatgpt.com" title="Initial Setup - SFTPGo docs">SFTPGo Docs</a>)</p>
<p>이건 앞서 이야기하셨던 <strong>bucket 하나 아래 prefix를 project별로 사용하는 구조</strong>와도 상당히 잘 맞습니다.</p>
<hr>
<h2 id="3-ftp가-필요하면-ftp보다-ftps를-추천">3. FTP가 필요하면 &quot;FTP&quot;보다 &quot;FTPS&quot;를 추천</h2>
<p>여기서는 조금 중요합니다.</p>
<p>일반 FTP:</p>
<pre><code class="language-text">FTP
21/tcp
+
PASV ports</code></pre>
<p>는 ID/password와 데이터가 암호화되지 않습니다.</p>
<p>따라서 외부 client가 FTP밖에 지원하지 않는 특별한 이유가 없다면:</p>
<pre><code class="language-text">FTPS Explicit TLS</code></pre>
<p>또는</p>
<pre><code class="language-text">SFTP</code></pre>
<p>를 권합니다.</p>
<p>SFTPGo는 FTP/FTPS를 제대로 지원하고 passive port range도 설정할 수 있습니다. (<a href="https://docs.sftpgo.com/enterprise/ftp/?utm_source=chatgpt.com" title="FTP/FTPS - SFTPGo docs">SFTPGo Docs</a>)</p>
<p>예:</p>
<pre><code class="language-text">Internet / DMZ
       |
       | TCP 21
       | TCP 50000-50100
       v
   LoadBalancer
       |
       v
   SFTPGo pods
       |
       | HTTPS/S3
       v
     AIStor</code></pre>
<p>Kubernetes 환경이라면 <strong>SFTPGo를 Deployment로 HA 구성</strong>하는 것도 가능합니다.</p>
<hr>
<h1 id="그런데-한-가지-중요한-문제가-있습니다">그런데 한 가지 중요한 문제가 있습니다</h1>
<p><strong>성능입니다.</strong></p>
<p>S3 client가 직접 AIStor에 업로드하는 경우:</p>
<pre><code class="language-text">Client
   |
   | S3 multipart
   v
AIStor</code></pre>
<p>인데,</p>
<p>SFTPGo를 넣으면:</p>
<pre><code class="language-text">Client
   |
   | FTP
   v
SFTPGo
   |
   | S3
   v
AIStor</code></pre>
<p>가 됩니다.</p>
<p>즉 <strong>SFTPGo가 protocol translation gateway</strong>가 됩니다.</p>
<p>따라서 대용량 파일을 많이 올리는 환경에서는:</p>
<pre><code class="language-text">Client → SFTPGo → AIStor</code></pre>
<p>의 SFTPGo가 병목이 될 수 있습니다.</p>
<p>특히 현재처럼 AIStor 자체의 network/storage throughput이 상당히 큰 환경에서는 이 부분을 반드시 benchmark해야 합니다.</p>
<hr>
<h1 id="그래도-sftpgo가-상당히-괜찮은-이유">그래도 SFTPGo가 상당히 괜찮은 이유</h1>
<p>S3 backend에서 단순히 파일을 받아 local filesystem에 저장했다가 다시 AIStor로 copy하는 구조가 아닙니다.</p>
<p>SFTPGo가 S3 backend를 직접 사용할 수 있고, S3 backend는 multipart transfer도 지원합니다. (<a href="https://docs.sftpgo.com/Enterprise/features/?utm_source=chatgpt.com" title="Features - SFTPGo docs">SFTPGo Docs</a>)</p>
<p>그래서 개념적으로:</p>
<pre><code class="language-text">FTP DATA stream
       ↓
     SFTPGo
       ↓
S3 multipart upload
       ↓
     AIStor</code></pre>
<p>로 처리할 수 있습니다.</p>
<p><strong>중간에 별도의 persistent local storage를 두지 않는 구조</strong>를 추천합니다.</p>
<hr>
<h1 id="오히려-sftpgo를-kubernetes에-넣는-게-좋습니다">오히려 SFTPGo를 Kubernetes에 넣는 게 좋습니다</h1>
<p>현재 환경이라면 저는 이런 식으로 설계하겠습니다.</p>
<pre><code class="language-text">                   External
                      |
                 LB / Ingress
                      |
             +--------+--------+
             |                 |
         SFTPGo-1          SFTPGo-2
             |                 |
             +--------+--------+
                      |
                   S3 API
                      |
                AIStor Service
                      |
                AIStor cluster</code></pre>
<p>그리고 SFTPGo는:</p>
<pre><code class="language-text">Deployment
  replicas: 2~3

Service
  TCP 21
  TCP 22
  passive ports

Config/DB
  PostgreSQL</code></pre>
<p>형태로 운영합니다.</p>
<p>다만 <strong>FTP passive mode 때문에 일반 HTTP Ingress만 사용하는 것은 적합하지 않습니다.</strong> TCP LoadBalancer/NodePort 계층을 설계해야 합니다.</p>
<hr>
<h1 id="또-하나-좋은-점-upload-staging">또 하나 좋은 점: upload staging</h1>
<p>SFTPGo에는 upload mode가 있습니다.</p>
<p>예를 들어 atomic upload를 사용하면:</p>
<pre><code class="language-text">client
   |
   | upload
   v
temporary object
   |
   | upload complete
   v
final object</code></pre>
<p>처럼 <strong>업로드 중인 파일을 최종 파일로 노출하지 않는 방식</strong>을 사용할 수 있습니다. SFTPGo는 일반/atomic/atomic+resume 등의 upload mode를 제공합니다. (<a href="https://docs.sftpgo.com/enterprise/config-file/?utm_source=chatgpt.com" title="Configuration file - SFTPGo docs">SFTPGo Docs</a>)</p>
<p>이건 외부 업체가 FTP로 파일을 올리는 환경에서 상당히 유용합니다.</p>
<p>예를 들어:</p>
<pre><code class="language-text">/report.csv</code></pre>
<p>를 받는 동안 다른 application이 그것을 읽으면 문제가 될 수 있는데,</p>
<pre><code class="language-text">report.csv.tmp
        ↓
upload complete
        ↓
report.csv</code></pre>
<p>같은 publish 모델을 만들 수 있습니다.</p>
<hr>
<h1 id="그리고-보안-측면에서도-꽤-좋습니다">그리고 보안 측면에서도 꽤 좋습니다</h1>
<p>SFTPGo에는 단순 FTP server 이상의 기능이 있습니다.</p>
<p>예를 들어:</p>
<pre><code class="language-text">Client
  |
  | FTPS
  v
SFTPGo
  |
  +-- Authentication
  +-- IP restriction
  +-- User restriction
  +-- Prefix restriction
  +-- Transfer limits
  +-- Audit
  +-- Event Manager
  |
  v
AIStor</code></pre>
<p>Enterprise 기능까지 사용한다면 ICAP 기반 antivirus/DLP 검사도 upload 전에 적용할 수 있습니다. (<a href="https://docs.sftpgo.com/Enterprise/execute-before-file-publish/?utm_source=chatgpt.com" title="Execute Before File Publish - SFTPGo docs">SFTPGo Docs</a>)</p>
<hr>
<h1 id="제가-가장-신경-쓸-부분은-aistor-권한">제가 가장 신경 쓸 부분은 &quot;AIStor 권한&quot;</h1>
<p>이 부분은 반드시 잘 설계해야 합니다.</p>
<p>예를 들어 SFTPGo가 AIStor에 접근하는 credential을 하나 만들어서</p>
<pre><code class="language-text">SFTPGo
   |
   | AccessKey: ftp-gateway
   v
AIStor</code></pre>
<p>모든 사용자가 이 credential을 공유하도록 하면 <strong>좋지 않습니다.</strong></p>
<p>가능하면:</p>
<pre><code class="language-text">SFTPGo user A
       ↓
S3 credential/policy
       ↓
bucket/prefix A

SFTPGo user B
       ↓
S3 credential/policy
       ↓
bucket/prefix B</code></pre>
<p>또는 SFTPGo 내부의 user/prefix ACL과 AIStor bucket policy를 <strong>이중으로 방어</strong>하는 구조를 권합니다.</p>
<p>즉,</p>
<blockquote>
<p>SFTPGo에서 제한했다고 해서 AIStor 자체의 credential 권한을 과도하게 주지 않는 것</p>
</blockquote>
<p>이 중요합니다.</p>
<hr>
<h1 id="결론">결론</h1>
<p>현재 말씀하신 <strong>&quot;기존 client가 FTP로 파일을 올려야 하고, backend는 AIStor&quot;</strong>라는 요구라면 저는 꽤 긍정적으로 봅니다.</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>평가</th>
</tr>
</thead>
<tbody><tr>
<td>AIStor에 FTP 기능을 직접 붙임</td>
<td>❌ 비추천</td>
</tr>
<tr>
<td>FTP → 별도 filesystem → AIStor</td>
<td>△ 운영 복잡</td>
</tr>
<tr>
<td>FTP → SFTPGo → AIStor S3</td>
<td><strong>◎ 추천</strong></td>
</tr>
<tr>
<td>SFTP → SFTPGo → AIStor S3</td>
<td><strong>◎ 추천</strong></td>
</tr>
<tr>
<td>Client가 직접 S3</td>
<td><strong>◎ 가장 효율적</strong></td>
</tr>
</tbody></table>
<p>특히 현재 Kubernetes + AIStor 환경이라면:</p>
<pre><code class="language-text">              Legacy FTP clients
                     |
                 FTPS/SFTP
                     |
              ┌─────────────┐
              │   SFTPGo    │
              │  2~3 pods   │
              └──────┬──────┘
                     │
                 S3 API/HTTPS
                     │
              ┌──────▼──────┐
              │   AIStor    │
              │             │
              │ bucket      │
              │  ├ projectA │
              │  ├ projectB │
              │  └ projectC │
              └─────────────┘</code></pre>
<p>이 구조가 <strong>현재 환경에는 상당히 현실적인 선택</strong>이라고 봅니다.</p>
<p>다만 <strong>수백 MB<del>수십 GB 파일을 동시에 수십</del>수백 개 업로드하는 환경</strong>이라면 SFTPGo를 단순히 설치하는 것보다, <strong>SFTPGo → AIStor throughput / concurrency / memory / connection 수를 실제로 POC하는 게 중요</strong>합니다. SFTPGo에는 host별 concurrent transfer 제한 같은 설정도 제공되므로 이를 AIStor의 API/RDMA 네트워크 부하와 함께 튜닝할 수 있습니다. (<a href="https://docs.sftpgo.com/enterprise/config-file/?utm_source=chatgpt.com" title="Configuration file - SFTPGo docs">SFTPGo Docs</a>)</p>
<p>원하시면 다음 단계로 <strong>현재 사용 중인 AIStor/K8s 구조 기준으로 <code>SFTPGo + AIStor</code>를 Kubernetes에 HA로 배치하는 구체적인 구성도(Deployment/Service/Passive FTP ports/DB/Secret/AIStor policy)와 함께, 10/50/100 concurrent FTP upload 성능 POC 항목</strong>까지 설계해드릴 수 있습니다.</p>
<p>===</p>
<p>가능합니다. 오히려 <strong>현재처럼 Cilium Native Routing + BGP + ECMP + Gateway API를 사용하는 환경이라면 SFTPGo를 별도 <code>LoadBalancer Service</code>로 노출하고, Gateway API는 HTTP 계열 관리 UI/API에만 사용하는 것</strong>을 권합니다.</p>
<p>핵심은 <strong>FTP/FTPS의 control connection과 passive data connection을 Gateway API의 HTTP routing으로 처리하려고 하지 않는 것</strong>입니다. FTP는 여러 TCP 포트를 사용하기 때문에 일반적인 HTTPRoute 구조와 맞지 않습니다. SFTPGo 공식 Kubernetes 가이드도 SFTP/FTP는 Ingress가 아니라 <code>LoadBalancer</code>/<code>NodePort</code> 같은 TCP 노출 방식을 권장합니다. (<a href="https://docs.sftpgo.com/enterprise/s3-compatible-services/?utm_source=chatgpt.com" title="S3-compatible services - SFTPGo docs">GitHub</a>)</p>
<h2 id="제가-권하는-전체-구조">제가 권하는 전체 구조</h2>
<pre><code class="language-text">                         External Network
                               |
                  +------------+------------+
                  |                         |
             VIP: FTP/SFTP             VIP: HTTPS
                  |                         |
           Cilium BGP/ECMP             Cilium Gateway
                  |                         |
        +---------+---------+             |
        |                   |             |
   SFTPGo Pod-1        SFTPGo Pod-2       |
        |                   |             |
        +---------+---------+             |
                  |                       |
                  +----------+------------+
                             |
                         S3 HTTPS
                             |
                         AIStor</code></pre>
<p>즉 <strong>두 개의 진입 경로</strong>로 분리합니다.</p>
<table>
<thead>
<tr>
<th>용도</th>
<th>노출 방식</th>
<th>Cilium</th>
</tr>
</thead>
<tbody><tr>
<td>FTP/FTPS</td>
<td><code>LoadBalancer Service</code></td>
<td>LB-IPAM + BGP + ECMP</td>
</tr>
<tr>
<td>SFTP</td>
<td><code>LoadBalancer Service</code></td>
<td>LB-IPAM + BGP + ECMP</td>
</tr>
<tr>
<td>SFTPGo Web Admin/API</td>
<td>Gateway API</td>
<td>Cilium Gateway</td>
</tr>
<tr>
<td>AIStor S3</td>
<td>기존 방식</td>
<td>Cilium Native/BGP/ECMP</td>
</tr>
</tbody></table>
<p>Cilium의 LB-IPAM은 <code>LoadBalancer</code> Service에 VIP를 할당하고, BGP Control Plane이 그 VIP를 BGP로 advertise하는 구조를 지원합니다. upstream router가 ECMP를 지원하면 같은 VIP를 여러 node에서 광고해서 ECMP load-balancing도 할 수 있습니다. (<a href="https://docs.sftpgo.com/enterprise/documentation-map/?utm_source=chatgpt.com" title="Documentation Map - SFTPGo docs">Cilium Documentation</a>)</p>
<hr>
<h1 id="1-sftpgo는-loadbalancer-service로-만든다">1. SFTPGo는 LoadBalancer Service로 만든다</h1>
<p>예를 들어 SFTPGo에 다음 endpoint를 제공한다고 하겠습니다.</p>
<pre><code class="language-text">sftp.example.com   TCP/22
ftp.example.com    TCP/21
ftps.example.com   TCP/21</code></pre>
<p>그리고 FTP passive range를:</p>
<pre><code class="language-text">50000-50100</code></pre>
<p>으로 잡습니다.</p>
<p>그러면 Kubernetes 입장에서는 최소한:</p>
<pre><code class="language-text">SFTPGo Service
 ├── 21/TCP
 ├── 22/TCP
 └── 50000-50100/TCP</code></pre>
<p>가 필요합니다.</p>
<p>SFTPGo의 기본 passive range도 50000–50100이며, 이 포트들이 client에서 접근 가능해야 합니다. (<a href="https://docs.sftpgo.com/?utm_source=chatgpt.com" title="SFTPGo docs">GitHub</a>)</p>
<hr>
<h1 id="2-그런데-여기서-가장-중요한-문제가-vip입니다">2. 그런데 여기서 가장 중요한 문제가 &quot;VIP&quot;입니다</h1>
<p>예를 들어 Cilium LB-IPAM pool을:</p>
<pre><code class="language-text">172.20.100.0/24</code></pre>
<p>라고 해보겠습니다.</p>
<p>SFTPGo에:</p>
<pre><code class="language-text">172.20.100.10</code></pre>
<p>이라는 VIP를 할당합니다.</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: sftpgo
  namespace: sftpgo
  annotations:
    lbipam.cilium.io/ips: &quot;172.20.100.10&quot;
spec:
  type: LoadBalancer
  loadBalancerClass: io.cilium/bgp-control-plane

  externalTrafficPolicy: Local

  selector:
    app: sftpgo

  ports:
  - name: ftp
    port: 21
    targetPort: 21
    protocol: TCP

  - name: sftp
    port: 22
    targetPort: 22
    protocol: TCP

  - name: passive-50000
    port: 50000
    targetPort: 50000
    protocol: TCP

  # ...</code></pre>
<p>다만 <strong>50000~50100을 전부 하나씩 Service port로 만드는 것은 운영상 상당히 귀찮습니다.</strong></p>
<p>따라서 여기서 SFTPGo의 passive FTP 구조와 Kubernetes Service 설계를 조금 더 신중하게 잡아야 합니다.</p>
<hr>
<h1 id="3-제가-추천하는-것은-passive-port-range를-줄이는-것">3. 제가 추천하는 것은 passive port range를 줄이는 것</h1>
<p>100개 포트를 무조건 사용할 필요가 없다면 예를 들어:</p>
<pre><code class="language-text">50000-50031</code></pre>
<p>정도로 시작합니다.</p>
<p>동시 FTP session이 32개 정도라면:</p>
<pre><code class="language-text">FTP control
21

Passive data
50000-50031</code></pre>
<p>정도로 시작하고 실제 concurrency에 따라 늘립니다.</p>
<p>SFTPGo 자체에서도 passive port range를 지정할 수 있습니다. (<a href="https://docs.sftpgo.com/?utm_source=chatgpt.com" title="SFTPGo docs">GitHub</a>)</p>
<p>예:</p>
<pre><code class="language-yaml">ftpd:
  bindings:
    - port: 21
      address: 0.0.0.0

  passive_port_range:
    start: 50000
    end: 50031</code></pre>
<hr>
<h1 id="4-gateway-api는-ftp에-사용하지-않는-것을-추천">4. Gateway API는 FTP에 사용하지 않는 것을 추천</h1>
<p>여기가 상당히 중요합니다.</p>
<p>현재 환경에서:</p>
<pre><code class="language-text">Gateway API
     |
     +-- HTTPRoute
     +-- TLSRoute
     +-- TCPRoute</code></pre>
<p>가 가능하지만, <strong>FTP 전체를 TCPRoute로 구성하는 것은 추천하지 않습니다.</strong></p>
<p>Cilium Gateway API 자체는 TCPRoute를 지원합니다. (<a href="https://docs.sftpgo.com/enterprise/initial-configuration/?utm_source=chatgpt.com" title="Initial Setup - SFTPGo docs">Cilium Documentation</a>)</p>
<p>하지만 FTP는:</p>
<pre><code class="language-text">TCP/21
   |
   +---- passive TCP/50000
   +---- passive TCP/50001
   +---- passive TCP/50002
   ...</code></pre>
<p>처럼 별도 data connection을 생성합니다.</p>
<p>그래서:</p>
<pre><code class="language-text">Gateway
  |
  +-- TCPRoute :21
  +-- TCPRoute :50000
  +-- TCPRoute :50001
  ...</code></pre>
<p>로 만들 수는 있어도 <strong>굉장히 불필요하게 복잡해집니다.</strong></p>
<p>특히 FTP client compatibility까지 생각하면 더욱 그렇습니다.</p>
<hr>
<h1 id="5-그래서-cilium-구조를-이렇게-나누는-게-좋습니다">5. 그래서 Cilium 구조를 이렇게 나누는 게 좋습니다</h1>
<h3 id="data-plane">Data plane</h3>
<pre><code class="language-text">              Internet
                 |
           SFTP/FTP clients
                 |
          172.20.100.10
                 |
        Cilium BGP/ECMP
                 |
       +---------+---------+
       |                   |
   Node A               Node B
       |                   |
   SFTPGo-1             SFTPGo-2</code></pre>
<h3 id="management-plane">Management plane</h3>
<pre><code class="language-text">Admin
 |
HTTPS
 |
Gateway API VIP
 |
HTTPRoute
 |
SFTPGo Web Admin</code></pre>
<p>즉:</p>
<pre><code class="language-text">FTP/SFTP
   ↓
LoadBalancer Service
   ↓
Cilium BGP/ECMP

Web Admin
   ↓
Gateway API
   ↓
Cilium Envoy
   ↓
SFTPGo</code></pre>
<p>이게 가장 깔끔합니다.</p>
<hr>
<h1 id="6-externaltrafficpolicy는-local을-우선-검토">6. <code>externalTrafficPolicy</code>는 <code>Local</code>을 우선 검토</h1>
<p>현재처럼 BGP + ECMP를 사용한다면 저는:</p>
<pre><code class="language-yaml">externalTrafficPolicy: Local</code></pre>
<p>을 우선 추천합니다.</p>
<p>이유는 client source IP 보존 때문입니다.</p>
<p>예를 들어:</p>
<pre><code class="language-text">Client
  |
  | src = 10.10.10.50
  v
VIP 172.20.100.10
  |
  | ECMP
  v
Node-A
  |
  v
SFTPGo</code></pre>
<p>SFTPGo가 실제 client IP를 볼 수 있게 하는 것이 좋습니다.</p>
<p>특히 FTP/SFTP 서버에서는:</p>
<ul>
<li>IP ACL</li>
<li>brute-force 방어</li>
<li>audit</li>
<li>접속 로그</li>
<li>사용자별 접속 추적</li>
</ul>
<p>등 때문에 source IP가 중요합니다.</p>
<p>Cilium Gateway API에서도 LoadBalancer/NodePort의 <code>externalTrafficPolicy</code>가 client IP visibility에 영향을 준다고 설명하고 있습니다. (<a href="https://docs.sftpgo.com/enterprise/ftp/?utm_source=chatgpt.com" title="FTP/FTPS - SFTPGo docs">Cilium Documentation</a>)</p>
<hr>
<h1 id="7-그런데-externaltrafficpolicy-local--ecmp에는-조건이-하나-있습니다">7. 그런데 <code>externalTrafficPolicy: Local</code> + ECMP에는 조건이 하나 있습니다</h1>
<p>이게 현재 환경에서 <strong>가장 중요한 Cilium 설정 포인트</strong>입니다.</p>
<p>예를 들어:</p>
<pre><code class="language-text">SFTPGo-1 → Node-A
SFTPGo-2 → Node-B
SFTPGo-3 → Node-C</code></pre>
<p>라고 하고,</p>
<p>BGP가:</p>
<pre><code class="language-text">VIP 172.20.100.10
  |
  +--- Node-A
  +--- Node-B
  +--- Node-C</code></pre>
<p>로 advertise하면 좋습니다.</p>
<p>그러나:</p>
<pre><code class="language-text">Node-A에는 SFTPGo endpoint 없음</code></pre>
<p>인데 Node-A가 VIP를 광고하면 문제가 생길 수 있습니다.</p>
<p>따라서 <strong>ECMP로 VIP를 advertise하는 node와 실제 SFTPGo endpoint가 있는 node의 관계</strong>를 확인해야 합니다.</p>
<p>Cilium BGP advertisement에서 Service의 LoadBalancer IP를 광고할 수 있고, ECMP router가 같은 VIP를 여러 node에서 받는 구조가 가능합니다. (<a href="https://docs.sftpgo.com/Enterprise/features/?utm_source=chatgpt.com" title="Features - SFTPGo docs">Cilium Documentation</a>)</p>
<hr>
<h1 id="8-sftpgo-pod-배치도-중요">8. SFTPGo Pod 배치도 중요</h1>
<p>저라면 SFTPGo를 단순히:</p>
<pre><code class="language-yaml">replicas: 2</code></pre>
<p>만 하지 않고 최소한:</p>
<pre><code class="language-text">Node A
 └─ SFTPGo-1

Node B
 └─ SFTPGo-2

Node C
 └─ SFTPGo-3</code></pre>
<p>처럼 분산합니다.</p>
<p>그리고:</p>
<pre><code class="language-yaml">topologySpreadConstraints
podAntiAffinity</code></pre>
<p>를 적용합니다.</p>
<p>그래야:</p>
<pre><code class="language-text">BGP ECMP
   ↓
Node A/B/C
   ↓
SFTPGo A/B/C</code></pre>
<p>구조가 자연스럽게 만들어집니다.</p>
<hr>
<h1 id="9-sftpgo-ha에서는-db가-중요">9. SFTPGo HA에서는 DB가 중요</h1>
<p>이것도 놓치면 안 됩니다.</p>
<p>SFTPGo를:</p>
<pre><code class="language-text">SFTPGo-1
SFTPGo-2
SFTPGo-3</code></pre>
<p>로 띄우려면 사용자/설정 정보를 외부 DB에 두는 것이 좋습니다.</p>
<p>현재 환경에서 이미 CNPG를 사용하고 있으니:</p>
<pre><code class="language-text">SFTPGo
  |
  +--- SFTPGo-1
  +--- SFTPGo-2
  +--- SFTPGo-3
             |
             v
         CNPG PostgreSQL</code></pre>
<p>구조가 적합합니다.</p>
<p>SFTPGo의 multi-node 구성에서도 external data provider가 필요합니다. (<a href="https://docs.sftpgo.com/enterprise/s3-compatible-services/?utm_source=chatgpt.com" title="S3-compatible services - SFTPGo docs">GitHub</a>)</p>
<hr>
<h1 id="10-aistor-쪽은-오히려-단순합니다">10. AIStor 쪽은 오히려 단순합니다</h1>
<p>SFTPGo:</p>
<pre><code class="language-text">SFTPGo
   |
   | HTTPS
   | S3 API
   v
AIStor VIP</code></pre>
<p>로 연결합니다.</p>
<p>예를 들어:</p>
<pre><code class="language-yaml">storage:
  provider: s3
  endpoint: https://aistor-s3.example.com
  bucket: customer-data</code></pre>
<p>SFTPGo는 S3-compatible backend를 직접 지원하기 때문에 중간 filesystem/PV를 반드시 둘 필요가 없습니다. (<a href="https://docs.sftpgo.com/enterprise/config-file/?utm_source=chatgpt.com" title="Configuration file - SFTPGo docs">SFTPGo Docs</a>)</p>
<hr>
<h1 id="11-prefix-isolation도-sftpgo에서-적용">11. Prefix isolation도 SFTPGo에서 적용</h1>
<p>예를 들어:</p>
<pre><code class="language-text">AIStor

bucket: data

data/
 ├── project001/
 ├── project002/
 └── project003/</code></pre>
<p>SFTPGo:</p>
<pre><code class="language-text">user: project001
storage:
  bucket: data
  key_prefix: project001/</code></pre>
<p>이런 식으로 구성합니다.</p>
<p>그리고 <strong>AIStor policy도 이중으로 제한</strong>하는 것을 권합니다.</p>
<pre><code class="language-text">SFTPGo ACL
    +
AIStor IAM/policy</code></pre>
<p>둘 중 하나가 잘못되어도 다른 쪽에서 방어할 수 있게 합니다.</p>
<hr>
<h1 id="12-networkpolicy도-하나-넣는-게-좋습니다">12. NetworkPolicy도 하나 넣는 게 좋습니다</h1>
<p>예를 들어 SFTPGo namespace에서:</p>
<pre><code class="language-text">Internet
   |
   v
SFTPGo
   |
   | 443
   v
AIStor</code></pre>
<p>만 허용하고,</p>
<pre><code class="language-text">SFTPGo
   |
   +-- PostgreSQL/CNPG</code></pre>
<p>도 필요한 포트만 허용합니다.</p>
<p>즉:</p>
<pre><code class="language-text">Ingress:
  TCP 21
  TCP 22
  TCP 50000-50031

Egress:
  AIStor :443
  CNPG :5432
  DNS :53</code></pre>
<p>정도로 최소화합니다.</p>
<hr>
<h1 id="13-ftp-passive-mode-때문에-gateway-api보다-이것이-더-중요">13. FTP passive mode 때문에 <code>Gateway API</code>보다 이것이 더 중요</h1>
<p>실제로 POC에서 가장 먼저 확인해야 할 것이:</p>
<pre><code class="language-text">FTP control connection
        ↓
      TCP/21

PASV
        ↓
SFTPGo가
&quot;172.20.100.10:50007&quot;
같은 주소를 client에게 전달

client
        ↓
172.20.100.10:50007
        ↓
Cilium
        ↓
SFTPGo</code></pre>
<p>입니다.</p>
<p>즉 SFTPGo가 client에게 <strong>정확한 VIP와 passive port를 알려줘야 합니다.</strong></p>
<p>SFTPGo는 NAT/proxy 환경에서 <code>force_passive_ip</code> 및 <code>passive_ip_overrides</code>를 제공하므로 이 부분을 명시적으로 설정할 수 있습니다. (<a href="https://docs.sftpgo.com/?utm_source=chatgpt.com" title="SFTPGo docs">GitHub</a>)</p>
<p>예를 들어:</p>
<pre><code class="language-yaml">ftpd:
  bindings:
    - port: 21
      force_passive_ip: 172.20.100.10

  passive_port_range:
    start: 50000
    end: 50031</code></pre>
<p><strong>이 설정이 굉장히 중요합니다.</strong></p>
<hr>
<h1 id="14-다만-여기서-cilium을-한-단계-더-고려해야-합니다">14. 다만 여기서 Cilium을 한 단계 더 고려해야 합니다</h1>
<p>현재 환경이:</p>
<pre><code class="language-text">Cilium
 ├─ Native routing
 ├─ BGP
 ├─ ECMP
 └─ Gateway API</code></pre>
<p>이기 때문에 제가 실제 구축한다면 <strong>SFTPGo FTP용 VIP와 기존 Gateway VIP를 분리</strong>하겠습니다.</p>
<p>예:</p>
<pre><code class="language-text">172.20.100.0/24
│
├── 172.20.100.10
│      SFTPGo FTP/SFTP VIP
│
├── 172.20.100.20
│      Gateway API VIP
│
├── 172.20.100.21
│      Gateway API VIP
│
└── ...</code></pre>
<p>그리고 BGP advertisement도:</p>
<pre><code class="language-text">SFTPGo LB VIP
       ↓
BGP
       ↓
ECMP</code></pre>
<p>로 별도 관리합니다.</p>
<p>Cilium LB-IPAM과 BGP Control Plane이 이런 LoadBalancer IP advertisement 구조를 지원합니다. (<a href="https://docs.sftpgo.com/enterprise/documentation-map/?utm_source=chatgpt.com" title="Documentation Map - SFTPGo docs">Cilium Documentation</a>)</p>
<hr>
<h1 id="제가-현재-환경이라면-최종적으로-이렇게-구성합니다">제가 현재 환경이라면 최종적으로 이렇게 구성합니다</h1>
<pre><code class="language-text">                         External Client
                              |
                 +------------+------------+
                 |                         |
             SFTP / FTPS                  HTTPS
                 |                         |
                 v                         v
       VIP 172.20.100.10             VIP 172.20.100.20
                 |                         |
                 | BGP + ECMP              | Gateway API
                 |                         |
        +--------+--------+                |
        |        |        |                |
      Node-A   Node-B   Node-C             |
        |        |        |                |
       S1       S2       S3 &lt;--------------+
        \        |        /
         \       |       /
          +------+------+
                 |
             S3 HTTPS
                 |
                 v
          AIStor Service/VIP
                 |
                 v
             AIStor</code></pre>
<h3 id="kubernetes-구성">Kubernetes 구성</h3>
<pre><code class="language-text">Namespace: sftpgo

Deployment
  replicas: 3

Service
  type: LoadBalancer
  loadBalancerClass: io.cilium/bgp-control-plane
  externalTrafficPolicy: Local

Ports
  21       FTP/FTPS
  22       SFTP
  50000-50031 passive FTP

Gateway
  HTTPS 443
  └── HTTPRoute
       └── SFTPGo WebAdmin/API

CNPG
  └── PostgreSQL

Secret
  ├── SFTPGo DB credential
  ├── AIStor access key
  └── AIStor secret key</code></pre>
<h3 id="cilium">Cilium</h3>
<pre><code class="language-text">LB-IPAM
   ↓
VIP allocation

BGP Control Plane
   ↓
advertise LoadBalancerIP

Router
   ↓
ECMP
   ↓
SFTPGo nodes</code></pre>
<hr>
<h2 id="그리고-한-가지-더-현재-환경에서는-sftp를-우선-추천">그리고 한 가지 더: 현재 환경에서는 <code>SFTP</code>를 우선 추천</h2>
<p>만약 client가 <strong>SFTP를 지원한다면 FTP/FTPS보다 SFTP를 우선</strong>하겠습니다.</p>
<p>이유가 단순합니다.</p>
<pre><code class="language-text">SFTP
  ↓
TCP/22 하나
  ↓
Cilium LB
  ↓
SFTPGo</code></pre>
<p>반면 FTP/FTPS는:</p>
<pre><code class="language-text">TCP/21
+
Passive TCP/50000-50031</code></pre>
<p>이 필요합니다.</p>
<p>즉 <strong>Cilium + BGP + ECMP 환경에서 운영 난이도가 SFTP가 훨씬 낮습니다.</strong></p>
<p>SFTPGo 자체도 SFTP, FTP/S 등 여러 protocol을 제공하며 S3-compatible backend를 사용할 수 있습니다. (<a href="https://docs.sftpgo.com/Enterprise/execute-before-file-publish/?utm_source=chatgpt.com" title="Execute Before File Publish - SFTPGo docs">SFTPGo Docs</a>)</p>
<hr>
<h3 id="결론적으로">결론적으로</h3>
<p>현재 환경에서는 <strong>SFTPGo를 Gateway API 뒤에 넣는 구조보다는 <code>SFTPGo 자체를 Cilium LoadBalancer Service로 노출</code>하고, SFTPGo Web UI/API만 Gateway API로 넣는 구조</strong>가 가장 좋습니다.</p>
<p>특히 다음 5가지를 중점적으로 설계하면 됩니다.</p>
<ol>
<li><strong>SFTPGo <code>LoadBalancer Service</code> + Cilium LB-IPAM</strong></li>
<li><strong>Cilium BGP advertisement + upstream ECMP</strong></li>
<li><strong><code>externalTrafficPolicy: Local</code> 검토</strong></li>
<li><strong>FTP 사용 시 passive port range + <code>force_passive_ip</code></strong></li>
<li><strong>SFTPGo 2~3 replica + CNPG + AIStor S3 backend</strong></li>
</ol>
<p>그리고 <strong>현재 사용 중인 Cilium 1.18.4 기준으로 실제 YAML을 작성한다면 약간 주의해야 합니다.</strong> 위 공식 문서는 최신 Cilium 문서이고, Gateway API/TCPRoute/LB-IPAM의 세부 동작은 1.18과 최신 버전 사이에 차이가 있을 수 있습니다. 따라서 실제 구축용으로는 <strong>현재 사용 중인 Cilium 1.18.4 + K8s 1.33.x 기준으로 <code>CiliumBGPAdvertisement</code>, <code>CiliumLoadBalancerIPPool</code>, SFTPGo Service, Gateway/HTTPRoute, NetworkPolicy까지 한 세트의 실제 YAML</strong>로 맞추는 게 좋습니다.</p>
<p>===</p>
<p>네. 두 질문 모두 <strong>&quot;아니요, 꼭 그렇지는 않습니다&quot;</strong>입니다. 오히려 지금 환경에서는 제가 앞서 설명한 내용을 조금 정정해서 보는 게 좋습니다.</p>
<h2 id="1-cnpg가-필수인가">1. CNPG가 필수인가?</h2>
<p><strong>아닙니다.</strong></p>
<p>SFTPGo의 DB는 필수가 아니라 <strong>배포 형태에 따라 선택</strong>입니다.</p>
<p>SFTPGo는 기본적으로 SQLite를 사용할 수 있고, PostgreSQL/MySQL/MariaDB 등을 사용할 수 있습니다. 다만 <strong>여러 SFTPGo instance가 하나의 설정/사용자 DB를 공유하는 multi-instance 구성</strong>에서는 PostgreSQL/MySQL/MariaDB/CockroachDB 같은 shared DB가 필요합니다. (<a href="https://docs.sftpgo.com/enterprise/s3-compatible-services/?utm_source=chatgpt.com" title="S3-compatible services - SFTPGo docs">SFTPGo Docs</a>)</p>
<h3 id="경우를-나누면">경우를 나누면</h3>
<table>
<thead>
<tr>
<th>구성</th>
<th>DB</th>
<th>CNPG 필요</th>
</tr>
</thead>
<tbody><tr>
<td>SFTPGo 1 Pod</td>
<td>SQLite</td>
<td>❌</td>
</tr>
<tr>
<td>SFTPGo 1 Pod + 장애 시 재기동</td>
<td>SQLite + PVC</td>
<td>❌</td>
</tr>
<tr>
<td>SFTPGo 2~3 Pod HA</td>
<td>PostgreSQL</td>
<td>❌ CNPG 자체는 필수 아님</td>
</tr>
<tr>
<td>SFTPGo 2~3 Pod HA + 기존 CNPG 활용</td>
<td>CNPG PostgreSQL</td>
<td>⭕ 추천</td>
</tr>
<tr>
<td>아주 단순한 FTP gateway POC</td>
<td>SQLite</td>
<td><strong>가장 간단</strong></td>
</tr>
</tbody></table>
<p>따라서 <strong>단순히 &quot;FTP → AIStor&quot; gateway 용도로 SFTPGo를 하나 띄우는 것이라면 CNPG까지 넣을 필요가 없습니다.</strong></p>
<p>SFTPGo 공식 문서도 SQLite를 기본 provider로 제공하고, PostgreSQL은 multi-instance/shared deployment에 사용할 수 있다고 명시합니다. (<a href="https://docs.sftpgo.com/enterprise/documentation-map/?utm_source=chatgpt.com" title="Documentation Map - SFTPGo docs">Mintlify</a>)</p>
<h3 id="제가-현재-환경이라면">제가 현재 환경이라면</h3>
<p>처음에는:</p>
<pre><code class="language-text">             SFTPGo
                |
        SQLite + PVC
                |
              AIStor</code></pre>
<p>로 POC를 합니다.</p>
<p>성능/운영 검증 후 production HA가 필요하면:</p>
<pre><code class="language-text">       SFTPGo-1       SFTPGo-2       SFTPGo-3
             \          |          /
                    CNPG
                      |
                   AIStor</code></pre>
<p>로 전환하겠습니다.</p>
<p>즉 <strong>CNPG를 처음부터 넣을 필요는 없습니다.</strong></p>
<hr>
<h1 id="2-방화벽은-vip만-열면-되나-각-node-ip도-열어야-하나">2. 방화벽은 VIP만 열면 되나, 각 Node IP도 열어야 하나?</h1>
<p>이건 <strong>Cilium BGP + LoadBalancer + ECMP를 어떻게 구성하느냐에 따라 달라지지만</strong>, 지금 말씀하신 구조라면 <strong>정상적인 Cilium BGP Service VIP 구조에서는 외부 방화벽은 VIP만 허용하는 방향이 맞습니다.</strong></p>
<p>예를 들어:</p>
<pre><code class="language-text">External Client
       |
       | TCP 21/22/50000-50010
       v
VIP 10.100.50.10
       |
       | BGP / ECMP
       +----------+
       |          |
    Node-A      Node-B
       |          |
    SFTPGo-1   SFTPGo-2</code></pre>
<p>외부 방화벽에서는:</p>
<pre><code class="language-text">ALLOW

destination = 10.100.50.10
TCP 21
TCP 22
TCP 50000-50010</code></pre>
<p>이면 됩니다.</p>
<p><strong>Node-A의 IP, Node-B의 IP를 외부 FTP client가 직접 접근할 필요는 없습니다.</strong></p>
<p>Cilium BGP Control Plane은 Service의 LoadBalancer VIP를 BGP peer에게 광고할 수 있고, upstream router가 ECMP를 지원하면 동일 VIP를 여러 node에서 광고하여 여러 node로 traffic을 분산할 수 있습니다. (<a href="https://docs.sftpgo.com/?utm_source=chatgpt.com" title="SFTPGo docs">Cilium Documentation</a>)</p>
<hr>
<h1 id="3-특히-externaltrafficpolicy-local이면-더-명확합니다">3. 특히 <code>externalTrafficPolicy: Local</code>이면 더 명확합니다</h1>
<p>이 구조를:</p>
<pre><code class="language-yaml">spec:
  type: LoadBalancer
  externalTrafficPolicy: Local</code></pre>
<p>로 구성하면 Cilium BGP가 <strong>해당 node에 local endpoint가 있을 때만 해당 node가 VIP를 advertise</strong>하도록 할 수 있습니다. local endpoint가 없어지면 advertisement도 중단됩니다. (<a href="https://docs.sftpgo.com/?utm_source=chatgpt.com" title="SFTPGo docs">Cilium Documentation</a>)</p>
<p>예를 들어:</p>
<pre><code class="language-text">SFTPGo-1 → Node-A
SFTPGo-2 → Node-B
SFTPGo-3 → Node-C</code></pre>
<p>이면:</p>
<pre><code class="language-text">          VIP
           |
     +-----+-----+
     |     |     |
   Node-A Node-B Node-C
     |     |     |
    S1    S2    S3</code></pre>
<p>BGP:</p>
<pre><code class="language-text">VIP → Node-A
VIP → Node-B
VIP → Node-C</code></pre>
<p>가 되고,</p>
<p>Node-B에서 SFTPGo가 죽으면:</p>
<pre><code class="language-text">VIP → Node-A
VIP → Node-C</code></pre>
<p>만 남도록 할 수 있습니다.</p>
<p>이게 <strong>BGP + ECMP + <code>externalTrafficPolicy: Local</code>의 장점</strong>입니다.</p>
<hr>
<h1 id="4-그러면-node-ip의-21225000050010은-방화벽에서-안-열어도-되나">4. 그러면 Node IP의 21/22/50000~50010은 방화벽에서 안 열어도 되나?</h1>
<p><strong>외부 client → Node IP 직접 접근을 막는 방화벽이라면 안 열어도 됩니다.</strong></p>
<p>즉:</p>
<pre><code class="language-text">             Internet
                |
                v
       FW / Router
                |
       VIP 10.100.50.10
                |
       Cilium BGP/ECMP
          /           \
      Node-A         Node-B
        |               |
      SFTPGo           SFTPGo</code></pre>
<p>방화벽:</p>
<pre><code class="language-text">VIP:
  TCP/21       ALLOW
  TCP/22       ALLOW
  TCP/50000-50010 ALLOW

Node IP:
  TCP/21       DENY
  TCP/22       DENY
  TCP/50000-50010 DENY</code></pre>
<p>이런 식이 이상적인 형태입니다.</p>
<hr>
<h1 id="5-다만-nodeport를-사용하는-경우는-이야기가-달라집니다">5. 다만 &quot;NodePort를 사용하는 경우&quot;는 이야기가 달라집니다</h1>
<p>여기서 중요한 차이가 있습니다.</p>
<h3 id="방식-a--loadbalancer--cilium-bgp">방식 A — LoadBalancer + Cilium BGP</h3>
<pre><code class="language-text">Client
  ↓
VIP:21
  ↓
Cilium
  ↓
SFTPGo Pod</code></pre>
<p>→ <strong>VIP만 방화벽 허용</strong></p>
<h3 id="방식-b--nodeport">방식 B — NodePort</h3>
<pre><code class="language-text">Client
  ↓
Node-A:30xxx
Node-B:30xxx
Node-C:30xxx
  ↓
SFTPGo</code></pre>
<p>→ <strong>Node IP + NodePort를 방화벽에서 허용해야 함</strong></p>
<p>따라서 지금 환경에서는 <strong>NodePort를 외부 방화벽에 노출시키는 구조는 피하고 Cilium LoadBalancer VIP를 사용하는 게 좋습니다.</strong></p>
<hr>
<h1 id="6-ftp의-passive-port도-vip로-처리해야-합니다">6. FTP의 passive port도 VIP로 처리해야 합니다</h1>
<p>이 부분이 핵심입니다.</p>
<p>FTP client가:</p>
<pre><code class="language-text">FTP control
10.100.50.10:21</code></pre>
<p>로 접속한 다음 PASV를 요청하면 SFTPGo가 예를 들어:</p>
<pre><code class="language-text">10.100.50.10:50003</code></pre>
<p>을 client에게 알려줍니다.</p>
<p>그 다음:</p>
<pre><code class="language-text">Client
   |
   +---- TCP/21 ----------&gt; VIP
   |
   +---- TCP/50003 -------&gt; VIP</code></pre>
<p>가 됩니다.</p>
<p>따라서 방화벽은:</p>
<pre><code class="language-text">VIP 10.100.50.10
 ├── 21
 ├── 22
 └── 50000-50010</code></pre>
<p>만 열면 됩니다.</p>
<p>SFTPGo 문서에서도 passive port range는 client에서 접근 가능해야 하고, NAT 환경에서는 외부에서 접근할 IP를 <code>force_passive_ip</code>로 지정할 수 있다고 설명합니다. (<a href="https://docs.sftpgo.com/enterprise/initial-configuration/?utm_source=chatgpt.com" title="Initial Setup - SFTPGo docs">GitHub</a>)</p>
<p>즉:</p>
<pre><code class="language-yaml">ftpd:
  bindings:
    - port: 21
      force_passive_ip: 10.100.50.10

  passive_port_range:
    start: 50000
    end: 50010</code></pre>
<p>같은 형태입니다.</p>
<hr>
<h1 id="7-그런데-한-가지-주의할-점">7. 그런데 한 가지 주의할 점</h1>
<p><strong><code>force_passive_ip</code>에 무엇을 넣느냐가 매우 중요합니다.</strong></p>
<p>외부 client가 접근하는 주소가:</p>
<pre><code class="language-text">10.100.50.10</code></pre>
<p>이라면:</p>
<pre><code class="language-text">force_passive_ip = 10.100.50.10</code></pre>
<p>입니다.</p>
<p>만약 실제로는:</p>
<pre><code class="language-text">Public IP
     ↓ NAT
10.100.50.10</code></pre>
<p>이라면:</p>
<pre><code class="language-text">force_passive_ip = Public IP</code></pre>
<p>가 되어야 합니다.</p>
<p>즉 client가 PASV 응답을 받고 <strong>실제로 접속 가능한 주소</strong>를 반환해야 합니다.</p>
<hr>
<h1 id="8-sftp는-훨씬-간단합니다">8. SFTP는 훨씬 간단합니다</h1>
<p>가능하면 저는 <strong>SFTP를 우선 사용</strong>하도록 하겠습니다.</p>
<p>SFTP:</p>
<pre><code class="language-text">Client
  |
  | TCP/22
  v
VIP
  |
  | BGP/ECMP
  v
SFTPGo
  |
  | HTTPS/443
  v
AIStor</code></pre>
<p>방화벽:</p>
<pre><code class="language-text">VIP:22 → ALLOW</code></pre>
<p>끝입니다.</p>
<p>반면 FTP/FTPS:</p>
<pre><code class="language-text">VIP:21
VIP:50000-50010</code></pre>
<p>이 필요합니다.</p>
<p>SFTPGo 자체도 공식적으로 SFTP를 FTP보다 우선 권장하고 있습니다. (<a href="https://docs.sftpgo.com/enterprise/ftp/?utm_source=chatgpt.com" title="FTP/FTPS - SFTPGo docs">GitHub</a>)</p>
<hr>
<h2 id="그래서-현재-환경에서-제가-추천하는-최종-구조">그래서 현재 환경에서 제가 추천하는 최종 구조</h2>
<h3 id="sftp만-필요하다면">SFTP만 필요하다면</h3>
<pre><code class="language-text">                 Firewall
                     |
                VIP 10.x.x.10
                  TCP/22
                     |
              Cilium BGP/ECMP
                /          \
           Node-A        Node-B
             |              |
         SFTPGo-1        SFTPGo-2
                \          /
                  AIStor
                    |
                  S3/443</code></pre>
<p><strong>외부 FW에는 VIP:22만 허용.</strong></p>
<hr>
<h3 id="ftpftps까지-꼭-필요하다면">FTP/FTPS까지 꼭 필요하다면</h3>
<pre><code class="language-text">                 Firewall
                     |
             VIP 10.x.x.10
                     |
        +------------+------------+
        |            |            |
       21           22       50000-50010
        |            |            |
        +------------+------------+
                     |
              Cilium BGP/ECMP
                     |
              SFTPGo Pods
                     |
                   AIStor</code></pre>
<p><strong>외부 FW에는 VIP의 21/22/50000-50010만 허용하고, Node IP에는 해당 포트를 열지 않는 구조</strong>를 권합니다.</p>
<hr>
<h3 id="그리고-cnpg">그리고 CNPG</h3>
<p>현재 목적이 <strong>&quot;기존 FTP client가 AIStor에 파일을 넣도록 하는 gateway&quot;</strong>라면 처음부터:</p>
<pre><code class="language-text">SFTPGo + CNPG + Gateway API + 별도 DB HA</code></pre>
<p>까지 만들 필요 없습니다.</p>
<p>저라면 1차 POC를:</p>
<pre><code class="language-text">SFTPGo 1~2 Pod
    +
SQLite/PVC
    +
Cilium LoadBalancer VIP
    +
BGP/ECMP
    +
AIStor S3</code></pre>
<p>로 최대한 단순하게 구성하고,</p>
<p><strong>실제 production에서 SFTPGo HA가 필요하다고 판단되면 그때 CNPG PostgreSQL로 바꾸겠습니다.</strong></p>
<p>그리고 현재 Cilium 1.18.4를 사용하시는 점을 감안하면, <strong>SFTPGo의 <code>LoadBalancer Service</code>를 실제로 어떻게 선언해야 Cilium 1.18.4에서 VIP가 BGP advertise되고, <code>externalTrafficPolicy: Local</code>에서 ECMP가 어떻게 동작하는지</strong>를 기준으로 YAML을 한번 정확히 잡아보는 게 다음 단계입니다. (<a href="https://docs.sftpgo.com/?utm_source=chatgpt.com" title="SFTPGo docs">Cilium Documentation</a>)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S05p]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S05p</link>
            <guid>https://velog.io/@youngkyoo_kim/26S05p</guid>
            <pubDate>Fri, 04 Sep 2026 20:05:41 GMT</pubDate>
            <description><![CDATA[<p>네. <strong>가능합니다.</strong> 오히려 지금 말씀하신 구조라면 <strong>private network가 구축되기 전에 RDMA 사전 검증을 해보는 것이 꽤 좋은 방법</strong>입니다.</p>
<p>다만 핵심은 <strong>“L3 routing이 가능하냐”가 아니라, RoCEv2가 그 L3 구간을 통과하도록 구성되어 있고 중간 스위치/라우터가 RDMA QoS를 제대로 보장하느냐</strong>입니다.</p>
<h3 id="결론부터">결론부터</h3>
<p>현재 구조가 예를 들어:</p>
<pre><code class="language-text">GPU Node
  CX-6
  10.10.10.10/24
       |
       |  Ethernet
       v
   [Switch]
       |
       | L3 Routing
       v
   [Router/L3 Switch]
       |
       v
   [Storage Switch]
       |
       v
AIStor Storage Node
E810
10.20.20.10/24</code></pre>
<p>이라면 <strong>RoCEv2 RDMA 자체는 L3 routed 환경에서도 테스트할 수 있습니다.</strong></p>
<p>RoCEv2는 IP/UDP 기반이므로 L3 routing이 가능합니다. 실제로 AIStor의 최신 RDMA 문서도 RoCEv2에서 DSCP를 사용해 L3 hop을 거쳐도 priority가 유지되도록 구성하는 방식을 설명하고 있습니다. (<a href="https://docs.min.io/aistor/operations/rdma/configure-the-rdma-fabric/?utm_source=chatgpt.com" title="Configure the RDMA fabric | MinIO AIStor Documentation">MinIO AIStor Documentation</a>)</p>
<p>다만 여기서 <strong>중요한 차이</strong>가 있습니다.</p>
<hr>
<h2 id="1-ib_write_bw-테스트는-충분히-해볼-수-있음">1. <code>ib_write_bw</code> 테스트는 충분히 해볼 수 있음</h2>
<p>예를 들어:</p>
<pre><code class="language-text">GPU Node
CX-6
  |
  | 10.10.10.0/24
  |
Switch
  |
  | L3
  |
Switch/Router
  |
  | 10.20.20.0/24
  |
Storage Node
E810</code></pre>
<p>이라면 GPU Node에서:</p>
<pre><code class="language-bash">ibv_devinfo
rdma link show
ibv_devices</code></pre>
<p>로 CX-6 RDMA device가 정상인지 확인하고,</p>
<p>Storage Node에서도 E810 RDMA device가 정상인지 확인한 뒤,</p>
<pre><code class="language-bash"># Storage
ib_write_bw -d &lt;rdma_device&gt; -x &lt;gid_index&gt;

# GPU node
ib_write_bw -d &lt;rdma_device&gt; -x &lt;gid_index&gt; &lt;storage_ip&gt;</code></pre>
<p>형태로 <strong>GPU Node ↔ Storage Node RDMA bandwidth test</strong>를 시도할 수 있습니다.</p>
<p>AIStor 문서에서도 <code>ib_write_bw</code>를 이용한 point-to-point RDMA 검증을 기본적인 fabric 테스트로 제시하고 있습니다. (<a href="https://docs.min.io/aistor/operations/rdma/validate-rdma/?utm_source=chatgpt.com" title="Validate the RDMA deployment | MinIO AIStor Documentation">MinIO AIStor Documentation</a>)</p>
<hr>
<h1 id="2-그런데-한-가지가-굉장히-중요합니다">2. 그런데 한 가지가 굉장히 중요합니다</h1>
<p>질문하신</p>
<blockquote>
<p>L3 routing이지만 스위치에서 지원한다면</p>
</blockquote>
<p>여기서 <strong>&quot;지원한다&quot;의 의미를 정확히 확인해야 합니다.</strong></p>
<p>단순히</p>
<pre><code class="language-text">IP routing 가능</code></pre>
<p>한 것만으로는 부족합니다.</p>
<p>RoCEv2에서는 다음이 중요합니다.</p>
<pre><code class="language-text">        GPU Node                         Storage Node
       ConnectX-6                           E810
          |                                   |
          | RoCEv2                            |
          v                                   v
       Switch ─── L3 ─── Router ─── L3 ─── Switch
                 │
                 │
          DSCP / QoS
          ECN
          PFC
          Buffer</code></pre>
<p>특히 <strong>PFC/ECN/DSCP가 L3 구간에서도 의도대로 동작하는지</strong>가 중요합니다.</p>
<p>AIStor는 RoCE fabric에서 inter-node RDMA를 사용할 경우 end-to-end lossless fabric을 요구하고, PFC와 ECN/DCQCN을 사용하도록 설명하고 있습니다. (<a href="https://docs.min.io/aistor/operations/rdma/configure-the-rdma-fabric/?utm_source=chatgpt.com" title="Configure the RDMA fabric | MinIO AIStor Documentation">MinIO AIStor Documentation</a>)</p>
<hr>
<h1 id="3-오히려-지금-테스트하면-좋은-이유">3. 오히려 지금 테스트하면 좋은 이유</h1>
<p>현재 상황에서는 저는 다음 순서로 테스트하는 것을 추천합니다.</p>
<h3 id="phase-a--지금-바로">Phase A — 지금 바로</h3>
<pre><code class="language-text">GPU Node CX-6
       │
       │ external network
       │
       ▼
Storage Node E810</code></pre>
<p>여기서:</p>
<h3 id="①-rdma-device-확인">① RDMA device 확인</h3>
<p>GPU:</p>
<pre><code class="language-bash">rdma link
ibv_devices
ibv_devinfo</code></pre>
<p>Storage:</p>
<pre><code class="language-bash">rdma link
ibv_devices
ibv_devinfo</code></pre>
<hr>
<h3 id="②-gid-확인">② GID 확인</h3>
<pre><code class="language-bash">show_gids</code></pre>
<p>또는 환경에 따라:</p>
<pre><code class="language-bash">ibv_devinfo -v</code></pre>
<p>여기서 <strong>RoCE v2 GID</strong>를 찾아야 합니다.</p>
<hr>
<h3 id="③-기본-rdma-bandwidth">③ 기본 RDMA bandwidth</h3>
<pre><code class="language-bash">ib_write_bw</code></pre>
<p>→ point-to-point</p>
<p>그 다음:</p>
<pre><code class="language-bash">ib_read_bw
ib_send_bw
ib_write_lat
ib_read_lat</code></pre>
<p>까지.</p>
<hr>
<h3 id="④-여러-message-size">④ 여러 message size</h3>
<p>예를 들어:</p>
<pre><code class="language-text">2KB
4KB
8KB
16KB
64KB
256KB
1MB
4MB
16MB
64MB</code></pre>
<p>로 측정합니다.</p>
<p>그리고 CX-6가 예를 들어 200GbE라면 단순히:</p>
<blockquote>
<p>200Gbps가 나왔다.</p>
</blockquote>
<p>만 보는 것이 아니라</p>
<pre><code class="language-text">Bandwidth
Latency
CPU utilization
PFC pause
ECN
packet drop
retry</code></pre>
<p>를 같이 봐야 합니다.</p>
<hr>
<h1 id="4-단-e810-↔-cx-6-조합은-조금-주의">4. 단, E810 ↔ CX-6 조합은 조금 주의</h1>
<p>여기가 이번 테스트에서 상당히 중요합니다.</p>
<p>GPU Node:</p>
<pre><code class="language-text">B300
  |
ConnectX-6</code></pre>
<p>Storage:</p>
<pre><code class="language-text">AIStor
  |
Intel E810</code></pre>
<p>이라면 <strong>RDMA 자체는 양쪽 NIC가 RDMA/RoCE를 지원하면 가능합니다.</strong></p>
<p>하지만 <strong>GPUDirect RDMA</strong>는 이야기가 달라집니다.</p>
<p>NVIDIA Network Operator의 GPUDirect RDMA 지원은 NVIDIA ConnectX/BlueField 계열 NIC를 대상으로 합니다. (<a href="https://docs.nvidia.com/networking/display/kubernetes2640/platform-support.html?utm_source=chatgpt.com" title="Platform Support - NVIDIA Docs">NVIDIA Docs</a>)</p>
<p>따라서:</p>
<pre><code class="language-text">B300
  │
  │ GPUDirect RDMA
  ▼
ConnectX-6
  │
  │ RoCEv2
  ▼
E810
  │
  ▼
AIStor</code></pre>
<p>여기서 GPU → CX-6 구간의 <strong>GPU Direct</strong>는 가능성을 검증할 수 있지만,</p>
<pre><code class="language-text">CX-6 → E810 → AIStor</code></pre>
<p>구간은 일반적인 RoCE RDMA 경로입니다.</p>
<p>즉 이 테스트는 두 가지를 분리해서 보는 게 좋습니다.</p>
<hr>
<h1 id="5-지금-할-수-있는-테스트를-3단계로-나누면">5. 지금 할 수 있는 테스트를 3단계로 나누면</h1>
<p>제가 지금 상황이라면 이렇게 하겠습니다.</p>
<h3 id="test-1--rdma-fabric">Test 1 — RDMA fabric</h3>
<pre><code class="language-text">GPU Node CX-6
      │
      │ L3 routed RoCEv2
      ▼
Storage E810</code></pre>
<p>테스트:</p>
<pre><code class="language-bash">ib_write_bw
ib_read_bw
ib_send_bw
ib_write_lat</code></pre>
<p><strong>목적</strong></p>
<blockquote>
<p>&quot;현재 external network의 L3 경로에서 RoCEv2 자체가 통과하는가?&quot;</p>
</blockquote>
<hr>
<h3 id="test-2--gpu-direct-rdma">Test 2 — GPU Direct RDMA</h3>
<p>GPU Node 내부에서:</p>
<pre><code class="language-text">B300
 │
 │ PCIe P2P
 ▼
CX-6</code></pre>
<p>을 검증합니다.</p>
<p>확인:</p>
<pre><code class="language-bash">nvidia-smi topo -m
lspci -tv</code></pre>
<p>그리고 GPU memory를 RDMA buffer로 사용하는 테스트를 합니다.</p>
<p>여기서는 <strong>PCIe ACS, GPU-NIC topology, GPU driver, CUDA, RDMA driver</strong>가 중요합니다. AIStor도 GPU-Direct RDMA에서 GPU-to-NIC PCIe peer-to-peer DMA를 요구하고 있습니다. (<a href="https://docs.min.io/aistor/operations/rdma/configure-the-rdma-fabric/?utm_source=chatgpt.com" title="Configure the RDMA fabric | MinIO AIStor Documentation">MinIO AIStor Documentation</a>)</p>
<hr>
<h3 id="test-3--aistor-s3-over-rdma">Test 3 — AIStor S3 over RDMA</h3>
<p>최종적으로:</p>
<pre><code class="language-text">B300
 │
 │ GPUDirect
 ▼
CX-6
 │
 │ RoCEv2 / L3
 ▼
E810
 │
 ▼
AIStor</code></pre>
<p>까지 가는 겁니다.</p>
<p>AIStor의 S3 over RDMA는 <code>GetObject</code>, <code>PutObject</code>, <code>UploadPart</code> 같은 payload operation에 RDMA path를 사용할 수 있고, GPU memory를 대상으로 하려면 GPU-to-NIC peer-to-peer DMA가 추가로 필요합니다. (<a href="https://docs.min.io/aistor/operations/core-concepts/ai-workloads/?utm_source=chatgpt.com" title="AI Workloads | MinIO AIStor Documentation">MinIO AIStor Documentation</a>)</p>
<hr>
<h1 id="6-그리고-이-테스트의-가장-큰-가치">6. 그리고 이 테스트의 가장 큰 가치</h1>
<p>이렇게 하면 <strong>private network가 완성되기 전에 상당 부분을 선행 검증</strong>할 수 있습니다.</p>
<p>제가 보면 지금 상황에서 아래처럼 바꾸는 게 좋습니다.</p>
<pre><code class="language-text">기존 계획

① HW
 ↓
② GPU Driver/Burn
 ↓
③ Private Network 구축
 ↓
④ RDMA
 ↓
⑤ vLLM
 ↓
⑥ AIStor</code></pre>
<p>보다는:</p>
<pre><code class="language-text">① HW / BIOS / Firmware
        ↓
② GPU Driver / CUDA / NCCL / Burn-in
        ↓
③ CX-6 / E810 RDMA stack
        ↓
④ CX-6 ↔ E810
   L3 Routed RoCEv2
   ib_write_bw / ib_read_bw
        ↓
⑤ GPU ↔ CX-6
   GPUDirect RDMA
        ↓
⑥ vLLM
        ↓
⑦ Compute ↔ GPU
        ↓
⑧ Private Network 구축
        ↓
⑨ Private RoCE Fabric
   PFC / ECN / DSCP / MTU
        ↓
⑩ AIStor S3 over RDMA
        ↓
⑪ GPU → CX-6 → RoCE → AIStor
        ↓
⑫ E2E</code></pre>
<p><strong>이게 훨씬 효율적입니다.</strong></p>
<p>특히 지금 external network가 이미 <strong>GPU Node ↔ Storage Node 간 L3 routing이 가능하고, 스위치가 RoCE QoS/ECN/PFC를 지원한다면</strong>, private network를 기다릴 이유가 없습니다.</p>
<p>다만 <strong>현재 external network에서 RDMA가 성공했다고 해서 private network의 RDMA fabric까지 PASS라고 판정하면 안 됩니다.</strong> Private network에서는 새로운 switch, VLAN/VRF, MTU, DSCP, PFC, ECN, buffer 설정이 들어가기 때문입니다. AIStor도 lossless RoCE에서는 경로상의 모든 스위치/호스트에서 일관된 QoS 구성이 필요하다고 명시합니다. (<a href="https://docs.min.io/aistor/operations/rdma/configure-the-rdma-fabric/?utm_source=chatgpt.com" title="Configure the RDMA fabric | MinIO AIStor Documentation">MinIO AIStor Documentation</a>)</p>
<h3 id="특히-제가-지금-확인하고-싶은-건-4개입니다">특히 제가 지금 확인하고 싶은 건 4개입니다.</h3>
<ol>
<li><strong>GPU Node CX-6가 몇 Gbps 포트인지</strong></li>
<li><strong>Storage Node E810이 몇 Gbps 포트인지</strong></li>
<li><strong>중간 L3 Switch/Router 모델</strong></li>
<li><strong>그 장비가 RoCEv2 + PFC + ECN + DSCP QoS를 L3 routed 환경에서 지원하는지</strong></li>
</ol>
<p>이 4개만 알면, <strong>지금 external network를 이용해서 바로 할 수 있는 RDMA 사전검증 절차를 <code>GPU node ↔ Storage node</code> 기준으로 명령어까지 포함해서 Test Case/Pass-Fail 표 형태로</strong> 만들어 드릴 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S05o]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S05o</link>
            <guid>https://velog.io/@youngkyoo_kim/26S05o</guid>
            <pubDate>Fri, 04 Sep 2026 20:03:56 GMT</pubDate>
            <description><![CDATA[<p>네. 지금 상황이라면 <strong>“GPU node를 당장 기존 Compute Cluster에 join시키는 것”을 1차 목표로 잡지 않는 것이 좋습니다.</strong>
오히려 <strong>GPU node 단독 검증 → GPU SW stack 검증 → 외부망을 통한 Compute 연동 → Private망 개통 후 K8s 편입 → Storage/RDMA → E2E 성능</strong> 순으로 가는 것이 리스크가 가장 낮습니다.</p>
<p>특히 현재 구조에서 가장 먼저 짚어야 할 것이 하나 있습니다.</p>
<blockquote>
<p><strong>ConnectX-6가 외부망, Intel E810이 private망</strong>이라는 NIC 배치는 향후 <strong>GPUDirect RDMA / AIStor S3 over RDMA</strong>를 하려는 경우 재검토가 필요합니다.</p>
</blockquote>
<p>NVIDIA Network Operator의 현재 문서상 GPUDirect RDMA는 NVIDIA ConnectX/BlueField 계열 NIC를 대상으로 하며, RHEL 10.2 + Kubernetes 1.32~1.36도 지원 조합에 포함됩니다. (<a href="https://docs.nvidia.com/networking/display/kubernetes2670/platform-support.html?utm_source=chatgpt.com" title="Platform Support - NVIDIA Docs">NVIDIA Docs</a>)
반면 AIStor의 RDMA 자체는 RDMA-capable NIC를 요구하고 ConnectX-6 이상을 권장하지만, <strong>GPU memory까지 직접 연결하는 GPUDirect RDMA 경로라면 GPU↔NVIDIA NIC가 핵심</strong>입니다. (<a href="https://docs.min.io/aistor/operations/rdma/configure-the-rdma-fabric/?utm_source=chatgpt.com" title="Configure the RDMA fabric | MinIO AIStor Documentation">MinIO AIStor Documentation</a>)</p>
<p>따라서 저는 아래처럼 진행하는 것을 추천합니다.</p>
<hr>
<h1 id="1-전체-구축검증-로드맵">1. 전체 구축/검증 로드맵</h1>
<p>전체적으로는 다음 6단계로 잡겠습니다.</p>
<pre><code class="language-text">                    ┌───────────────────────────────┐
                    │       GPU Server / B300 x8    │
                    │                               │
                    │  B300 x 8                     │
                    │       │                       │
                    │  PCIe / NVLink / NVSwitch     │
                    │       │                       │
                    │  ConnectX-6 ── External NET   │
                    │  Intel E810 ── Private NET    │
                    └──────────────┬────────────────┘
                                   │
             ┌─────────────────────┼─────────────────────┐
             │                     │                     │
       [External Network]    [Private Network]      [Management]
             │                     │
       Compute Cluster        Storage Cluster
       API access             AIStor</code></pre>
<h3 id="phase-0--인프라하드웨어-수령-검증">Phase 0 — 인프라/하드웨어 수령 검증</h3>
<p>↓</p>
<h3 id="phase-1--gpu-node-osnicpcie-기본-검증">Phase 1 — GPU Node OS/NIC/PCIe 기본 검증</h3>
<p>↓</p>
<h3 id="phase-2--nvidia-drivergpuncclburn-in">Phase 2 — NVIDIA Driver/GPU/NCCL/Burn-in</h3>
<p>↓</p>
<h3 id="phase-3--vllm-단독-구동--compute-cluster-연동">Phase 3 — vLLM 단독 구동 + Compute Cluster 연동</h3>
<p>↓</p>
<h3 id="phase-4--private-network-구축--computestorage-연결">Phase 4 — Private Network 구축 + Compute/Storage 연결</h3>
<p>↓</p>
<h3 id="phase-5--aistor--rdma--gpudirect-rdma-검증">Phase 5 — AIStor / RDMA / GPUDirect RDMA 검증</h3>
<p>↓</p>
<h3 id="phase-6--e2e-성능--장애--안정성-검증">Phase 6 — E2E 성능 / 장애 / 안정성 검증</h3>
<hr>
<h1 id="2-phase-0--인프라로부터-노드-받은-직후">2. Phase 0 — 인프라로부터 노드 받은 직후</h1>
<p>이 단계에서는 <strong>아직 Kubernetes에 join하지 않습니다.</strong></p>
<p>목표는:</p>
<blockquote>
<p>&quot;이 서버 자체가 정상인가?&quot;</p>
</blockquote>
<p>입니다.</p>
<h2 id="2-1-hardware-inventory">2-1. Hardware inventory</h2>
<p>먼저 다음을 확보합니다.</p>
<pre><code class="language-bash">dmidecode
lscpu
free -h
lsblk
lspci -nn
lspci -nn | grep -Ei &#39;nvidia|mellanox|ethernet|vga|3d&#39;</code></pre>
<p>GPU:</p>
<pre><code class="language-bash">nvidia-smi</code></pre>
<p>단, NVIDIA driver가 아직 없다면:</p>
<pre><code class="language-bash">lspci -nn | grep -i nvidia</code></pre>
<p>로 PCIe enumeration부터 확인합니다.</p>
<p>NIC:</p>
<pre><code class="language-bash">lspci -nn | grep -Ei &#39;ethernet|mellanox|intel&#39;
ip -br link
ip -br addr</code></pre>
<p>특히 다음을 기록해 두는 것이 좋습니다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>확인</th>
</tr>
</thead>
<tbody><tr>
<td>GPU 개수</td>
<td>8</td>
</tr>
<tr>
<td>GPU PCIe BDF</td>
<td>각각 기록</td>
</tr>
<tr>
<td>GPU SKU</td>
<td>B300</td>
</tr>
<tr>
<td>NVLink/NVSwitch</td>
<td>topology 확인</td>
</tr>
<tr>
<td>ConnectX-6</td>
<td>PCIe BDF / firmware</td>
</tr>
<tr>
<td>E810</td>
<td>PCIe BDF / firmware</td>
</tr>
<tr>
<td>CPU</td>
<td>model/socket/core</td>
</tr>
<tr>
<td>NUMA</td>
<td>GPU ↔ CPU ↔ NIC affinity</td>
</tr>
<tr>
<td>RAM</td>
<td>capacity/speed</td>
</tr>
<tr>
<td>NVMe</td>
<td>device/firmware</td>
</tr>
<tr>
<td>BIOS</td>
<td>version</td>
</tr>
<tr>
<td>BMC</td>
<td>version</td>
</tr>
<tr>
<td>OS</td>
<td>RHEL 10.2</td>
</tr>
<tr>
<td>Kernel</td>
<td>exact version</td>
</tr>
<tr>
<td>containerd</td>
<td>version</td>
</tr>
</tbody></table>
<hr>
<h1 id="3-phase-1--os--pcie--nic-기본-테스트">3. Phase 1 — OS + PCIe + NIC 기본 테스트</h1>
<p>이 단계에서 <strong>성능 테스트를 하지 말고 정상 동작 여부만 확인</strong>합니다.</p>
<h2 id="3-1-numa-topology">3-1. NUMA topology</h2>
<p>B300 8장에서는 상당히 중요합니다.</p>
<pre><code class="language-bash">numactl -H
lscpu -e
lspci -tv</code></pre>
<p>그리고 가능하면:</p>
<pre><code class="language-bash">nvidia-smi topo -m</code></pre>
<p>결과를 저장합니다.</p>
<p>예를 들어 이상적인 형태는:</p>
<pre><code class="language-text">        GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7
GPU0     X   NV   NV   NV   SYS  ...
...
NIC0    PIX  PIX  ...
NIC1    SYS  SYS</code></pre>
<p>여기서 중요한 것은:</p>
<p><strong>GPU ↔ NIC의 PCIe locality</strong></p>
<p>입니다.</p>
<hr>
<h1 id="4-nic-검증">4. NIC 검증</h1>
<h3 id="connectx-6">ConnectX-6</h3>
<pre><code class="language-bash">ethtool -i &lt;cx6-interface&gt;
ethtool &lt;cx6-interface&gt;</code></pre>
<p>Mellanox/NVIDIA 계열이면:</p>
<pre><code class="language-bash">mlxconfig -d &lt;device&gt; q
mlxlink -d &lt;device&gt;</code></pre>
<p>등을 확인합니다.</p>
<h3 id="intel-e810">Intel E810</h3>
<pre><code class="language-bash">ethtool -i &lt;e810-interface&gt;
ethtool &lt;e810-interface&gt;</code></pre>
<p>그리고:</p>
<pre><code class="language-bash">ip -d link show &lt;interface&gt;</code></pre>
<hr>
<h1 id="5-nic-firmware도-반드시-baseline을-잡아야-합니다">5. NIC firmware도 반드시 baseline을 잡아야 합니다</h1>
<p>GPU/RDMA POC에서 의외로 많이 걸리는 부분입니다.</p>
<p>다음 정보를 표로 남겨두세요.</p>
<pre><code class="language-text">BIOS
BMC
GPU firmware
ConnectX-6 firmware
Intel E810 firmware
RHEL kernel
NVIDIA driver
CUDA
NCCL
GPU Operator
Network Operator
containerd
Kubernetes
Cilium</code></pre>
<p><strong>이 버전을 POC 기준선(baseline)으로 freeze</strong>하는 것을 권합니다.</p>
<hr>
<h1 id="6-phase-2--nvidia-driver--gpu-검증">6. Phase 2 — NVIDIA Driver / GPU 검증</h1>
<p>여기부터 GPU software stack을 설치합니다.</p>
<p>현재 NVIDIA Network Operator 26.7 계열은 RHEL 10.2 + Kubernetes 1.32~1.36을 지원하고, ConnectX-6의 Ethernet/RoCE도 지원 대상으로 명시하고 있습니다. (<a href="https://docs.nvidia.com/networking/display/kubernetes2670/platform-support.html?utm_source=chatgpt.com" title="Platform Support - NVIDIA Docs">NVIDIA Docs</a>)</p>
<p>따라서 향후 현재 사용 중인 K8s 1.33 계열과도 방향은 맞습니다.</p>
<hr>
<h2 id="6-1-gpu-driver">6-1. GPU Driver</h2>
<p>먼저 standalone node에서 driver를 설치합니다.</p>
<p>확인:</p>
<pre><code class="language-bash">nvidia-smi
nvidia-smi -L</code></pre>
<p>8장이 모두 나와야 합니다.</p>
<p>예:</p>
<pre><code class="language-text">GPU 0: NVIDIA B300
GPU 1: NVIDIA B300
...
GPU 7: NVIDIA B300</code></pre>
<hr>
<h1 id="7-gpu-기본-테스트">7. GPU 기본 테스트</h1>
<h3 id="pcie">PCIe</h3>
<pre><code class="language-bash">nvidia-smi -q</code></pre>
<p>특히:</p>
<ul>
<li>PCIe Gen</li>
<li>PCIe width</li>
<li>BAR1</li>
<li>ECC</li>
<li>temperature</li>
<li>power</li>
<li>clocks</li>
<li>memory</li>
</ul>
<p>확인.</p>
<h3 id="gpu-memory">GPU memory</h3>
<p>CUDA sample 또는 간단한 CUDA test로:</p>
<pre><code class="language-text">GPU memory allocation
GPU → memory copy
memory bandwidth</code></pre>
<p>확인합니다.</p>
<hr>
<h1 id="8-gpu-burn-in">8. GPU Burn-in</h1>
<p>여기서는 단순히 <code>nvidia-smi</code>가 된다고 PASS하지 않는 게 좋습니다.</p>
<p>최소:</p>
<h3 id="test-a--1-gpu">Test A — 1 GPU</h3>
<pre><code class="language-text">GPU0 100%</code></pre>
<h3 id="test-b--8-gpu-동시">Test B — 8 GPU 동시</h3>
<pre><code class="language-text">GPU0~GPU7 100%</code></pre>
<h3 id="test-c--장시간">Test C — 장시간</h3>
<p>최소:</p>
<pre><code class="language-text">1h</code></pre>
<p>가능하면:</p>
<pre><code class="language-text">4h</code></pre>
<p>그리고 production acceptance라면:</p>
<pre><code class="language-text">8~24h</code></pre>
<p>까지.</p>
<p>관찰할 항목:</p>
<pre><code class="language-text">GPU temperature
GPU power
GPU utilization
GPU memory utilization
ECC error
XID error
PCIe error
GPU reset
kernel error</code></pre>
<p>특히:</p>
<pre><code class="language-bash">dmesg -T | grep -Ei &#39;NVRM|Xid|AER|PCIe&#39;</code></pre>
<p>를 계속 확인합니다.</p>
<hr>
<h1 id="9-nvlink--nvswitch-검증">9. NVLink / NVSwitch 검증</h1>
<p>B300 8-GPU라면 상당히 중요합니다.</p>
<pre><code class="language-bash">nvidia-smi topo -m</code></pre>
<p>그리고 NCCL 테스트를 합니다.</p>
<hr>
<h1 id="10-nccl-test">10. NCCL Test</h1>
<p>이건 <strong>GPU 서버 acceptance test에서 필수</strong>로 넣는 것을 추천합니다.</p>
<p>대표적으로:</p>
<pre><code class="language-text">all_reduce
all_gather
broadcast
reduce_scatter</code></pre>
<p>특히:</p>
<pre><code class="language-text">8 GPU AllReduce</code></pre>
<p>를 봅니다.</p>
<p>테스트 축:</p>
<table>
<thead>
<tr>
<th>GPU</th>
<th>Message</th>
</tr>
</thead>
<tbody><tr>
<td>2</td>
<td>1MB ~ 1GB</td>
</tr>
<tr>
<td>4</td>
<td>1MB ~ 1GB</td>
</tr>
<tr>
<td>8</td>
<td>1MB ~ 1GB</td>
</tr>
</tbody></table>
<p>그리고:</p>
<pre><code class="language-text">latency
bandwidth
algorithm bandwidth
bus bandwidth</code></pre>
<p>를 기록합니다.</p>
<hr>
<h1 id="11-phase-3--vllm-standalone">11. Phase 3 — vLLM standalone</h1>
<p>이 단계에서도 <strong>Kubernetes에 join하지 않아도 됩니다.</strong></p>
<p>GPU node에서 먼저:</p>
<pre><code class="language-text">vLLM
  ↓
B300 x8
  ↓
model</code></pre>
<p>을 검증합니다.</p>
<p>vLLM 자체가 정상 동작하는지부터 확인합니다.</p>
<p>최근 NVIDIA의 vLLM 26.07 문서에서도 Blackwell B200/B300 대상 NVFP4 MoE 최적화 등이 별도로 언급되고 있으므로, <strong>사용하려는 실제 모델/quantization 조합을 별도로 검증</strong>하는 것이 좋습니다. (<a href="https://docs.nvidia.com/deeplearning/frameworks/vllm-release-notes/rel-26-07.html?utm_source=chatgpt.com" title="vLLM Release 26.07 - NVIDIA Docs">NVIDIA Docs</a>)</p>
<hr>
<h1 id="12-vllm-테스트">12. vLLM 테스트</h1>
<p>처음에는 작은 모델부터:</p>
<pre><code class="language-text">7B
14B
32B
70B</code></pre>
<p>순으로 테스트하고,</p>
<p>최종 목표 모델을 올립니다.</p>
<p>측정:</p>
<h3 id="기본">기본</h3>
<pre><code class="language-text">model load time
GPU memory
GPU utilization</code></pre>
<h3 id="inference">Inference</h3>
<pre><code class="language-text">TTFT
TPOT
ITL
request latency
tokens/sec
output tokens/sec</code></pre>
<h3 id="concurrency">concurrency</h3>
<pre><code class="language-text">1
2
4
8
16
32
64
128</code></pre>
<p>식으로 올립니다.</p>
<hr>
<h1 id="13-이-시점에서-compute-cluster와-연동">13. 이 시점에서 Compute Cluster와 연동</h1>
<p>여기서 중요한 포인트입니다.</p>
<p><strong>Private network가 아직 없다면 GPU node를 Compute Cluster에 억지로 join시키지 않아도 됩니다.</strong></p>
<p>대신:</p>
<pre><code class="language-text">Compute Cluster
       │
       │ External Network
       ▼
ConnectX-6
       │
       ▼
GPU Node
       │
       ▼
vLLM</code></pre>
<p>형태로 <strong>API-level integration</strong>을 먼저 할 수 있습니다.</p>
<p>예:</p>
<pre><code class="language-text">Compute Cluster application
        ↓
http://&lt;GPU-node-IP&gt;:8000
        ↓
vLLM OpenAI API
        ↓
B300 x8</code></pre>
<p>이렇게 하면 <strong>&quot;GPU compute가 기존 cluster에서 호출 가능한가?&quot;</strong>를 먼저 검증할 수 있습니다.</p>
<hr>
<h1 id="14-이때-반드시-테스트할-것">14. 이때 반드시 테스트할 것</h1>
<h3 id="functional">Functional</h3>
<pre><code class="language-text">Compute → vLLM</code></pre>
<ul>
<li>connection</li>
<li>authentication</li>
<li>model discovery</li>
<li>completion</li>
<li>chat completion</li>
<li>streaming</li>
<li>timeout</li>
<li>retry</li>
</ul>
<h3 id="performance">Performance</h3>
<p>Compute cluster에서:</p>
<pre><code class="language-text">1 request
10
100
1000</code></pre>
<p>식으로 부하를 줍니다.</p>
<p>그리고:</p>
<pre><code class="language-text">TTFT
TPOT
tokens/sec
concurrency
HTTP latency
network bandwidth
CPU
GPU utilization
GPU memory</code></pre>
<p>를 동시에 봅니다.</p>
<hr>
<h1 id="15-phase-4--private-network-구성">15. Phase 4 — Private Network 구성</h1>
<p>이제 인프라에서 private network가 준비되면 <strong>여기부터 진짜 cluster integration</strong>을 시작합니다.</p>
<p>현재 구조를 제가 이해한 그림은:</p>
<pre><code class="language-text">                Compute Cluster
                     │
                  bond1
                     │
               Private Network
                     │
                ┌────┴────┐
                │         │
           Storage      GPU Node
           Cluster
             bond1       E810</code></pre>
<p>이 형태입니다.</p>
<p>여기서 GPU node는:</p>
<pre><code class="language-text">E810 → Private network
CX6  → External network</code></pre>
<p>가 됩니다.</p>
<hr>
<h1 id="16-가장-중요한-설계-결정">16. 가장 중요한 설계 결정</h1>
<p>여기서 반드시 결정해야 합니다.</p>
<h3 id="option-a">Option A</h3>
<pre><code class="language-text">E810
  ↓
K8s private network
  ↓
Compute + Storage</code></pre>
<p>그리고:</p>
<pre><code class="language-text">CX6
  ↓
External network
  ↓
vLLM API</code></pre>
<h3 id="option-b">Option B</h3>
<pre><code class="language-text">CX6
  ↓
RoCE/RDMA private fabric
  ↓
AIStor</code></pre>
<p>그리고:</p>
<pre><code class="language-text">E810
  ↓
K8s private network</code></pre>
<p><strong>GPUDirect RDMA까지 목표라면 B에 가까운 구성이 더 적절합니다.</strong></p>
<p>왜냐하면 GPUDirect RDMA의 핵심은:</p>
<pre><code class="language-text">GPU
 ↓
PCIe P2P
 ↓
NVIDIA NIC
 ↓
RoCE
 ↓
Storage</code></pre>
<p>이기 때문입니다.</p>
<p>NVIDIA 문서에서도 GPUDirect RDMA는 지원 GPU와 <strong>NVIDIA ConnectX/BlueField NIC</strong> 조합을 요구합니다. (<a href="https://docs.nvidia.com/networking/display/kubernetes2670/platform-support.html?utm_source=chatgpt.com" title="Platform Support - NVIDIA Docs">NVIDIA Docs</a>)</p>
<p>따라서 현재처럼</p>
<pre><code class="language-text">GPU
 │
 ├── ConnectX-6 → External
 │
 └── E810      → Private</code></pre>
<p>이라면,</p>
<p><strong>E810 private망으로 AIStor까지 일반 TCP 통신은 가능하지만, GPU Direct RDMA를 하려는 경우에는 구조적인 제약이 생길 가능성이 높습니다.</strong></p>
<p>이건 POC 초기에 반드시 결정해야 합니다.</p>
<hr>
<h1 id="17-private-network-기본-테스트">17. Private Network 기본 테스트</h1>
<p>E810 연결 후에는 먼저 K8s를 올리지 말고 network 자체를 검증합니다.</p>
<p>GPU node:</p>
<pre><code class="language-bash">ip addr
ip route
ip neigh</code></pre>
<p>Compute node:</p>
<pre><code class="language-bash">ping &lt;GPU-private-IP&gt;</code></pre>
<p>Storage node:</p>
<pre><code class="language-bash">ping &lt;GPU-private-IP&gt;</code></pre>
<p>다음:</p>
<pre><code class="language-text">GPU ↔ Compute
GPU ↔ Storage</code></pre>
<p>각각:</p>
<pre><code class="language-text">ping
MTU
TCP
bandwidth
packet loss</code></pre>
<p>검증.</p>
<hr>
<h1 id="18-mtu는-반드시-확인">18. MTU는 반드시 확인</h1>
<p>RoCE를 할 계획이면 특히 중요합니다.</p>
<p>예:</p>
<pre><code class="language-bash">ip link show</code></pre>
<p>그리고:</p>
<pre><code class="language-bash">ping -M do -s &lt;size&gt; &lt;peer&gt;</code></pre>
<p>로 MTU를 검증합니다.</p>
<p>가능하면:</p>
<pre><code class="language-text">MTU 1500
MTU 9000</code></pre>
<p>중 실제 설계값을 확정합니다.</p>
<p><strong>한쪽만 jumbo frame인 상태가 가장 위험합니다.</strong></p>
<hr>
<h1 id="19-iperf3">19. iperf3</h1>
<p>최소:</p>
<pre><code class="language-text">GPU Node ↔ Compute
GPU Node ↔ Storage</code></pre>
<p>를 테스트합니다.</p>
<p>예:</p>
<pre><code class="language-bash">iperf3 -s</code></pre>
<p>반대쪽:</p>
<pre><code class="language-bash">iperf3 -c &lt;server&gt; -P 1
iperf3 -c &lt;server&gt; -P 4
iperf3 -c &lt;server&gt; -P 8</code></pre>
<p>그리고:</p>
<pre><code class="language-text">1 stream
4 streams
8 streams
16 streams</code></pre>
<p>을 비교합니다.</p>
<p>여기서 중요한 것은 단순 bandwidth뿐 아니라:</p>
<pre><code class="language-text">packet loss
retransmission
CPU utilization
NIC utilization</code></pre>
<p>입니다.</p>
<hr>
<h1 id="20-rdma는-별도로-검증">20. RDMA는 별도로 검증</h1>
<p>Private network가 RoCE로 구성된다면:</p>
<pre><code class="language-bash">ibv_devices
ibv_devinfo
rdma link
rdma dev</code></pre>
<p>확인.</p>
<p>그리고:</p>
<pre><code class="language-text">perftest
 ├── ib_write_bw
 ├── ib_read_bw
 ├── ib_send_bw
 ├── ib_write_lat
 └── ib_read_lat</code></pre>
<p>등으로 검증합니다.</p>
<hr>
<h1 id="21-roce-fabric-검증">21. RoCE fabric 검증</h1>
<p>이 부분은 일반 Ethernet 테스트와 별개입니다.</p>
<p>확인:</p>
<pre><code class="language-text">RoCEv2
PFC
ECN
DSCP/PCP
DCB
QoS
MTU
switch buffer</code></pre>
<p>특히 PFC를 사용한다면:</p>
<pre><code class="language-text">RDMA traffic
       ↓
lossless queue
       ↓
PFC</code></pre>
<p>가 제대로 구성됐는지 확인해야 합니다.</p>
<p><strong>여기서 기존 Cilium/K8s traffic에 영향을 주지 않는지도 반드시 검증</strong>해야 합니다.</p>
<hr>
<h1 id="22-phase-5--storage-cluster--aistor-연동">22. Phase 5 — Storage Cluster / AIStor 연동</h1>
<p>여기서는 두 단계로 나누는 것을 추천합니다.</p>
<h2 id="stage-1--tcp">Stage 1 — TCP</h2>
<p>먼저:</p>
<pre><code class="language-text">GPU Node
   │
 E810
   │
Private Network
   │
AIStor</code></pre>
<p>에서 일반 S3/TCP 성능을 측정합니다.</p>
<hr>
<h2 id="stage-2--rdma">Stage 2 — RDMA</h2>
<p>그 다음:</p>
<pre><code class="language-text">GPU
 ↓
GPUDirect RDMA
 ↓
NIC
 ↓
RoCE
 ↓
AIStor</code></pre>
<p>를 검증합니다.</p>
<p>AIStor의 현재 RDMA 문서에서는 RoCE v2를 사용하려면 end-to-end lossless fabric과 PFC 등이 필요하다고 명시하고 있습니다. (<a href="https://docs.min.io/aistor/operations/rdma/configure-the-rdma-fabric/?utm_source=chatgpt.com" title="Configure the RDMA fabric | MinIO AIStor Documentation">MinIO AIStor Documentation</a>)</p>
<hr>
<h1 id="23-여기서-aistor-테스트를-3개로-분리하세요">23. 여기서 AIStor 테스트를 3개로 분리하세요</h1>
<h3 id="test-a--s3tcp">Test A — S3/TCP</h3>
<pre><code class="language-text">GPU Node
   ↓ TCP
AIStor</code></pre>
<h3 id="test-b--s3rdma">Test B — S3/RDMA</h3>
<pre><code class="language-text">GPU Node
   ↓ RDMA
AIStor</code></pre>
<h3 id="test-c--gpu-direct">Test C — GPU Direct</h3>
<pre><code class="language-text">GPU memory
   ↓
NIC
   ↓
RoCE
   ↓
AIStor</code></pre>
<p>이렇게 분리해야 합니다.</p>
<p>그렇지 않으면 문제가 생겼을 때:</p>
<blockquote>
<p>GPU 문제인지 / NIC 문제인지 / RDMA 문제인지 / AIStor 문제인지</p>
</blockquote>
<p>구분이 안 됩니다.</p>
<hr>
<h1 id="24-aistor-storage-성능-테스트">24. AIStor Storage 성능 테스트</h1>
<p>최소:</p>
<pre><code class="language-text">GET
PUT
multipart upload
large object
small object
concurrent objects</code></pre>
<p>를 테스트합니다.</p>
<p>예를 들어:</p>
<table>
<thead>
<tr>
<th align="right">Object</th>
<th align="right">Concurrency</th>
</tr>
</thead>
<tbody><tr>
<td align="right">1 MB</td>
<td align="right">1/16/64/256</td>
</tr>
<tr>
<td align="right">64 MB</td>
<td align="right">1/16/64</td>
</tr>
<tr>
<td align="right">1 GB</td>
<td align="right">1/4/16/64</td>
</tr>
<tr>
<td align="right">10 GB</td>
<td align="right">1/4/16</td>
</tr>
</tbody></table>
<p>측정:</p>
<pre><code class="language-text">MB/s
GB/s
IOPS
latency
CPU
NIC
GPU utilization
AIStor disk latency
AIStor network</code></pre>
<hr>
<h1 id="25-gpudirect-storageaistor를-한다면-추가-검증">25. GPUDirect Storage/AIStor를 한다면 추가 검증</h1>
<p>여기서는 단순히:</p>
<pre><code class="language-text">ibv_devinfo = OK</code></pre>
<p>만으로 PASS하면 안 됩니다.</p>
<p>확인해야 할 것이:</p>
<pre><code class="language-text">GPU ↔ NIC PCIe P2P
IOMMU
ACS
BAR1
nvidia_peermem
RDMA device
GPU/NIC affinity</code></pre>
<p>입니다.</p>
<p>AIStor 문서도 GPU S3 over RDMA의 경우 GPU-to-NIC peer-to-peer DMA와 NVIDIA GPU/RDMA 지원을 요구합니다. (<a href="https://docs.min.io/aistor/operations/rdma/configure-the-rdma-fabric/?utm_source=chatgpt.com" title="Configure the RDMA fabric | MinIO AIStor Documentation">MinIO AIStor Documentation</a>)</p>
<hr>
<h1 id="26-phase-6--최종-e2e-test">26. Phase 6 — 최종 E2E Test</h1>
<p>최종적으로는 실제 workload와 최대한 비슷하게 만듭니다.</p>
<pre><code class="language-text">User / Application
       │
       ▼
Compute Cluster
       │
       │ API
       ▼
vLLM
       │
       ▼
B300 x8
       │
       │ RDMA / TCP
       ▼
AIStor
       │
       ▼
Storage</code></pre>
<hr>
<h1 id="27-e2e-테스트-시나리오">27. E2E 테스트 시나리오</h1>
<h2 id="test-1--inference-only">Test 1 — inference only</h2>
<pre><code class="language-text">Compute → vLLM → GPU</code></pre>
<p>측정:</p>
<pre><code class="language-text">TTFT
TPOT
tokens/sec
QPS
GPU utilization</code></pre>
<hr>
<h2 id="test-2--inference--model-loading">Test 2 — inference + model loading</h2>
<pre><code class="language-text">AIStor
 ↓
GPU
 ↓
vLLM</code></pre>
<p>측정:</p>
<pre><code class="language-text">model load time
storage throughput
GPU utilization
network</code></pre>
<hr>
<h2 id="test-3--concurrent-inference">Test 3 — concurrent inference</h2>
<pre><code class="language-text">Compute
 ↓
vLLM
 ↓
8 GPU</code></pre>
<p>concurrency:</p>
<pre><code class="language-text">1
2
4
8
16
32
64
128
256</code></pre>
<p>까지 올려봅니다.</p>
<hr>
<h1 id="28-test-4--storage--inference-동시-부하">28. Test 4 — Storage + inference 동시 부하</h1>
<p>이게 실제 운영에서는 상당히 중요합니다.</p>
<pre><code class="language-text">             ┌── inference
Compute ─────┤
             └── model/data access
                    ↓
                  AIStor</code></pre>
<p>동시에:</p>
<pre><code class="language-text">AIStor PUT/GET
+
vLLM inference</code></pre>
<p>를 발생시킵니다.</p>
<p>이때:</p>
<pre><code class="language-text">GPU utilization
NIC utilization
AIStor latency
vLLM TTFT
vLLM TPOT</code></pre>
<p>가 서로 영향을 주는지 봅니다.</p>
<hr>
<h1 id="29-test-5--network-saturation">29. Test 5 — Network saturation</h1>
<p>예:</p>
<pre><code class="language-text">25G
50G
100G
200G</code></pre>
<p>환경에 맞게 최대 bandwidth까지 올려봅니다.</p>
<p>특히 <strong>NIC bandwidth가 증가하면서 vLLM latency가 얼마나 영향을 받는지</strong>가 중요합니다.</p>
<hr>
<h1 id="30-test-6--장애-테스트">30. Test 6 — 장애 테스트</h1>
<p>POC에서 반드시 넣는 것을 추천합니다.</p>
<h3 id="gpu">GPU</h3>
<pre><code class="language-text">GPU reset
GPU process crash
GPU XID</code></pre>
<h3 id="network">Network</h3>
<pre><code class="language-text">CX6 link down
E810 link down
network packet loss
RoCE congestion</code></pre>
<h3 id="aistor">AIStor</h3>
<pre><code class="language-text">AIStor node unavailable
network path unavailable</code></pre>
<h3 id="vllm">vLLM</h3>
<pre><code class="language-text">pod restart
container restart
model reload</code></pre>
<h3 id="compute">Compute</h3>
<pre><code class="language-text">client restart
connection timeout
retry</code></pre>
<hr>
<h1 id="31-특히-중요한-장애-시나리오">31. 특히 중요한 장애 시나리오</h1>
<p>실제 운영에서는 다음이 중요합니다.</p>
<pre><code class="language-text">Compute
   │
   ▼
vLLM
   │
   ▼
GPU</code></pre>
<p>중간에 vLLM이 죽으면:</p>
<pre><code class="language-text">HTTP timeout
retry
connection reset</code></pre>
<p>이 발생합니다.</p>
<p>따라서 Compute 측에서:</p>
<pre><code class="language-text">timeout
retry
backoff
circuit breaker</code></pre>
<p>를 검증해야 합니다.</p>
<hr>
<h1 id="32-제가-보는-가장-중요한-risk-8개">32. 제가 보는 가장 중요한 Risk 8개</h1>
<h2 id="risk-1--nic-역할이-잘못-잡힐-가능성-★★★★★">Risk 1 — NIC 역할이 잘못 잡힐 가능성 ★★★★★</h2>
<p>현재:</p>
<pre><code class="language-text">CX6 → External
E810 → Private</code></pre>
<p>인데 GPUDirect RDMA가 목표라면 재검토가 필요합니다.</p>
<p><strong>가장 먼저 결정해야 합니다.</strong></p>
<hr>
<h2 id="risk-2--gpu-node를-너무-빨리-기존-k8s에-join">Risk 2 — GPU Node를 너무 빨리 기존 K8s에 join</h2>
<p>추천하지 않습니다.</p>
<p>왜냐하면:</p>
<pre><code class="language-text">GPU driver
CUDA
NCCL
CNI
RDMA
Network Operator
GPU Operator
Cilium</code></pre>
<p>문제가 한꺼번에 섞입니다.</p>
<p>따라서:</p>
<blockquote>
<p><strong>Standalone → Network → GPU → vLLM → Storage → K8s</strong></p>
</blockquote>
<p>순서가 좋습니다.</p>
<hr>
<h2 id="risk-3--osdriver-버전">Risk 3 — OS/Driver 버전</h2>
<p>특히 GPU Operator / Network Operator / CUDA / driver 조합입니다.</p>
<p>현재 Network Operator 26.7 기준으로 RHEL 10.2와 K8s 1.32~1.36이 지원되므로 현재 환경과의 방향은 맞지만, <strong>실제 B300 + 해당 driver/CUDA 조합은 POC에서 고정해서 검증</strong>해야 합니다. (<a href="https://docs.nvidia.com/networking/display/kubernetes2670/platform-support.html?utm_source=chatgpt.com" title="Platform Support - NVIDIA Docs">NVIDIA Docs</a>)</p>
<hr>
<h2 id="risk-4--roce는-nic만-연결하면-되는-것이-아님">Risk 4 — RoCE는 &quot;NIC만 연결하면 되는 것&quot;이 아님</h2>
<pre><code class="language-text">NIC
+
switch
+
PFC
+
ECN
+
DSCP
+
MTU
+
routing</code></pre>
<p>전체가 맞아야 합니다.</p>
<p>AIStor도 RoCE 환경에서 end-to-end lossless fabric을 요구합니다. (<a href="https://docs.min.io/aistor/operations/rdma/configure-the-rdma-fabric/?utm_source=chatgpt.com" title="Configure the RDMA fabric | MinIO AIStor Documentation">MinIO AIStor Documentation</a>)</p>
<hr>
<h2 id="risk-5--cilium과-rdma-traffic-충돌">Risk 5 — Cilium과 RDMA traffic 충돌</h2>
<p>현재 Cilium이:</p>
<pre><code class="language-text">Native routing
BGP
ECMP
Gateway API</code></pre>
<p>를 사용하고 있으므로 <strong>K8s 일반 traffic과 RDMA fabric을 논리적으로 분리</strong>하는 것을 권합니다.</p>
<p>즉:</p>
<pre><code class="language-text">K8s traffic
   ↓
Cilium / E810

RDMA traffic
   ↓
RoCE / dedicated NIC</code></pre>
<p>구조가 가장 깔끔합니다.</p>
<hr>
<h1 id="33-risk-6--mtu-mismatch">33. Risk 6 — MTU mismatch</h1>
<p>예:</p>
<pre><code class="language-text">GPU 9000
  ↓
Switch 9000
  ↓
Storage 1500</code></pre>
<p>이면 일반 TCP에서는 일부 동작하더라도 RDMA에서 문제가 훨씬 심각해질 수 있습니다.</p>
<hr>
<h1 id="34-risk-7--gpunic-pcie-topology">34. Risk 7 — GPU/NIC PCIe topology</h1>
<p>8-GPU B300에서는 단순히:</p>
<pre><code class="language-text">GPU 8개
NIC 1개</code></pre>
<p>가 중요한 게 아닙니다.</p>
<p><strong>어떤 GPU가 어떤 NIC/PCIe root complex와 연결되는지</strong>가 중요합니다.</p>
<p>반드시:</p>
<pre><code class="language-bash">nvidia-smi topo -m</code></pre>
<p>와 NUMA topology를 기준으로 benchmark 해야 합니다.</p>
<hr>
<h1 id="35-risk-8--성능-테스트가-최고값-위주가-되는-것">35. Risk 8 — 성능 테스트가 &quot;최고값&quot; 위주가 되는 것</h1>
<p>예를 들어:</p>
<pre><code class="language-text">Peak bandwidth = 180Gbps</code></pre>
<p>가 나왔다고 성공한 것이 아닙니다.</p>
<p>실제 운영에서는:</p>
<pre><code class="language-text">P99 latency
P99.9 latency
GPU utilization
network utilization
storage latency
error rate</code></pre>
<p>가 훨씬 중요합니다.</p>
<hr>
<h1 id="36-최종적으로-passfail-기준을-이렇게-잡는-것을-추천">36. 최종적으로 PASS/FAIL 기준을 이렇게 잡는 것을 추천</h1>
<table>
<thead>
<tr>
<th>영역</th>
<th>PASS 기준</th>
</tr>
</thead>
<tbody><tr>
<td>Hardware</td>
<td>모든 GPU/NIC/PCIe 정상</td>
</tr>
<tr>
<td>GPU Driver</td>
<td>8 GPU 정상 인식</td>
</tr>
<tr>
<td>GPU Burn</td>
<td>장시간 XID/ECC 오류 없음</td>
</tr>
<tr>
<td>NVLink</td>
<td>topology 정상</td>
</tr>
<tr>
<td>NCCL</td>
<td>8GPU expected BW 달성</td>
</tr>
<tr>
<td>vLLM</td>
<td>model load/inference 정상</td>
</tr>
<tr>
<td>Compute→vLLM</td>
<td>API 안정적</td>
</tr>
<tr>
<td>Private Network</td>
<td>packet loss 0</td>
</tr>
<tr>
<td>Network</td>
<td>목표 BW 달성</td>
</tr>
<tr>
<td>RDMA</td>
<td>ibv/perftest 정상</td>
</tr>
<tr>
<td>RoCE</td>
<td>PFC/ECN 정상</td>
</tr>
<tr>
<td>AIStor TCP</td>
<td>목표 throughput</td>
</tr>
<tr>
<td>AIStor RDMA</td>
<td>목표 throughput</td>
</tr>
<tr>
<td>GPUDirect</td>
<td>GPU memory direct path 확인</td>
</tr>
<tr>
<td>E2E</td>
<td>목표 TPS/latency</td>
</tr>
<tr>
<td>Failure</td>
<td>장애 후 recovery 정상</td>
</tr>
</tbody></table>
<hr>
<h1 id="37-제가-추천하는-실제-작업-순서">37. 제가 추천하는 실제 작업 순서</h1>
<p>실제로 현장에서 작업한다면 저는 아래 순서로 진행하겠습니다.</p>
<pre><code class="language-text">[DAY 1]
GPU Server HW inspection
        ↓
BIOS/BMC/FW
        ↓
RHEL 10.2
        ↓
NIC basic test
        ↓
GPU PCIe enumeration

[DAY 2]
NVIDIA Driver
        ↓
nvidia-smi
        ↓
8 GPU validation
        ↓
GPU burn-in

[DAY 3]
CUDA
        ↓
NCCL
        ↓
NVLink/NVSwitch
        ↓
8GPU NCCL benchmark

[DAY 4]
vLLM
        ↓
single request
        ↓
concurrency
        ↓
long-run test

[DAY 5]
Compute Cluster
        ↓
External network
        ↓
vLLM API
        ↓
E2E API test

[PRIVATE NETWORK READY]

[DAY 6]
E810 private network
        ↓
GPU ↔ Compute
        ↓
GPU ↔ Storage
        ↓
MTU
        ↓
iperf3

[DAY 7]
RDMA
        ↓
RDMA device
        ↓
perftest
        ↓
RoCE PFC/ECN

[DAY 8]
AIStor TCP
        ↓
AIStor RDMA
        ↓
GPU Direct RDMA

[DAY 9~10]
vLLM + AIStor
        ↓
E2E performance
        ↓
stress
        ↓
failure/recovery</code></pre>
<hr>
<h1 id="38-그리고-가장-중요한-구축-전략">38. 그리고 가장 중요한 &quot;구축 전략&quot;</h1>
<p>저라면 이번 POC에서는 <strong>Kubernetes Join을 마지막 쪽으로 미룹니다.</strong></p>
<p>즉,</p>
<h3 id="①-먼저-bare-metal-gpu-validation">① 먼저 Bare-metal GPU validation</h3>
<pre><code class="language-text">RHEL
 ├─ NVIDIA Driver
 ├─ CUDA
 ├─ NCCL
 ├─ GPU Burn
 ├─ CX6
 └─ E810</code></pre>
<h3 id="②-그-다음-vllm">② 그 다음 vLLM</h3>
<pre><code class="language-text">vLLM
 ↓
B300 x8</code></pre>
<h3 id="③-그-다음-compute-integration">③ 그 다음 Compute integration</h3>
<pre><code class="language-text">Existing Compute
       ↓
     HTTP
       ↓
    vLLM</code></pre>
<h3 id="④-private-network-개통">④ Private Network 개통</h3>
<pre><code class="language-text">GPU
 │
E810
 │
Private
 ├── Compute
 └── Storage</code></pre>
<h3 id="⑤-rdma">⑤ RDMA</h3>
<pre><code class="language-text">GPU
 │
CX6
 │
RoCE
 │
AIStor</code></pre>
<h3 id="⑥-마지막에-kubernetes">⑥ 마지막에 Kubernetes</h3>
<pre><code class="language-text">Existing Compute K8s
        │
        │ private
        ▼
     GPU Node
        │
        ├── GPU Operator
        ├── Network Operator
        ├── Cilium
        └── vLLM</code></pre>
<p>이렇게 가는 것이 좋습니다.</p>
<p><strong>특히 지금 CX6가 외부망이고 E810이 private망이라는 사실 때문에, <code>GPU → Storage → GPUDirect RDMA</code>를 최종 목표로 한다면 네트워크 설계를 먼저 확정한 후 장비를 물리는 것을 강하게 권합니다.</strong> CX6를 단순히 외부 API용 NIC로 사용하고 E810을 AIStor용 private NIC로 쓰는 구성이라면, <strong>TCP 기반 GPU↔AIStor 연동은 가능하지만 NVIDIA GPUDirect RDMA라는 최종 목표와는 맞지 않을 수 있습니다.</strong></p>
<p>참고로 AIStor의 최근 RDMA build는 RDMA fabric이 없으면 TCP로 동작하므로, 초기 TCP 검증과 이후 RDMA 검증을 분리해서 진행하는 전략도 가능합니다. (<a href="https://dl.min.io/aistor/minio/release/notes/release-notes-RELEASE.2026-08-07T18-34-35Z.md?utm_source=chatgpt.com" title="release-notes-RELEASE.2026-08-07T18-34-35Z.md - MinIO Downloads">MinIO</a>)</p>
<p>원하시면 다음 단계로 제가 이걸 <strong>실제 POC 수행용 &quot;GPU Node 구축/검증 체크리스트&quot; 형태로 만들어서</strong>, 각 단계별로 <code>설치 명령어 → 확인 명령어 → Prometheus/Grafana metric → PASS/FAIL 기준 → 예상 장애 → rollback 방법</code>까지 넣은 <strong>Excel 형태의 테스트 시나리오</strong>로 구성해드릴 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S05h]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S05h</link>
            <guid>https://velog.io/@youngkyoo_kim/26S05h</guid>
            <pubDate>Fri, 04 Sep 2026 20:01:37 GMT</pubDate>
            <description><![CDATA[<h1 id="gpu-node-온보딩--클러스터-연동-계획">GPU Node 온보딩 &amp; 클러스터 연동 계획</h1>
<h2 id="0-현황-정리-및-전제">0. 현황 정리 및 전제</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>GPU Node NIC</td>
<td>ConnectX-6 (외부망), Intel E810 (private망 예정, 미구성)</td>
</tr>
<tr>
<td>기존 Compute Cluster</td>
<td>bond1(내부망)로 연결</td>
</tr>
<tr>
<td>기존 Storage Cluster</td>
<td>bond1(내부망)로 연결</td>
</tr>
<tr>
<td>목표</td>
<td>GPU Node를 즉시 join하지 않고, 단계적 검증 후 연동</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>주의</strong>: ConnectX-6는 Mellanox/NVIDIA 계열로 RDMA/GPUDirect(RoCE, GPUDirect Storage, NCCL 가속)에 최적화된 카드인데 현재 <strong>외부망</strong>에 물려 있고, private망(스토리지/내부 통신 예정 구간)에는 <strong>Intel E810</strong>이 붙어 있습니다. E810도 RoCEv2를 지원하지만 GPUDirect RDMA/GPUDirect Storage 생태계 성숙도는 ConnectX 계열보다 낮습니다. Phase 3~5에서 스토리지 처리량이나 멀티 GPU 통신 성능이 기대치에 못 미치면 <strong>NIC 역할을 바꾸는 것(케이블 재배선)을 재검토 대상으로 열어두는 것</strong>을 권장합니다.</p>
</blockquote>
<hr>
<h2 id="phase-0-사전-설계-private망-구성-전-반드시-결정할-것">Phase 0. 사전 설계 (Private망 구성 전 반드시 결정할 것)</h2>
<p>Private망이 없는 상태이므로, &quot;당장 GPU 노드를 어떻게 접근/제어할 것인가&quot;부터 정해야 합니다.</p>
<p><strong>구성 단계</strong></p>
<ul>
<li>GPU Node 임시 접근 경로 결정: 외부망(ConnectX-6) 통한 SSH/관리 접근으로 우선 운용</li>
<li>Private망 설계 확정: subnet, VLAN ID, MTU(Jumbo frame 사용 여부 — storage/GPU 트래픽이면 9000 권장), bonding 정책(기존 compute/storage가 bond1이므로 GPU node도 동일 bonding mode 맞출지 결정)</li>
<li>스위치 포트 매핑 및 방화벽/ACL 정책 초안 작성 (외부망 노출 최소화 원칙)</li>
<li>DNS/hosts, NTP, 사설 저장소(yum/apt mirror), 시간 동기화 서버를 임시로 외부망 경유로 붙일지 결정</li>
<li>오케스트레이션 방식 확정 (bare-metal / Slurm / Kubernetes 등) — 이후 단계 설치 방식에 영향</li>
</ul>
<p><strong>체크 포인트 (테스트 아님, 의사결정)</strong></p>
<ul>
<li><input disabled="" type="checkbox"> Private망 subnet/VLAN 확정</li>
<li><input disabled="" type="checkbox"> GPU Node bonding 구성 방식(모드 802.3ad 등) 기존 cluster와 일치 여부</li>
<li><input disabled="" type="checkbox"> 외부망 임시 접근에 대한 보안 정책(방화벽, 접근 IP 제한) 확정</li>
</ul>
<hr>
<h2 id="phase-1-node-확인-및-기본-테스트">Phase 1. Node 확인 및 기본 테스트</h2>
<p><strong>구성 단계</strong></p>
<ol>
<li>하드웨어 인벤토리 확인 (CPU, RAM, GPU 개수/모델, NIC, NVMe/디스크 구성)</li>
<li>BMC/IPMI(iDRAC, iLO 등) 원격 관리 접속 확인, FW 버전 점검</li>
<li>BIOS 설정 점검: SR-IOV, VT-d/IOMMU, NUMA 설정, Above 4G Decoding, Power Profile(Performance 모드)</li>
<li>OS 설치 (기존 cluster와 동일 배포판/커널 버전 맞추는 것 권장)</li>
<li>NIC 펌웨어/드라이버 버전 확인 및 최신화 (ConnectX-6는 MFT/mlxconfig, E810은 ice 드라이버)</li>
<li>커널 파라미터 기본 설정: hugepages, <code>net.core.rmem/wmem</code>, <code>vm.swappiness</code> 등 baseline</li>
<li>시간 동기화(NTP/chrony), 로컬 계정/패스워드 정책, OS 보안 패치 baseline 적용</li>
</ol>
<p><strong>테스트</strong></p>
<ul>
<li><input disabled="" type="checkbox"> CPU/메모리 sanity test (stress-ng, memtester)</li>
<li><input disabled="" type="checkbox"> NVMe/디스크 기본 I/O 테스트 (fio baseline, SMART 상태 확인)</li>
<li><input disabled="" type="checkbox"> dmesg / journalctl 에러(특히 PCIe, ECC, NIC link flap) 확인</li>
<li><input disabled="" type="checkbox"> 센서/전력/온도 baseline 기록 (ipmitool sensor, 추후 burn test와 비교군)</li>
<li><input disabled="" type="checkbox"> ConnectX-6 링크 속도/상태 확인 (ethtool, mlxlink) — 외부망 연결 기준</li>
<li><input disabled="" type="checkbox"> E810 링크 상태 확인 (private망 미구성이어도 link up/negotiation은 점검 가능)</li>
<li><input disabled="" type="checkbox"> 외부망 통한 기본 네트워크 처리량 측정 (iperf3) — 이후 private망 구성 후 비교 기준점 확보</li>
</ul>
<hr>
<h2 id="phase-2-gpu-driver-설치-및-gpu-burn-test">Phase 2. GPU Driver 설치 및 GPU Burn Test</h2>
<p><strong>구성 단계</strong></p>
<ol>
<li>NVIDIA Driver 설치 (기존 compute cluster에 GPU가 있다면 버전 정합성 확인, 없다면 vLLM이 지원하는 CUDA 버전 기준으로 driver 선정)</li>
<li>CUDA Toolkit 설치 (필요 시), <code>nvidia-persistenced</code> persistence mode 활성화</li>
<li>Multi-GPU 노드라면 NVLink 여부 확인 후 <code>nvidia-fabricmanager</code> 설치/활성화</li>
<li>NVIDIA Container Toolkit 설치 (컨테이너로 vLLM 운용 예정이라면 필수)</li>
<li>MIG(Multi-Instance GPU) 사용 여부 결정 및 설정 (vLLM 운용 방식에 따라)</li>
<li>DCGM(Data Center GPU Manager) 설치 — 모니터링/burn test용</li>
</ol>
<p><strong>테스트</strong></p>
<ul>
<li><input disabled="" type="checkbox"> <code>nvidia-smi</code> 인식 GPU 개수/모델/드라이버 버전 확인</li>
<li><input disabled="" type="checkbox"> <code>nvidia-smi topo -m</code> 으로 GPU-GPU, GPU-NIC 간 PCIe/NVLink topology 확인</li>
<li><input disabled="" type="checkbox"> PCIe Link Width/Speed가 스펙대로 잡히는지 확인 (<code>lspci -vv</code>, x16 Gen4/5 여부 — 절반만 잡히는 경우 흔함)</li>
<li><input disabled="" type="checkbox"> ECC 에러 카운트 확인 (<code>nvidia-smi -q -d ECC</code>)</li>
<li><input disabled="" type="checkbox"> GPU Burn Test (<code>gpu-burn</code>, 또는 <code>dcgmi diag -r 3</code>) — 최소 30분~수 시간, 클럭/온도/전력 스로틀링 여부 관찰</li>
<li><input disabled="" type="checkbox"> 멀티 GPU면 NCCL 테스트 (<code>nccl-tests</code>의 all_reduce_perf 등)로 GPU간 대역폭 확인</li>
<li><input disabled="" type="checkbox"> Burn test 중 전력/온도 로그와 Phase1 baseline 비교, 스로틀링/에러 없는지 확인</li>
<li><input disabled="" type="checkbox"> 장시간(soak) 안정성 테스트 (예: 4~12시간 idle 대비 부하 반복)</li>
</ul>
<hr>
<h2 id="phase-3-vllm-설치-및-compute-cluster-연동-테스트">Phase 3. vLLM 설치 및 Compute Cluster 연동 테스트</h2>
<p><strong>구성 단계</strong></p>
<ol>
<li>Python 가상환경/conda 또는 컨테이너 이미지로 vLLM 설치 (CUDA 버전과 vLLM 요구 버전 호환성 확인 필수)</li>
<li>모델 저장 경로 결정 — 이 시점엔 storage cluster 연동이 안 된 상태이므로 우선 <strong>로컬 NVMe 캐시</strong>로 모델 다운로드/테스트</li>
<li>vLLM API 서버 기동 (OpenAI-compatible endpoint) 및 포트/방화벽 오픈 정책 수립</li>
<li>Compute cluster → GPU node 호출 경로 확보:<ul>
<li>임시로는 외부망(ConnectX-6) 경유 또는 접근 가능한 임시 라우팅으로 연결</li>
<li>인증/TLS 적용 여부 결정 (사내망이라도 임시 우회 경로면 최소한의 인증 권장)</li>
</ul>
</li>
<li>로깅/모니터링 연동 (Prometheus exporter 등, vLLM 자체 metrics endpoint 활용)</li>
</ol>
<p><strong>테스트</strong></p>
<ul>
<li><input disabled="" type="checkbox"> 단일 요청 inference 정상 동작 확인 (latency, 토큰 생성 속도 ms/token)</li>
<li><input disabled="" type="checkbox"> 동시 요청(concurrency) 부하 테스트 — throughput, queueing 지연 확인</li>
<li><input disabled="" type="checkbox"> Compute cluster 노드에서 실제 API 호출 테스트 (기존 서비스 로직과 동일한 방식으로)</li>
<li><input disabled="" type="checkbox"> 네트워크 장애 시나리오: 링크 단절/재접속 시 재시도 로직 동작 확인</li>
<li><input disabled="" type="checkbox"> 장시간 연속 요청 시 GPU 메모리 누수/OOM 여부 확인</li>
<li><input disabled="" type="checkbox"> Compute cluster ↔ GPU node 간 왕복 지연(RTT) 측정 — 추후 private망 구성 후 재측정 위한 baseline</li>
</ul>
<hr>
<h2 id="phase-4-storage-cluster-↔-gpu-node-연동-확인-및-테스트">Phase 4. Storage Cluster ↔ GPU Node 연동 확인 및 테스트</h2>
<p><strong>구성 단계</strong></p>
<ol>
<li>Storage cluster 접근 프로토콜 확인 (NFS / Ceph(RBD, CephFS) / Object storage(S3 호환) / 병렬 파일시스템 등 — 프로토콜에 따라 구성이 크게 달라지므로 우선 확인 필요)</li>
<li>Private망 구성이 완료되었다면 E810을 통해 storage 네트워크 경로 연결, 미완료 시 임시 경로(외부망) 사용 여부 결정</li>
<li>Mount point / 클라이언트 패키지 설치, 인증(Kerberos, cephx 등) 설정</li>
<li>네트워크 튜닝: MTU 일치(Jumbo Frame), RDMA(RoCE) 사용 여부에 따른 설정 (E810의 RoCEv2 지원 활용 시 별도 설정 필요)</li>
</ol>
<p><strong>테스트</strong></p>
<ul>
<li><input disabled="" type="checkbox"> 연결성/마운트 정상 확인</li>
<li><input disabled="" type="checkbox"> 처리량 테스트 (fio, dd) — sequential/random read 성능, 특히 대용량 모델 체크포인트 로딩 시나리오 기준</li>
<li><input disabled="" type="checkbox"> 실제 모델 파일(수십 GB급)을 storage에서 GPU node로 로딩하는 시간 측정 → 로컬 NVMe 캐시 대비 비교</li>
<li><input disabled="" type="checkbox"> 대량 동시 접근(다중 GPU 프로세스가 동시에 모델 로딩) 시 성능 저하 여부</li>
<li><input disabled="" type="checkbox"> 네트워크 단절/재연결 시 마운트 복구 동작 확인</li>
<li><input disabled="" type="checkbox"> (RDMA 사용 시) RDMA 정상 협상 여부 및 미사용 대비 처리량/CPU 사용률 비교</li>
</ul>
<hr>
<h2 id="phase-5-전체-성능-테스트-end-to-end">Phase 5. 전체 성능 테스트 (End-to-End)</h2>
<p><strong>시나리오 기반 테스트</strong></p>
<ul>
<li><input disabled="" type="checkbox"> Storage에서 모델 로드 → vLLM 기동 → Compute cluster에서 실제 요청 발생 → 응답까지 전체 파이프라인 E2E 테스트</li>
<li><input disabled="" type="checkbox"> 실제 서비스 트래픽 패턴을 모사한 부하 테스트 (동시 사용자 수, 요청 분포)</li>
<li><input disabled="" type="checkbox"> 장기 안정성(soak) 테스트 — 최소 24시간 이상 연속 운영, 메모리/온도/에러 로그 모니터링</li>
<li><input disabled="" type="checkbox"> 장애 주입 테스트 (Chaos): 네트워크 단절, storage 일시 접속 불가, GPU 프로세스 강제 종료 시 복구 동작</li>
<li><input disabled="" type="checkbox"> 성능 지표 종합: GPU 활용률, 처리량(req/s, tokens/s), P50/P95/P99 latency, 네트워크 대역폭 활용률, storage I/O 처리량</li>
<li><input disabled="" type="checkbox"> Phase 1~4에서 기록한 baseline과 비교하여 병목 구간(네트워크? storage? GPU?) 최종 판별</li>
</ul>
<hr>
<h2 id="종합-고려사항-및-리스크">종합 고려사항 및 리스크</h2>
<ol>
<li><strong>NIC 역할 배치 문제</strong>: ConnectX-6(고성능/RDMA 특화)가 외부망에, E810이 private망(storage/내부 트래픽 예상)에 배치되어 있어, 향후 GPUDirect Storage나 NCCL 기반 멀티노드 통신이 필요해지면 케이블/역할 재배치가 필요할 수 있습니다. 초기 설계 단계에서 미리 논의해 두는 것이 좋습니다.</li>
<li><strong>임시 우회 경로의 보안 리스크</strong>: Private망 구성 전 외부망을 통해 GPU node ↔ compute/storage를 연동할 경우, 인증/방화벽 없이 노출되면 보안 사고 위험이 있습니다. 최소한 IP 화이트리스트 + 인증은 필수로 적용 권장.</li>
<li><strong>Bonding 정책 불일치</strong>: 기존 cluster는 bond1로 구성되어 있는데 GPU node의 bonding 모드/정책이 다르면 나중에 private망 편입 시 네트워크 재설계가 필요할 수 있습니다. 가능하면 Phase 0에서 미리 맞추는 것이 좋습니다.</li>
<li><strong>Driver/CUDA/vLLM 버전 정합성</strong>: vLLM은 특정 CUDA/PyTorch 버전에 민감하므로, 기존 compute cluster에 GPU 워크로드가 있다면 버전 불일치로 인한 운영 복잡도가 생길 수 있습니다.</li>
<li><strong>GPU Burn Test 시 전력/냉각</strong>: GPU node가 랙에 새로 들어온 직후라면 랙 전력 용량과 냉각(공조) 여력을 사전에 확인해야 burn test 중 스로틀링/장애를 오진단하지 않습니다.</li>
<li><strong>Storage 프로토콜 미확정 리스크</strong>: Phase 4는 storage cluster의 프로토콜(NFS/Ceph/기타)에 따라 구성이 크게 달라집니다. 이 부분이 정해지지 않았다면 Phase 3~4 사이에 반드시 확정해야 합니다.</li>
<li><strong>RDMA 활용 여부</strong>: E810이 RoCEv2를 지원하긴 하지만, 실제로 storage cluster/스위치 인프라가 RDMA/RoCE를 지원하지 않으면 일반 TCP/IP 경로로 fallback되어 기대 성능이 안 나올 수 있습니다. 스위치 단에서 PFC/ECN(Lossless Ethernet) 설정 여부도 확인이 필요합니다.</li>
<li><strong>Rollback 계획</strong>: 각 Phase마다 실패 시 이전 단계로 되돌릴 수 있는 기준(예: burn test 실패 시 드라이버/펌웨어 롤백, 네트워크 설정 롤백)을 미리 정의해두면 일정 지연을 최소화할 수 있습니다.</li>
<li><strong>모니터링 공백</strong>: GPU node가 아직 기존 모니터링 체계(Zabbix/Prometheus 등)에 편입되지 않았다면, 임시로라도 DCGM exporter + node exporter를 붙여서 Phase 2부터 지표를 남겨두는 것이 이후 성능 이슈 원인 분석에 큰 도움이 됩니다.</li>
</ol>
<p>===</p>
<h1 id="rhel-102-bare-metal-nvidia-gpu-driver-설치-가이드">RHEL 10.2 Bare-Metal NVIDIA GPU Driver 설치 가이드</h1>
<h2 id="핵심-변경-사항-rhel-89와의-차이">핵심 변경 사항 (RHEL 8/9와의 차이)</h2>
<p>RHEL 10부터 Red Hat이 NVIDIA/AMD AI 가속기 드라이버를 <strong>자체 Secure Software Supply Chain으로 빌드/서명</strong>하여 <code>Extensions</code> 리포지토리로 제공합니다. 이 방식을 쓰면:</p>
<ul>
<li><code>rhel-drivers</code>라는 통합 설치 명령 하나로 GPU 감지 + 드라이버 설치가 자동으로 됨 (<code>nvidia-detect</code> 불필요)</li>
<li><strong>Secure Boot 활성화 상태에서도 MOK(Machine Owner Key) 수동 enroll 없이</strong> 바로 동작 (Red Hat 서명 키가 이미 신뢰 체인에 포함)</li>
</ul>
<p>반면 기존처럼 NVIDIA 공식 CUDA repo(dnf module 방식)로 설치하면, Secure Boot 환경에서는 <strong>MOK enroll이 별도로 필요</strong>합니다. 두 방법을 아래에 모두 정리하니 환경에 맞게 선택하세요. (데이터센터 GPU + Secure Boot 환경이면 <strong>방법 A를 우선 권장</strong>합니다.)</p>
<blockquote>
<p>참고: 아래 절차는 RHEL 10.1 기준 공개 자료를 기반으로 검증했습니다. RHEL 10.2에서도 동일한 메커니즘(Extensions repo, <code>rhel-drivers</code>)이 유지되지만, 실제 설치 전 <code>subscription-manager repos --list</code> 등으로 10.2용 리포지토리 이름이 정확한지 한 번 확인하시길 권장합니다.</p>
</blockquote>
<hr>
<h2 id="0-사전-점검-설치-전-필수-확인">0. 사전 점검 (설치 전 필수 확인)</h2>
<pre><code class="language-bash"># OS/커널 버전 확인
cat /etc/redhat-release
uname -r

# Secure Boot 상태 확인
mokutil --sb-state

# GPU 인식 확인 (PCI 레벨)
lspci -nn | grep -i nvidia

# Subscription 등록 상태 확인
subscription-manager status</code></pre>
<ul>
<li><input disabled="" type="checkbox"> BIOS에서 SR-IOV, VT-d/IOMMU, Above 4G Decoding 활성화 확인</li>
<li><input disabled="" type="checkbox"> 커널 버전이 최신 patch 상태인지 확인 (<code>dnf update</code> 후 재부팅 권장 — 드라이버 설치 전에 커널을 고정해두는 것이 이후 트러블슈팅에 유리)</li>
<li><input disabled="" type="checkbox"> 기존에 nouveau 관련 설정이 남아있지 않은지 확인 (<code>lsmod | grep nouveau</code>)</li>
</ul>
<hr>
<h2 id="방법-a-rhel-공식-간소화-방식-권장-extensions-repo--rhel-drivers">방법 A. RHEL 공식 간소화 방식 (권장, Extensions repo + <code>rhel-drivers</code>)</h2>
<h3 id="a-1-필요-리포지토리-활성화">A-1. 필요 리포지토리 활성화</h3>
<pre><code class="language-bash">sudo subscription-manager repos --enable=rhel-10-for-x86_64-appstream-rpms
sudo subscription-manager repos --enable=rhel-10-for-x86_64-baseos-rpms
sudo subscription-manager repos --enable=codeready-builder-for-rhel-10-x86_64-rpms</code></pre>
<p>Extensions 리포지토리는 기본 비활성 상태이므로 별도 활성화가 필요할 수 있습니다 (버전에 따라 <code>rhel-drivers</code> 패키지 설치 시 자동으로 요구하는 경우도 있음):</p>
<pre><code class="language-bash">sudo subscription-manager repos --list | grep -i extensions
# 목록에 있는 extensions repo 이름을 확인 후 활성화
sudo subscription-manager repos --enable=&lt;확인된-extensions-repo-이름&gt;</code></pre>
<h3 id="a-2-rhel-drivers-설치-및-실행">A-2. <code>rhel-drivers</code> 설치 및 실행</h3>
<pre><code class="language-bash">sudo dnf install -y rhel-drivers
sudo rhel-drivers install nvidia</code></pre>
<p>이 명령이 GPU 모델을 자동 감지하여 적합한 커널 모듈 + 유저모드 드라이버를 함께 설치합니다.</p>
<h3 id="a-3-재부팅-및-검증">A-3. 재부팅 및 검증</h3>
<pre><code class="language-bash">sudo reboot</code></pre>
<pre><code class="language-bash">nvidia-smi</code></pre>
<p>정상 설치 시 드라이버 버전, GPU 모델, 온도/전력 정보가 출력됩니다.</p>
<p><strong>Secure Boot 상태에서도 별도 MOK enroll 없이 바로 동작해야 합니다.</strong> 만약 <code>nvidia-smi</code>가 &quot;NVIDIA-SMI has failed because it couldn&#39;t communicate with the NVIDIA driver&quot; 를 출력하면 → 아래 <a href="#%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85">트러블슈팅</a> 섹션 참고.</p>
<hr>
<h2 id="방법-b-전통적-nvidia-cuda-repo-방식-특정-드라이버-버전-고정이-필요할-때">방법 B. 전통적 NVIDIA CUDA Repo 방식 (특정 드라이버 버전 고정이 필요할 때)</h2>
<p>vLLM/CUDA 버전 호환성 때문에 특정 드라이버 버전을 고정해야 하는 경우 이 방법이 더 유연합니다.</p>
<h3 id="b-1-커널-개발-패키지-및-의존성-설치">B-1. 커널 개발 패키지 및 의존성 설치</h3>
<pre><code class="language-bash">sudo dnf install -y kernel-devel-$(uname -r) kernel-headers-$(uname -r) \
    gcc make dkms acpid pciutils</code></pre>
<h3 id="b-2-nvidia-cuda-리포지토리-등록">B-2. NVIDIA CUDA 리포지토리 등록</h3>
<pre><code class="language-bash">sudo dnf config-manager --add-repo \
  https://developer.download.nvidia.com/compute/cuda/repos/rhel10/x86_64/cuda-rhel10.repo</code></pre>
<h3 id="b-3-드라이버-설치-버전-고정-예시">B-3. 드라이버 설치 (버전 고정 예시)</h3>
<pre><code class="language-bash"># 사용 가능한 드라이버 스트림 확인
sudo dnf module list nvidia-driver

# 최신 버전 설치
sudo dnf module install -y nvidia-driver:latest-dkms

# 또는 특정 버전 고정 (예: 570 브랜치)
sudo dnf module install -y nvidia-driver:570-dkms</code></pre>
<ul>
<li>데이터센터 GPU(A100/H100 등)라면 <strong>open-dkms(오픈소스 커널 모듈)</strong> 사용을 권장합니다 (NVIDIA가 기본 권장 방향으로 전환):<pre><code class="language-bash">sudo dnf module install -y nvidia-driver:latest-open-dkms</code></pre>
</li>
</ul>
<h3 id="b-4-secure-boot-환경이라면-mok-enroll-필요">B-4. Secure Boot 환경이라면 MOK Enroll 필요</h3>
<p>DKMS 방식은 빌드 시점에 자체 키로 서명하고, 이 키를 부팅 시 MOK로 enroll해야 커널이 모듈 로드를 허용합니다.</p>
<pre><code class="language-bash"># DKMS가 자동 생성한 키 확인
ls /var/lib/dkms/mok.pub

# MOK enroll (재부팅 후 파란 화면에서 암호 입력하여 최종 승인)
sudo mokutil --import /var/lib/dkms/mok.pub
sudo reboot</code></pre>
<p>재부팅 시 &quot;MOK management&quot; 화면이 뜨면 <strong>Enroll MOK → Continue → 암호 입력 → 승인</strong> 순서로 진행합니다. 이 과정을 건너뛰면 드라이버가 서명 미검증으로 로드되지 않고 nouveau로 폴백됩니다.</p>
<h3 id="b-5-검증">B-5. 검증</h3>
<pre><code class="language-bash">nvidia-smi
lsmod | grep nvidia   # 결과 있어야 정상
lsmod | grep nouveau  # 결과 없어야 정상</code></pre>
<hr>
<h2 id="1-cuda-toolkit-설치-nvcc-cublas-cudnn-nccl-등">1. CUDA Toolkit 설치 (nvcc, cuBLAS, cuDNN, NCCL 등)</h2>
<p>방법 A/B 어느 쪽으로 드라이버를 설치했든 공통:</p>
<pre><code class="language-bash">sudo dnf config-manager --add-repo \
  https://developer.download.nvidia.com/compute/cuda/repos/rhel10/x86_64/cuda-rhel10.repo
sudo dnf install -y cuda-toolkit</code></pre>
<p>PATH 설정:</p>
<pre><code class="language-bash">echo &#39;export PATH=/usr/local/cuda/bin:$PATH&#39; | sudo tee /etc/profile.d/cuda.sh
echo &#39;export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH&#39; | sudo tee -a /etc/profile.d/cuda.sh
source /etc/profile.d/cuda.sh</code></pre>
<p>검증:</p>
<pre><code class="language-bash">nvcc --version</code></pre>
<blockquote>
<p><code>nvidia-smi</code>에 표시되는 &quot;CUDA Version&quot;은 <strong>드라이버가 지원 가능한 최대 CUDA 버전</strong>을 의미할 뿐, CUDA Toolkit이 실제 설치됐다는 뜻이 아닙니다. <code>nvcc --version</code>으로 실제 설치 여부를 별도 확인해야 합니다.</p>
</blockquote>
<hr>
<h2 id="2-데이터센터멀티-gpu-서버-추가-설정">2. 데이터센터/멀티-GPU 서버 추가 설정</h2>
<p>GPU node가 멀티 GPU(특히 NVLink 탑재 HGX류) 서버라면 아래 추가 구성이 필요합니다.</p>
<h3 id="2-1-persistence-mode-활성화-재부팅유휴-시-드라이버-언로드-방지-응답속도-개선">2-1. Persistence Mode 활성화 (재부팅/유휴 시 드라이버 언로드 방지, 응답속도 개선)</h3>
<pre><code class="language-bash">sudo systemctl enable --now nvidia-persistenced
nvidia-smi -pm 1</code></pre>
<h3 id="2-2-fabric-manager-8-gpu-nvlinknvswitch-시스템만-해당">2-2. Fabric Manager (8-GPU NVLink/NVSwitch 시스템만 해당)</h3>
<pre><code class="language-bash">sudo dnf install -y nvidia-fabric-manager
sudo systemctl enable --now nvidia-fabricmanager
systemctl status nvidia-fabricmanager</code></pre>
<h3 id="2-3-iommu-설정-코어-수-많은-서버-권장">2-3. IOMMU 설정 (코어 수 많은 서버 권장)</h3>
<p>코어 수가 많은 시스템(대략 256코어 이상)에서는 IOMMU를 끄기보다 <strong>pass-through 모드</strong>를 권장합니다. <code>/etc/default/grub</code>의 <code>GRUB_CMDLINE_LINUX</code>에 추가:</p>
<pre><code>intel_iommu=on iommu=pt      # Intel CPU
amd_iommu=on iommu=pt        # AMD CPU</code></pre><p>적용 후:</p>
<pre><code class="language-bash">sudo grub2-mkconfig -o /boot/grub2/grub.cfg
sudo reboot</code></pre>
<h3 id="2-4-dcgm-모니터링burn-test용-이후-phase-2-gpu-burn-test에-필요">2-4. DCGM (모니터링/Burn Test용, 이후 Phase 2 GPU burn test에 필요)</h3>
<pre><code class="language-bash">sudo dnf install -y datacenter-gpu-manager
sudo systemctl enable --now nvidia-dcgm
dcgmi discovery -l</code></pre>
<hr>
<h2 id="3-nvidia-container-toolkit-향후-vllm을-컨테이너로-운용할-계획이라면">3. NVIDIA Container Toolkit (향후 vLLM을 컨테이너로 운용할 계획이라면)</h2>
<p>Bare-metal 드라이버 설치와는 별개로, vLLM을 컨테이너(Podman)로 띄울 계획이라면 미리 설치해두는 것을 권장합니다.</p>
<pre><code class="language-bash">sudo dnf config-manager --add-repo \
  https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo
sudo dnf install -y nvidia-container-toolkit

# RHEL 기본 컨테이너 런타임인 Podman용 CDI 설정 생성
sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
nvidia-ctk cdi list</code></pre>
<p>검증 (컨테이너 내부에서 GPU 인식 테스트):</p>
<pre><code class="language-bash">podman run --rm --device nvidia.com/gpu=all \
  nvidia/cuda:12.4.1-base-ubi9 nvidia-smi</code></pre>
<hr>
<h2 id="트러블슈팅">트러블슈팅</h2>
<table>
<thead>
<tr>
<th>증상</th>
<th>원인 / 조치</th>
</tr>
</thead>
<tbody><tr>
<td><code>nvidia-smi</code> 통신 실패</td>
<td>드라이버 커널 모듈 미로드. <code>lsmod | grep nvidia</code> 확인, <code>dmesg | grep -i nvidia</code>로 로드 실패 원인 확인</td>
</tr>
<tr>
<td>Secure Boot 상태에서 nouveau로 폴백</td>
<td>방법 B(DKMS) 사용 시 MOK enroll 누락. <code>mokutil --list-enrolled</code>로 확인 후 재enroll</td>
</tr>
<tr>
<td>커널 업데이트 후 드라이버 깨짐</td>
<td>DKMS 방식은 커널 업데이트마다 재빌드 필요 (<code>dkms status</code>로 확인). 방법 A(precompiled, Red Hat 서명)는 이 이슈가 적음</td>
</tr>
<tr>
<td><code>kernel-devel</code> 버전 불일치 오류</td>
<td><code>kernel-devel-$(uname -r)</code> 정확히 현재 실행 중인 커널 버전과 일치하는지 확인</td>
</tr>
<tr>
<td>PCIe Link Width가 예상보다 낮게 표시</td>
<td><code>lspci -vv</code>로 실제 협상된 Link Width/Speed 확인, 슬롯 장착 상태·BIOS 설정(Gen4/5, bifurcation) 점검</td>
</tr>
<tr>
<td>ECC 에러 발생</td>
<td><code>nvidia-smi -q -d ECC</code>로 카운트 확인, 지속 증가 시 하드웨어 이슈 가능성 → burn test(Phase 2)에서 별도 정밀 검증 필요</td>
</tr>
</tbody></table>
<hr>
<h2 id="다음-단계-phase-2-연계">다음 단계 (Phase 2 연계)</h2>
<p>드라이버 설치 후에는 이전 계획의 <strong>Phase 2 (GPU Burn Test)</strong>로 이어가면 됩니다:</p>
<ul>
<li><code>nvidia-smi topo -m</code>으로 GPU-GPU/GPU-NIC PCIe topology 확인</li>
<li><code>gpu-burn</code> 또는 <code>dcgmi diag -r 3</code>로 burn test</li>
<li>멀티 GPU라면 <code>nccl-tests</code>로 GPU 간 대역폭 확인</li>
</ul>
<p>===</p>
<p>GPU 노드 온보딩 시나리오를 보니, 핵심 이슈는 <strong>&quot;private망이 없는 상태에서 어떻게 안전하게 단계적으로 검증하며 기존 cluster와 연동해 나갈 것인가&quot;</strong>로 보입니다. 5단계 계획에 각 단계별 구성/테스트 항목과, 놓치기 쉬운 리스크를 정리한 문서를 만들어 드리겠습니다.가장 눈여겨볼 지점은 <strong>ConnectX-6가 외부망에, E810이 private망(스토리지 예정 구간)에 배치</strong>되어 있다는 점입니다 — 보통은 반대로(고성능 RDMA 카드를 storage/GPU 통신용 내부망에) 두는 구성이 많아서, Phase 4~5에서 성능 병목이 나오면 이 배치를 재검토 대상으로 열어두시는 게 좋습니다.</p>
<p>문서에는 Phase 0(private망 설계 전 반드시 정해야 할 것들)을 별도로 추가했습니다 — 지금 private망이 없는 상태에서 GPU node에 어떻게 접근/관리할지부터 결정하지 않으면 Phase 1도 시작하기 어렵기 때문입니다.</p>
<p>네, 가능합니다. 다만 &quot;가능하다&quot;와 &quot;그 결과가 최종 프로덕션 성능을 대표한다&quot;는 별개의 문제라서, 전제조건과 한계를 먼저 짚어야 합니다.</p>
<h2 id="결론-먼저">결론 먼저</h2>
<p>외부망(bond0)을 통해 GPU node(ConnectX-6) ↔ AIStor(storage) 간 RDMA/RoCE 테스트는 <strong>개념 검증(PoC) 목적으로는 유효</strong>합니다. 하지만 최종적으로는 private망(E810)에 배치될 예정이므로, 이 테스트는 &quot;GDS 스택이 정상 동작하는가&quot;를 검증하는 것이지 &quot;E810 private망에서도 같은 성능이 나온다&quot;는 것을 보장하지 않습니다.</p>
<hr>
<h2 id="1-사전-확인해야-할-전제조건">1. 사전 확인해야 할 전제조건</h2>
<p><strong>네트워크/하드웨어</strong></p>
<ul>
<li><input disabled="" type="checkbox"> AIStor 노드의 <strong>bond0(외부망) NIC이 RDMA 지원 하드웨어인지</strong> 확인 (모델, 펌웨어, RoCE 활성화 여부) — storage cluster의 주력 RDMA 트래픽은 보통 bond1(내부망) 쪽에 물려있을 가능성이 높아서, 외부망 NIC이 애초에 RDMA를 지원 안 하거나 비활성 상태일 수 있습니다. 이게 안 되면 이 테스트 자체가 불가능합니다.</li>
<li><input disabled="" type="checkbox"> GPU node ↔ AIStor 사이가 <strong>동일 L2 또는 라우팅 가능한 L3</strong>인지, 중간에 방화벽/NAT가 있는지 확인 — RDMA(RoCEv2)는 NAT를 통과하지 못하거나 성능이 크게 저하될 수 있습니다.</li>
<li><input disabled="" type="checkbox"> 스위치 단에서 <strong>Lossless Ethernet(PFC/ECN, DCBX)</strong> 설정이 가능한 구간인지 — 외부망은 보통 이런 QoS 설정이 안 되어 있어서, 설정 없이 진행하면 &quot;lossy RoCE&quot;로 동작하며 패킷 손실 시 성능이 급격히 떨어집니다.</li>
<li><input disabled="" type="checkbox"> MTU 일치 (Jumbo Frame 9000) 가능 여부</li>
<li><input disabled="" type="checkbox"> RoCEv2 사용 포트(UDP 4791) 및 관련 포트 방화벽 오픈 여부</li>
<li><input disabled="" type="checkbox"> <strong>네트워크팀/보안팀 협의</strong>: 외부망에 RDMA 트래픽을 흘려도 되는지 — &quot;외부망&quot;이 순수 사내 서비스망인지, 실제 인터넷向 구간과 얼마나 가까운지에 따라 리스크가 다릅니다.</li>
</ul>
<p><strong>GPUDirect RDMA / GPUDirect Storage(GDS) 추가 전제조건</strong></p>
<ul>
<li><input disabled="" type="checkbox"> <code>nvidia-fs</code> 커널 모듈 및 GDS 지원 드라이버 스택 설치</li>
<li><input disabled="" type="checkbox"> <code>/etc/cufile.json</code> 등 cuFile 설정 완료</li>
<li><input disabled="" type="checkbox"> AIStor(다수의 경우 MinIO 기반 오브젝트 스토리지로 알고 있습니다) 쪽에서 <strong>GDS/cuFile 연동 기능이 활성화된 버전인지</strong> 확인 — 벤더 문서에서 GDS 지원 여부와 최소 버전을 확인해야 합니다.</li>
<li><input disabled="" type="checkbox"> <strong>가장 중요한 리스크</strong>: NVIDIA GDS 공식 호환 NIC은 대부분 ConnectX 계열 중심입니다. 최종 배치될 <strong>Intel E810이 GDS 경로에서 ConnectX-6와 동일 수준으로 지원되는지는 별도 검증이 필요</strong>합니다. 즉 지금 ConnectX-6로 테스트해서 잘 되더라도, private망 구성 후 E810으로 옮기면 GDS 자체가 아예 안 되거나 성능이 다르게 나올 수 있습니다. (이게 앞서 말씀드린 &quot;NIC 배치가 바뀐 것 같다&quot;는 우려와 직결됩니다.)</li>
</ul>
<hr>
<h2 id="2-단계별-테스트-절차">2. 단계별 테스트 절차</h2>
<p><strong>Step 1 — 순수 RDMA 연결성 테스트 (GPU 배제)</strong></p>
<ul>
<li><code>ibv_devinfo</code>, <code>rdma link show</code>로 두 노드에서 RDMA 디바이스 인식 확인</li>
<li><code>ib_write_bw</code> / <code>ib_send_bw</code> (perftest 패키지)로 GPU node ↔ AIStor 노드 간 순수 NIC-to-NIC RDMA 대역폭/지연 측정</li>
<li>이 단계가 실패하면 GDS는 시도할 필요도 없습니다 — 네트워크/NIC 설정부터 다시 봐야 합니다.</li>
</ul>
<p><strong>Step 2 — GPUDirect RDMA (peer-to-peer) 단위 테스트</strong></p>
<ul>
<li><code>perftest</code>의 <code>--use-cuda</code> 옵션 또는 <code>gdrcopy</code> 툴로 GPU 메모리 ↔ NIC 간 직접 전송 확인</li>
<li><code>nvidia-smi topo -m</code>으로 GPU와 ConnectX-6 간 PCIe 경로(같은 NUMA 노드/PCIe 스위치 하위인지)도 재확인 — topology가 안 좋으면 GPUDirect 이득이 거의 없습니다.</li>
</ul>
<p><strong>Step 3 — GPUDirect Storage 테스트</strong></p>
<ul>
<li>NVIDIA에서 제공하는 <code>gdsio</code> 벤치마크 툴로 실제 AIStor 대상 read/write 테스트</li>
<li>AIStor 측에 cuFile 호환 엔드포인트/설정이 있다면 그 경로로 테스트, 없다면 일반 S3/NFS 경로 대비 비교 불가하니 이 부분은 AIStor 벤더 문서·지원팀 확인이 선행되어야 합니다.</li>
</ul>
<p><strong>Step 4 — 비교 기준선 확보</strong></p>
<ul>
<li>같은 구간에서 <strong>일반 TCP/IP 경로(CPU copy 경유)</strong> 성능도 함께 측정해서, GDS 사용 시 CPU 사용률 감소·지연 감소 효과가 실제로 있는지 비교 데이터를 남겨두세요. 이 데이터는 나중에 E810/private망 결과와 비교할 baseline이 됩니다.</li>
</ul>
<hr>
<h2 id="3-결과-해석-시-주의할-점">3. 결과 해석 시 주의할 점</h2>
<ol>
<li>이 테스트가 성공해도 이는 <strong>&quot;ConnectX-6 + 외부망 경로에서 GDS 스택 자체는 정상 동작한다&quot;</strong>는 것을 증명하는 것이지, private망(E810) 프로덕션 성능을 보장하지 않습니다 — private망 구성 후 반드시 재검증 필요.</li>
<li>외부망이라 QoS/Lossless 설정이 없다면 지금 나온 성능 수치는 &quot;최선의 경우&quot;가 아니라 &quot;제약된 조건에서의 수치&quot;일 수 있습니다. 수치가 나쁘다고 해서 인프라 자체에 문제가 있다고 단정하지 말고, 조건을 함께 기록해두세요.</li>
<li>임시 배선/설정이 테스트 후 다시 원복되어야 한다면, <strong>재배선 계획과 일정도 미리 잡아두는 것</strong>을 권장합니다.</li>
<li>가장 근본적인 질문은 &quot;최종적으로 GDS 트래픽이 어느 NIC(E810 vs ConnectX-6)를 탈 것인가&quot;입니다. 만약 GDS가 핵심 요구사항이라면, 지금이라도 <strong>ConnectX-6를 private/storage망 쪽으로, E810을 외부망 쪽으로 재배치하는 안</strong>을 인프라팀과 논의해보시는 걸 권합니다. 이번 테스트 결과가 그 논의에 좋은 근거자료가 될 것입니다.</li>
</ol>
<p>RHEL 10.2는 비교적 최근 버전이라 최신 설치 절차와 호환성 정보를 확인하고 답변드리겠습니다.RHEL 10 계열은 기존 RHEL 8/9와 드라이버 설치 방식이 꽤 달라졌습니다(Red Hat이 자체 서명한 드라이버를 Extensions 리포지토리로 제공하는 방식이 새로 생김). 두 가지 방법(공식 간소화 방식 vs 전통적인 NVIDIA repo 방식)을 비교해서 문서로 정리해 드리겠습니다.가장 큰 변화는 <strong>RHEL 10부터 Red Hat이 자체 서명한 드라이버를 제공</strong>한다는 점입니다 — <code>rhel-drivers install nvidia</code> 명령 하나로 Secure Boot 환경에서도 MOK enroll 없이 바로 설치됩니다 (방법 A). 반면 특정 드라이버 버전을 고정해야 하거나(vLLM/CUDA 호환성 이슈 등) 좀 더 세밀한 제어가 필요하면 기존 NVIDIA repo + DKMS 방식(방법 B)을 쓰되, 이땐 Secure Boot 상태에서 MOK enroll을 수동으로 해줘야 합니다.</p>
<p>멀티 GPU(NVLink/HGX) 서버라면 persistence mode, Fabric Manager, IOMMU pass-through 설정도 함께 해두시는 게 이후 Phase 2 burn test·NCCL 테스트에서 안정적인 결과를 얻는 데 도움이 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S05d]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S05d</link>
            <guid>https://velog.io/@youngkyoo_kim/26S05d</guid>
            <pubDate>Fri, 04 Sep 2026 19:59:30 GMT</pubDate>
            <description><![CDATA[<p>vLLM 서빙에서 Throughput(전체 처리량)과 TTFT(첫 토큰 지연 시간)는 트레이드오프 관계를 가집니다. 대규모 배치 처리는 Throughput을 극대화하지만 긴 Prefill 연산으로 인해 기존 요청의 ITL과 신규 요청의 TTFT를 저하시킵니다.</p>
<p>워크로드 특성(RAG 기반 무거운 프롬프트 vs 실시간 대화형 서비스)에 맞춘 핵심 엔진 파라미터 조합과 튜닝 전략을 정리했습니다.</p>
<hr>
<h3 id="핵심-튜닝-플래그-상세-가이드">핵심 튜닝 플래그 상세 가이드</h3>
<h4 id="1-chunked-prefill---enable-chunked-prefill">1. Chunked Prefill (<code>--enable-chunked-prefill</code>)</h4>
<ul>
<li><strong>역할</strong>: 긴 프롬프트(Prompt Prefill)를 여러 덩어리(Chunk)로 쪼개어 다른 세션의 디코딩(Decoding) 단계와 한 번의 반복(Iteration) 내에서 <strong>공동 배치(Co-batching)</strong> 처리합니다.</li>
<li><strong>효과</strong>:</li>
<li>장문 프롬프트가 들어와도 기존 디코딩 요청이 멈추지 않아 <strong>ITL(Inter-Token Latency) 튐 현상 방지</strong>.</li>
<li>긴 프롬프트 처리 중에도 다른 짧은 요청의 Prefill이 함께 끼어들 수 있어 <strong>P99 TTFT 대폭 개선</strong>.</li>
<li>Prefill과 Decode 단계가 함께 스케줄링되어 GPU 연산기(Compute Core) 활용률이 극대화되므로 <strong>Throughput 동시 상승</strong>.</li>
</ul>
<ul>
<li><strong>설정</strong>:
```bash</li>
<li>-enable-chunked-prefill</li>
</ul>
<pre><code>


#### 2. 배치 토큰 크기 (`--max-num-batched-tokens`)

* **역할**: 단일 포워드 패스(Iteration)에서 엔진이 한 번에 처리할 수 있는 최대 토큰 수(Prefill 토큰 + Decode 토큰)를 제한합니다.
* **튜닝 가이드**:
* Chunked Prefill 활성화 시 이 값이 **단일 Chunk의 크기**를 결정합니다.
* **기본값 (512 / 모델별 상이)**: TTFT와 ITL 안정성에 유리하지만 Throughput이 다소 희생됩니다.
* **Throughput 우선 (2048 ~ 8192)**: H100/A100 등 대규모 VRAM 및 높은 연산력을 가진 GPU에서 배치 밀도를 높여 GPU 포화를 유도할 때 권장합니다.
* **TTFT 우선 (512 ~ 1024)**: 프롬프트 길이가 길고 실시간 응답 체감이 중요한 챗봇/에이전트 서비스에 적합합니다.



#### 3. 동시 처리 시퀀스 수 (`--max-num-seqs`)

* **역할**: 한 번에 동시 실행(Running 상태) 가능한 최대 요청 수(Concurrency)를 지정합니다.
* **튜닝 가이드**:
* 기본값은 256입니다.
* 동시 인입 요청이 많아 KV Cache가 부족해지면 vLLM은 요청을 큐(Waiting)에 두거나 선점(Preemption - CPU 스왑 또는 재연산)합니다.
* 선점이 발생하면 TTFT와 ITL이 급격히 무너지므로, 최대 허용 부하 수준(예: 64, 128, 256)으로 상한을 두어 엔진 크래시 및 재연산 스왑을 방지합니다.



#### 4. GPU 메모리 할당 비율 (`--gpu-memory-utilization`)

* **역할**: 전체 VRAM 중 모델 가중치 로드 및 KV Cache 할당을 위해 vLLM 프로세스가 선점할 메모리 비율을 지정합니다.
* **튜닝 가이드**:
* 기본값: `0.90` (90%).
* 단독 GPU 노드 환경이라면 `0.92 ~ 0.95`까지 상향하여 **KV Cache 블록 수를 최대로 확보**하는 것이 Throughput에 유리합니다.
* 지나치게 높일 경우(`&gt; 0.96`) CUDA Graph 캡처 메모리나 임시 활성화 텐서(Activation) 공간 부족으로 OOM이 발생할 수 있습니다.



#### 5. 최대 컨텍스트 길이 제약 (`--max-model-len`)

* **역할**: 모델이 허용하는 최대 시퀀스 길이를 제한합니다 (예: Llama 3.1의 128k 컨텍스트).
* **튜닝 가이드**:
* vLLM은 모델의 네이티브 컨텍스트 길이(`max_position_embeddings`)를 기준으로 KV Cache 슬롯 공간을 계산합니다.
* 실제 서비스 환경에서 128k를 전부 쓰지 않고 8k~16k 내외만 쓴다면 반드시 `--max-model-len 8192` 또는 `16384`로 제약해야 합니다.
* 컨텍스트 길이를 제한하면 단일 요청이 점유할 수 있는 최대 KV Cache가 줄어들어, **더 많은 동시 요청(Concurrency)을 담을 수 있는 KV Cache 블록이 확보**됩니다.



#### 6. Prefix Caching (`--enable-prefix-caching`)

* **역할**: 동일하거나 중복되는 프롬프트 접두사(System Prompt, 공통 RAG 지침, Few-shot 예시 등)의 KV Cache를 버리지 않고 재사용합니다.
* **효과**:
* 반복되는 긴 프롬프트가 들어올 때 Prefill 연산 자체를 건너뛰므로 **TTFT가 수 밀리초 단위(Near-Zero)로 단축**.
* GPU 연산량 절감으로 인해 전체 **Serving Throughput 급상승**.



---

### 워크로드별 권장 실행 파라미터 조합

#### 시나리오 1: 대화형 서비스 (TTFT 및 실시간성 최우선)

* **목표**: 일관된 빠른 첫 토큰 반응 속도, 짧은 대기 시간, 낮은 ITL 지터(Jitter).

```bash
python3 -m vllm.entrypoints.openai.api_server \
    --model /models/Llama-3.1-70B-Instruct \
    --tensor-parallel-size 8 \
    --gpu-memory-utilization 0.92 \
    --max-model-len 8192 \
    --enable-chunked-prefill \
    --max-num-batched-tokens 512 \
    --max-num-seqs 128 \
    --enable-prefix-caching
</code></pre><h4 id="시나리오-2-rag--배치-요약-처리-throughput-최우선">시나리오 2: RAG / 배치 요약 처리 (Throughput 최우선)</h4>
<ul>
<li><strong>목표</strong>: 긴 입력 프롬프트(4k~8k 토큰)가 다량 인입될 때 GPU 연산기 포화 및 초당 토큰 처리량 극대화.</li>
</ul>
<pre><code class="language-bash">python3 -m vllm.entrypoints.openai.api_server \
    --model /models/Llama-3.1-70B-Instruct \
    --tensor-parallel-size 8 \
    --gpu-memory-utilization 0.95 \
    --max-model-len 16384 \
    --enable-chunked-prefill \
    --max-num-batched-tokens 2048 \
    --max-num-seqs 256 \
    --enable-prefix-caching
</code></pre>
<hr>
<h3 id="튜닝-검증-및-모니터링-체크포인트">튜닝 검증 및 모니터링 체크포인트</h3>
<p>파라미터 변경 후 Phase 5 부하 테스트를 수행하며 <code>/metrics</code> 엔드포인트에서 아래 3가지 메트릭 추이를 확인해야 합니다.</p>
<ol>
<li><strong><code>vllm:num_requests_waiting</code> (대기 큐 지표)</strong></li>
</ol>
<ul>
<li>이 값이 지속적으로 증가한다면 서빙 한계치에 도달한 것입니다. <code>--max-num-seqs</code>를 늘리기보다는 클러스터 차원의 Rate Limiting 또는 노드 스케일아웃이 필요합니다.</li>
</ul>
<ol start="2">
<li><strong><code>vllm:gpu_cache_usage_factor</code> (KV Cache 포화도)</strong></li>
</ol>
<ul>
<li>부하 중 0.8~0.9 사이를 유지하는 것이 이상적입니다. 1.0에 도달하면 신규 요청이 블로킹되거나 Prefill 지연이 급증합니다.</li>
</ul>
<ol start="3">
<li><strong><code>vllm:time_to_first_token_seconds</code> (TTFT 분포)</strong></li>
</ol>
<ul>
<li><code>--enable-chunked-prefill</code> 적용 전후의 P95/P99 구간을 비교하여 긴 프롬프트 유입 시 TTFT 스파이크가 해소되었는지 검증합니다.</li>
</ul>
<p>===</p>
<p>vLLM 서빙 엔진과 GPU 하드웨어(DCGM Exporter)의 상태를 실시간 수집하고 이상 징후를 감지하기 위한 Prometheus 설정 및 Alertmanager 경보 규칙입니다.</p>
<hr>
<h3 id="1-prometheus-수집-설정-scrape_configs">1. Prometheus 수집 설정 (<code>scrape_configs</code>)</h3>
<p>vLLM의 메트릭 엔드포인트(기본 포트 <code>8000</code>, 경로 <code>/metrics</code>)와 NVIDIA DCGM Exporter(기본 포트 <code>9400</code>)를 정기적으로 폴링하도록 설정합니다.</p>
<pre><code class="language-yaml"># prometheus.yml
scrape_configs:
  # ----------------------------------------------------
  # 1. vLLM Serving Engine Metrics
  # ----------------------------------------------------
  - job_name: &#39;vllm-serving&#39;
    scrape_interval: 5s            # 큐 상태 및 실시간 캐시 변동 추적을 위해 짧은 주기 권장
    scrape_timeout: 4s
    metrics_path: /metrics
    static_configs:
      - targets: [&#39;&lt;GPU_NODE_IP&gt;:8000&#39;]
        labels:
          cluster: &#39;gpu-platform&#39;
          role: &#39;llm-inference&#39;
          model: &#39;llama-3.1-70b&#39;

  # ----------------------------------------------------
  # 2. NVIDIA DCGM Exporter (Hardware &amp; GPU Metrics)
  # ----------------------------------------------------
  - job_name: &#39;dcgm-exporter&#39;
    scrape_interval: 10s           # 하드웨어 센서/전력 모니터링 주기
    scrape_timeout: 8s
    metrics_path: /metrics
    static_configs:
      - targets: [&#39;&lt;GPU_NODE_IP&gt;:9400&#39;]
        labels:
          cluster: &#39;gpu-platform&#39;
          role: &#39;gpu-telemetry&#39;
</code></pre>
<hr>
<h3 id="2-핵심-알람-규칙-정의-alert_rulesyml">2. 핵심 알람 규칙 정의 (<code>alert_rules.yml</code>)</h3>
<p>엔진 레벨의 성능 병목(큐잉, KV Cache 고갈, TTFT 지연)과 하드웨어 레벨의 치명적 결함(ECC 에러, 과열, XID 오류)을 분리하여 감시합니다.</p>
<pre><code class="language-yaml">groups:
  # ====================================================
  # Group 1: vLLM Inference Engine Alerts
  # ====================================================
  - name: vllm_serving_alerts
    rules:
      - alert: VLLMInstanceDown
        expr: up{job=&quot;vllm-serving&quot;} == 0
        for: 30s
        labels:
          severity: critical
        annotations:
          summary: &quot;vLLM serving instance is down&quot;
          description: &quot;Target {{ $labels.instance }} has been unreachable for more than 30 seconds.&quot;

      - alert: VLLMKVCacheSaturation
        expr: vllm:gpu_cache_usage_factor &gt; 0.95
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: &quot;vLLM KV Cache is nearly saturated (&gt;95%)&quot;
          description: &quot;Instance {{ $labels.instance }} GPU cache usage is at {{ $value | humanizePercentage }}. Risk of request eviction or queuing.&quot;

      - alert: VLLMHighRequestQueuing
        expr: vllm:num_requests_waiting &gt; 10
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: &quot;vLLM request queue backlog detected&quot;
          description: &quot;Instance {{ $labels.instance }} has {{ $value }} requests queued for over 1 minute. Serving capacity is saturated.&quot;

      - alert: VLLMHighTTFTLatency
        expr: |
          histogram_quantile(0.95, sum(rate(vllm:time_to_first_token_seconds_bucket[5m])) by (le, instance, model_name)) &gt; 2.5
        for: 3m
        labels:
          severity: warning
        annotations:
          summary: &quot;P95 TTFT latency exceeds 2.5s&quot;
          description: &quot;Model {{ $labels.model_name }} on {{ $labels.instance }} P95 TTFT is {{ $value }}s for the last 5 minutes.&quot;

      - alert: VLLMRequestPreemptionDetected
        expr: rate(vllm:num_preemptions_total[2m]) &gt; 0
        for: 30s
        labels:
          severity: critical
        annotations:
          summary: &quot;vLLM request preemptions occurring&quot;
          description: &quot;Instance {{ $labels.instance }} is preempting/recomputing requests due to strict memory limits.&quot;

  # ====================================================
  # Group 2: GPU Hardware &amp; DCGM Telemetry Alerts
  # ====================================================
  - name: gpu_hardware_alerts
    rules:
      - alert: DCGMExporterDown
        expr: up{job=&quot;dcgm-exporter&quot;} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: &quot;DCGM Exporter is down&quot;
          description: &quot;Hardware telemetry on {{ $labels.instance }} is unavailable.&quot;

      - alert: GPUCriticalTemperature
        expr: DCGM_FI_DEV_GPU_TEMP &gt; 83
        for: 30s
        labels:
          severity: critical
        annotations:
          summary: &quot;GPU temperature is critical (&gt;83°C)&quot;
          description: &quot;GPU {{ $labels.gpu }} on {{ $labels.instance }} has reached {{ $value }}°C. Thermal throttling imminent.&quot;

      - alert: GPUClockThrottled
        expr: DCGM_FI_DEV_CLOCK_THROTTLE_REASONS &gt; 0
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: &quot;GPU clock throttling active&quot;
          description: &quot;GPU {{ $labels.gpu }} on {{ $labels.instance }} is throttled (Reason bitmask: {{ $value }}).&quot;

      - alert: GPUEccDoubleBitError
        expr: increase(DCGM_FI_DEV_ECC_DBE_VOL_TOTAL[5m]) &gt; 0
        labels:
          severity: critical
        annotations:
          summary: &quot;Uncorrectable ECC double-bit error detected&quot;
          description: &quot;GPU {{ $labels.gpu }} on {{ $labels.instance }} detected double-bit ECC memory corruption. Immediate hardware check required.&quot;

      - alert: GPUXidErrorOccurred
        expr: DCGM_FI_DEV_XID_ERRORS &gt; 0
        labels:
          severity: critical
        annotations:
          summary: &quot;NVIDIA driver XID error detected&quot;
          description: &quot;GPU {{ $labels.gpu }} on {{ $labels.instance }} logged XID error code: {{ $value }}.&quot;

      - alert: GPUNVLinkErrorDetected
        expr: increase(DCGM_FI_DEV_NVLINK_CRC_FLIT_ERROR_COUNT_TOTAL[5m]) &gt; 0
        labels:
          severity: warning
        annotations:
          summary: &quot;NVLink CRC error count increasing&quot;
          description: &quot;NVLink on GPU {{ $labels.gpu }} ({{ $labels.instance }}) is reporting transmission CRC errors.&quot;
</code></pre>
<hr>
<h3 id="3-주요-메트릭-및-대시보드-쿼리-참조표">3. 주요 메트릭 및 대시보드 쿼리 참조표</h3>
<table>
<thead>
<tr>
<th>모니터링 영역</th>
<th>PromQL 표현식</th>
<th>이상 기준 / 해석</th>
</tr>
</thead>
<tbody><tr>
<td><strong>토큰 생성 처리량</strong></td>
<td><code>sum(rate(vllm:request_generation_tokens_total[1m])) by (instance)</code></td>
<td>초당 생성 토큰 수(TPS) 측정</td>
</tr>
<tr>
<td><strong>KV Cache 여유량</strong></td>
<td><code>(1 - vllm:gpu_cache_usage_factor) * 100</code></td>
<td>10% 미만으로 떨어질 경우 대기열 적체 임박</td>
</tr>
<tr>
<td><strong>실행 중인 동시 요청</strong></td>
<td><code>vllm:num_requests_running</code></td>
<td>현재 GPU에서 동시 Decoding 중인 시퀀스 수</td>
</tr>
<tr>
<td><strong>대기 큐 요청 수</strong></td>
<td><code>vllm:num_requests_waiting</code></td>
<td>지속적으로 0보다 크면 서빙 노드 증설 필요</td>
</tr>
<tr>
<td><strong>GPU 전력 사용량</strong></td>
<td><code>DCGM_FI_DEV_POWER_USAGE</code></td>
<td>스펙상 정격 TDP 대비 피크 도달 여부 점검</td>
</tr>
<tr>
<td><strong>GPU SM 연산 점유율</strong></td>
<td><code>DCGM_FI_DEV_GPU_UTIL</code></td>
<td>Prefill 구간에서 100% 도달, Decode 구간에서는 통상 HBM 대역폭(<code>DCGM_FI_DEV_MEM_COPY_UTIL</code>)과 함께 확인</td>
</tr>
</tbody></table>
<p>===</p>
<p>K8s 조인 전 GPU 노드 단독 환경에서는 RHEL 10.2의 Podman과 systemd(또는 Podman Pod)를 활용해 Prometheus, Grafana, DCGM Exporter를 간략하게 묶어 올리는 구성이 가장 깔끔합니다.</p>
<p>호스트 네트워크 모드(<code>--net=host</code>)를 사용하면 복잡한 포트 포워딩이나 브리지 인터페이스 설정 없이, 호스트에 떠 있는 vLLM(<code>:8000</code>)과 DCGM Exporter(<code>:9400</code>)의 메트릭을 즉시 스크랩하고 Grafana(<code>:3000</code>) 웹 UI로 모니터링할 수 있습니다.</p>
<hr>
<h3 id="step-1-사전-준비-설정-파일-및-디렉터리-생성">Step 1. 사전 준비 (설정 파일 및 디렉터리 생성)</h3>
<p>GPU 노드의 호스트 디렉터리에 Prometheus 설정과 Grafana 데이터 경로를 생성합니다.</p>
<pre><code class="language-bash"># 1. 설정 및 데이터 디렉터리 생성
mkdir -p /opt/monitoring/{prometheus,grafana_data}
chmod 777 /opt/monitoring/grafana_data  # Grafana 컨테이너 UID(472) 쓰기 권한

# 2. Prometheus 수집 설정 파일 작성 (/opt/monitoring/prometheus/prometheus.yml)
cat &lt;&lt;&#39;EOF&#39; &gt; /opt/monitoring/prometheus/prometheus.yml
global:
  scrape_interval: 5s
  evaluation_interval: 5s

scrape_configs:
  # 1. GPU 하드웨어 메트릭 (DCGM Exporter)
  - job_name: &#39;dcgm&#39;
    static_configs:
      - targets: [&#39;127.0.0.1:9400&#39;]

  # 2. vLLM 추론 엔진 서빙 메트릭
  - job_name: &#39;vllm&#39;
    metrics_path: /metrics
    static_configs:
      - targets: [&#39;127.0.0.1:8000&#39;]
EOF
</code></pre>
<hr>
<h3 id="step-2-podman-컨테이너-3종-기동">Step 2. Podman 컨테이너 3종 기동</h3>
<p>호스트의 GPU와 네트워크를 직접 공유하도록 실행합니다.</p>
<h4 id="1-nvidia-dcgm-exporter-실행-gpu-메트릭-노출-9400-포트">1) NVIDIA DCGM Exporter 실행 (GPU 메트릭 노출: 9400 포트)</h4>
<p>RHEL 10의 CDI(Container Device Interface)를 통해 GPU 장치 접근 권한을 넘겨줍니다.</p>
<pre><code class="language-bash">podman run -d --name dcgm-exporter \
    --restart unless-stopped \
    --device nvidia.com/gpu=all \
    --net=host \
    nexus.internal:8082/nvidia/k8s-device-plugin/dcgm-exporter:latest
</code></pre>
<p><em>동작 확인:</em></p>
<pre><code class="language-bash">curl -s http://127.0.0.1:9400/metrics | grep DCGM_FI_DEV_GPU_TEMP
</code></pre>
<h4 id="2-prometheus-실행-시계열-데이터-수집-9090-포트">2) Prometheus 실행 (시계열 데이터 수집: 9090 포트)</h4>
<pre><code class="language-bash">podman run -d --name prometheus \
    --restart unless-stopped \
    --net=host \
    -v /opt/monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro,Z \
    nexus.internal:8082/prom/prometheus:latest \
    --config.file=/etc/prometheus/prometheus.yml \
    --storage.tsdb.retention.time=7d
</code></pre>
<p><em>동작 확인:</em></p>
<pre><code class="language-bash">curl -s http://127.0.0.1:9090/-/ready
# &quot;Prometheus is Ready.&quot; 출력 확인
</code></pre>
<h4 id="3-grafana-실행-시각화-대시보드-3000-포트">3) Grafana 실행 (시각화 대시보드: 3000 포트)</h4>
<pre><code class="language-bash">podman run -d --name grafana \
    --restart unless-stopped \
    --net=host \
    -v /opt/monitoring/grafana_data:/var/lib/grafana:Z \
    nexus.internal:8082/grafana/grafana:latest
</code></pre>
<hr>
<h3 id="step-3-grafana-대시보드-연동">Step 3. Grafana 대시보드 연동</h3>
<ol>
<li><strong>웹 브라우저 접속</strong></li>
</ol>
<ul>
<li>URL: <code>http://&lt;GPU_NODE_IP&gt;:3000</code> (외부망 ConnectX-6 IP)</li>
<li>초기 계정/비밀번호: <code>admin</code> / <code>admin</code> (첫 로그인 시 변경)</li>
</ul>
<ol start="2">
<li><strong>Prometheus 데이터 소스 등록</strong></li>
</ol>
<ul>
<li><strong>Connections</strong> $\rightarrow$ <strong>Data Sources</strong> $\rightarrow$ <strong>Add data source</strong> $\rightarrow$ <strong>Prometheus</strong></li>
<li>Server URL: <code>[http://127.0.0.1:9090](http://127.0.0.1:9090)</code> (호스트 네트워크 모드이므로 로컬호스트 지정)</li>
<li><strong>Save &amp; test</strong> 클릭 후 정상 연결 확인.</li>
</ul>
<ol start="3">
<li><strong>오프라인 대시보드 등록 (에어갭 환경)</strong></li>
</ol>
<ul>
<li>인터넷 연결이 없으므로 대시보드 번호(ID) 입력 방식 대신, JSON 파일을 Import합니다.</li>
<li><strong>GPU 하드웨어 대시보드</strong>: NVIDIA 공식 DCGM Exporter 대시보드 JSON (외부망에서 사전 다운로드한 <a href="https://grafana.com/grafana/dashboards/12239-nvidia-dcgm-exporter-dashboard/">NVIDIA Grafana Dashboard JSON</a>)을 <strong>Dashboards</strong> $\rightarrow$ <strong>New</strong> $\rightarrow$ <strong>Import</strong>로 업로드.</li>
<li><strong>vLLM 대시보드</strong>: vLLM GitHub 저장소(<code>examples/vllm_prometheus_grafana/</code>)에 포함된 기본 대시보드 JSON을 Import하여 TTFT, TPS, KV Cache 추이를 시각화.</li>
</ul>
<hr>
<h3 id="step-4-테스트-종료-후-일괄-정리-one-liner">Step 4. 테스트 종료 후 일괄 정리 (One-liner)</h3>
<p>성능 테스트가 끝나고 추후 정식 K8s Worker 노드로 조인할 때는 아래 명령으로 깔끔하게 제거할 수 있습니다.</p>
<pre><code class="language-bash"># 모니터링 컨테이너 일괄 중지 및 삭제
podman rm -f dcgm-exporter prometheus grafana

# (선택) 임시 수집 데이터 정리
rm -rf /opt/monitoring
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S05c]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S05c</link>
            <guid>https://velog.io/@youngkyoo_kim/26S05c</guid>
            <pubDate>Fri, 04 Sep 2026 19:51:30 GMT</pubDate>
            <description><![CDATA[<p>GPU 노드 도입 및 검증 과정에서 클라이언트(엔지니어/운영자 PC) 접근용 UI 포트와, 각 클러스터(Compute/Storage) 간 연동 시 필요한 방화벽 오픈 대상 포트 목록입니다.</p>
<hr>
<h3 id="1-클라이언트운영자엔지니어-pc-rightarrow-gpu-노드툴-ui-포트">1. 클라이언트(운영자/엔지니어 PC) $\rightarrow$ GPU 노드/툴 UI 포트</h3>
<p>인수 테스트, vLLM 테스트, 스토리지 점검 시 웹 브라우저나 데스크톱 도구에서 노드로 직접 붙을 때 오픈이 필요한 포트입니다.</p>
<table>
<thead>
<tr>
<th>포트 / 프로토콜</th>
<th>용도 및 기본 서비스</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><strong>8000 / TCP</strong></td>
<td><strong>vLLM OpenAI 호환 엔드포인트</strong></td>
<td>Swagger UI (<code>/docs</code>) 및 추론 API 테스트 호출</td>
</tr>
<tr>
<td><strong>9400 / TCP</strong></td>
<td><strong>NVIDIA DCGM Exporter</strong></td>
<td>GPU 메트릭(온도, 전력, VRAM 사용률 등) 확인</td>
</tr>
<tr>
<td><strong>3000 / TCP</strong></td>
<td><strong>Grafana (임시 대시보드 구동 시)</strong></td>
<td>Burn-In 및 리소스 메트릭 모니터링 UI</td>
</tr>
<tr>
<td><strong>9001 / TCP</strong></td>
<td><strong>MinIO / AIStor Console (Web UI)</strong></td>
<td>스토리지 웹 관리자 콘솔 접근 (기본 콘솔 포트)</td>
</tr>
<tr>
<td><strong>443 / TCP</strong></td>
<td><strong>서버 BMC / IPMI Web GUI</strong></td>
<td>iDRAC/iLO 등 원격 콘솔, 전원 제어, SEL 센서 확인</td>
</tr>
<tr>
<td><strong>5900 / TCP</strong></td>
<td><strong>BMC Virtual Console (KVM)</strong></td>
<td>Java/HTML5 KVM 뷰어 화면 연결용</td>
</tr>
</tbody></table>
<hr>
<h3 id="2-gpu-노드-leftrightarrow-compute--storage-클러스터-간-방화벽-포트-매트릭스">2. GPU 노드 $\leftrightarrow$ Compute / Storage 클러스터 간 방화벽 포트 매트릭스</h3>
<h4 id="case-1-gpu-노드를-기존-k8s-클러스터의-worker-node로-join할-때">Case 1. GPU 노드를 기존 K8s 클러스터의 Worker Node로 Join할 때</h4>
<p>GPU 노드가 기존 Compute K8s 클러스터의 워커 노드로 합류할 때 필요한 제어 평면 및 CNI 오버레이 통신 포트입니다.</p>
<ul>
<li><strong>GPU Node $\rightarrow$ K8s Control Plane (출발: GPU, 목적지: 마스터 노드)</strong></li>
<li><strong>6443 / TCP</strong>: Kubernetes API Server 통신 (필수)</li>
</ul>
<ul>
<li><strong>K8s Control Plane $\rightarrow$ GPU Node (출발: 마스터 노드, 목적지: GPU)</strong></li>
<li><strong>10250 / TCP</strong>: Kubelet API (로그 확인 <code>kubectl logs</code>, 파드 exec 등에 필수)</li>
</ul>
<ul>
<li><strong>GPU Node $\leftrightarrow$ 기존 Worker Nodes (양방향)</strong></li>
<li><strong>CNI 오버레이 터널링 포트 (사용 CNI에 따라 택1)</strong>:</li>
<li><strong>8472 / UDP</strong>: VXLAN (Cilium / Flannel 기본)</li>
<li><strong>4789 / UDP</strong>: Geneve (Calico 기본 오버레이 또는 OVN)</li>
<li><strong>Direct Routing / BGP 모드인 경우</strong>: <strong>179 / TCP</strong> (BGP Mesh)</li>
</ul>
<ul>
<li><strong>Cilium 사용 시 추가 포트 (Cilium CNI 환경인 경우)</strong>:</li>
<li><strong>4240 / TCP</strong>: Cilium Node-to-Node Health Check</li>
<li><strong>4244 / TCP</strong>: Hubble Relay / Server 메트릭 통신</li>
</ul>
<ul>
<li><strong>30000-32767 / TCP</strong>: Kubernetes NodePort 서비스 접근 필요 시</li>
</ul>
<hr>
<h4 id="case-2-gpu-node-rightarrow-storage-cluster-minio--aistor-연동">Case 2. GPU Node $\rightarrow$ Storage Cluster (MinIO / AIStor) 연동</h4>
<p>모델 가중치 로드, 임베딩 적재, 체크포인트 저장을 위해 GPU 노드가 스토리지 클러스터로 접근할 때 필요한 포트입니다.</p>
<ul>
<li><strong>GPU Node $\rightarrow$ Storage Cluster 노드들 / VIP (내부망 bond1/E810 경로)</strong></li>
<li><strong>9000 / TCP</strong>: <strong>AIStor / MinIO S3 API 엔드포인트</strong> (데이터 Get/Put, 모델 다운로드에 필수)</li>
<li><strong>9001 / TCP</strong>: AIStor Console UI (브라우저나 관리 도구 접근 시)</li>
</ul>
<ul>
<li><em>(참고)</em> 스토리지가 S3 외에 블록/파일 인터페이스를 병행 제공하는 경우:</li>
<li><strong>2049 / TCP</strong>: NFS 마운트 필요 시</li>
<li><strong>3260 / TCP</strong>: iSCSI 연결 필요 시</li>
</ul>
<hr>
<h4 id="case-3-compute-cluster-rightarrow-gpu-node-연동-vllm-호출-및-추론-파이프라인">Case 3. Compute Cluster $\rightarrow$ GPU Node 연동 (vLLM 호출 및 추론 파이프라인)</h4>
<p>GPU 노드가 K8s에 아직 조인되지 않고 Standalone vLLM 컨테이너로 서빙 중이거나, 별도 파드로 띄운 상태에서 Compute 클러스터의 App/워크플로우가 요청을 보낼 때입니다.</p>
<ul>
<li><strong>Compute Cluster Nodes / Pods $\rightarrow$ GPU Node (외부망 CX-6 또는 내부망 E810)</strong></li>
<li><strong>8000 / TCP</strong> (또는 사용자 지정 포트): <strong>vLLM API 엔드포인트</strong> (<code>/v1/chat/completions</code>, <code>/v1/embeddings</code>)</li>
<li><strong>9400 / TCP</strong>: Prometheus 스크랩 (Compute 클러스터의 Prometheus/Thanos가 GPU 메트릭을 수집할 경우)</li>
<li><strong>8000 / TCP (또는 /metrics)</strong>: vLLM 내부 서빙 메트릭 수집 (<code>http://&lt;GPU_IP&gt;:8000/metrics</code>)</li>
</ul>
<hr>
<h4 id="case-4-기타-연동-및-인프라-필수-포트">Case 4. 기타 연동 및 인프라 필수 포트</h4>
<ul>
<li><strong>DNS &amp; 시각 동기화 (GPU Node $\rightarrow$ 내부 인프라 서버)</strong></li>
<li><strong>53 / UDP, TCP</strong>: 사내 내부 DNS 서버 (스토리지/클러스터 도메인 질의용)</li>
<li><strong>123 / UDP</strong>: NTP / Chrony 서버 (분산 환경, 토큰 발급, 인증서 검증 시 시간 오차 방지)</li>
</ul>
<ul>
<li><strong>인증 및 레지스트리 (GPU Node $\rightarrow$ 내부 플랫폼)</strong></li>
<li><strong>389 / 636 / TCP</strong>: 사내 LDAP / LDAPS 인증 연동 (필요 시)</li>
<li><strong>443 / TCP</strong>: 사내 프라이빗 컨테이너 레지스트리 (Harbor 등) 및 패키지 미러 서버</li>
</ul>
<ul>
<li><strong>로그 및 이벤트 스트리밍 (GPU Node $\rightarrow$ Kafka / 관제)</strong></li>
<li><strong>9092 / 9094 / TCP</strong>: Kafka Broker 통신 (추론 로그, 스토리지 이벤트 알림 전송 시)</li>
</ul>
<ul>
<li><strong>원격 접근 및 기본 관리</strong></li>
<li><strong>22 / TCP</strong>: Bastion/점프 서버 $\rightarrow$ GPU Node SSH</li>
</ul>
<hr>
<h3 id="방화벽-정책-수립-시-주의-사항">방화벽 정책 수립 시 주의 사항</h3>
<ul>
<li><strong>MTU 9000 적용 대역의 ICMP Type 3 Code 4 허용</strong>:</li>
<li>내부망 스토리지(Case 2) 연동 시 점보 프레임을 사용할 경우, 중간 방화벽/L4 장비에서 <code>Path MTU Discovery (PMTUD)</code>를 위한 ICMP(&quot;Fragmentation Needed and DF set&quot;) 패킷을 차단하면 대용량 가중치 전송 시 세션이 멈추는(Blackhole) 현상이 발생합니다. ICMP 통신은 반드시 열려 있어야 합니다.</li>
</ul>
<ul>
<li><strong>비대칭 경로</strong>:</li>
<li>Case 3 호출이 ConnectX-6(외부망)으로 인입되고 Case 2 스토리지가 E810(내부망)으로 나가는 환경이므로, 스테이트풀(Stateful) 방화벽을 통과할 때 돌아가는 패킷이 유실되지 않도록 라우팅 테이블(PBR)과 방화벽 인터페이스 매핑을 사전에 일치시켜야 합니다.</li>
</ul>
<p>===</p>
<p>에어갭(Air-gap) 환경에서는 컨테이너 이미지가 프라이빗 Nexus/Harbor 등으로 캐싱되더라도, <strong>대용량 LLM 모델 가중치(Weights), CUDA/드라이버 오프라인 패키지, Python Wheel 의존성, 벤치마크 바이너리</strong> 등은 인터넷 연결망(DMZ/외부 반출입 서버)에서 사전에 온전히 패키징하여 무결성 검증을 거친 뒤 반입해야 합니다.</p>
<p>외부망 다운로드부터 물리적/논리적 망 연계, 내부 AIStor(MinIO) 및 GPU 노드 적재까지의 전체 반입 절차를 5단계로 정리했습니다.</p>
<hr>
<h3 id="step-1-반입-대상-식별-및-패키지-카탈로그-정의">Step 1. 반입 대상 식별 및 패키지 카탈로그 정의</h3>
<p>에어갭 내부에서 추가 빌드나 외부 조회가 발생하지 않도록 의존성을 완전히 고립시켜 정의합니다.</p>
<ul>
<li><strong>LLM 모델 가중치 및 토크나이저 에셋</strong></li>
<li><code>*.safetensors</code>, <code>config.json</code>, <code>tokenizer.json</code>, <code>tokenizer_config.json</code>, <code>generation_config.json</code> 등 필수 메타데이터 전수 포함.</li>
<li>(주의) 가중치 파일 분할(Sharding) 누락이 없도록 HuggingFace Git LFS/Snapshot 전체 확보.</li>
</ul>
<ul>
<li><strong>드라이버 및 런타임 오프라인 아카이브 (Nexus에 없을 경우)</strong></li>
<li>NVIDIA Driver <code>.run</code> 파일, Fabric Manager deb/rpm 패키지, DCGM deb/rpm.</li>
<li>Intel E810 <code>ice</code> 드라이버 및 DDP(Dynamic Device Personalization) 펌웨어 패키지.</li>
</ul>
<ul>
<li><strong>추가 Python Wheel 및 벤치마크 바이너리</strong></li>
<li>PyTorch, vLLM 부가 의존성 wheel 파일 (<code>pip download --dest</code>).</li>
<li><code>iperf3</code>, <code>fio</code>, <code>gpu-burn</code> 빌드 아티팩트, <code>s3-benchmark</code> / <code>warp</code> 바이너리.</li>
</ul>
<hr>
<h3 id="step-2-외부-다운로드-서버dmz인터넷망-수집-및-무결성-패키징">Step 2. 외부 다운로드 서버(DMZ/인터넷망) 수집 및 무결성 패키징</h3>
<p>외부망 서버에서 스크립트를 통해 온전한 파일 셋을 확보하고 체크섬을 생성합니다.</p>
<ol>
<li><strong>HuggingFace 공식 CLI 기반 모델 Snapshot 다운로드</strong></li>
</ol>
<ul>
<li>심볼릭 링크를 배제하고 실데이터(<code>--local-dir-use-symlinks False</code>)로 완전히 내려받습니다.</li>
</ul>
<pre><code class="language-bash">pip install -U &quot;huggingface_hub[cli]&quot;

# 토큰 필요 모델(Llama 등) 로그인
huggingface-cli login --token &lt;HF_TOKEN&gt;

# 모델 전체 파일 다운로드
huggingface-cli download meta-llama/Llama-3.1-70B-Instruct \
    --local-dir /export/models/Llama-3.1-70B-Instruct \
    --local-dir-use-symlinks False \
    --exclude &quot;*.bin&quot; &quot;*.pth&quot;  # Safetensors만 반입할 경우 레거시 제외
</code></pre>
<ol start="2">
<li><strong>OS 패키지 및 바이너리 수집</strong><pre><code class="language-bash">mkdir -p /export/packages
# 예: Ubuntu 기준 의존성 일괄 다운로드
apt-get download $(apt-cache depends --recurse --no-recommends --no-suggests \
 --no-conflicts --no-breaks --no-replaces --no-enhances \
 datacenter-gpu-manager nvidia-fabricmanager-550 | grep &quot;^\w&quot; | sort -u)
</code></pre>
</li>
</ol>
<pre><code>

3. **체크섬 매니페스트(Manifest) 생성 (무결성 검증 기준점)**
* 반입 도중 파일 깨짐이나 누락을 방지하기 위해 파일별 SHA-256 목록을 생성합니다.


```bash
cd /export
find models/ packages/ -type f -exec sha256sum {} + &gt; manifest_sha256.txt
</code></pre><hr>
<h3 id="step-3-보안-검사-및-망-연계-전송-반입-파이프라인">Step 3. 보안 검사 및 망 연계 전송 (반입 파이프라인)</h3>
<p>사내 보안 반입 규정에 따라 승인 절차와 악성코드 검사를 수행합니다.</p>
<pre><code>[인터넷망 다운로드 서버]
          │
          ▼
 [안티바이러스 / CDR 검사]  (Pickle 취약점 및 악성 스크립트 스캔)
          │
          ▼
 [망연계 솔루션 / 전용 SFTP / 외장 스토리지]
          │
          ▼
 [에어갭 내부망 인제스천 Bastion]
</code></pre><ul>
<li><strong>Safetensors 사전 점검 (보안성)</strong></li>
<li><code>.bin</code>이나 <code>.pt</code> 같은 Python <code>pickle</code> 기반 가중치는 임의 코드 실행(RCE) 위험이 있어 보안 검사에서 반려되는 경우가 많습니다. 가능한 순수 텐서 직렬화 포맷인 <code>safetensors</code>만 통과시키는 것이 원칙입니다.</li>
</ul>
<ul>
<li><strong>망연계 시스템 전송</strong></li>
<li>대용량 파일(단일 100GB 이상) 전송 시 타임아웃 방지를 위해 <code>split -b 20G</code> 형태로 분할하여 전송하거나, 망연계 전용 고속 SFTP/MFT(Managed File Transfer) 채널을 경유합니다.</li>
</ul>
<hr>
<h3 id="step-4-내부망-도착-후-무결성-검증-및-복원">Step 4. 내부망 도착 후 무결성 검증 및 복원</h3>
<p>에어갭 내부 Bastion 또는 데이터 인제스천 노드에서 체크섬을 일괄 대조합니다.</p>
<ol>
<li><strong>분할 파일 결합 (분할 반입 시)</strong><pre><code class="language-bash">cat Llama-3.1-70B-Instruct.tar.gz.part* &gt; Llama-3.1-70B-Instruct.tar.gz
tar -xvf Llama-3.1-70B-Instruct.tar.gz
</code></pre>
</li>
</ol>
<pre><code>

2. **SHA-256 전수 검증**
```bash
sha256sum -c manifest_sha256.txt
</code></pre><ul>
<li>1개 파일이라도 <code>FAILED</code>가 출력되면 모델 로드 시 Silent Data Corruption 또는 Weight 포맷 에러가 발생하므로 재반입해야 합니다.</li>
</ul>
<hr>
<h3 id="step-5-내부-aistorminio-업로드-및-gpu-노드-배포">Step 5. 내부 AIStor(MinIO) 업로드 및 GPU 노드 배포</h3>
<p>검증이 끝난 모델을 내부 공용 오브젝트 스토리지(AIStor)에 업로드하고, GPU 노드가 이를 소비할 수 있도록 경로를 표준화합니다.</p>
<pre><code>[내부망 Bastion] 
       │
       │ (mc / s3 API, 내부망 bond1 MTU 9000)
       ▼
[AIStor Cluster (MinIO)]  ── (S3 엔드포인트: 9000)
       │
       │ (E810 내부망 초고속 스트리밍 / 다운로드)
       ▼
[GPU Node: /data/models/ 로컬 NVMe 캐시]
       │
       ▼
[vLLM 컨테이너 바인드 마운트]
</code></pre><ol>
<li><strong>AIStor 전용 버킷에 모델 업로드</strong></li>
</ol>
<ul>
<li>MinIO Client(<code>mc</code>) 또는 멀티파트 병렬 업로드 도구를 활용해 업로드합니다.</li>
</ul>
<pre><code class="language-bash"># AIStor 호스트 등록
mc alias set aistor http://&lt;AISTOR_INTERNAL_IP&gt;:9000 &lt;ACCESS_KEY&gt; &lt;SECRET_KEY&gt;

# 버킷 생성 및 병렬 업로드
mc mb --ignore-existing aistor/model-registry
mc cp --recursive /data/models/Llama-3.1-70B-Instruct aistor/model-registry/Llama-3.1-70B-Instruct
</code></pre>
<ol start="2">
<li><strong>GPU 노드에서의 최종 모델 취합 (vLLM 기동 준비)</strong></li>
</ol>
<ul>
<li>GPU 노드의 E810(내부망)을 통해 AIStor에서 로컬 고속 NVMe 디스크로 모델을 내려받습니다.</li>
</ul>
<pre><code class="language-bash">mkdir -p /data/models/Llama-3.1-70B-Instruct

# S3 멀티파트 동시 다운로드
mc cp --recursive aistor/model-registry/Llama-3.1-70B-Instruct/ /data/models/Llama-3.1-70B-Instruct/
</code></pre>
<ul>
<li>다운로드가 완료되면 vLLM 기동 스크립트에서 로컬 디스크 볼륨(<code>-v /data/models:/models</code>)으로 즉시 연결하여 오프라인 서빙을 시작합니다.</li>
</ul>
<hr>
<h3 id="에어갭-모델-반입-시-주요-체크리스트">에어갭 모델 반입 시 주요 체크리스트</h3>
<table>
<thead>
<tr>
<th>점검 영역</th>
<th>핵심 리스크</th>
<th>예방 및 조치 방안</th>
</tr>
</thead>
<tbody><tr>
<td><strong>토크나이저 누락</strong></td>
<td>가중치 파일만 받고 <code>tokenizer.json</code> 등 메타데이터를 빠뜨려 vLLM 구동 실패</td>
<td><code>snapshot_download</code>를 써서 리포지토리 루트 전체를 통째로 다운로드</td>
</tr>
<tr>
<td><strong>Safetensors vs Bin</strong></td>
<td>보안 정책상 <code>.bin</code>(Pickle) 파일 반입 차단</td>
<td><code>safetensors</code> 포맷으로만 수집 (HuggingFace 허브 상에서 safetensors 변환 버전 확보)</td>
</tr>
<tr>
<td><strong>전송 중 체크섬 오류</strong></td>
<td>대용량 전송 중 패킷 손상으로 텐서 가중치 깨짐 (추론 시 gibberish 텍스트 출력)</td>
<td>외부에서 생성한 <code>sha256sum</code> 매니페스트로 내부 반입 직후 전수 자동 검증</td>
</tr>
<tr>
<td><strong>AIStor 디렉터리 구조</strong></td>
<td>S3 Prefix 경로가 어긋나 vLLM 로더가 모델 루트 경로를 찾지 못함</td>
<td><code>s3://model-registry/&lt;Model-Name&gt;/</code> 밑에 설정 파일과 safetensors가 바로 위치하도록 표준화</td>
</tr>
</tbody></table>
<p>===</p>
<p>RHEL 10.2 기반 베어메탈 환경 및 에어갭(Air-gap) 제약 조건을 기준으로, 1~5단계 테스트에 필요한 툴들의 설치 방식을 정리했습니다.</p>
<p>RHEL 10 환경에서는 Docker 대신 기본 컨테이너 런타임으로 Podman(및 CDI 기반 NVIDIA Container Toolkit)을 사용하며, 클러스터 합류 후에는 <strong>K8s Pod</strong>로 배포됩니다.</p>
<hr>
<h3 id="테스트-단계별-필요-툴-맵핑-요약">테스트 단계별 필요 툴 맵핑 요약</h3>
<table>
<thead>
<tr>
<th>단계</th>
<th>주요 필요 툴</th>
<th>주요 역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Phase 1</strong></td>
<td><code>pciutils</code>, <code>iproute</code>, <code>ethtool</code>, <code>numactl</code>, <code>ipmitool</code>, <code>nvme-cli</code>, <code>stress-ng</code>, <code>fio</code></td>
<td>하드웨어 인식, PCIe 링크, NUMA, 온도, 디스크/메모리 부하</td>
</tr>
<tr>
<td><strong>Phase 2</strong></td>
<td><code>nvidia-driver</code>, <code>nvidia-fabricmanager</code>, <code>cuda-samples</code>, <code>dcgm</code>, <code>gpu-burn</code></td>
<td>드라이버/CUDA, NVLink P2P 대역폭, VRAM 무결성, GPU 풀로드 번인</td>
</tr>
<tr>
<td><strong>Phase 3</strong></td>
<td><code>vllm</code>, <code>curl</code>, <code>jq</code>, <code>netcat(ncat)</code></td>
<td>LLM 추론 서빙, REST API 호출, 스트리밍 검증</td>
</tr>
<tr>
<td><strong>Phase 4</strong></td>
<td><code>iperf3</code>, <code>fio</code>, <code>minio-client(mc)</code>, <code>warp</code> 또는 <code>s3-benchmark</code></td>
<td>점보프레임 대역폭, S3/NFS I/O 처리량, 가중치 다운로드</td>
</tr>
<tr>
<td><strong>Phase 5</strong></td>
<td><code>benchmark_serving.py</code>(vLLM 내장), <code>k6</code>, <code>locust</code>, <code>dcgm-exporter</code>, <code>prometheus</code></td>
<td>TTFT/ITL 벤치마크, 동시성 분산 부하, 메트릭 수집</td>
</tr>
</tbody></table>
<hr>
<h3 id="1-phase-1-하드웨어-및-시스템-무결성-점검-툴">1. Phase 1: 하드웨어 및 시스템 무결성 점검 툴</h3>
<p>이 단계는 하드웨어와 커널 상태를 직접 제어·진단해야 하므로 <strong>베어메탈 OS 직접 설치가 원칙</strong>입니다.</p>
<ul>
<li><strong>OS(RHEL 10.2) 기본 Repo 설치 가능 여부</strong>:</li>
<li>대부분 RHEL BaseOS / AppStream / EPEL(사내 Nexus 미러)에서 RPM으로 제공됩니다.</li>
</ul>
<ul>
<li><strong>설치 방법</strong>:</li>
<li><strong>[Bare-metal OS]</strong>:<pre><code class="language-bash"># BaseOS / AppStream 기본 제공
dnf install -y pciutils iproute ethtool numactl ipmitool nvme-cli fio
</code></pre>
</li>
</ul>
<h1 id="stress-ng-epel-저장소-필요---사내-nexus-rpm-미러에서-수집">stress-ng (EPEL 저장소 필요 -&gt; 사내 Nexus RPM 미러에서 수집)</h1>
<p>dnf install -y stress-ng</p>
<pre><code>

* **[Podman Container]** *(호스트 진단용 컨테이너 실행 시)*:
호스트의 하드웨어 버스(`--privileged`, `/dev`, `/sys`)를 그대로 마운트해야 합니다.
```bash
podman run --rm -it --privileged \
    -v /dev:/dev -v /sys:/sys:ro -v /tmp:/tmp \
    nexus.internal:8082/infra/diag-tools:latest stress-ng --cpu 0 --timeout 60s
</code></pre><ul>
<li><strong>[K8s Pod]</strong>: Phase 1은 노드 인수 직후 OS 베이스라인 검증이므로 K8s Pod 설치는 부적합합니다. (불가피할 경우 <code>privileged: true</code>, <code>hostPID: true</code>, <code>hostNetwork: true</code> 데몬셋 사용)</li>
</ul>
<hr>
<h3 id="2-phase-2-gpu-드라이버-및-burn-in-테스트-툴">2. Phase 2: GPU 드라이버 및 Burn-In 테스트 툴</h3>
<p>드라이버와 Fabric Manager는 커널 모듈이므로 베어메탈에 설치해야 하며, 진단/번인 도구는 컨테이너 방식이 권장됩니다.</p>
<h4 id="1-nvidia-driver--fabric-manager">1) NVIDIA Driver &amp; Fabric Manager</h4>
<ul>
<li><strong>설치 위치</strong>: 반드시 <strong>Bare-metal OS</strong></li>
<li><strong>다운로드</strong>: 외부망에서 RHEL 10 호환 NVIDIA Data Center GPU 드라이버 RPM 또는 <code>.run</code> 파일 반입.</li>
<li><strong>설치 방법</strong>:<pre><code class="language-bash"># 오프라인 반입된 RPM 설치
dnf localinstall -y nvidia-driver-*.rpm nvidia-fabricmanager-*.rpm
systemctl enable --now nvidia-fabricmanager
systemctl enable --now nvidia-persistenced
</code></pre>
</li>
</ul>
<pre><code>


#### 2) CUDA Samples (`p2pBandwidthLatencyTest`) &amp; DCGM

* **설치 방법**:
* **[Bare-metal OS]**: NVIDIA 공식 CUDA Toolkit / DCGM 오프라인 패키지 반입 후 설치 (`dnf localinstall -y datacenter-gpu-manager-*.rpm`).
* **[Podman Container (권장)]**:
RHEL 10의 Podman은 CDI(Container Device Interface)를 통해 GPU를 인식합니다.
```bash
# P2P 대역폭 측정
podman run --rm --device nvidia.com/gpu=all \
    nexus.internal:8082/nvidia/cuda-sample:vectorAdd-cuda12.5.0 \
    p2pBandwidthLatencyTest

# DCGM 진단
podman run --rm --privileged --device nvidia.com/gpu=all \
    nexus.internal:8082/nvidia/dcgm:latest dcgmi diag -r 3
</code></pre><h4 id="3-gpu-burn">3) GPU-Burn</h4>
<ul>
<li><strong>다운로드</strong>: 외부망에서 <code>wilic/gpu-burn</code> 이미지를 <code>docker save</code> 후 내부 Nexus에 push.</li>
<li><strong>설치 방법</strong>:</li>
<li><strong>[Podman Container]</strong>:<pre><code class="language-bash">podman run --rm --device nvidia.com/gpu=all \
  nexus.internal:8082/benchmark/gpu-burn:latest 3600
</code></pre>
</li>
</ul>
<pre><code>

* **[K8s Pod (추후 유지보수용)]**:
```yaml
apiVersion: v1
kind: Pod
metadata:
  name: gpu-burn-test
spec:
  restartPolicy: Never
  containers:
  - name: gpu-burn
    image: nexus.internal:8082/benchmark/gpu-burn:latest
    args: [&quot;3600&quot;]
    resources:
      limits:
        nvidia.com/gpu: &quot;8&quot;
</code></pre><hr>
<h3 id="3-phase-3-vllm-서빙-및-연동-테스트-툴">3. Phase 3: vLLM 서빙 및 연동 테스트 툴</h3>
<h4 id="1-vllm">1) vLLM</h4>
<ul>
<li><strong>OS 직접 설치 여부</strong>: <strong>비권장</strong> (PyTorch, FlashAttention, CUDA 라이브러리 간 컴파일 의존성이 복잡하여 RHEL 10 베어메탈 직접 빌드는 실패 위험이 매우 큼). 공식 컨테이너 이미지를 Nexus로 반입해 사용하는 것이 표준입니다.</li>
<li><strong>설치 방법</strong>:</li>
<li><strong>[Podman Container (조인 전 독립 구동)]</strong>:
RHEL Podman 실행 시 공유 메모리(<code>--ipc=host</code> 또는 <code>--shm-size</code>)와 디바이스 플래그가 중요합니다.<pre><code class="language-bash">podman run -d --name vllm-server \
  --device nvidia.com/gpu=all \
  --ipc=host \
  --net=host \
  -v /data/models:/models:ro \
  nexus.internal:8082/vllm/vllm-openai:latest \
  --model /models/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 8 \
  --port 8000
</code></pre>
</li>
</ul>
<pre><code>

* **[K8s Pod (조인 후 워크로드 구동)]**:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-serving
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: vllm
        image: nexus.internal:8082/vllm/vllm-openai:latest
        args: [&quot;--model&quot;, &quot;/models/Llama-3.1-70B-Instruct&quot;, &quot;--tensor-parallel-size&quot;, &quot;8&quot;]
        resources:
          limits:
            nvidia.com/gpu: &quot;8&quot;
        volumeMounts:
        - name: model-cache
          mountPath: /models
        - name: dshm
          mountPath: /dev/shm
      volumes:
      - name: model-cache
        hostPath: { path: /data/models }
      - name: dshm
        emptyDir: { medium: Memory, sizeLimit: 32Gi }
</code></pre><h4 id="2-검증-클라이언트-curl-jq-ncat">2) 검증 클라이언트 (<code>curl</code>, <code>jq</code>, <code>ncat</code>)</h4>
<ul>
<li><strong>설치 방법</strong>:</li>
<li><strong>[Bare-metal OS / Compute Nodes]</strong>: RHEL 기본 Repo에서 제공.<pre><code class="language-bash">dnf install -y curl jq nmap-ncat
</code></pre>
</li>
</ul>
<pre><code>

* **[K8s Pod]**: Compute 클러스터 디버그 파드로 배포.
```bash
kubectl run net-test --image=nexus.internal:8082/tools/curlimages:latest -- sleep 3600
</code></pre><hr>
<h3 id="4-phase-4-스토리지-네트워크--io-벤치마크-툴">4. Phase 4: 스토리지 네트워크 &amp; I/O 벤치마크 툴</h3>
<h4 id="1-iperf3">1) iperf3</h4>
<ul>
<li><strong>설치 방법</strong>:</li>
<li><strong>[Bare-metal OS]</strong>: <code>dnf install -y iperf3</code> (BaseOS/AppStream 기본 제공).</li>
<li><strong>[Podman Container]</strong>: <code>podman run --net=host nexus.internal:8082/network/iperf3:latest -c &lt;IP&gt; -P 8</code></li>
<li><strong>[K8s Pod]</strong>: Compute 노드와 GPU 노드에 HostNetwork 기반 파드로 배포하여 Pod 간 네트워크 병목 측정.</li>
</ul>
<h4 id="2-minio-client-mc--warp-또는-s3-benchmark">2) MinIO Client (<code>mc</code>) &amp; Warp (또는 s3-benchmark)</h4>
<ul>
<li><strong>OS 기본 제공 여부</strong>: RHEL 기본 Repo에 없음. 외부에서 단일 바이너리(Go compile standalone)를 다운로드하여 에어갭으로 반입.</li>
<li><strong>설치 방법</strong>:</li>
<li><strong>[Bare-metal OS]</strong>:<pre><code class="language-bash"># 반입된 바이너리에 실행 권한 부여 후 /usr/local/bin에 배치
install -m 755 mc /usr/local/bin/mc
install -m 755 warp /usr/local/bin/warp
</code></pre>
</li>
</ul>
<pre><code>

* **[Podman Container]**:
```bash
podman run --rm --net=host \
    -v /data/models:/data \
    nexus.internal:8082/minio/mc:latest \
    cp --recursive aistor/model-registry/Llama-3.1-70B-Instruct/ /data/
</code></pre><hr>
<h3 id="5-phase-5-성능-및-부하-테스트-툴">5. Phase 5: 성능 및 부하 테스트 툴</h3>
<h4 id="1-vllm-내장-벤치마크-도구-benchmark_servingpy">1) vLLM 내장 벤치마크 도구 (<code>benchmark_serving.py</code>)</h4>
<ul>
<li><strong>다운로드</strong>: vLLM 컨테이너 이미지 내부 <code>/workspace/vllm/benchmarks/</code> 또는 vLLM 패키지에 포함되어 있으므로 별도 설치 불필요.</li>
<li><strong>실행 방법</strong>:</li>
<li><strong>[Podman Container]</strong>: 이미 실행 중인 vLLM 파드/컨테이너에 exec로 들어가거나, 동일 이미지를 띄워 실행.<pre><code class="language-bash">podman exec -it vllm-server python3 -m vllm.entrypoints.openai.benchmark_serving \
  --backend vllm \
  --model /models/Llama-3.1-70B-Instruct \
  --dataset-path /data/ShareGPT_V3_unfiltered_cleaned_split.json \
  --request-rate 10
</code></pre>
</li>
</ul>
<pre><code>




#### 2) k6 / Locust (Compute Cluster $\rightarrow$ GPU 부하 생성기)

* **다운로드**: 외부망에서 공식 바이너리(k6) 또는 도커 이미지(k6, locust)를 내부 Nexus로 반입.
* **설치 방법**:
* **[Bare-metal / Bastion]**: k6 독립 실행 바이너리를 반입하여 `/usr/local/bin/k6`로 복사.
* **[K8s Pod (분산 부하 권장)]**: Compute 클러스터에서 부하 인스턴스를 수십 개로 분산할 때 가장 적합.
```yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: k6-loadtest
spec:
  parallelism: 4     # 4개 파드에서 동시 타격
  template:
    spec:
      containers:
      - name: k6
        image: nexus.internal:8082/loadtest/k6:latest
        command: [&quot;k6&quot;, &quot;run&quot;, &quot;/scripts/vllm_load.js&quot;]
        volumeMounts:
        - name: script-vol
          mountPath: /scripts
      volumes:
      - name: script-vol
        configMap: { name: k6-test-script }
      restartPolicy: Never
</code></pre><h4 id="3-dcgm-exporter">3) DCGM Exporter</h4>
<ul>
<li><strong>설치 방법</strong>:</li>
<li><strong>[Bare-metal OS]</strong>: RPM 설치 후 Systemd 서비스로 구동 (<code>systemctl start nvidia-dcgm-exporter</code>).</li>
<li><strong>[Podman Container]</strong>:<pre><code class="language-bash">podman run -d --name dcgm-exporter \
  --device nvidia.com/gpu=all \
  --net=host \
  nexus.internal:8082/nvidia/k8s-device-plugin/dcgm-exporter:latest
</code></pre>
</li>
</ul>
<p>```</p>
<ul>
<li><strong>[K8s Pod]</strong>: 노드 조인 후 K8s GPU Operator 또는 DaemonSet 형태로 배포 (<code>port: 9400</code>).</li>
</ul>
<hr>
<h3 id="배포-형태별-빠른-권장-가이드">배포 형태별 빠른 권장 가이드</h3>
<ul>
<li><strong>Bare-metal OS 설치 필수</strong>: NVIDIA Driver, Fabric Manager, 커널 유틸(<code>ethtool</code>, <code>ip</code>, <code>numactl</code>), 로컬 벤치마크 바이너리(<code>mc</code>, <code>warp</code>).</li>
<li><strong>Podman Container 실행 권장 (Phase 1~4 인수 단계)</strong>: <code>p2pBandwidthLatencyTest</code>, <code>gpu-burn</code>, <code>vllm</code>(초기 단독 검증).</li>
<li><strong>K8s Pod 실행 권장 (Compute 연동 및 Phase 5 단계)</strong>: 부하 생성기(<code>k6</code>/<code>locust</code>), K8s 조인 후의 정규 <code>vLLM</code> 서빙 파드, <code>dcgm-exporter</code>.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S05b]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S05b</link>
            <guid>https://velog.io/@youngkyoo_kim/26S05b</guid>
            <pubDate>Fri, 04 Sep 2026 19:45:20 GMT</pubDate>
            <description><![CDATA[<p>앞서 정리한 1~4단계 점검 및 테스트 과정을 현장에서 바로 실행할 수 있도록 단계별 Bash 스크립트로 분리하여 작성했습니다.</p>
<p>실행 환경에 맞게 스크립트 상단의 변수(<code>CX6_IFACE</code>, <code>E810_IFACE</code>, <code>STORAGE_IP</code> 등)만 지정해 사용하시면 됩니다.</p>
<hr>
<h3 id="phase-1-하드웨어-및-os-무결성-점검-스크립트-phase1_hw_checksh">Phase 1: 하드웨어 및 OS 무결성 점검 스크립트 (<code>phase1_hw_check.sh</code>)</h3>
<pre><code class="language-bash">#!/usr/bin/env bash
set -euo pipefail

# ================= Configuration =================
CX6_IFACE=&quot;${CX6_IFACE:-eth0}&quot;      # ConnectX-6 외부망 인터페이스명
E810_IFACE=&quot;${E810_IFACE:-eth1}&quot;    # Intel E810 내부망 인터페이스명
NVME_DEV=&quot;${NVME_DEV:-/dev/nvme0n1}&quot; # 테스트할 로컬 NVMe 디바이스
STRESS_TIME=&quot;10m&quot;                   # 초기 인수 점검용 (전수 검사 시 1h 권장)
# =================================================

echo &quot;==================================================&quot;
echo &quot; [Phase 1] Hardware &amp; System Integrity Check&quot;
echo &quot;==================================================&quot;

echo &quot;&gt;&gt;&gt; 1. PCIe Device Enumeration (GPU &amp; NIC)&quot;
lspci -nn | grep -iE &quot;3d|vga|mellanox|ethernet controller.*810&quot; || true

echo -e &quot;\n&gt;&gt;&gt; 2. PCIe Link Speed &amp; Width Verification&quot;
# NVIDIA (10de) &amp; Mellanox (15b3) 디바이스의 LnkCap vs LnkSta 비교
for bdf in $(lspci -d 10de: -d 15b3: | awk &#39;{print $1}&#39;); do
    echo &quot;--- Device: $bdf ---&quot;
    lspci -s &quot;$bdf&quot; -vvv | grep -E &quot;LnkCap:|LnkSta:&quot;
done

echo -e &quot;\n&gt;&gt;&gt; 3. NUMA Topology Check&quot;
numactl -H || true
for dev in $(lspci -d 10de: -d 15b3: | awk &#39;{print $1}&#39;); do
    echo &quot;Device $dev -&gt; NUMA Node: $(cat /sys/bus/pci/devices/0000:$dev/numa_node 2&gt;/dev/null || echo &#39;N/A&#39;)&quot;
done

echo -e &quot;\n&gt;&gt;&gt; 4. Kernel Hardware Error Check (AER / MCE / EDAC)&quot;
dmesg -T | grep -iE &quot;mce|edac|aer|pcie.*error|corrupted&quot; | tail -n 20 || echo &quot;No critical hardware errors found in dmesg.&quot;

echo -e &quot;\n&gt;&gt;&gt; 5. NIC Link &amp; Firmware Status&quot;
echo &quot;--- ConnectX-6 ($CX6_IFACE) ---&quot;
ethtool -i &quot;$CX6_IFACE&quot; || true
ethtool &quot;$CX6_IFACE&quot; | grep -E &quot;Speed:|Duplex:|Link detected:&quot; || true

echo &quot;--- Intel E810 ($E810_IFACE) ---&quot;
ethtool -i &quot;$E810_IFACE&quot; || true
ethtool &quot;$E810_IFACE&quot; | grep -E &quot;Speed:|Duplex:|Link detected:&quot; || true

echo -e &quot;\n&gt;&gt;&gt; 6. Local NVMe SMART Health &amp; FIO Benchmark&quot;
if command -v nvme &gt;/dev/null 2&gt;&amp;1; then
    nvme smart-log &quot;$NVME_DEV&quot; || true
fi
fio --name=nvme_bench --filename=/tmp/fio_test_tmp --size=5G --rw=write --bs=1M --direct=1 --ioengine=libaio --runtime=15 --group_reporting
rm -f /tmp/fio_test_tmp

echo -e &quot;\n&gt;&gt;&gt; 7. CPU &amp; RAM Stress Test ($STRESS_TIME)&quot;
stress-ng --cpu &quot;$(nproc)&quot; --vm 4 --vm-bytes 80% --verify --timeout &quot;$STRESS_TIME&quot; --metrics-brief

echo -e &quot;\n[Phase 1] Completed successfully.&quot;
</code></pre>
<hr>
<h3 id="phase-2-gpu-드라이버-설치-및-번인-테스트-phase2_gpu_burnsh">Phase 2: GPU 드라이버 설치 및 번인 테스트 (<code>phase2_gpu_burn.sh</code>)</h3>
<pre><code class="language-bash">#!/usr/bin/env bash
set -euo pipefail

# ================= Configuration =================
BURN_DURATION=&quot;1800&quot; # gpu-burn 실행 시간 (초 단위, 권장: 3600)
# =================================================

echo &quot;==================================================&quot;
echo &quot; [Phase 2] GPU Stack Setup &amp; Burn-In Test&quot;
echo &quot;==================================================&quot;

echo &quot;&gt;&gt;&gt; 1. Checking Driver, Fabric Manager &amp; Persistence Mode&quot;
nvidia-smi
nvidia-smi -pm 1

if systemctl is-active --quiet nvidia-fabricmanager; then
    echo &quot;nvidia-fabricmanager is running.&quot;
else
    echo &quot;Checking Fabric Manager status:&quot;
    systemctl status nvidia-fabricmanager || true
fi

echo -e &quot;\n&gt;&gt;&gt; 2. Topology &amp; NVLink Status&quot;
nvidia-smi topo -m
nvidia-smi nvlink -s || true
nvidia-smi nvlink -e || true

echo -e &quot;\n&gt;&gt;&gt; 3. P2P Bandwidth &amp; Latency Test (via Container)&quot;
docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectorAdd-cuda12.5.0 \
    p2pBandwidthLatencyTest

echo -e &quot;\n&gt;&gt;&gt; 4. DCGM Diagnostic Level 3 (HW &amp; Memory Stress)&quot;
if command -v dcgmi &gt;/dev/null 2&gt;&amp;1; then
    dcgmi diag -r 3
else
    echo &quot;dcgmi not found. Skipping DCGM diag.&quot;
fi

echo -e &quot;\n&gt;&gt;&gt; 5. GPU-Burn Full Load Test (${BURN_DURATION}s)&quot;
docker run --rm --gpus all \
    wilic/gpu-burn:latest \
    &quot;$BURN_DURATION&quot;

echo -e &quot;\n[Phase 2] Completed successfully.&quot;
</code></pre>
<hr>
<h3 id="phase-3-vllm-서빙-및-연동-테스트-phase3_vllm_verifysh">Phase 3: vLLM 서빙 및 연동 테스트 (<code>phase3_vllm_verify.sh</code>)</h3>
<pre><code class="language-bash">#!/usr/bin/env bash
set -euo pipefail

# ================= Configuration =================
GPU_HOST_IP=&quot;${GPU_HOST_IP:-127.0.0.1}&quot; # 외부망 통신 시 GPU의 ConnectX-6 IP
MODEL_PATH=&quot;/data/models/Llama-3.1-8B-Instruct&quot;
SERVED_NAME=&quot;llama-3.1-8b&quot;
PORT=&quot;8000&quot;
# =================================================

echo &quot;==================================================&quot;
echo &quot; [Phase 3] vLLM Deployment &amp; Ingestion Test&quot;
echo &quot;==================================================&quot;

echo &quot;&gt;&gt;&gt; 1. Kernel Network Parameter Adjustment (PBR / Asymmetric Routing Safe)&quot;
sysctl -w net.ipv4.conf.all.rp_filter=2
sysctl -w net.ipv4.conf.default.rp_filter=2

echo &quot;&gt;&gt;&gt; 2. Launching vLLM Test Container (Single GPU / Eager Mode)&quot;
docker rm -f vllm-test &gt;/dev/null 2&gt;&amp;1 || true
docker run -d --name vllm-test \
    --runtime nvidia \
    --gpus &#39;&quot;device=0&quot;&#39; \
    -v /data/models:/models \
    -p &quot;${PORT}:8000&quot; \
    --ipc=host \
    vllm/vllm-openai:latest \
    --model &quot;/models/$(basename &quot;$MODEL_PATH&quot;)&quot; \
    --served-model-name &quot;$SERVED_NAME&quot; \
    --port 8000 \
    --max-model-len 4096 \
    --gpu-memory-utilization 0.90 \
    --enforce-eager

echo &quot;Waiting for vLLM server to become healthy...&quot;
until curl -s &quot;http://localhost:${PORT}/health&quot; &gt;/dev/null 2&gt;&amp;1; do
    sleep 3
    echo -n &quot;.&quot;
done
echo -e &quot;\nvLLM is ready.&quot;

echo -e &quot;\n&gt;&gt;&gt; 3. Local API Completion Test&quot;
curl -s -X POST &quot;http://localhost:${PORT}/v1/chat/completions&quot; \
    -H &quot;Content-Type: application/json&quot; \
    -d &quot;{
      \&quot;model\&quot;: \&quot;$SERVED_NAME\&quot;,
      \&quot;messages\&quot;: [{\&quot;role\&quot;: \&quot;user\&quot;, \&quot;content\&quot;: \&quot;Ping test from GPU Node local.\&quot;}],
      \&quot;max_tokens\&quot;: 30
    }&quot; | grep -o &#39;&quot;content&quot;:&quot;[^&quot;]*&quot;&#39; || true

echo -e &quot;\n&gt;&gt;&gt; 4. Remote Test Command (Execute on Compute Cluster Node)&quot;
cat &lt;&lt;EOF
[Compute Cluster Node Execution Command]
curl -X POST http://${GPU_HOST_IP}:${PORT}/v1/chat/completions \\
  -H &quot;Content-Type: application/json&quot; \\
  -d &#39;{
    &quot;model&quot;: &quot;$SERVED_NAME&quot;,
    &quot;messages&quot;: [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Hello from Compute Cluster&quot;}],
    &quot;max_tokens&quot;: 50
  }&#39;
EOF

echo -e &quot;\n[Phase 3] Completed successfully.&quot;
</code></pre>
<hr>
<h3 id="phase-4-스토리지-네트워크--io-벤치마크-phase4_storage_testsh">Phase 4: 스토리지 네트워크 &amp; I/O 벤치마크 (<code>phase4_storage_test.sh</code>)</h3>
<pre><code class="language-bash">#!/usr/bin/env bash
set -euo pipefail

# ================= Configuration =================
E810_IFACE=&quot;${E810_IFACE:-eth1}&quot;
STORAGE_IP=&quot;${STORAGE_IP:-10.100.0.10}&quot;     # 스토리지 클러스터 타깃 IP
STORAGE_SUBNET=&quot;${STORAGE_SUBNET:-10.100.0.0/16}&quot;
MOUNT_DIR=&quot;${MOUNT_DIR:-/mnt/storage}&quot;
# =================================================

echo &quot;==================================================&quot;
echo &quot; [Phase 4] Storage Interconnect &amp; I/O Benchmark&quot;
echo &quot;==================================================&quot;

echo &quot;&gt;&gt;&gt; 1. Static Route &amp; Jumbo Frame (MTU 9000) Setup&quot;
ip link set dev &quot;$E810_IFACE&quot; mtu 9000
ip route replace &quot;$STORAGE_SUBNET&quot; dev &quot;$E810_IFACE&quot; proto static || true

echo &quot;Checking Path MTU with DF bit set (ICMP payload 8972 + 28 = 9000 bytes):&quot;
ping -c 3 -M do -s 8972 &quot;$STORAGE_IP&quot;

echo -e &quot;\n&gt;&gt;&gt; 2. Network Layer Throughput Benchmark (iperf3)&quot;
echo &quot;Target Storage Server must be running: &#39;iperf3 -s&#39;&quot;
iperf3 -c &quot;$STORAGE_IP&quot; -P 8 -t 15 -O 2

echo -e &quot;\n&gt;&gt;&gt; 3. Storage I/O Benchmark (POSIX / NFS Mount)&quot;
if mountpoint -q &quot;$MOUNT_DIR&quot;; then
    echo &quot;Running FIO Sequential Read on mounted storage ($MOUNT_DIR)...&quot;
    fio --name=storage_seq_read \
        --directory=&quot;$MOUNT_DIR&quot; \
        --rw=read \
        --bs=1M \
        --size=20G \
        --numjobs=8 \
        --iodepth=16 \
        --ioengine=libaio \
        --direct=1 \
        --group_reporting
else
    echo &quot;Directory $MOUNT_DIR is not mounted. Skipping POSIX FIO.&quot;
    echo &quot;If S3 Object Storage is used, run MinIO Warp or s3-benchmark instead:&quot;
    echo &quot;  warp get --host=&lt;S3_ENDPOINT&gt; --access-key=&lt;KEY&gt; --secret-key=&lt;SECRET&gt; --bucket=&lt;BUCKET&gt; --concurrent=8&quot;
fi

echo -e &quot;\n[Phase 4] Completed successfully.&quot;
</code></pre>
<p>===</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S05a]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S05a</link>
            <guid>https://velog.io/@youngkyoo_kim/26S05a</guid>
            <pubDate>Fri, 04 Sep 2026 19:40:15 GMT</pubDate>
            <description><![CDATA[<p>기존 클러스터 노드들과 마찬가지로 외부망과 내부망이 모두 존재하는 환경에서, GPU 노드 인수 직후 OS/플랫폼 팀이 수행해야 할 1단계(HW 무결성 및 시스템 점검)의 상세 실행 절차입니다.</p>
<p>특히 GPU 노드는 고전력 소모, 복잡한 PCIe/NUMA 토폴로지, 그리고 고속 NIC(ConnectX-6 + Intel E810)가 혼재되어 있으므로 초기 HW 결함(불량 PCIe 레인, 메모리 비트 에러, 발열 이슈)을 걸러내는 작업이 필수적입니다.</p>
<hr>
<h3 id="1-하드웨어-인식-및-pcie-토폴로지-점검">1. 하드웨어 인식 및 PCIe 토폴로지 점검</h3>
<p>GPU와 NIC가 스펙시트대로 전체 대역폭을 확보한 상태로 링크 협상(Negotiation)을 마쳤는지 확인합니다.</p>
<ul>
<li><strong>GPU 및 NIC 장치 인식 여부</strong><pre><code class="language-bash"># GPU 인식 수량 확인 (NVIDIA 벤더 ID: 10de)
lspci -nn | grep -i 3d
</code></pre>
</li>
</ul>
<h1 id="nic-인식-확인-mellanox-15b3-intel-e810-8086">NIC 인식 확인 (Mellanox: 15b3, Intel E810: 8086)</h1>
<p>lspci -nn | grep -E &quot;Mellanox|Ethernet controller.*810&quot;</p>
<pre><code>

* **PCIe Link Speed 및 Width 다운그레이드 여부 확인**
라이저 카드 불량, 소켓 결착 불량, 또는 보드 불량 시 Gen4/Gen5 x16이 x8이나 Gen3 이하로 다운그레이드되는 경우가 잦습니다.
```bash
# 각 디바이스의 LnkCap(스펙상 지원)과 LnkSta(현재 동작 상태) 비교
# LnkSta의 Speed(16GT/s: Gen4, 32GT/s: Gen5)와 Width(x16) 확인
for bdf in $(lspci -d 10de: -d 15b3: | awk &#39;{print $1}&#39;); do
    echo &quot;=== Device $bdf ===&quot;
    lspci -s $bdf -vvv | grep -E &quot;LnkCap:|LnkSta:&quot;
done
</code></pre><ul>
<li><strong>NUMA 노드 바인딩 토폴로지 분석</strong>
GPU와 네트워크 카드가 물리적으로 어떤 CPU 소켓(NUMA 노드)에 직결되어 있는지 파악해야 추후 vLLM 코어 핀닝 및 zero-copy 경로 최적화가 가능합니다.<pre><code class="language-bash"># CPU 소켓별 NUMA 노드 구조
numactl -H
</code></pre>
</li>
</ul>
<h1 id="각-디바이스가-물려있는-numa-노드-확인">각 디바이스가 물려있는 NUMA 노드 확인</h1>
<p>for dev in $(lspci -d 10de: -d 15b3: | awk &#39;{print $1}&#39;); do
    echo &quot;Device $dev -&gt; NUMA Node: $(cat /sys/bus/pci/devices/0000:$dev/numa_node)&quot;
done</p>
<pre><code>


---

### 2. 하드웨어 에러 로그 및 시스템 건강 상태(Health) 전수 점검

OS 부팅 과정에서 발생한 메모리 비트 에러나 PCIe AER(Advanced Error Reporting) 이벤트를 확인합니다.

* **커널 하드웨어 에러 로그 점검**
```bash
# PCIe corrected/uncorrectable 에러 및 하드웨어 결함 점검
dmesg -T | grep -iE &quot;mce|edac|aer|pcie.*error|fail|corrupted&quot;
</code></pre><p><em>(AER Corrected 에러가 수백 번 이상 반복 카운팅된다면 슬롯/라이저 결착 불량이거나 불량 케이블일 가능성이 높음)</em></p>
<ul>
<li><strong>IPMI/BMC 시스템 이벤트 로그(SEL) 확인</strong>
OS가 올라오기 전 BIOS 레벨에서 감지된 PSU, Fan, 센서 에러를 체크합니다.<pre><code class="language-bash">ipmitool sel elist | tail -n 30
ipmitool sdr list | grep -iE &quot;fan|temp|status&quot;
</code></pre>
</li>
</ul>
<pre><code>


---

### 3. CPU 및 메모리(RAM) 스트레스 테스트

GPU 번인에 앞서 호스트 시스템의 기본 연산 및 시스템 메모리의 안정성을 먼저 확인합니다.

* **메모리(ECC) 무결성 및 CPU 부하 테스트**
* `stress-ng`를 이용해 시스템 메모리의 약 80~85%를 할당하고 전체 vCPU 코어를 1시간 동안 포화 상태로 구동합니다.


```bash
# 전체 CPU 코어 수 확인 후 실행
stress-ng --cpu $(nproc) --vm 4 --vm-bytes 80% --verify --timeout 1h --metrics-brief
</code></pre><ul>
<li><strong>동시 모니터링 포인트 (백그라운드 터미널)</strong></li>
<li>EDAC 메모리 정정 에러 카운터 증가 여부:<pre><code class="language-bash">watch -n 1 &#39;cat /sys/devices/system/edac/mc/mc*/ce_count&#39;
</code></pre>
</li>
</ul>
<pre><code>

* CPU 코어 온도 및 Throttling 점검:
```bash
watch -n 2 &#39;sensors | grep -i &quot;Package id&quot;&#39;
</code></pre><hr>
<h3 id="4-로컬-스토리지osnvme-속도-및-smart-상태-점검">4. 로컬 스토리지(OS/NVMe) 속도 및 SMART 상태 점검</h3>
<p>vLLM 구동 시 대용량 모델 가중치(Checkpoint)를 로컬 캐시에 저장하고 로드해야 하므로, 로컬 NVMe의 성능과 상태가 중요합니다.</p>
<ul>
<li><strong>NVMe 드라이브 SMART 상태</strong><pre><code class="language-bash"># nvme-cli 설치 확인 후 상태 점검
nvme list
nvme smart-log /dev/nvme0n1
</code></pre>
</li>
</ul>
<pre><code>

*(Critical Warning이 `0`, Available Spare가 `100%`, Media and Data Integrity Errors가 `0`인지 확인)*
* **로컬 디스크 Direct I/O 속도 측정**
```bash
fio --name=nvme_write --filename=/tmp/test_fio --size=10G --rw=write --bs=1M --direct=1 --ioengine=libaio --runtime=30 --group_reporting
rm -f /tmp/test_fio
</code></pre><hr>
<h3 id="5-네트워크-인터페이스nic-물리-링크-및-펌웨어-정합성-점검">5. 네트워크 인터페이스(NIC) 물리 링크 및 펌웨어 정합성 점검</h3>
<p>현재 외부망은 Mellanox ConnectX-6, 내부망은 Intel E810입니다. 기존 클러스터(모두 E810 계열)와의 이종 구성을 감안하여 링크 레벨을 먼저 정리해야 합니다.</p>
<ul>
<li><strong>ConnectX-6 (외부망) 점검</strong></li>
<li>Mellanox 드라이버(MLNX_OFED 또는 커널 인박스 <code>mlx5_core</code>) 로드 및 펌웨어 버전 확인:<pre><code class="language-bash">ethtool -i &lt;cx6_iface_name&gt;
</code></pre>
</li>
</ul>
<pre><code>

* 지원 링크 스피드(100G/200G 등) 정상 협상 여부 및 패킷 드랍 카운터:
```bash
ethtool &lt;cx6_iface_name&gt; | grep -E &quot;Speed:|Duplex:|Link detected:&quot;
ip -s link show &lt;cx6_iface_name&gt;
</code></pre><ul>
<li><strong>Intel E810 (내부망) 점검</strong></li>
<li>E810 드라이버(<code>ice</code>) 및 펌웨어/DDP(Package) 버전 확인:<pre><code class="language-bash">ethtool -i &lt;e810_iface_name&gt;
# driver: ice, version, firmware-version 확인
</code></pre>
</li>
</ul>
<pre><code>

* Intel E810 계열은 DDP(Dynamic Device Personalization) 펌웨어 버전 불일치 시 패킷 처리 이상이나 링크 플랩(Link Flap)이 발생할 수 있으므로 **기존 클러스터 노드들의 `ethtool -i` 출력값과 펌웨어 버전을 일치시키는 작업**이 권장됩니다.
* 내부망 스위치와 연결 전이라도 포트 LED 및 `ethtool`에서 NO-CARRIER 상태가 정상 유지되는지 확인합니다.



---

### Phase 1 완료 판정 체크리스트 (Go/No-Go Criteria)

| 점검 항목 | 통과 기준 (Pass Criteria) | 조치 사항 (Fail 시) |
| --- | --- | --- |
| **GPU/NIC PCIe 링크** | 전 장치 LnkSta = LnkCap (최대 Gen / x16 유지) | 슬롯 재장착, Riser 카드 교체 요청 |
| **AER / MCE 로그** | 커널 로그 상 Uncorrectable 에러 0건 | CPU/메모리/보드 점검 요청 |
| **CPU/RAM 부하** | `stress-ng` 1시간 통과, EDAC 에러 0건, 스로틀링 없음 | 불량 메모리 모듈 교체 |
| **NVMe 상태** | Critical Warning 0, 읽기/쓰기 스펙 속도 도달 | NVMe 교체 또는 슬롯 점검 |
| **NIC 펌웨어** | CX-6 최신 FW, E810 펌웨어/드라이버 기존 노드와 일치 | 펌웨어 업데이트 수행 |

이 단계가 모두 통과되면 HW 불량 이슈는 배제할 수 있으므로, 안심하고 Phase 2 (NVIDIA Driver / CUDA 설치 및 gpu-burn)로 진입하시면 됩니다.

===

2단계의 핵심 목표는 **GPU 소프트웨어 스택(Driver, CUDA, Fabric Manager, Container Toolkit)을 안정적으로 배포**하고, 실제 고부하 추론/학습 환경을 모사하여 **전력 피크(Power Surge), 발열(Thermal Throttling), GPU 간 P2P 통신(NVLink/PCIe), VRAM 무결성**을 사전에 검증하는 것입니다.

현재 노드는 외부망(ConnectX-6)만 연결되어 있으므로, 외부망을 통해 공식 리포지토리로부터 패키지를 내려받아 구성합니다.

---

### Step 1. GPU 소프트웨어 스택 설치 및 베이스라인 구성

안정성이 검증된 Data Center(Production) 브랜치 드라이버와 CUDA 스택을 구성합니다.

1. **사전 필수 패키지 및 기본 드라이버 충돌 방지**
```bash
# Nouveau 비활성화 확인
lsmod | grep nouveau  # 아무것도 출력되지 않아야 함

# 커널 헤더 및 빌드 도구 설치
apt-get install -y build-essential linux-headers-$(uname -r) dkms
# (RHEL/Rocky 계열인 경우: dnf install -y kernel-devel-$(uname -r) kernel-headers gcc make)
</code></pre><ol start="2">
<li><strong>NVIDIA Datacenter 드라이버 및 Fabric Manager 설치</strong></li>
</ol>
<ul>
<li>HGX/SXM 계열(A100, H100 등 NVSwitch 탑재 모델)의 경우 <strong><code>nvidia-fabricmanager</code>가 필수</strong>입니다. 드라이버 버전과 Fabric Manager 버전은 반드시 <strong>완전히 일치</strong>해야 합니다.</li>
</ul>
<pre><code class="language-bash"># 공식 NVIDIA 리포지토리 등록 후 드라이버 및 Fabric Manager 설치
apt-get install -y cuda-drivers-fabricmanager-550 nvidia-driver-550-server

# Fabric Manager 서비스 활성화 및 시작
systemctl enable --now nvidia-fabricmanager
systemctl status nvidia-fabricmanager
</code></pre>
<ol start="3">
<li><strong>NVIDIA Container Toolkit 설치</strong></li>
</ol>
<ul>
<li>vLLM을 컨테이너 기반으로 격리 배포하기 위해 Docker/Containerd에 런타임을 바인딩합니다.</li>
</ul>
<pre><code class="language-bash">apt-get install -y nvidia-container-toolkit
nvidia-ctk runtime configure --runtime=docker
systemctl restart docker
</code></pre>
<ol start="4">
<li><strong>NVIDIA Persistence Daemon 활성화</strong></li>
</ol>
<ul>
<li>드라이버 초기화 오버헤드 방지 및 절전 모드 진입 방지를 위해 상시 활성화합니다.</li>
</ul>
<pre><code class="language-bash">nvidia-smi -pm 1
systemctl enable --now nvidia-persistenced
</code></pre>
<hr>
<h3 id="step-2-기본-상태-및-p2p--nvlink-토폴로지-검증">Step 2. 기본 상태 및 P2P / NVLink 토폴로지 검증</h3>
<p>드라이버가 로드된 후 GPU 간 통신 대역폭과 토폴로지 연결 상태를 점검합니다.</p>
<ol>
<li><strong>GPU 상태 및 링크 확인</strong><pre><code class="language-bash">nvidia-smi
# 전체 GPU 인식 수량, VRAM 용량, 드라이버/CUDA 버전 확인
</code></pre>
</li>
</ol>
<h1 id="gpu-간-토폴로지-매트릭스-확인">GPU 간 토폴로지 매트릭스 확인</h1>
<p>nvidia-smi topo -m</p>
<pre><code>

* SXM 구조라면 모든 GPU 간 연결이 `NV#` (NVLink)로 표시되어야 합니다.
* PCIe 구조라면 `PIX`(단일 스위치), `PXB`(PCIe 브리지), 또는 `SYS`(호스트/CPU 경유)로 표시됩니다.


2. **NVLink 상태 및 에러 카운터 점검 (SXM 모델 기준)**
```bash
nvidia-smi nvlink -s
# 모든 NVLink 포트가 Active 상태인지 확인

nvidia-smi nvlink -e
# CRC 에러, 리플레이 에러가 0인지 확인
</code></pre><ol start="3">
<li><strong>P2P 대역폭 및 Latency 실측 (<code>p2pBandwidthLatencyTest</code>)</strong></li>
</ol>
<ul>
<li>NVIDIA CUDA Samples 컨테이너를 통해 GPU 상호 간 실제 메모리 복사 대역폭을 측정합니다.</li>
</ul>
<pre><code class="language-bash">docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectorAdd-cuda12.5.0 \
    p2pBandwidthLatencyTest
</code></pre>
<ul>
<li><strong>검증 기준</strong>:</li>
<li>H100 SXM5: 단방향 약 450 GB/s, 양방향 약 900 GB/s 내외</li>
<li>A100 SXM4: 단방향 약 300 GB/s, 양방향 약 600 GB/s 내외</li>
<li>PCIe Gen4/Gen5: PCIe 버스 대역폭(약 25~50 GB/s) 수준 측정 확인</li>
</ul>
<hr>
<h3 id="step-3-vram-무결성-및-dcgm-level-3-진단">Step 3. VRAM 무결성 및 DCGM Level 3 진단</h3>
<p>하드웨어 수준의 VRAM 비트 결함, 온보드 컨트롤러 결함을 조기에 발견하기 위해 NVIDIA DCGM(Data Center GPU Manager) 진단을 수행합니다.</p>
<ol>
<li><strong>DCGM 설치 및 서비스 기동</strong><pre><code class="language-bash">apt-get install -y datacenter-gpu-manager
systemctl enable --now nvidia-dcgm
</code></pre>
</li>
</ol>
<pre><code>

2. **DCGM Diagnostic Level 3 (Hardware Stress &amp; Memory Test) 실행**
* 메모리 대역폭, PCIe 대역폭, VRAM 무결성, E-Switch 진단을 심층 수행합니다 (약 15~30분 소요).


```bash
dcgmi diag -r 3
</code></pre><ul>
<li>모든 테스트 항목(<code>PCIe</code>, <code>Memory</code>, <code>Diagnostic</code>, <code>SM Stress</code>)에서 <strong>PASS</strong>가 떨어지는지 확인합니다.</li>
<li>특히 <code>Page Retirement</code>나 <code>Uncorrectable ECC Errors</code>가 검출되는 카드는 즉시 HW 교체 대상입니다.</li>
</ul>
<hr>
<h3 id="step-4-전체-gpu-풀-로드-burn-in-테스트-12시간-연속">Step 4. 전체 GPU 풀 로드 Burn-In 테스트 (1~2시간 연속)</h3>
<p>vLLM 구동 시 발생할 수 있는 최대 전력 피크(TDP Saturation)와 랙/서버 섀시 내 쿨링 성능을 한계치까지 검증합니다.</p>
<ol>
<li><strong><code>gpu-burn</code> 도커 컨테이너 실행</strong></li>
</ol>
<ul>
<li>8개(또는 장착된 전체) GPU를 동시에 100% 가동합니다.</li>
</ul>
<pre><code class="language-bash">docker run --rm --gpus all \
    -v /tmp:/tmp \
    wilic/gpu-burn:latest \
    3600  # 3600초 (1시간 구동)
</code></pre>
<ol start="2">
<li><strong>동시 모니터링 포인트 (별도 세션)</strong></li>
</ol>
<ul>
<li><strong>GPU 전력, 온도, 스로틀링 추이 모니터링:</strong><pre><code class="language-bash">nvidia-smi dmon -s pucvmet -d 2
</code></pre>
</li>
</ul>
<pre><code>

* `pwr`: 각 GPU가 스펙상 최대 TDP(예: 400W, 700W) 근처까지 안정적으로 도달하는지 확인.
* `temp`: GPU 온도가 쓰로틀링 임계치(통상 80~83℃ 이상)를 넘지 않고 안정적인 플래토(Plateau)를 형성하는지 점검.
* `clocks`: 써멀 또는 파워 스로틀링으로 인해 클록이 급격히 튀거나(Drop) 다운되지 않는지 확인.


* **서버 섀시 전원 및 팬 속도 (IPMI):**
```bash
watch -n 5 &#39;ipmitool sensor | grep -iE &quot;fan|psu|watt|temp&quot;&#39;
</code></pre><ul>
<li>전원 공급 장치(PSU)가 서지 전류를 견디지 못하고 리셋되거나, 팬 속도가 100%로 회전해도 방열이 안 되는 이슈를 체크합니다.</li>
</ul>
<hr>
<h3 id="phase-2-완료-판정-체크리스트-gono-go-criteria">Phase 2 완료 판정 체크리스트 (Go/No-Go Criteria)</h3>
<table>
<thead>
<tr>
<th>점검 항목</th>
<th>통과 기준 (Pass Criteria)</th>
<th>실패 시 원인 및 조치</th>
</tr>
</thead>
<tbody><tr>
<td><strong>드라이버 &amp; Fabric Manager</strong></td>
<td><code>nvidia-smi</code> 인식 정상, Fabric Manager <code>Active (running)</code></td>
<td>버전 불일치 확인 후 재설치</td>
</tr>
<tr>
<td><strong>NVLink / P2P 대역폭</strong></td>
<td>모든 GPU 간 에러 0건, 스펙 대역폭 달성</td>
<td>NVSwitch 보드 결착 점검 / 드라이버 재적재</td>
</tr>
<tr>
<td><strong>DCGM Level 3 진단</strong></td>
<td>전체 테스트 항목 <strong>PASS</strong>, ECC Uncorrectable 0건</td>
<td>불량 GPU RMA 요청</td>
</tr>
<tr>
<td><strong>gpu-burn (1시간)</strong></td>
<td>연산 에러(<code>Faulty: 0</code>) 0건, 서버 리셋/전원 꺼짐 없음</td>
<td>PSU 용량 부족 또는 전원 레일 결함 점검</td>
</tr>
<tr>
<td><strong>써멀 및 쿨링</strong></td>
<td>풀 로드 상태에서 최대 동작 온도 이하 유지 (클록 강하 없음)</td>
<td>섀시 팬 불량, 써멀 구리스/방열판 밀착 불량</td>
</tr>
</tbody></table>
<p>위 테스트를 통과하면 GPU 하드웨어와 기본 런타임의 신뢰성은 확보된 것이므로, 다음 단계인 Phase 3 (vLLM 설치 및 Compute Cluster 호출 연동)으로 진입하시면 됩니다.</p>
<p>===</p>
<p>Phase 3의 핵심 목표는 <strong>vLLM 서빙 환경을 안정적으로 구성</strong>하고, 아직 사설망(E810)이 개통되지 않은 과도기 상황에서 <strong>기존 Compute 클러스터 노드들과의 네트워크 통신 경로를 확보하여 실제 추론 API 호출 파이프라인을 검증</strong>하는 것입니다.</p>
<hr>
<h3 id="step-1-네트워크-통신-경로-확보-사설망-개통-전-우회-전략">Step 1. 네트워크 통신 경로 확보 (사설망 개통 전 우회 전략)</h3>
<p>Compute 클러스터(외부망 + 내부망 bond1 보유)와 GPU 노드(현재 외부망 ConnectX-6만 활성화) 간 통신을 위해 가장 현실적인 방식을 먼저 결정하고 경로를 엽니다.</p>
<ul>
<li><strong>방안 A (외부망 L3 통신 - 가장 권장)</strong></li>
<li>Compute 클러스터 노드들의 외부망 인터페이스 IP $\leftrightarrow$ GPU 노드의 ConnectX-6 외부망 IP 간 통신.</li>
<li><strong>사전 확인</strong>: 데이터센터/망 내부 방화벽(L4/ACL)에서 Compute 클러스터 외부 IP $\rightarrow$ GPU 노드 ConnectX-6 IP의 vLLM 포트(예: <code>8000</code>) 인바운드 허용.</li>
</ul>
<ul>
<li><strong>비대칭 라우팅(Asymmetric Routing) 방지 점검</strong></li>
<li>향후 내부망(E810) 연결 준비 및 다중 NIC 환경에서 패킷 드랍을 방지하기 위해 커널 필터를 조정해 둡니다.<pre><code class="language-bash"># 인입된 인터페이스와 나가는 인터페이스 불일치 시 드랍 방지 (Loose Reverse Path Filter)
sysctl -w net.ipv4.conf.all.rp_filter=2
sysctl -w net.ipv4.conf.default.rp_filter=2
</code></pre>
</li>
</ul>
<pre><code>



* **L3/L4 도달 가능성(Reachability) 사전 검증**
* Compute 클러스터 워커 노드 중 1대에서 GPU 노드로 기본 통신 확인:
```bash
ping -c 3 &lt;GPU_NODE_CX6_IP&gt;
# 포트 오픈 여부 (임시 nc/python 서버 등으로 포트 열고 확인)
nc -zv &lt;GPU_NODE_CX6_IP&gt; 8000
</code></pre><hr>
<h3 id="step-2-로컬-nvme-캐시-구성-및-모델-가중치-사전-배치">Step 2. 로컬 NVMe 캐시 구성 및 모델 가중치 사전 배치</h3>
<p>Phase 4(Storage 클러스터 연동) 전이므로, 검증용 모델을 <strong>GPU 노드의 로컬 고속 NVMe 디스크</strong>에 먼저 배치합니다.</p>
<ol>
<li><strong>로컬 고속 NVMe 마운트 경로 준비</strong><pre><code class="language-bash">mkdir -p /data/models /data/hf_home
export HF_HOME=/data/hf_home
</code></pre>
</li>
</ol>
<pre><code>

2. **테스트용 모델 다운로드 (외부망 ConnectX-6 활용)**
* 파이프라인 검증용 경량 모델 1종과 멀티 GPU/텐서 병렬화(TP) 검증용 표준 LLM 1종을 준비합니다.
* **기능 검증용**: `meta-llama/Llama-3.1-8B-Instruct` (단일 GPU TP=1)
* **토폴로지/TP 검증용**: `meta-llama/Llama-3.1-70B-Instruct` 또는 `Qwen2.5-72B-Instruct` (8-GPU TP=8)




```bash
# huggingface-cli 이용 다운로드 (외부망 Direct)
huggingface-cli download meta-llama/Llama-3.1-8B-Instruct \
    --local-dir /data/models/Llama-3.1-8B-Instruct \
    --local-dir-use-symlinks False
</code></pre><hr>
<h3 id="step-3-vllm-엔진-구동-및-gpu-서빙-튜닝">Step 3. vLLM 엔진 구동 및 GPU 서빙 튜닝</h3>
<p>vLLM을 공식 컨테이너 기반으로 기동하여 호스트 종속성을 최소화하고, 안정적인 서빙 옵션을 적용합니다.</p>
<ol>
<li><strong>vLLM 컨테이너 기동 (단일 GPU 기능 테스트: 8B 모델)</strong><pre><code class="language-bash">docker run -d --name vllm-test \
 --runtime nvidia \
 --gpus &#39;&quot;device=0&quot;&#39; \
 -v /data/models:/models \
 -p 8000:8000 \
 --ipc=host \
 --restart unless-stopped \
 vllm/vllm-openai:latest \
 --model /models/Llama-3.1-8B-Instruct \
 --served-model-name llama-3.1-8b \
 --port 8000 \
 --max-model-len 8192 \
 --gpu-memory-utilization 0.90 \
 --enforce-eager
</code></pre>
</li>
</ol>
<pre><code>

* *주요 옵션 체크*:
* `--ipc=host`: 파이토치 및 vLLM 내부 공유 메모리(Shared Memory) 부족 에러(OOM/Crash) 방지.
* `--enforce-eager`: 초기 테스트 시 CUDA Graph 컴파일 오버헤드를 배제하고 즉각적인 서빙 및 메모리 할당 검증.




2. **vLLM 컨테이너 기동 (멀티 GPU Tensor Parallel 테스트: 70B 모델)**
* 8-GPU 풀 노드 활용 시:


```bash
docker run -d --name vllm-tp-test \
    --runtime nvidia \
    --gpus all \
    -v /data/models:/models \
    -p 8000:8000 \
    --ipc=host \
    vllm/vllm-openai:latest \
    --model /models/Llama-3.1-70B-Instruct \
    --served-model-name llama-3.1-70b \
    --tensor-parallel-size 8 \
    --gpu-memory-utilization 0.90 \
    --max-model-len 8192
</code></pre><ul>
<li>컨테이너 로그(<code>docker logs -f vllm-tp-test</code>)에서 8개 rank의 NCCL 초기화 및 Weight Loading 완료 메시지 확인.</li>
</ul>
<hr>
<h3 id="step-4-로컬-및-compute-cluster-연동-검증">Step 4. 로컬 및 Compute Cluster 연동 검증</h3>
<ol>
<li><strong>GPU 노드 로컬(Localhost) 루프백 테스트</strong><pre><code class="language-bash">curl http://localhost:8000/v1/chat/completions \
-H &quot;Content-Type: application/json&quot; \
-d &#39;{
 &quot;model&quot;: &quot;llama-3.1-8b&quot;,
 &quot;messages&quot;: [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Ping test&quot;}],
 &quot;max_tokens&quot;: 30
}&#39;
</code></pre>
</li>
</ol>
<pre><code>

* 정상 JSON 응답 및 `usage.total_tokens` 반환 여부 확인.


2. **Compute Cluster 워커 노드/Bastion에서의 원격 호출 테스트**
* Compute 클러스터 노드 셸에서 GPU 노드 외부망 IP로 직접 요청을 전송합니다.


```bash
curl -X POST http://&lt;GPU_NODE_CX6_IP&gt;:8000/v1/chat/completions \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{
    &quot;model&quot;: &quot;llama-3.1-8b&quot;,
    &quot;messages&quot;: [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Explain Kubernetes pods in 2 sentences.&quot;}],
    &quot;max_tokens&quot;: 100,
    &quot;temperature&quot;: 0.7
  }&#39;
</code></pre><ul>
<li>응답 시간(Latency), HTTP 200 수신, 스트리밍(<code>&quot;stream&quot;: true</code>) 모드 시 Chunk 수신 정상 여부 점검.</li>
</ul>
<ol start="3">
<li><strong>Compute Cluster K8s Pod 기반 연동 테스트</strong></li>
</ol>
<ul>
<li>추후 애플리케이션 서비스가 K8s 파드 형태로 배포되므로, Compute 클러스터의 Pod 내부에서 GPU 노드로 나가는 Egress 트래픽을 검증합니다.</li>
</ul>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: vllm-client-test
  namespace: default
spec:
  containers:
  - name: curl-client
    image: curlimages/curl:latest
    command: [&quot;sleep&quot;, &quot;3600&quot;]
</code></pre>
<ul>
<li>Pod 진입 후 호출:<pre><code class="language-bash">kubectl exec -it vllm-client-test -- curl http://&lt;GPU_NODE_CX6_IP&gt;:8000/v1/models
</code></pre>
</li>
</ul>
<pre><code>

* *체크 포인트*: K8s Calico/Cilium 등의 CNI Egress NAT(IP 마스커레이딩)를 타고 나갈 때 외부 방화벽에서 차단되지 않는지 확인.



---

### Step 5. 기본 동시성 및 리소스 모니터링 검증

Phase 5(본격 성능 테스트)로 넘어가기 전, Compute 노드에서 단시간 경량 동시 호출을 날려 vLLM의 PagedAttention 및 큐잉 동작을 확인합니다.

* **동시 5~10 req 테스트 (Python / Shell)**
```bash
for i in {1..10}; do
  curl -s http://&lt;GPU_NODE_CX6_IP&gt;:8000/v1/chat/completions \
    -H &quot;Content-Type: application/json&quot; \
    -d &#39;{&quot;model&quot;: &quot;llama-3.1-8b&quot;, &quot;messages&quot;: [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Count from 1 to 50&quot;}], &quot;max_tokens&quot;: 100}&#39; &amp;
done; wait
</code></pre><ul>
<li><strong>동시 모니터링</strong>:</li>
<li>GPU 노드에서 <code>nvidia-smi</code>를 통해 VRAM 사용률 고정 여부(PagedAttention 블록 테이블 확보) 확인.</li>
<li>vLLM <code>/metrics</code> 엔드포인트(<code>http://&lt;GPU_NODE_CX6_IP&gt;:8000/metrics</code>)를 스크랩하여 Prometheus 메트릭(<code>vllm:num_requests_running</code>, <code>vllm:num_requests_waiting</code>) 출력 확인.</li>
</ul>
<hr>
<h3 id="phase-3-완료-판정-체크리스트-gono-go-criteria">Phase 3 완료 판정 체크리스트 (Go/No-Go Criteria)</h3>
<table>
<thead>
<tr>
<th>점검 항목</th>
<th>통과 기준 (Pass Criteria)</th>
<th>조치 사항 (Fail 시)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>외부망 도달성</strong></td>
<td>Compute 노드 $\rightarrow$ GPU 노드 8000 포트 지연 없이 통신 성공</td>
<td>인프라/보안팀 방화벽 정책 확인</td>
</tr>
<tr>
<td><strong>K8s Egress 연동</strong></td>
<td>Compute 클러스터 내부 Pod에서 API 호출 성공</td>
<td>CNI Egress NAT 대역 방화벽 등록</td>
</tr>
<tr>
<td><strong>단일/멀티 GPU 서빙</strong></td>
<td>TP=1 및 TP=8 모델 구동 시 NCCL 오류 없이 구동 완료</td>
<td><code>nvidia-fabricmanager</code> 상태 및 GPU 토폴로지 재점검</td>
</tr>
<tr>
<td><strong>스트리밍 응답</strong></td>
<td>Server-Sent Events(SSE) 스트리밍 토큰 유실 없이 수신</td>
<td>프록시/방화벽의 HTTP 버퍼링/타임아웃 옵션 점검</td>
</tr>
</tbody></table>
<p>이 단계가 완료되면 Compute 클러스터 애플리케이션 관점에서 GPU 노드는 서빙 가능한 상태가 되며, 이어지는 Phase 4 (Storage Cluster $\leftrightarrow$ GPU Node 연동 및 가중치 직접 로딩)로 진입할 수 있습니다.</p>
<p>===</p>
<p>Phase 4의 핵심 목표는 <strong>Storage 클러스터(S3/NFS/Ceph/AIStor 등)와 GPU 노드 간의 데이터 파이프라인을 연결하고, 수십~수백 GB에 달하는 대규모 LLM 모델 가중치를 병목 없이 초고속으로 전송·로딩할 수 있는지 검증</strong>하는 것입니다.</p>
<p>기존 Storage 클러스터는 내부망 <code>bond1</code>에 연결되어 있으므로, GPU 노드 측의 네트워크 경로 설정과 MTU 일치 작업이 선행되어야 합니다.</p>
<hr>
<h3 id="step-1-스토리지-네트워크-경로-및-l2l3-통신-구성">Step 1. 스토리지 네트워크 경로 및 L2/L3 통신 구성</h3>
<p>사설망(E810) 정식 구성 전후 상태에 따라 스토리지와의 통신 경로를 확보합니다.</p>
<ol>
<li><strong>인터페이스 및 라우팅 설정</strong></li>
</ol>
<ul>
<li><strong>케이스 A (E810 사설망 임시/정식 개통 시 - 권장)</strong>:</li>
<li>E810 포트에 스토리지 내부망 대역과 통신 가능한 사설 IP 할당.</li>
<li>스토리지 서브넷 전용 정적 라우팅(Static Route) 추가 (기본 게이트웨이는 외부망 CX-6 유지):<pre><code class="language-bash"># 스토리지 클러스터 대역이 10.100.0.0/16 인 경우
ip route add 10.100.0.0/16 dev &lt;E810_IFACE&gt; proto static
</code></pre>
</li>
</ul>
<pre><code>



* **케이스 B (사설망 미개통 시 - 외부망 우회)**:
* Storage 클러스터의 외부망 인터페이스 또는 L3 라우터를 경유하여 ConnectX-6을 통해 접근하도록 방화벽 및 ACL 오픈.




2. **MTU(Jumbo Frame) 정합성 확인**
* 고속 스토리지 전송 환경에서는 통상 MTU 9000이 적용되어 있습니다. 경로 상의 MTU가 어긋나면 대용량 전송 중 패킷 단편화(Fragmentation) 또는 연결 드랍이 발생합니다.


```bash
# E810 인터페이스 MTU를 기존 노드 및 스토리지와 동일하게 설정 (예: 9000)
ip link set dev &lt;E810_IFACE&gt; mtu 9000

# Don&#39;t Fragment 플래그를 걸고 실제 점보 패킷(8972 bytes = 9000 - 28바이트 헤더) 왕복 검증
ping -M do -s 8972 &lt;STORAGE_ENDPOINT_IP&gt;
</code></pre><hr>
<h3 id="step-2-네트워크-대역폭-및-tcp-전송-계층-벤치마크">Step 2. 네트워크 대역폭 및 TCP 전송 계층 벤치마크</h3>
<p>스토리지 애플리케이션 레벨로 넘어가기 전, GPU 노드 $\leftrightarrow$ Storage 노드 간 순수 네트워크 대역폭을 먼저 확인합니다.</p>
<ol>
<li><strong>iperf3 병렬 스트림 대역폭 실측</strong></li>
</ol>
<ul>
<li>Storage 노드(또는 게이트웨이)를 iperf3 서버(<code>iperf3 -s</code>)로 두고, GPU 노드에서 8~16개 멀티 스트림으로 테스트:</li>
</ul>
<pre><code class="language-bash">iperf3 -c &lt;STORAGE_IP&gt; -P 8 -t 30 -O 3
</code></pre>
<ul>
<li><em>확인 기준</em>: NIC 링크 스펙(예: 25G, 100G)에 근접한 유효 전송 속도(Line Rate의 85~95% 이상) 달성 여부 및 재전송(Retr) 횟수 0에 수렴하는지 확인.</li>
</ul>
<ol start="2">
<li><strong>커널 TCP 버퍼 튜닝 점검 (필요시)</strong></li>
</ol>
<ul>
<li>고속 네트워크에서 단일 TCP 세션의 윈도우 크기 한계로 속도가 저하되지 않도록 조정:</li>
</ul>
<pre><code class="language-bash">sysctl -w net.core.rmem_max=67108864
sysctl -w net.core.wmem_max=67108864
sysctl -w net.ipv4.tcp_rmem=&quot;4096 87380 33554432&quot;
sysctl -w net.ipv4.tcp_wmem=&quot;4096 65536 33554432&quot;
</code></pre>
<hr>
<h3 id="step-3-스토리지-프로토콜별-io-벤치마크-s3-api-vs-posixnfs">Step 3. 스토리지 프로토콜별 I/O 벤치마크 (S3 API vs POSIX/NFS)</h3>
<p>스토리지 구성 방식(오브젝트 스토리지 S3 API 또는 공유 파일시스템 NFS/CephFS)에 맞춰 실측 테스트를 수행합니다.</p>
<h4 id="option-a-s3-오브젝트-스토리지-환경인-경우">Option A. S3 오브젝트 스토리지 환경인 경우</h4>
<p>LLM 모델 가중치는 Safetensors 파일(수 GB ~ 수십 GB 크기의 단일 오브젝트 수십 개) 형태로 구성되므로, <strong>멀티파트 동시 읽기(Multipart Concurrent Read)</strong> 성능이 핵심입니다.</p>
<ol>
<li><strong>S3 벤치마크 툴 (warp 또는 s3-benchmark) 실행</strong></li>
</ol>
<ul>
<li>MinIO Warp 또는 s3-benchmark를 활용하여 대용량 객체 Get Throughput 측정:</li>
</ul>
<pre><code class="language-bash"># 예: 10GB 오브젝트 기준 동시 Get Throughput 측정 (Warp 활용 예시)
warp get --host=&lt;S3_ENDPOINT&gt; --access-key=&lt;KEY&gt; --secret-key=&lt;SECRET&gt; \
    --bucket=model-weights --obj.size=10GiB --concurrent=8 --duration=1m
</code></pre>
<ol start="2">
<li><strong>실제 모델 체크포인트 풀 다운로드 시간 측정</strong></li>
</ol>
<ul>
<li>70B 파라미터 모델(약 140GB 가중치)을 고속 S3 CLI(<code>aws s3 cp</code> with multi-threading, 또는 <code>s3cmd</code>, <code>mc</code>)로 로컬 NVMe에 다운로드:</li>
</ul>
<pre><code class="language-bash"># 멀티스레드 다운로드 설정 후 시간 측정
time aws s3 cp s3://model-weights/Llama-3.1-70B-Instruct/ /data/models/Llama-3.1-70B-Instruct/ \
    --recursive --endpoint-url http://&lt;STORAGE_S3_ENDPOINT&gt;
</code></pre>
<ul>
<li><em>기대 속도</em>: 100Gbps 망 기준 140GB 모델이 약 15~25초 내에 다운로드 완료되는지 확인 (디스크 쓰기 병목 및 네트워크 대역폭 포화 수준 확인).</li>
</ul>
<h4 id="option-b-네트워크-파일시스템nfs--shared-posix-환경인-경우">Option B. 네트워크 파일시스템(NFS / Shared POSIX) 환경인 경우</h4>
<p>스토리지 볼륨을 GPU 노드에 직접 마운트하여 사용할 경우입니다.</p>
<ol>
<li><strong>마운트 옵션 최적화</strong><pre><code class="language-bash"># NFS v4.2, rsize/wsize 1M, TCP 옵션 적용
mount -t nfs -o vers=4.2,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noatime &lt;STORAGE_IP&gt;:/models /mnt/models
</code></pre>
</li>
</ol>
<pre><code>

2. **FIO Direct I/O 순차 읽기 벤치마크**
* 모델 로딩 동작을 모사한 1MB 블록 순차 읽기(Sequential Read):


```bash
fio --name=storage_seq_read \
    --directory=/mnt/models \
    --rw=read \
    --bs=1M \
    --size=50G \
    --numjobs=8 \
    --iodepth=16 \
    --ioengine=libaio \
    --direct=1 \
    --group_reporting
</code></pre><hr>
<h3 id="step-4-vllm-모델-직접-로딩-및-cold-start-검증">Step 4. vLLM 모델 직접 로딩 및 Cold-Start 검증</h3>
<p>실제 스토리지에 적재된 대형 모델 가중치를 기반으로 vLLM 엔진이 기동되는 전체 파이프라인(Cold Start)을 검증합니다.</p>
<ol>
<li><strong>NFS/공유 스토리지 Direct Mount 로딩 테스트</strong></li>
</ol>
<ul>
<li>스토리지를 직접 컨테이너에 마운트하여 vLLM을 기동하고, 엔진 초기화 시간 및 가중치 읽기 속도를 측정합니다.</li>
</ul>
<pre><code class="language-bash">time docker run --rm \
    --runtime nvidia \
    --gpus all \
    -v /mnt/models:/models \
    --ipc=host \
    vllm/vllm-openai:latest \
    --model /models/Llama-3.1-70B-Instruct \
    --tensor-parallel-size 8 \
    --enforce-eager
</code></pre>
<ul>
<li><em>체크 포인트</em>: 모델 로드 중 네트워크 버퍼 지연으로 인한 NCCL Timeout (<code>NCCL_ASYNC_ERROR_HANDLING=1</code>, Timeout 30분 기본값) 발생 여부 점검.</li>
</ul>
<ol start="2">
<li><strong>S3 동기화 방식(Local Cache Sync) 로딩 테스트</strong></li>
</ol>
<ul>
<li>스토리지가 S3인 경우: 컨테이너 기동 전 <code>Init 스크립트</code>가 S3에서 로컬 고속 NVMe(<code>/data/models</code>)로 동기화한 뒤 vLLM을 올리는 패턴 검증.</li>
<li>첫 기동(Cold Start) 시간 vs 이후 로컬 캐시 히트(Warm Start) 기동 시간 비교 분석.</li>
</ul>
<hr>
<h3 id="phase-4-완료-판정-체크리스트-gono-go-criteria">Phase 4 완료 판정 체크리스트 (Go/No-Go Criteria)</h3>
<table>
<thead>
<tr>
<th>점검 항목</th>
<th>통과 기준 (Pass Criteria)</th>
<th>조치 사항 (Fail 시)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>L2/L3 도달성 &amp; MTU</strong></td>
<td><code>ping -M do -s 8972</code> 무손실 통과</td>
<td>스위치 포트 및 인터페이스 MTU 9000 재구성</td>
</tr>
<tr>
<td><strong>원시 전송 대역폭 (iperf3)</strong></td>
<td>E810 링크 스펙의 85% 이상 대역폭 확보</td>
<td>TCP 윈도우 튜닝 및 인터페이스 링 버퍼(<code>ethtool -G</code>) 점검</td>
</tr>
<tr>
<td><strong>I/O 처리량 (FIO / S3)</strong></td>
<td>로컬 NVMe 쓰기 한계치 또는 스토리지 링크 한계치 도달</td>
<td>스토리지 노드 I/O 병목 또는 멀티파트 세션 수 증설</td>
</tr>
<tr>
<td><strong>vLLM Cold Start</strong></td>
<td>70B 모델 기준 1분~2분 이내 Weight Load 완료 후 Serving 준비 완료</td>
<td>로딩 전략 변경 (직접 마운트 $\rightarrow$ 로컬 NVMe 캐싱)</td>
</tr>
</tbody></table>
<p>===</p>
<p>Phase 5는 <strong>vLLM 서빙 엔진의 실무 한계치(Saturation Point)와 서비스 수준 목표(SLO)를 도출</strong>하고, 실제 다중 사용자 워크로드가 Compute 클러스터에서 유입될 때의 <strong>처리량(Throughput), 지연 시간(TTFT/ITL), 동시성(Concurrency), 리소스 병목</strong>을 정량화하는 단계입니다.</p>
<p>에어갭 환경이므로 외부 네트워크 의존 없이 내부 데이터셋과 오프라인 벤치마크 도구를 활용해 4개 하위 단계로 진행합니다.</p>
<hr>
<h3 id="1-테스트-환경-및-핵심-측정-메트릭-정의">1. 테스트 환경 및 핵심 측정 메트릭 정의</h3>
<p>LLM 서빙 성능 평가는 일반 HTTP 처리량과 달리 토큰 생성 특성을 반영한 4대 핵심 지표를 측정합니다.</p>
<ul>
<li><strong>TTFT (Time To First Token)</strong>: 첫 번째 출력 토큰이 반환되기까지 걸리는 시간 (Prompt Prefill 단계의 지연시간, 체감 반응속도 결정).</li>
<li><strong>ITL (Inter-Token Latency) / TPOT (Time Per Output Token)</strong>: 첫 토큰 이후 후속 토큰이 생성되는 간격 (Decoding 단계 속도, 초당 생성 토큰 수와 직결).</li>
<li><strong>Throughput (Tokens/sec)</strong>:</li>
<li>Input/Prompt Throughput: 초당 처리한 입력 토큰 수.</li>
<li>Output/Generation Throughput: 초당 생성한 출력 토큰 수.</li>
</ul>
<ul>
<li><strong>GPU 리소스 포화도</strong>: VRAM 블록 점유율, KV Cache 사용률, SM 연산 포화도, 전력 스로틀링 유무.</li>
</ul>
<hr>
<h3 id="2-워크로드-시나리오-설계-트래픽-프로파일">2. 워크로드 시나리오 설계 (트래픽 프로파일)</h3>
<p>실제 엔터프라이즈 업무 환경을 반영하여 3가지 대표 시나리오를 구성합니다.</p>
<table>
<thead>
<tr>
<th>시나리오</th>
<th>입력 토큰 (Input)</th>
<th>출력 토큰 (Output)</th>
<th>대표 업무 유형</th>
<th>성능 특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>A. 대화형 챗봇 (Interactive)</strong></td>
<td>~512</td>
<td>~256</td>
<td>질의응답, 에이전트 대화</td>
<td>ITL(TPOT) 중심 검증, 동시 세션 처리</td>
</tr>
<tr>
<td><strong>B. RAG / 문서 요약 (Heavy Prefill)</strong></td>
<td>~4,096</td>
<td>~512</td>
<td>문서 검색 후 요약, 로그 분석</td>
<td>Prefill 병목, TTFT 및 KV Cache 압박</td>
</tr>
<tr>
<td><strong>C. 코드 생성 / 장문 생성 (Heavy Decode)</strong></td>
<td>~1,024</td>
<td>~2,048</td>
<td>코드 생성, 리포트 작성</td>
<td>Decode 병목, Throughput 및 VRAM 대역폭 포화</td>
</tr>
</tbody></table>
<hr>
<h3 id="3-단계별-벤치마크-실행-절차">3. 단계별 벤치마크 실행 절차</h3>
<h4 id="step-5-1-엔진-자체-서빙-벤치마크-vllm-internal-benchmark">Step 5-1. 엔진 자체 서빙 벤치마크 (vLLM Internal Benchmark)</h4>
<p>Compute 클러스터 트래픽 유입 전, GPU 노드 로컬에서 vLLM 내장 도구(<code>benchmark_serving.py</code>)를 이용해 엔진 본연의 베이스라인 성능을 측정합니다.</p>
<ul>
<li><strong>오프라인 데이터셋 준비 (ShareGPT 기반)</strong>:
에어갭 내부 Bastion에 사전 반입된 <code>ShareGPT_V3_unfiltered_cleaned_split.json</code> 활용.</li>
<li><strong>실행 명령 (동시 동적 요청 주입: RPS 스위프)</strong>:<pre><code class="language-bash"># vLLM 컨테이너 내부 또는 Python 가상환경에서 실행
python3 -m vllm.entrypoints.openai.benchmark_serving \
  --backend vllm \
  --model /models/Llama-3.1-70B-Instruct \
  --dataset-name sharegpt \
  --dataset-path /data/benchmarks/ShareGPT_V3_unfiltered_cleaned_split.json \
  --num-prompts 1000 \
  --request-rate 10 \
  --max-concurrency 64 \
  --save-result \
  --result-filename /tmp/vllm_result_tp8_concur64.json
</code></pre>
</li>
</ul>
<pre><code>


#### Step 5-2. Compute Cluster 발(發) 분산 부하 테스트 (Concurrency Test)

실제 운영 환경을 모사하여 Compute 클러스터의 여러 워커 노드/Pod에서 GPU 노드로 동시 요청을 부하시킵니다.

* **부하 도구**: `locust` (분산 모드) 또는 `k6` 컨테이너를 Compute 클러스터의 Pod 4~8개로 분산 배포.
* **부하 단계별 Concurrency 계단식 증가 (Ramp-up)**:
* **단계 1 (Cold-up)**: Concurrency = 1, 4, 8 (기저 Latency 및 단일 요청 기준 성능 측정)
* **단계 2 (Normal Load)**: Concurrency = 16, 32, 64 (정상 운영 목표 부하 구간)
* **단계 3 (Stress &amp; Saturation)**: Concurrency = 128, 256 (큐잉 발생 시점 및 KV Cache 포화 한계치 파악)


* **K6 분산 부하 스크립트 예시 (`vllm_load.js`)**:
```javascript
import http from &#39;k6/http&#39;;
import { check } from &#39;k6&#39;;

export const options = {
  stages: [
    { duration: &#39;2m&#39;, target: 16 },
    { duration: &#39;3m&#39;, target: 64 },
    { duration: &#39;3m&#39;, target: 128 },
    { duration: &#39;2m&#39;, target: 0 },
  ],
};

export default function () {
  const payload = JSON.stringify({
    model: &#39;llama-3.1-70b&#39;,
    messages: [{ role: &#39;user&#39;, content: &#39;Explain distributed data lakehouse architecture in detail.&#39; }],
    max_tokens: 512,
    stream: false
  });

  const params = { headers: { &#39;Content-Type&#39;: &#39;application/json&#39; }, timeout: &#39;60s&#39; };
  const res = http.post(&#39;http://&lt;GPU_NODE_IP&gt;:8000/v1/chat/completions&#39;, payload, params);
  check(res, { &#39;status is 200&#39;: (r) =&gt; r.status === 200 });
}
</code></pre><h4 id="step-5-3-스트리밍sse-및-ttft-정밀-검증-python-비동기-클라이언트">Step 5-3. 스트리밍(SSE) 및 TTFT 정밀 검증 (Python 비동기 클라이언트)</h4>
<p>사용자 UI 연동 시 체감 성능인 TTFT를 정확히 추출하기 위해 스트리밍 청크 단위 타임스탬프를 기록합니다.</p>
<pre><code class="language-python"># benchmark_ttft.py (Compute Cluster의 Pod/Bastion에서 실행)
import asyncio, time, httpx

API_URL = &quot;http://&lt;GPU_NODE_IP&gt;:8000/v1/chat/completions&quot;
CONCURRENCY = 32

async def send_streaming_request(client, prompt_id):
    payload = {
        &quot;model&quot;: &quot;llama-3.1-70b&quot;,
        &quot;messages&quot;: [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Tell me a comprehensive history of computing.&quot;}],
        &quot;max_tokens&quot;: 256,
        &quot;stream&quot;: True
    }
    t0 = time.perf_counter()
    first_token_time = None
    total_tokens = 0

    async with client.stream(&quot;POST&quot;, API_URL, json=payload, timeout=60.0) as resp:
        async for chunk in resp.aiter_lines():
            if chunk.startswith(&quot;data: &quot;) and not chunk.endswith(&quot;[DONE]&quot;):
                if first_token_time is None:
                    first_token_time = time.perf_counter()
                total_tokens += 1

    end_time = time.perf_counter()
    ttft = (first_token_time - t0) * 1000 if first_token_time else 0
    tpot = ((end_time - first_token_time) / total_tokens * 1000) if total_tokens &gt; 0 else 0
    return {&quot;ttft&quot;: ttft, &quot;tpot&quot;: tpot, &quot;tokens&quot;: total_tokens}

async def main():
    limits = httpx.Limits(max_connections=100, max_keepalive_connections=50)
    async with httpx.AsyncClient(limits=limits) as client:
        tasks = [send_streaming_request(client, i) for i in range(CONCURRENCY)]
        results = await asyncio.gather(*tasks)

    avg_ttft = sum(r[&quot;ttft&quot;] for r in results) / len(results)
    avg_tpot = sum(r[&quot;tpot&quot;] for r in results) / len(results)
    print(f&quot;[Concurrency {CONCURRENCY}] Avg TTFT: {avg_ttft:.2f} ms | Avg TPOT (ITL): {avg_tpot:.2f} ms&quot;)

asyncio.run(main())
</code></pre>
<hr>
<h3 id="4-동시-인프라-관제-및-모니터링-수집-항목">4. 동시 인프라 관제 및 모니터링 수집 항목</h3>
<p>부하 인가 중 GPU 노드에서 아래 지표들을 1~2초 주기로 수집하여 병목 원인을 규명합니다.</p>
<ul>
<li><strong>vLLM 내부 큐 및 캐시 메트릭 (<code>http://&lt;GPU_NODE_IP&gt;:8000/metrics</code>)</strong></li>
<li><code>vllm:num_requests_running</code>: 동시 처리 중인 요청 수 (배치 크기).</li>
<li><code>vllm:num_requests_waiting</code>: 대기 큐(Queue)에 쌓인 요청 수 (0보다 크면 서빙 용량 포화).</li>
<li><code>vllm:gpu_cache_usage_factor</code>: GPU KV Cache 메모리 점유율 (1.0 도달 시 요청 선점/Preemption 또는 큐잉 발생).</li>
</ul>
<ul>
<li><strong>NVIDIA DCGM / GPU 메트릭</strong></li>
<li><code>DCGM_FI_DEV_GPU_UTIL</code>: GPU SM 사용률 (Compute 병목 여부).</li>
<li><code>DCGM_FI_DEV_MEM_COPY_UTIL</code>: HBM/VRAM 메모리 대역폭 활용률 (LLM Decoding 시 주된 병목 지점).</li>
<li><code>DCGM_FI_DEV_POWER_USAGE</code>: TDP 대비 실제 소비 전력 (스로틀링 검증).</li>
</ul>
<hr>
<h3 id="5-phase-5-완료-판정-기준-및-slo-도출-acceptance-criteria">5. Phase 5 완료 판정 기준 및 SLO 도출 (Acceptance Criteria)</h3>
<table>
<thead>
<tr>
<th>평가 항목</th>
<th>목표 성능 지표 (70B 모델, 8-GPU TP=8 기준 예시)</th>
<th>이상 징후 발생 시 점검 사항</th>
</tr>
</thead>
<tbody><tr>
<td><strong>TTFT (Latency)</strong></td>
<td>P95 기준 <strong>&lt; 1.5초</strong> (입력 1k 토큰 기준)</td>
<td><code>--gpu-memory-utilization</code> 상향, Chunked Prefill 활성화 여부 점검</td>
</tr>
<tr>
<td><strong>ITL / TPOT</strong></td>
<td>평균 <strong>&lt; 30ms/token</strong> (초당 30 token 이상 생성 체감)</td>
<td>KV Cache 스왑 발생 여부, Tensor Parallel 통신 지연(NVLink) 점검</td>
</tr>
<tr>
<td><strong>동시성 처리</strong></td>
<td>동시 32개 세션 인가 시 에러율(5xx/Timeout) <strong>0%</strong></td>
<td>vLLM <code>--max-num-seqs</code> 조정 및 Compute 노드 HTTP Timeout 설정 검토</td>
</tr>
<tr>
<td><strong>안정성 (VRAM)</strong></td>
<td>Concurrency 포화 상태에서도 OOM 크래시 없이 대기 큐 정상 소화</td>
<td><code>--max-model-len</code> 및 PagedAttention 블록 크기 최적화</td>
</tr>
</tbody></table>
<p>위 기준을 바탕으로 얻은 최종 Throughput-Latency 커브 데이터를 토대로 Compute 클러스터의 API 게이트웨이 및 HPA(또는 Rate Limiting) 임계값을 설정하시면 전체 노드 도입 및 서비스 연동 절차가 완료됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S03d]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S03d</link>
            <guid>https://velog.io/@youngkyoo_kim/26S03d</guid>
            <pubDate>Thu, 03 Sep 2026 05:08:20 GMT</pubDate>
            <description><![CDATA[<p>```sh
#!/usr/bin/env bash
set -euo pipefail</p>
<p>PATTERN=&quot;${1:-}&quot;</p>
<p>if [ -z &quot;$PATTERN&quot; ]; then
  echo &quot;사용법: $0 <pod-name-pattern>&quot;
  echo &quot;예시:   $0 starrocks&quot;
  exit 1
fi</p>
<p>echo &quot;검색 패턴: &#39;$PATTERN&#39;&quot;
echo &quot;클러스터 파드 정보 수집 및 분석 중...&quot;</p>
<h1 id="1-파드별-cpu-reqlim-mcore-단위-추출-및-lim-그룹-키-생성">1. 파드별 CPU Req/Lim (mcore 단위) 추출 및 Lim 그룹 키 생성</h1>
<p>kubectl get pods -A <br>  --field-selector=status.phase!=Succeeded,status.phase!=Failed <br>  -o json | jq -r --arg pat &quot;$PATTERN&quot; &#39;
  def parse_cpu:
    if . == null then 0
    elif endswith(&quot;m&quot;) then (rtrimstr(&quot;m&quot;) | tonumber)
    elif endswith(&quot;n&quot;) then (rtrimstr(&quot;n&quot;) | tonumber / 1000000)
    else (tonumber * 1000)
    end;</p>
<p>  .items[]
  | select(.metadata.name | test($pat))
  | {
      name: .metadata.name,
      req: ([.spec.containers[].resources.requests.cpu? // null | parse_cpu] | add // 0),
      lim: ([.spec.containers[].resources.limits.cpu? // null | parse_cpu] | add // 0)
    }
  | if .lim == 0 and .req == 0 then &quot;NO_SPEC\tNO_SPEC&quot;
    elif .lim == 0 then &quot;NO_LIMIT\tNO_LIMIT&quot;
    else
      # Limit 코어 크기 라벨 (예: 250m, 1, 2, 4, 8 등 Core 단위 환산)
      (
        if (.lim % 1000 == 0) then &quot;((.lim / 1000 | tostring)) Core&quot;
        else &quot;((.lim / 1000 | tostring)) Core&quot;
        end
      ) + &quot;\t&quot; + ((.req / .lim) * 100 | tostring)
    end
&#39; | awk -F&#39;\t&#39; &#39;
BEGIN {</p>
<h1 id="구간-인덱스-초기화">구간 인덱스 초기화</h1>
<p>  bin_names[0] = &quot;0<del>10%&quot;
  bin_names[1] = &quot;10</del>20%&quot;
  bin_names[2] = &quot;20<del>30%&quot;
  bin_names[3] = &quot;30</del>40%&quot;
  bin_names[4] = &quot;40<del>50%&quot;
  bin_names[5] = &quot;50</del>60%&quot;
  bin_names[6] = &quot;60<del>70%&quot;
  bin_names[7] = &quot;70</del>80%&quot;
  bin_names[8] = &quot;80<del>90%&quot;
  bin_names[9] = &quot;90</del>100%&quot;
  bin_names[10] = &quot;&gt;100%&quot;
}</p>
<p>{
  lim_group = $1
  val = $2</p>
<p>  if (!seen_group[lim_group]++) {
    group_list[++group_cnt] = lim_group
  }
  total_by_group[lim_group]++
  grand_total++</p>
<p>  if (val == &quot;NO_LIMIT&quot; || val == &quot;NO_SPEC&quot;) {
    # 예외 그룹
    special_cnt[lim_group]++
  } else {
    ratio = val + 0
    if (ratio &gt;= 0 &amp;&amp; ratio &lt; 10)        b = 0
    else if (ratio &gt;= 10 &amp;&amp; ratio &lt; 20)  b = 1
    else if (ratio &gt;= 20 &amp;&amp; ratio &lt; 30)  b = 2
    else if (ratio &gt;= 30 &amp;&amp; ratio &lt; 40)  b = 3
    else if (ratio &gt;= 40 &amp;&amp; ratio &lt; 50)  b = 4
    else if (ratio &gt;= 50 &amp;&amp; ratio &lt; 60)  b = 5
    else if (ratio &gt;= 60 &amp;&amp; ratio &lt; 70)  b = 6
    else if (ratio &gt;= 70 &amp;&amp; ratio &lt; 80)  b = 7
    else if (ratio &gt;= 80 &amp;&amp; ratio &lt; 90)  b = 8
    else if (ratio &gt;= 90 &amp;&amp; ratio &lt;= 100) b = 9
    else b = 10</p>
<pre><code>matrix[lim_group, b]++</code></pre><p>  }
}</p>
<p>END {
  if (grand_total == 0) {
    print &quot;조건에 매칭되는 파드가 없습니다.&quot;
    exit 0
  }</p>
<p>  printf &quot;\n=== CPU LIMIT 크기별 REQ/LIM 비율 분포 ===\n\n&quot;</p>
<h1 id="헤더-출력">헤더 출력</h1>
<p>  printf &quot;%-12s | %-5s |&quot;, &quot;CPU LIMIT&quot;, &quot;TOTAL&quot;
  for (i = 0; i &lt; 10; i++) {
    printf &quot; %-7s&quot;, bin_names[i]
  }
  printf &quot; | %-6s\n&quot;, &quot;&gt;100%&quot;</p>
<p>  print &quot;-------------+-------+---------------------------------------------------------------------------------+--------&quot;</p>
<p>  for (g = 1; g &lt;= group_cnt; g++) {
    grp = group_list[g]
    if (grp == &quot;NO_LIMIT&quot; || grp == &quot;NO_SPEC&quot;) continue</p>
<pre><code>printf &quot;%-12s | %-5d |&quot;, grp, total_by_group[grp]
for (i = 0; i &lt; 10; i++) {
  c = matrix[grp, i] + 0
  if (c &gt; 0) printf &quot; %-7d&quot;, c
  else printf &quot; %-7s&quot;, &quot;-&quot;
}
over = matrix[grp, 10] + 0
if (over &gt; 0) printf &quot; | %-6d\n&quot;, over
else printf &quot; | %-6s\n&quot;, &quot;-&quot;</code></pre><p>  }</p>
<p>  print &quot;-------------+-------+---------------------------------------------------------------------------------+--------&quot;</p>
<h1 id="예외-케이스-표기">예외 케이스 표기</h1>
<p>  if (special_cnt[&quot;NO_LIMIT&quot;] &gt; 0) {
    printf &quot;%-12s | %-5d | (Limit 미설정 파드)\n&quot;, &quot;No Limit&quot;, special_cnt[&quot;NO_LIMIT&quot;]
  }
  if (special_cnt[&quot;NO_SPEC&quot;] &gt; 0) {
    printf &quot;%-12s | %-5d | (Req/Limit 모두 미설정)\n&quot;, &quot;No Spec&quot;, special_cnt[&quot;NO_SPEC&quot;]
  }</p>
<p>  printf &quot;\n총 분석 대상 파드: %d개\n&quot;, grand_total
}
&#39;</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S03c]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S03c</link>
            <guid>https://velog.io/@youngkyoo_kim/26S03c</guid>
            <pubDate>Thu, 03 Sep 2026 05:03:13 GMT</pubDate>
            <description><![CDATA[<p>노드별 파드 개수와 할당량(Allocatable) 대비 사용률을 확인하는 Bash 원라이너 및 상세 스크립트입니다. <code>Succeeded</code>나 <code>Failed</code> 상태인 완료된 파드는 제외하고 실제 러닝 중인 워크로드만 집계합니다.</p>
<hr>
<p><strong>1. 터미널 즉시 실행용 원라이너 (jq 기반)</strong></p>
<pre><code class="language-bash">kubectl get pods -A --field-selector=status.phase!=Succeeded,status.phase!=Failed -o jsonpath=&#39;{range .items[*]}{.spec.nodeName}{&quot;\n&quot;}{end}&#39; | grep -v &#39;^$&#39; | sort | uniq -c | sort -rn
</code></pre>
<p>출력 결과:</p>
<pre><code class="language-text">  42 node-worker-01
  38 node-worker-02
  15 node-worker-03
</code></pre>
<hr>
<p><strong>2. 노드별 파드 수 + Allocatable 용량 + 사용률 요약 스크립트</strong></p>
<p>노드마다 사양(Max Pods)이 다를 수 있으므로, 단순 파드 개수뿐 아니라 노드 용량 대비 점유율(%)을 함께 계산하여 내림차순으로 정렬합니다.</p>
<pre><code class="language-bash">#!/usr/bin/env bash
set -euo pipefail

echo -e &quot;NODE\t\t\t\tCURRENT\tALLOCATABLE\tUSAGE(%)&quot;
echo -e &quot;------------------------------------------------------------------&quot;

# 1. 실행 중인 파드의 노드 할당 집계
POD_COUNTS=$(kubectl get pods -A \
  --field-selector=status.phase!=Succeeded,status.phase!=Failed \
  -o jsonpath=&#39;{range .items[*]}{.spec.nodeName}{&quot;\n&quot;}{end}&#39; \
  | grep -v &#39;^$&#39; | sort | uniq -c)

# 2. 노드별 allocatable pods 용량 조회 및 결합
kubectl get nodes -o jsonpath=&#39;{range .items[*]}{.metadata.name}{&quot; &quot;}{.status.allocatable.pods}{&quot;\n&quot;}{end}&#39; | while read -r node allocatable; do
  current=$(echo &quot;$POD_COUNTS&quot; | awk -v n=&quot;$node&quot; &#39;$2 == n {print $1}&#39;)
  current=${current:-0}

  if [ &quot;$allocatable&quot; -gt 0 ]; then
    usage=$(awk &quot;BEGIN {printf \&quot;%.1f\&quot;, ($current / $allocatable) * 100}&quot;)
  else
    usage=&quot;0.0&quot;
  fi

  printf &quot;%-30s\t%-7d\t%-11d\t%s%%\n&quot; &quot;$node&quot; &quot;$current&quot; &quot;$allocatable&quot; &quot;$usage&quot;
done | sort -k4 -nr
</code></pre>
<hr>
<p><strong>3. 프로메테우스(PromQL)로 지속 모니터링할 경우</strong></p>
<p>Grafana 대시보드나 Alertmanager로 불균형을 감시할 때 유용한 쿼리입니다.</p>
<ul>
<li><strong>노드별 파드 개수 현황:</strong><pre><code class="language-promql">count by (node) (kube_pod_info{node!=&quot;&quot;})
</code></pre>
</li>
</ul>
<pre><code>

* **노드별 파드 할당률 (%):**
```promql
(count by (node) (kube_pod_info{node!=&quot;&quot;}) / kube_node_status_allocatable{resource=&quot;pods&quot;}) * 100
</code></pre><ul>
<li><strong>클러스터 내 노드 간 파드 수 표준편차 (불균형 지표):</strong><pre><code class="language-promql">stddev(count by (node) (kube_pod_info{node!=&quot;&quot;}))
</code></pre>
</li>
</ul>
<pre><code>
===

파드 이름을 인자로 받아 CPU Request/Limit을 millicore(`m`) 단위로 정규화한 뒤, **`Request / Limit * 100`** 비율을 계산하여 10% 단위 구간별(0~10%, ..., 90~100%, 100% 초과 및 Limit 미설정 예외)로 집계하는 Bash + `jq` 스크립트입니다.

멀티 컨테이너 파드의 경우 모든 컨테이너의 Request 합과 Limit 합을 기준으로 계산합니다.

---

### 스크립트 (`calc_cpu_ratio.sh`)

```bash
#!/usr/bin/env bash
set -euo pipefail

PATTERN=&quot;${1:-}&quot;

if [ -z &quot;$PATTERN&quot; ]; then
  echo &quot;사용법: $0 &lt;pod-name-pattern&gt;&quot;
  echo &quot;예시:   $0 kafka&quot;
  exit 1
fi

echo &quot;검색 패턴: &#39;$PATTERN&#39;&quot;
echo &quot;클러스터 파드 정보 수집 중...&quot;

# 1. 파드 정보 추출 (이름 필터링, 완료된 파드 제외, cpu request/limit 파싱)
kubectl get pods -A \
  --field-selector=status.phase!=Succeeded,status.phase!=Failed \
  -o json | jq -r --arg pat &quot;$PATTERN&quot; &#39;
  def parse_cpu:
    if . == null then 0
    elif endswith(&quot;m&quot;) then (rtrimstr(&quot;m&quot;) | tonumber)
    elif endswith(&quot;n&quot;) then (rtrimstr(&quot;n&quot;) | tonumber / 1000000)
    else (tonumber * 1000)
    end;

  .items[]
  | select(.metadata.name | test($pat))
  | {
      name: .metadata.name,
      namespace: .metadata.namespace,
      req: ([.spec.containers[].resources.requests.cpu? // null | parse_cpu] | add // 0),
      lim: ([.spec.containers[].resources.limits.cpu? // null | parse_cpu] | add // 0)
    }
  | if .lim == 0 and .req == 0 then &quot;NO_SPEC&quot;
    elif .lim == 0 then &quot;NO_LIMIT&quot;
    elif .req == 0 then &quot;0&quot;
    else ((.req / .lim) * 100 | tostring)
    end
&#39; | awk &#39;
BEGIN {
  # 10개 기본 구간 초기화
  for (i = 0; i &lt; 10; i++) {
    bin[i] = 0
  }
  over_100 = 0
  no_limit = 0
  no_spec = 0
  total = 0
}

{
  val = $1
  total++

  if (val == &quot;NO_LIMIT&quot;) {
    no_limit++
  } else if (val == &quot;NO_SPEC&quot;) {
    no_spec++
  } else {
    ratio = val + 0
    if (ratio &gt;= 0 &amp;&amp; ratio &lt; 10)       bin[0]++
    else if (ratio &gt;= 10 &amp;&amp; ratio &lt; 20) bin[1]++
    else if (ratio &gt;= 20 &amp;&amp; ratio &lt; 30) bin[2]++
    else if (ratio &gt;= 30 &amp;&amp; ratio &lt; 40) bin[3]++
    else if (ratio &gt;= 40 &amp;&amp; ratio &lt; 50) bin[4]++
    else if (ratio &gt;= 50 &amp;&amp; ratio &lt; 60) bin[5]++
    else if (ratio &gt;= 60 &amp;&amp; ratio &lt; 70) bin[6]++
    else if (ratio &gt;= 70 &amp;&amp; ratio &lt; 80) bin[7]++
    else if (ratio &gt;= 80 &amp;&amp; ratio &lt; 90) bin[8]++
    else if (ratio &gt;= 90 &amp;&amp; ratio &lt;= 100) bin[9]++
    else bin_over++
  }
}

END {
  printf &quot;\n%-20s %-10s %-10s\n&quot;, &quot;CPU REQ/LIM RANGE&quot;, &quot;COUNT&quot;, &quot;RATIO(%)&quot;
  print &quot;---------------------------------------------&quot;

  labels[0] = &quot;  0% ~  10%&quot;
  labels[1] = &quot; 10% ~  20%&quot;
  labels[2] = &quot; 20% ~  30%&quot;
  labels[3] = &quot; 30% ~  40%&quot;
  labels[4] = &quot; 40% ~  50%&quot;
  labels[5] = &quot; 50% ~  60%&quot;
  labels[6] = &quot; 60% ~  70%&quot;
  labels[7] = &quot; 70% ~  80%&quot;
  labels[8] = &quot; 80% ~  90%&quot;
  labels[9] = &quot; 90% ~ 100%&quot;

  for (i = 0; i &lt; 10; i++) {
    pct = (total &gt; 0) ? (bin[i] / total) * 100 : 0
    printf &quot;%-20s %-10d %6.1f%%\n&quot;, labels[i], bin[i], pct
  }

  print &quot;---------------------------------------------&quot;
  if (bin_over &gt; 0) {
    pct = (total &gt; 0) ? (bin_over / total) * 100 : 0
    printf &quot;%-20s %-10d %6.1f%%\n&quot;, &quot;&gt; 100% (Overcommit)&quot;, bin_over, pct
  }
  if (no_limit &gt; 0) {
    pct = (total &gt; 0) ? (no_limit / total) * 100 : 0
    printf &quot;%-20s %-10d %6.1f%%\n&quot;, &quot;No Limit (Req Only)&quot;, no_limit, pct
  }
  if (no_spec &gt; 0) {
    pct = (total &gt; 0) ? (no_spec / total) * 100 : 0
    printf &quot;%-20s %-10d %6.1f%%\n&quot;, &quot;No Spec (Req=0,Lim=0)&quot;, no_spec, pct
  }

  printf &quot;%-20s %-10d %6.1f%%\n&quot;, &quot;TOTAL PODS&quot;, total, 100.0
}
&#39;
</code></pre><hr>
<h3 id="실행-방법-및-출력-예시">실행 방법 및 출력 예시</h3>
<pre><code class="language-bash">chmod +x calc_cpu_ratio.sh
./calc_cpu_ratio.sh starrocks
</code></pre>
<p><strong>출력 결과:</strong></p>
<pre><code class="language-text">검색 패턴: &#39;starrocks&#39;
클러스터 파드 정보 수집 중...

CPU REQ/LIM RANGE    COUNT      RATIO(%)  
---------------------------------------------
  0% ~  10%          2            4.0%
 10% ~  20%          0            0.0%
 20% ~  30%          1            2.0%
 30% ~  40%          5           10.0%
 40% ~  50%          8           16.0%
 50% ~  60%         14           28.0%
 60% ~  70%          6           12.0%
 70% ~  80%          4            8.0%
 80% ~  90%          2            4.0%
 90% ~ 100%          8           16.0%
---------------------------------------------
No Limit (Req Only)  4            8.0%
TOTAL PODS           50         100.0%
</code></pre>
<ul>
<li>정규표현식(<code>test($pat)</code>)이 적용되어 있어 <code>kafka.*broker</code>나 <code>^minio</code> 같은 패턴 검색도 가능합니다.</li>
<li>Limit이 설정되지 않은 파드(<code>No Limit</code>)나 스펙 자체가 누락된 파드(<code>No Spec</code>)는 0으로 나누어지는 오류를 방지하기 위해 하단 예외 행으로 분리 표기됩니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S03b]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S03b</link>
            <guid>https://velog.io/@youngkyoo_kim/26S03b</guid>
            <pubDate>Wed, 02 Sep 2026 23:38:49 GMT</pubDate>
            <description><![CDATA[<p>Kubernetes 리소스 단위(코어 <code>m</code>, 메모리 <code>Gi/Mi/Ki</code>)를 정규화한 뒤, <code>(Request / Limit) * 100</code>을 계산하여 반올림한 비율별로 카운트하는 스크립트입니다.</p>
<p>Bash와 Python(추천, 부동소수점 및 단위 변환 처리가 안정적) 중 환경에 맞는 방식을 선택해 실행할 수 있습니다.</p>
<hr>
<h3 id="python-인라인-스크립트-가장-정확하고-편리한-방식">Python 인라인 스크립트 (가장 정확하고 편리한 방식)</h3>
<p><code>kubectl</code> 출력을 받아 밀리코어(<code>m</code>) 단위로 자동 정규화한 뒤, 반올림된 비율(<code>50%</code>, <code>40%</code>, <code>30%</code>, 기타)을 집계합니다.</p>
<pre><code class="language-bash">kubectl get pods -A -o json | python3 -c &#39;
import sys, json

KEYWORD = &quot;YOUR_POD_NAME_KEYWORD&quot;  # 필터링할 Pod 이름 키워드

def parse_cpu(val):
    if not val:
        return 0.0
    val = str(val).strip()
    if val.endswith(&quot;m&quot;):
        return float(val[:-1])
    return float(val) * 1000.0

counts = {
    &quot;50% (round=50)&quot;: 0,
    &quot;40% (round=40)&quot;: 0,
    &quot;30% (round=30)&quot;: 0,
    &quot;Others&quot;: 0
}

data = json.load(sys.stdin)
for pod in data.get(&quot;items&quot;, []):
    pod_name = pod.get(&quot;metadata&quot;, {}).get(&quot;name&quot;, &quot;&quot;)
    if KEYWORD not in pod_name:
        continue

    for c in pod.get(&quot;spec&quot;, {}).get(&quot;containers&quot;, []):
        res = c.get(&quot;resources&quot;, {})
        req_cpu = parse_cpu(res.get(&quot;requests&quot;, {}).get(&quot;cpu&quot;))
        lim_cpu = parse_cpu(res.get(&quot;limits&quot;, {}).get(&quot;cpu&quot;))

        if lim_cpu &gt; 0:
            ratio = (req_cpu / lim_cpu) * 100
            rounded = round(ratio)

            if rounded == 50:
                counts[&quot;50% (round=50)&quot;] += 1
            elif rounded == 40:
                counts[&quot;40% (round=40)&quot;] += 1
            elif rounded == 30:
                counts[&quot;30% (round=30)&quot;] += 1
            else:
                counts[&quot;Others&quot;] += 1

print(json.dumps(counts, indent=2))
&#39;
</code></pre>
<hr>
<h3 id="jq-기반-스크립트-외부-런타임-없이-bashjq만-사용할-때">jq 기반 스크립트 (외부 런타임 없이 bash/jq만 사용할 때)</h3>
<p>순수 <code>jq</code>로만 처리하려면 아래 명령어를 사용합니다.</p>
<pre><code class="language-bash">kubectl get pods -A -o json | jq -r --arg kw &quot;YOUR_POD_NAME_KEYWORD&quot; &#39;
  def parse_cpu:
    if . == null or . == &quot;&quot; then 0
    elif endswith(&quot;m&quot;) then (.[0:-1] | tonumber)
    else ((. | tonumber) * 1000)
    end;

  [
    .items[]
    | select(.metadata.name | contains($kw))
    | .spec.containers[]
    | {
        req: (.resources.requests.cpu | parse_cpu),
        lim: (.resources.limits.cpu | parse_cpu)
      }
    | select(.lim &gt; 0)
    | ((.req / .lim) * 100 | round)
  ]
  | {
      &quot;50% (round=50)&quot;: (map(select(. == 50)) | length),
      &quot;40% (round=40)&quot;: (map(select(. == 40)) | length),
      &quot;30% (round=30)&quot;: (map(select(. == 30)) | length),
      &quot;Others&quot;: (map(select(. != 50 and . != 40 and . != 30)) | length)
    }
&#39;
</code></pre>
<hr>
<p><strong>동작 세부 내용</strong></p>
<ul>
<li><strong>반올림 기준</strong>: <code>round((req / lim) * 100)</code>을 적용하므로 예를 들어 40%는 <code>35.0% ~ 44.9%</code> 범위, 30%는 <code>25.0% ~ 34.9%</code> 범위가 매칭됩니다. (3/8 = 37.5%는 38%이므로 Others로 빠지며, 3/8을 40%로 보고 싶다면 반올림 자리수 조정이 필요합니다.)</li>
<li><strong>메모리 집계로 변경 시</strong>: <code>parse_cpu</code> 함수 대신 <code>Mi</code>/<code>Gi</code> 단위를 바이트 단위로 환산하는 로직으로 변경하여 동일하게 적용할 수 있습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S03a]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S03a</link>
            <guid>https://velog.io/@youngkyoo_kim/26S03a</guid>
            <pubDate>Wed, 02 Sep 2026 23:30:18 GMT</pubDate>
            <description><![CDATA[<p><code>kubectl</code>과 <code>jq</code>를 사용해 특정 Pod 이름 문자열을 필터링하고 리소스(CPU 또는 메모리) 조건에 맞는 개수를 집계하는 명령어입니다.</p>
<p><strong>CPU 기준 집계 명령어 (단일 컨테이너 기준)</strong></p>
<p>특정 네임스페이스(<code>-n &lt;namespace&gt;</code>) 또는 전체 클러스터(<code>-A</code>)에서 Pod 이름에 특정 문자열(<code>YOUR_POD_NAME_KEYWORD</code>)이 포함된 대상을 집계합니다.</p>
<pre><code class="language-bash">kubectl get pods -A -o json | jq -r &#39;
  [
    .items[] 
    | select(.metadata.name | contains(&quot;YOUR_POD_NAME_KEYWORD&quot;))
    | .spec.containers[] 
    | {
        req: (.resources.requests.cpu // &quot;0&quot;),
        lim: (.resources.limits.cpu // &quot;0&quot;)
      }
  ] 
  | {
      &quot;Limit 8 / Request 4&quot;: map(select((.lim == &quot;8&quot; or .lim == &quot;8000m&quot;) and (.req == &quot;4&quot; or .req == &quot;4000m&quot;))) | length,
      &quot;Limit 8 / Request 3&quot;: map(select((.lim == &quot;8&quot; or .lim == &quot;8000m&quot;) and (.req == &quot;3&quot; or .req == &quot;3000m&quot;))) | length
    }
&#39;
</code></pre>
<p><strong>메모리(Gi 단위) 기준 집계 명령어</strong></p>
<p>메모리 단위(Gi) 기준인 경우 필터 조건에 단위(<code>Gi</code>)를 매칭합니다.</p>
<pre><code class="language-bash">kubectl get pods -A -o json | jq -r &#39;
  [
    .items[] 
    | select(.metadata.name | contains(&quot;YOUR_POD_NAME_KEYWORD&quot;))
    | .spec.containers[] 
    | {
        req: (.resources.requests.memory // &quot;0&quot;),
        lim: (.resources.limits.memory // &quot;0&quot;)
      }
  ] 
  | {
      &quot;Limit 8Gi / Request 4Gi&quot;: map(select(.lim == &quot;8Gi&quot; and .req == &quot;4Gi&quot;)) | length,
      &quot;Limit 8Gi / Request 3Gi&quot;: map(select(.lim == &quot;8Gi&quot; and .req == &quot;3Gi&quot;)) | length
    }
&#39;
</code></pre>
<p><strong>참고 사항</strong></p>
<ul>
<li><strong>밀리코어 표기 대응</strong>: CPU의 경우 매니페스트에 따라 <code>8</code> 대신 <code>8000m</code>, <code>4</code> 대신 <code>4000m</code> 등으로 입력되어 있을 수 있어 조건문에 두 형식을 모두 포함했습니다.</li>
<li><strong>다중 컨테이너 Pod</strong>: Pod 하나에 컨테이너가 여러 개 있거나 컨테이너 리소스의 총합(Pod 레벨)을 구해야 한다면 <code>containers</code> 배열의 합산 로직이 추가되어야 합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S02m]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S02m</link>
            <guid>https://velog.io/@youngkyoo_kim/26S02m</guid>
            <pubDate>Wed, 02 Sep 2026 06:12:27 GMT</pubDate>
            <description><![CDATA[<p>대규모 클러스터(128대 이상) 환경에서 <code>kubectl describe</code>를 쓰면 속도가 매우 느리므로, <code>kubectl</code>의 JSON 출력을 파싱하거나 Kubernetes API를 직접 호출해 <strong>노드 Allocatable 대비 Pod CPU Requests 합산치, 할당률(%), 잔여 Core</strong>를 산출하는 Python 스크립트와 경량 Bash 스크립트입니다.</p>
<hr>
<p><strong>Python 기반 노드별 잔여 CPU Core 산출 스크립트 (<code>calc_node_cpu.py</code>)</strong></p>
<p>별도의 무거운 패키지 없이 로컬 <code>kubectl</code> 권한(<code>~/.kube/config</code>)을 활용하여 실행 가능하며, 단위(<code>m</code>, 정수) 변환 및 종료된 Pod(Succeeded/Failed) 자동 제외 로직이 포함되어 있습니다.</p>
<pre><code class="language-python">#!/usr/bin/env python3
import json
import subprocess
import sys

def parse_cpu_to_millicores(val: str) -&gt; int:
    &quot;&quot;&quot;CPU 문자열(&#39;64&#39;, &#39;500m&#39; 등)을 millicores 정수로 변환&quot;&quot;&quot;
    if not val:
        return 0
    val = str(val).strip()
    if val.endswith(&#39;m&#39;):
        return int(val[:-1])
    return int(float(val) * 1000)

def main():
    # 1. Node 정보 수집 (Allocatable CPU)
    print(&quot;[*] Fetching node specifications...&quot;, file=sys.stderr)
    try:
        nodes_raw = subprocess.check_output(
            [&quot;kubectl&quot;, &quot;get&quot;, &quot;nodes&quot;, &quot;-o&quot;, &quot;json&quot;], stderr=subprocess.PIPE
        )
        nodes_data = json.loads(nodes_raw)
    except subprocess.CalledProcessError as e:
        print(f&quot;Error fetching nodes: {e.stderr.decode()}&quot;, file=sys.stderr)
        sys.exit(1)

    node_stats = {}
    for item in nodes_data.get(&quot;items&quot;, []):
        node_name = item[&quot;metadata&quot;][&quot;name&quot;]
        allocatable_cpu = item[&quot;status&quot;][&quot;allocatable&quot;].get(&quot;cpu&quot;, &quot;0&quot;)
        alloc_m = parse_cpu_to_millicores(allocatable_cpu)
        node_stats[node_name] = {
            &quot;allocatable_m&quot;: alloc_m,
            &quot;requested_m&quot;: 0,
        }

    # 2. 모든 Namespace의 Pod 정보 수집
    print(&quot;[*] Fetching active pods and CPU requests...&quot;, file=sys.stderr)
    try:
        pods_raw = subprocess.check_output(
            [&quot;kubectl&quot;, &quot;get&quot;, &quot;pods&quot;, &quot;-A&quot;, &quot;-o&quot;, &quot;json&quot;], stderr=subprocess.PIPE
        )
        pods_data = json.loads(pods_raw)
    except subprocess.CalledProcessError as e:
        print(f&quot;Error fetching pods: {e.stderr.decode()}&quot;, file=sys.stderr)
        sys.exit(1)

    for item in pods_data.get(&quot;items&quot;, []):
        status = item.get(&quot;status&quot;, {})
        phase = status.get(&quot;phase&quot;, &quot;&quot;)
        # 종료되었거나 실패한 Pod 제외
        if phase in [&quot;Succeeded&quot;, &quot;Failed&quot;]:
            continue

        spec = item.get(&quot;spec&quot;, {})
        node_name = spec.get(&quot;nodeName&quot;)
        if not node_name or node_name not in node_stats:
            continue

        # App 컨테이너 requests 합산
        pod_req_m = 0
        for container in spec.get(&quot;containers&quot;, []):
            resources = container.get(&quot;resources&quot;, {})
            requests = resources.get(&quot;requests&quot;, {})
            cpu_req = requests.get(&quot;cpu&quot;, &quot;0&quot;)
            pod_req_m += parse_cpu_to_millicores(cpu_req)

        # Init 컨테이너 고려 (Kubernetes 표준: max(initContainers, sum(appContainers)))
        init_req_m = 0
        for init_c in spec.get(&quot;initContainers&quot;, []):
            req = init_c.get(&quot;resources&quot;, {}).get(&quot;requests&quot;, {}).get(&quot;cpu&quot;, &quot;0&quot;)
            init_req_m = max(init_req_m, parse_cpu_to_millicores(req))

        effective_req_m = max(pod_req_m, init_req_m)
        node_stats[node_name][&quot;requested_m&quot;] += effective_req_m

    # 3. 산출 결과 계산 및 출력 (남은 Core 적은 순 정렬)
    results = []
    for node, data in node_stats.items():
        alloc_m = data[&quot;allocatable_m&quot;]
        req_m = data[&quot;requested_m&quot;]
        remain_m = alloc_m - req_m
        usage_ratio = (req_m / alloc_m * 100.0) if alloc_m &gt; 0 else 0.0

        results.append({
            &quot;node&quot;: node,
            &quot;alloc_cores&quot;: alloc_m / 1000.0,
            &quot;req_cores&quot;: req_m / 1000.0,
            &quot;ratio_pct&quot;: usage_ratio,
            &quot;remain_cores&quot;: remain_m / 1000.0
        })

    # 잔여 CPU가 가장 부족한 노드부터 오름차순 정렬
    results.sort(key=lambda x: x[&quot;remain_cores&quot;])

    print(f&quot;\n{&#39;NODE NAME&#39;:&lt;40} {&#39;ALLOC(Core)&#39;:&gt;12} {&#39;REQUEST(Core)&#39;:&gt;14} {&#39;USAGE(%)&#39;:&gt;10} {&#39;REMAIN(Core)&#39;:&gt;14}&quot;)
    print(&quot;-&quot; * 94)
    for r in results:
        print(f&quot;{r[&#39;node&#39;]:&lt;40} {r[&#39;alloc_cores&#39;]:&gt;12.2f} {r[&#39;req_cores&#39;]:&gt;14.2f} {r[&#39;ratio_pct&#39;]:&gt;9.1f}% {r[&#39;remain_cores&#39;]:&gt;14.2f}&quot;)

if __name__ == &quot;__main__&quot;:
    main()
</code></pre>
<hr>
<p><strong>Bash + <code>jq</code> 경량 1-Liner 스크립트</strong></p>
<p>파이썬 없이 터미널에서 즉시 노드별 남은 Core와 할당률을 확인해야 할 때 유용합니다.</p>
<pre><code class="language-bash">kubectl get nodes -o json | jq -r &#39;
  .items[] | 
  .metadata.name as $node | 
  (.status.allocatable.cpu | if endswith(&quot;m&quot;) then (rtrimstr(&quot;m&quot;) | tonumber) else (tonumber * 1000) end) as $alloc |
  &quot;\($node) \($alloc)&quot;
&#39; | while read node alloc_m; do
  req_m=$(kubectl get pods -A --field-selector spec.nodeName=$node -o json | jq &#39;
    [ .items[] | select(.status.phase != &quot;Succeeded&quot; and .status.phase != &quot;Failed&quot;) | 
      .spec.containers[].resources.requests.cpu // &quot;0&quot; | 
      if endswith(&quot;m&quot;) then (rtrimstr(&quot;m&quot;) | tonumber) else (tonumber * 1000) end 
    ] | add // 0
  &#39;)

  remain_cores=$(awk &quot;BEGIN {printf \&quot;%.2f\&quot;, ($alloc_m - $req_m) / 1000}&quot;)
  ratio=$(awk &quot;BEGIN {printf \&quot;%.1f\&quot;, ($req_m / $alloc_m) * 100}&quot;)
  alloc_cores=$(awk &quot;BEGIN {printf \&quot;%.2f\&quot;, $alloc_m / 1000}&quot;)
  req_cores=$(awk &quot;BEGIN {printf \&quot;%.2f\&quot;, $req_m / 1000}&quot;)

  printf &quot;%-40s | Alloc: %6s Core | Req: %6s Core (%5s%%) | Remain: %6s Core\n&quot; &quot;$node&quot; &quot;$alloc_cores&quot; &quot;$req_cores&quot; &quot;$ratio&quot; &quot;$remain_cores&quot;
done
</code></pre>
<hr>
<p><strong>운영 팁</strong></p>
<ul>
<li><strong><code>Allocatable</code> 기준 적용</strong>: <code>capacity</code>가 아닌 <code>allocatable</code>을 기준으로 삼아 kubelet, OS 예약 영역(<code>kube-reserved</code>, <code>system-reserved</code>)을 제외한 실제 워크로드 스케줄링 가능 여유 Core만 계산합니다.</li>
<li><strong>Init Container 스펙 계산</strong>: Python 스크립트는 쿠버네티스 기본 스케줄러 알고리즘대로 <code>max(sum(appContainers), max(initContainers))</code> 규칙을 반영하여 오차를 없앴습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S02l]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S02l</link>
            <guid>https://velog.io/@youngkyoo_kim/26S02l</guid>
            <pubDate>Wed, 02 Sep 2026 03:57:56 GMT</pubDate>
            <description><![CDATA[<p>```json
{
  &quot;annotations&quot;: {
    &quot;list&quot;: []
  },
  &quot;editable&quot;: true,
  &quot;fiscalYearStartMonth&quot;: 0,
  &quot;graphTooltip&quot;: 1,
  &quot;id&quot;: null,
  &quot;links&quot;: [],
  &quot;liveNow&quot;: false,
  &quot;panels&quot;: [
    {
      &quot;collapsed&quot;: false,
      &quot;gridPos&quot;: {
        &quot;h&quot;: 1,
        &quot;w&quot;: 24,
        &quot;x&quot;: 0,
        &quot;y&quot;: 0
      },
      &quot;id&quot;: 1,
      &quot;title&quot;: &quot;Cluster Resource Allocation Overview (Lightweight)&quot;,
      &quot;type&quot;: &quot;row&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;description&quot;: &quot;클러스터 전체 Allocatable 대비 Pod CPU Requests 할당률 (1시간 다운샘플링)&quot;,
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;color&quot;: {
            &quot;mode&quot;: &quot;palette-classic&quot;
          },
          &quot;custom&quot;: {
            &quot;axisBorderShow&quot;: false,
            &quot;axisCenteredZero&quot;: false,
            &quot;axisColorMode&quot;: &quot;text&quot;,
            &quot;axisLabel&quot;: &quot;Allocation Ratio (%)&quot;,
            &quot;axisPlacement&quot;: &quot;auto&quot;,
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;fillOpacity&quot;: 15,
            &quot;lineInterpolation&quot;: &quot;linear&quot;,
            &quot;lineWidth&quot;: 2,
            &quot;pointSize&quot;: 5,
            &quot;scaleDistribution&quot;: {
              &quot;type&quot;: &quot;linear&quot;
            },
            &quot;showPoints&quot;: &quot;never&quot;,
            &quot;spanNulls&quot;: false,
            &quot;thresholdsStyle&quot;: {
              &quot;mode&quot;: &quot;line&quot;
            }
          },
          &quot;max&quot;: 100,
          &quot;min&quot;: 0,
          &quot;thresholds&quot;: {
            &quot;mode&quot;: &quot;absolute&quot;,
            &quot;steps&quot;: [
              {
                &quot;color&quot;: &quot;green&quot;,
                &quot;value&quot;: null
              },
              {
                &quot;color&quot;: &quot;semi-dark-orange&quot;,
                &quot;value&quot;: 75
              },
              {
                &quot;color&quot;: &quot;red&quot;,
                &quot;value&quot;: 85
              }
            ]
          },
          &quot;unit&quot;: &quot;percent&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 12,
        &quot;x&quot;: 0,
        &quot;y&quot;: 1
      },
      &quot;id&quot;: 2,
      &quot;interval&quot;: &quot;1h&quot;,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [&quot;mean&quot;, &quot;lastNotNull&quot;, &quot;max&quot;],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;,
          &quot;showLegend&quot;: true
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;single&quot;,
          &quot;sort&quot;: &quot;none&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;editorMode&quot;: &quot;code&quot;,
          &quot;expr&quot;: &quot;100 * sum(kube_pod_container_resource_requests{cluster=<del>&quot;$cluster&quot;, resource=&quot;cpu&quot;}) / sum(kube_node_status_allocatable{cluster=</del>&quot;$cluster&quot;, resource=&quot;cpu&quot;})&quot;,
          &quot;legendFormat&quot;: &quot;Cluster CPU Request Rate&quot;,
          &quot;range&quot;: true,
          &quot;refId&quot;: &quot;A&quot;
        }
      ],
      &quot;title&quot;: &quot;Cluster CPU Request Allocation Rate (30d)&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;description&quot;: &quot;클러스터 전체 Allocatable 대비 Pod Memory Requests 할당률 (1시간 다운샘플링)&quot;,
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;color&quot;: {
            &quot;mode&quot;: &quot;palette-classic&quot;
          },
          &quot;custom&quot;: {
            &quot;axisBorderShow&quot;: false,
            &quot;axisCenteredZero&quot;: false,
            &quot;axisColorMode&quot;: &quot;text&quot;,
            &quot;axisLabel&quot;: &quot;Allocation Ratio (%)&quot;,
            &quot;axisPlacement&quot;: &quot;auto&quot;,
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;fillOpacity&quot;: 15,
            &quot;lineInterpolation&quot;: &quot;linear&quot;,
            &quot;lineWidth&quot;: 2,
            &quot;pointSize&quot;: 5,
            &quot;scaleDistribution&quot;: {
              &quot;type&quot;: &quot;linear&quot;
            },
            &quot;showPoints&quot;: &quot;never&quot;,
            &quot;spanNulls&quot;: false,
            &quot;thresholdsStyle&quot;: {
              &quot;mode&quot;: &quot;line&quot;
            }
          },
          &quot;max&quot;: 100,
          &quot;min&quot;: 0,
          &quot;thresholds&quot;: {
            &quot;mode&quot;: &quot;absolute&quot;,
            &quot;steps&quot;: [
              {
                &quot;color&quot;: &quot;green&quot;,
                &quot;value&quot;: null
              },
              {
                &quot;color&quot;: &quot;semi-dark-orange&quot;,
                &quot;value&quot;: 75
              },
              {
                &quot;color&quot;: &quot;red&quot;,
                &quot;value&quot;: 85
              }
            ]
          },
          &quot;unit&quot;: &quot;percent&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 12,
        &quot;x&quot;: 12,
        &quot;y&quot;: 1
      },
      &quot;id&quot;: 3,
      &quot;interval&quot;: &quot;1h&quot;,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [&quot;mean&quot;, &quot;lastNotNull&quot;, &quot;max&quot;],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;,
          &quot;showLegend&quot;: true
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;single&quot;,
          &quot;sort&quot;: &quot;none&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;editorMode&quot;: &quot;code&quot;,
          &quot;expr&quot;: &quot;100 * sum(kube_pod_container_resource_requests{cluster=<del>&quot;$cluster&quot;, resource=&quot;memory&quot;}) / sum(kube_node_status_allocatable{cluster=</del>&quot;$cluster&quot;, resource=&quot;memory&quot;})&quot;,
          &quot;legendFormat&quot;: &quot;Cluster Memory Request Rate&quot;,
          &quot;range&quot;: true,
          &quot;refId&quot;: &quot;A&quot;
        }
      ],
      &quot;title&quot;: &quot;Cluster Memory Request Allocation Rate (30d)&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;collapsed&quot;: false,
      &quot;gridPos&quot;: {
        &quot;h&quot;: 1,
        &quot;w&quot;: 24,
        &quot;x&quot;: 0,
        &quot;y&quot;: 9
      },
      &quot;id&quot;: 4,
      &quot;title&quot;: &quot;Breakdown by Namespace (Top 10 Requests)&quot;,
      &quot;type&quot;: &quot;row&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;description&quot;: &quot;네임스페이스별 CPU Requests 점유량 (상위 10개, 1h 단위)&quot;,
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;color&quot;: {
            &quot;mode&quot;: &quot;palette-classic&quot;
          },
          &quot;custom&quot;: {
            &quot;axisBorderShow&quot;: false,
            &quot;axisCenteredZero&quot;: false,
            &quot;axisColorMode&quot;: &quot;text&quot;,
            &quot;axisLabel&quot;: &quot;CPU Cores&quot;,
            &quot;axisPlacement&quot;: &quot;auto&quot;,
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;fillOpacity&quot;: 10,
            &quot;lineInterpolation&quot;: &quot;linear&quot;,
            &quot;lineWidth&quot;: 1.5,
            &quot;pointSize&quot;: 5,
            &quot;showPoints&quot;: &quot;never&quot;,
            &quot;spanNulls&quot;: false
          },
          &quot;unit&quot;: &quot;short&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 12,
        &quot;x&quot;: 0,
        &quot;y&quot;: 10
      },
      &quot;id&quot;: 5,
      &quot;interval&quot;: &quot;1h&quot;,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [&quot;mean&quot;, &quot;lastNotNull&quot;],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;,
          &quot;showLegend&quot;: true
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;multi&quot;,
          &quot;sort&quot;: &quot;desc&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;editorMode&quot;: &quot;code&quot;,
          &quot;expr&quot;: &quot;topk(10, sum by (namespace) (kube_pod_container_resource_requests{cluster=<del>&quot;$cluster&quot;, resource=&quot;cpu&quot;}))&quot;,
          &quot;legendFormat&quot;: &quot;{{namespace}}&quot;,
          &quot;range&quot;: true,
          &quot;refId&quot;: &quot;A&quot;
        }
      ],
      &quot;title&quot;: &quot;Top 10 Namespaces by CPU Requests (30d)&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;description&quot;: &quot;네임스페이스별 Memory Requests 점유량 (상위 10개, 1h 단위)&quot;,
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;color&quot;: {
            &quot;mode&quot;: &quot;palette-classic&quot;
          },
          &quot;custom&quot;: {
            &quot;axisBorderShow&quot;: false,
            &quot;axisCenteredZero&quot;: false,
            &quot;axisColorMode&quot;: &quot;text&quot;,
            &quot;axisLabel&quot;: &quot;Memory Bytes&quot;,
            &quot;axisPlacement&quot;: &quot;auto&quot;,
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;fillOpacity&quot;: 10,
            &quot;lineInterpolation&quot;: &quot;linear&quot;,
            &quot;lineWidth&quot;: 1.5,
            &quot;pointSize&quot;: 5,
            &quot;showPoints&quot;: &quot;never&quot;,
            &quot;spanNulls&quot;: false
          },
          &quot;unit&quot;: &quot;bytes&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 12,
        &quot;x&quot;: 12,
        &quot;y&quot;: 10
      },
      &quot;id&quot;: 6,
      &quot;interval&quot;: &quot;1h&quot;,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [&quot;mean&quot;, &quot;lastNotNull&quot;],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;,
          &quot;showLegend&quot;: true
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;multi&quot;,
          &quot;sort&quot;: &quot;desc&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;editorMode&quot;: &quot;code&quot;,
          &quot;expr&quot;: &quot;topk(10, sum by (namespace) (kube_pod_container_resource_requests{cluster=</del>&quot;$cluster&quot;, resource=&quot;memory&quot;}))&quot;,
          &quot;legendFormat&quot;: &quot;{{namespace}}&quot;,
          &quot;range&quot;: true,
          &quot;refId&quot;: &quot;A&quot;
        }
      ],
      &quot;title&quot;: &quot;Top 10 Namespaces by Memory Requests (30d)&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    }
  ],
  &quot;refresh&quot;: &quot;&quot;,
  &quot;schemaVersion&quot;: 38,
  &quot;style&quot;: &quot;dark&quot;,
  &quot;tags&quot;: [&quot;kubernetes&quot;, &quot;capacity-planning&quot;, &quot;30d-optimized&quot;],
  &quot;templating&quot;: {
    &quot;list&quot;: [
      {
        &quot;current&quot;: {},
        &quot;hide&quot;: 0,
        &quot;includeAll&quot;: false,
        &quot;label&quot;: &quot;Data Source&quot;,
        &quot;multi&quot;: false,
        &quot;name&quot;: &quot;DS_PROMETHEUS&quot;,
        &quot;options&quot;: [],
        &quot;query&quot;: &quot;prometheus&quot;,
        &quot;refresh&quot;: 1,
        &quot;type&quot;: &quot;datasource&quot;
      },
      {
        &quot;allValue&quot;: &quot;.*&quot;,
        &quot;current&quot;: {},
        &quot;datasource&quot;: {
          &quot;type&quot;: &quot;prometheus&quot;,
          &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
        },
        &quot;definition&quot;: &quot;label_values(kube_node_status_allocatable, cluster)&quot;,
        &quot;hide&quot;: 0,
        &quot;includeAll&quot;: false,
        &quot;label&quot;: &quot;Cluster&quot;,
        &quot;multi&quot;: false,
        &quot;name&quot;: &quot;cluster&quot;,
        &quot;options&quot;: [],
        &quot;query&quot;: {
          &quot;query&quot;: &quot;label_values(kube_node_status_allocatable, cluster)&quot;,
          &quot;refId&quot;: &quot;Prometheus-Cluster-Variable&quot;
        },
        &quot;refresh&quot;: 2,
        &quot;sort&quot;: 1,
        &quot;type&quot;: &quot;query&quot;
      }
    ]
  },
  &quot;time&quot;: {
    &quot;from&quot;: &quot;now-30d&quot;,
    &quot;to&quot;: &quot;now&quot;
  },
  &quot;timepicker&quot;: {
    &quot;refresh_intervals&quot;: [
      &quot;15m&quot;,
      &quot;30m&quot;,
      &quot;1h&quot;,
      &quot;2h&quot;
    ]
  },
  &quot;timezone&quot;: &quot;browser&quot;,
  &quot;title&quot;: &quot;Kubernetes Cluster Resource Allocation (30d Optimized)&quot;,
  &quot;uid&quot;: &quot;k8s-cluster-alloc-30d&quot;
}</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S02k]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S02k</link>
            <guid>https://velog.io/@youngkyoo_kim/26S02k</guid>
            <pubDate>Wed, 02 Sep 2026 03:53:37 GMT</pubDate>
            <description><![CDATA[<p>```json
{
  &quot;annotations&quot;: {
    &quot;list&quot;: []
  },
  &quot;editable&quot;: true,
  &quot;fiscalYearStartMonth&quot;: 0,
  &quot;graphTooltip&quot;: 1,
  &quot;id&quot;: null,
  &quot;links&quot;: [],
  &quot;liveNow&quot;: false,
  &quot;panels&quot;: [
    {
      &quot;collapsed&quot;: false,
      &quot;gridPos&quot;: {
        &quot;h&quot;: 1,
        &quot;w&quot;: 24,
        &quot;x&quot;: 0,
        &quot;y&quot;: 0
      },
      &quot;id&quot;: 1,
      &quot;title&quot;: &quot;Cluster Resource Allocation Overview&quot;,
      &quot;type&quot;: &quot;row&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;description&quot;: &quot;클러스터 전체 Allocatable 대비 Pod CPU Requests 할당률&quot;,
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;color&quot;: {
            &quot;mode&quot;: &quot;palette-classic&quot;
          },
          &quot;custom&quot;: {
            &quot;axisBorderShow&quot;: false,
            &quot;axisCenteredZero&quot;: false,
            &quot;axisColorMode&quot;: &quot;text&quot;,
            &quot;axisLabel&quot;: &quot;Allocation Ratio (%)&quot;,
            &quot;axisPlacement&quot;: &quot;auto&quot;,
            &quot;barAlignment&quot;: 0,
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;fillOpacity&quot;: 15,
            &quot;gradientMode&quot;: &quot;none&quot;,
            &quot;hideFrom&quot;: {
              &quot;legend&quot;: false,
              &quot;tooltip&quot;: false,
              &quot;viz&quot;: false
            },
            &quot;lineInterpolation&quot;: &quot;smooth&quot;,
            &quot;lineWidth&quot;: 2,
            &quot;pointSize&quot;: 5,
            &quot;scaleDistribution&quot;: {
              &quot;type&quot;: &quot;linear&quot;
            },
            &quot;showPoints&quot;: &quot;never&quot;,
            &quot;spanNulls&quot;: false,
            &quot;stacking&quot;: {
              &quot;group&quot;: &quot;A&quot;,
              &quot;mode&quot;: &quot;none&quot;
            },
            &quot;thresholdsStyle&quot;: {
              &quot;mode&quot;: &quot;line&quot;
            }
          },
          &quot;mappings&quot;: [],
          &quot;max&quot;: 100,
          &quot;min&quot;: 0,
          &quot;thresholds&quot;: {
            &quot;mode&quot;: &quot;absolute&quot;,
            &quot;steps&quot;: [
              {
                &quot;color&quot;: &quot;green&quot;,
                &quot;value&quot;: null
              },
              {
                &quot;color&quot;: &quot;semi-dark-orange&quot;,
                &quot;value&quot;: 75
              },
              {
                &quot;color&quot;: &quot;red&quot;,
                &quot;value&quot;: 85
              }
            ]
          },
          &quot;unit&quot;: &quot;percent&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 12,
        &quot;x&quot;: 0,
        &quot;y&quot;: 1
      },
      &quot;id&quot;: 2,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [&quot;mean&quot;, &quot;lastNotNull&quot;, &quot;max&quot;],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;,
          &quot;showLegend&quot;: true
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;single&quot;,
          &quot;sort&quot;: &quot;none&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;editorMode&quot;: &quot;code&quot;,
          &quot;expr&quot;: &quot;100 * sum(kube_pod_container_resource_requests{cluster=<del>&quot;$cluster&quot;, resource=&quot;cpu&quot;} and on(pod, namespace, cluster) kube_pod_status_phase{cluster=</del>&quot;$cluster&quot;, phase=<del>&quot;Running|Pending&quot;} == 1) / sum(kube_node_status_allocatable{cluster=</del>&quot;$cluster&quot;, resource=&quot;cpu&quot;})&quot;,
          &quot;legendFormat&quot;: &quot;Cluster CPU Request Commitment Rate&quot;,
          &quot;range&quot;: true,
          &quot;refId&quot;: &quot;A&quot;
        }
      ],
      &quot;title&quot;: &quot;Cluster CPU Request Allocation Rate&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;description&quot;: &quot;클러스터 전체 Allocatable 대비 Pod Memory Requests 할당률&quot;,
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;color&quot;: {
            &quot;mode&quot;: &quot;palette-classic&quot;
          },
          &quot;custom&quot;: {
            &quot;axisBorderShow&quot;: false,
            &quot;axisCenteredZero&quot;: false,
            &quot;axisColorMode&quot;: &quot;text&quot;,
            &quot;axisLabel&quot;: &quot;Allocation Ratio (%)&quot;,
            &quot;axisPlacement&quot;: &quot;auto&quot;,
            &quot;barAlignment&quot;: 0,
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;fillOpacity&quot;: 15,
            &quot;gradientMode&quot;: &quot;none&quot;,
            &quot;hideFrom&quot;: {
              &quot;legend&quot;: false,
              &quot;tooltip&quot;: false,
              &quot;viz&quot;: false
            },
            &quot;lineInterpolation&quot;: &quot;smooth&quot;,
            &quot;lineWidth&quot;: 2,
            &quot;pointSize&quot;: 5,
            &quot;scaleDistribution&quot;: {
              &quot;type&quot;: &quot;linear&quot;
            },
            &quot;showPoints&quot;: &quot;never&quot;,
            &quot;spanNulls&quot;: false,
            &quot;stacking&quot;: {
              &quot;group&quot;: &quot;A&quot;,
              &quot;mode&quot;: &quot;none&quot;
            },
            &quot;thresholdsStyle&quot;: {
              &quot;mode&quot;: &quot;line&quot;
            }
          },
          &quot;mappings&quot;: [],
          &quot;max&quot;: 100,
          &quot;min&quot;: 0,
          &quot;thresholds&quot;: {
            &quot;mode&quot;: &quot;absolute&quot;,
            &quot;steps&quot;: [
              {
                &quot;color&quot;: &quot;green&quot;,
                &quot;value&quot;: null
              },
              {
                &quot;color&quot;: &quot;semi-dark-orange&quot;,
                &quot;value&quot;: 75
              },
              {
                &quot;color&quot;: &quot;red&quot;,
                &quot;value&quot;: 85
              }
            ]
          },
          &quot;unit&quot;: &quot;percent&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 12,
        &quot;x&quot;: 12,
        &quot;y&quot;: 1
      },
      &quot;id&quot;: 3,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [&quot;mean&quot;, &quot;lastNotNull&quot;, &quot;max&quot;],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;,
          &quot;showLegend&quot;: true
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;single&quot;,
          &quot;sort&quot;: &quot;none&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;editorMode&quot;: &quot;code&quot;,
          &quot;expr&quot;: &quot;100 * sum(kube_pod_container_resource_requests{cluster=<del>&quot;$cluster&quot;, resource=&quot;memory&quot;} and on(pod, namespace, cluster) kube_pod_status_phase{cluster=</del>&quot;$cluster&quot;, phase=<del>&quot;Running|Pending&quot;} == 1) / sum(kube_node_status_allocatable{cluster=</del>&quot;$cluster&quot;, resource=&quot;memory&quot;})&quot;,
          &quot;legendFormat&quot;: &quot;Cluster Memory Request Commitment Rate&quot;,
          &quot;range&quot;: true,
          &quot;refId&quot;: &quot;A&quot;
        }
      ],
      &quot;title&quot;: &quot;Cluster Memory Request Allocation Rate&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;collapsed&quot;: false,
      &quot;gridPos&quot;: {
        &quot;h&quot;: 1,
        &quot;w&quot;: 24,
        &quot;x&quot;: 0,
        &quot;y&quot;: 9
      },
      &quot;id&quot;: 4,
      &quot;title&quot;: &quot;Breakdown by Namespace (Top 10 Requests)&quot;,
      &quot;type&quot;: &quot;row&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;description&quot;: &quot;네임스페이스별 CPU Requests 점유량 (상위 10개)&quot;,
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;color&quot;: {
            &quot;mode&quot;: &quot;palette-classic&quot;
          },
          &quot;custom&quot;: {
            &quot;axisBorderShow&quot;: false,
            &quot;axisCenteredZero&quot;: false,
            &quot;axisColorMode&quot;: &quot;text&quot;,
            &quot;axisLabel&quot;: &quot;CPU Cores&quot;,
            &quot;axisPlacement&quot;: &quot;auto&quot;,
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;fillOpacity&quot;: 10,
            &quot;lineInterpolation&quot;: &quot;smooth&quot;,
            &quot;lineWidth&quot;: 1.5,
            &quot;pointSize&quot;: 5,
            &quot;showPoints&quot;: &quot;never&quot;,
            &quot;spanNulls&quot;: false,
            &quot;stacking&quot;: {
              &quot;group&quot;: &quot;A&quot;,
              &quot;mode&quot;: &quot;none&quot;
            }
          },
          &quot;unit&quot;: &quot;short&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 12,
        &quot;x&quot;: 0,
        &quot;y&quot;: 10
      },
      &quot;id&quot;: 5,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [&quot;mean&quot;, &quot;lastNotNull&quot;],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;,
          &quot;showLegend&quot;: true
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;multi&quot;,
          &quot;sort&quot;: &quot;desc&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;editorMode&quot;: &quot;code&quot;,
          &quot;expr&quot;: &quot;topk(10, sum by (namespace) (kube_pod_container_resource_requests{cluster=<del>&quot;$cluster&quot;, resource=&quot;cpu&quot;} and on(pod, namespace, cluster) kube_pod_status_phase{cluster=</del>&quot;$cluster&quot;, phase=<del>&quot;Running|Pending&quot;} == 1))&quot;,
          &quot;legendFormat&quot;: &quot;{{namespace}}&quot;,
          &quot;range&quot;: true,
          &quot;refId&quot;: &quot;A&quot;
        }
      ],
      &quot;title&quot;: &quot;Top 10 Namespaces by CPU Requests&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;description&quot;: &quot;네임스페이스별 Memory Requests 점유량 (상위 10개)&quot;,
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;color&quot;: {
            &quot;mode&quot;: &quot;palette-classic&quot;
          },
          &quot;custom&quot;: {
            &quot;axisBorderShow&quot;: false,
            &quot;axisCenteredZero&quot;: false,
            &quot;axisColorMode&quot;: &quot;text&quot;,
            &quot;axisLabel&quot;: &quot;Memory Bytes&quot;,
            &quot;axisPlacement&quot;: &quot;auto&quot;,
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;fillOpacity&quot;: 10,
            &quot;lineInterpolation&quot;: &quot;smooth&quot;,
            &quot;lineWidth&quot;: 1.5,
            &quot;pointSize&quot;: 5,
            &quot;showPoints&quot;: &quot;never&quot;,
            &quot;spanNulls&quot;: false,
            &quot;stacking&quot;: {
              &quot;group&quot;: &quot;A&quot;,
              &quot;mode&quot;: &quot;none&quot;
            }
          },
          &quot;unit&quot;: &quot;bytes&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 12,
        &quot;x&quot;: 12,
        &quot;y&quot;: 10
      },
      &quot;id&quot;: 6,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [&quot;mean&quot;, &quot;lastNotNull&quot;],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;,
          &quot;showLegend&quot;: true
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;multi&quot;,
          &quot;sort&quot;: &quot;desc&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;editorMode&quot;: &quot;code&quot;,
          &quot;expr&quot;: &quot;topk(10, sum by (namespace) (kube_pod_container_resource_requests{cluster=</del>&quot;$cluster&quot;, resource=&quot;memory&quot;} and on(pod, namespace, cluster) kube_pod_status_phase{cluster=<del>&quot;$cluster&quot;, phase=</del>&quot;Running|Pending&quot;} == 1))&quot;,
          &quot;legendFormat&quot;: &quot;{{namespace}}&quot;,
          &quot;range&quot;: true,
          &quot;refId&quot;: &quot;A&quot;
        }
      ],
      &quot;title&quot;: &quot;Top 10 Namespaces by Memory Requests&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    }
  ],
  &quot;refresh&quot;: &quot;1m&quot;,
  &quot;schemaVersion&quot;: 38,
  &quot;style&quot;: &quot;dark&quot;,
  &quot;tags&quot;: [&quot;kubernetes&quot;, &quot;capacity-planning&quot;, &quot;allocation&quot;],
  &quot;templating&quot;: {
    &quot;list&quot;: [
      {
        &quot;current&quot;: {},
        &quot;hide&quot;: 0,
        &quot;includeAll&quot;: false,
        &quot;label&quot;: &quot;Data Source&quot;,
        &quot;multi&quot;: false,
        &quot;name&quot;: &quot;DS_PROMETHEUS&quot;,
        &quot;options&quot;: [],
        &quot;query&quot;: &quot;prometheus&quot;,
        &quot;refresh&quot;: 1,
        &quot;type&quot;: &quot;datasource&quot;
      },
      {
        &quot;allValue&quot;: &quot;.*&quot;,
        &quot;current&quot;: {},
        &quot;datasource&quot;: {
          &quot;type&quot;: &quot;prometheus&quot;,
          &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
        },
        &quot;definition&quot;: &quot;label_values(kube_node_status_allocatable, cluster)&quot;,
        &quot;hide&quot;: 0,
        &quot;includeAll&quot;: false,
        &quot;label&quot;: &quot;Cluster&quot;,
        &quot;multi&quot;: false,
        &quot;name&quot;: &quot;cluster&quot;,
        &quot;options&quot;: [],
        &quot;query&quot;: {
          &quot;query&quot;: &quot;label_values(kube_node_status_allocatable, cluster)&quot;,
          &quot;refId&quot;: &quot;Prometheus-Cluster-Variable&quot;
        },
        &quot;refresh&quot;: 2,
        &quot;sort&quot;: 1,
        &quot;type&quot;: &quot;query&quot;
      }
    ]
  },
  &quot;time&quot;: {
    &quot;from&quot;: &quot;now-24h&quot;,
    &quot;to&quot;: &quot;now&quot;
  },
  &quot;timepicker&quot;: {
    &quot;refresh_intervals&quot;: [
      &quot;30s&quot;,
      &quot;1m&quot;,
      &quot;5m&quot;,
      &quot;15m&quot;,
      &quot;30m&quot;,
      &quot;1h&quot;
    ]
  },
  &quot;timezone&quot;: &quot;browser&quot;,
  &quot;title&quot;: &quot;Kubernetes Cluster Resource Allocation&quot;,
  &quot;uid&quot;: &quot;k8s-cluster-alloc-rate&quot;
}</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[26S02i]]></title>
            <link>https://velog.io/@youngkyoo_kim/26S02i</link>
            <guid>https://velog.io/@youngkyoo_kim/26S02i</guid>
            <pubDate>Wed, 02 Sep 2026 02:58:21 GMT</pubDate>
            <description><![CDATA[<p>```json
{
  &quot;annotations&quot;: {
    &quot;list&quot;: [
      {
        &quot;builtIn&quot;: 1,
        &quot;datasource&quot;: {
          &quot;type&quot;: &quot;datasource&quot;,
          &quot;uid&quot;: &quot;grafana&quot;
        },
        &quot;enable&quot;: true,
        &quot;hide&quot;: true,
        &quot;name&quot;: &quot;Annotations &amp; Alerts&quot;,
        &quot;type&quot;: &quot;dashboard&quot;
      }
    ]
  },
  &quot;editable&quot;: true,
  &quot;fiscalYearStartMonth&quot;: 0,
  &quot;graphTooltip&quot;: 1,
  &quot;id&quot;: null,
  &quot;links&quot;: [],
  &quot;liveNow&quot;: false,
  &quot;panels&quot;: [
    {
      &quot;collapsed&quot;: false,
      &quot;gridPos&quot;: {
        &quot;h&quot;: 1,
        &quot;w&quot;: 24,
        &quot;x&quot;: 0,
        &quot;y&quot;: 0
      },
      &quot;id&quot;: 100,
      &quot;title&quot;: &quot;1. Bucket ListObjectsV2 Bottleneck &amp; Lock Contention Identification&quot;,
      &quot;type&quot;: &quot;row&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;custom&quot;: {
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;lineInterpolation&quot;: &quot;smooth&quot;,
            &quot;lineWidth&quot;: 2
          },
          &quot;unit&quot;: &quot;short&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 8,
        &quot;x&quot;: 0,
        &quot;y&quot;: 1
      },
      &quot;id&quot;: 1,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [
            &quot;mean&quot;,
            &quot;max&quot;,
            &quot;lastNotNull&quot;
          ],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;multi&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;topk(10, minio_cluster_usage_buckets_objects_count{namespace=<del>&quot;$namespace&quot;})&quot;,
          &quot;legendFormat&quot;: &quot;{{bucket}} (Objects)&quot;,
          &quot;refId&quot;: &quot;A&quot;
        },
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;topk(5, minio_cluster_usage_buckets_versions_count{namespace=</del>&quot;$namespace&quot;})&quot;,
          &quot;legendFormat&quot;: &quot;{{bucket}} (Versions)&quot;,
          &quot;refId&quot;: &quot;B&quot;
        }
      ],
      &quot;title&quot;: &quot;Top 10 Buckets by Object &amp; Version Count (Metadata Load)&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;custom&quot;: {
            &quot;align&quot;: &quot;auto&quot;,
            &quot;displayMode&quot;: &quot;auto&quot;
          },
          &quot;mappings&quot;: [],
          &quot;thresholds&quot;: {
            &quot;mode&quot;: &quot;absolute&quot;,
            &quot;steps&quot;: [
              {
                &quot;color&quot;: &quot;green&quot;,
                &quot;value&quot;: null
              },
              {
                &quot;color&quot;: &quot;orange&quot;,
                &quot;value&quot;: 5
              },
              {
                &quot;color&quot;: &quot;red&quot;,
                &quot;value&quot;: 30
              }
            ]
          }
        },
        &quot;overrides&quot;: [
          {
            &quot;matcher&quot;: {
              &quot;id&quot;: &quot;byName&quot;,
              &quot;options&quot;: &quot;Objects Count&quot;
            },
            &quot;properties&quot;: [
              {
                &quot;id&quot;: &quot;unit&quot;,
                &quot;value&quot;: &quot;short&quot;
              },
              {
                &quot;id&quot;: &quot;custom.displayMode&quot;,
                &quot;value&quot;: &quot;color-background&quot;
              }
            ]
          },
          {
            &quot;matcher&quot;: {
              &quot;id&quot;: &quot;byName&quot;,
              &quot;options&quot;: &quot;Versions Count&quot;
            },
            &quot;properties&quot;: [
              {
                &quot;id&quot;: &quot;unit&quot;,
                &quot;value&quot;: &quot;short&quot;
              },
              {
                &quot;id&quot;: &quot;custom.displayMode&quot;,
                &quot;value&quot;: &quot;color-background&quot;
              }
            ]
          },
          {
            &quot;matcher&quot;: {
              &quot;id&quot;: &quot;byName&quot;,
              &quot;options&quot;: &quot;Total Bytes&quot;
            },
            &quot;properties&quot;: [
              {
                &quot;id&quot;: &quot;unit&quot;,
                &quot;value&quot;: &quot;bytes&quot;
              }
            ]
          }
        ]
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 8,
        &quot;x&quot;: 8,
        &quot;y&quot;: 1
      },
      &quot;id&quot;: 10,
      &quot;options&quot;: {
        &quot;cellHeight&quot;: &quot;sm&quot;,
        &quot;footer&quot;: {
          &quot;countRows&quot;: false,
          &quot;fields&quot;: &quot;&quot;,
          &quot;reducer&quot;: [
            &quot;sum&quot;
          ],
          &quot;show&quot;: false
        },
        &quot;sortBy&quot;: [
          {
            &quot;desc&quot;: true,
            &quot;displayName&quot;: &quot;Objects Count&quot;
          }
        ]
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;topk(10, minio_cluster_usage_buckets_objects_count{namespace=<del>&quot;$namespace&quot;})&quot;,
          &quot;format&quot;: &quot;table&quot;,
          &quot;instant&quot;: true,
          &quot;legendFormat&quot;: &quot;Objects Count&quot;,
          &quot;refId&quot;: &quot;A&quot;
        },
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;minio_cluster_usage_buckets_versions_count{namespace=</del>&quot;$namespace&quot;}&quot;,
          &quot;format&quot;: &quot;table&quot;,
          &quot;instant&quot;: true,
          &quot;legendFormat&quot;: &quot;Versions Count&quot;,
          &quot;refId&quot;: &quot;B&quot;
        },
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;minio_cluster_usage_buckets_total_bytes{namespace=<del>&quot;$namespace&quot;}&quot;,
          &quot;format&quot;: &quot;table&quot;,
          &quot;instant&quot;: true,
          &quot;legendFormat&quot;: &quot;Total Bytes&quot;,
          &quot;refId&quot;: &quot;C&quot;
        }
      ],
      &quot;transformations&quot;: [
        {
          &quot;id&quot;: &quot;joinByField&quot;,
          &quot;options&quot;: {
            &quot;byField&quot;: &quot;bucket&quot;,
            &quot;mode&quot;: &quot;outer&quot;
          }
        },
        {
          &quot;id&quot;: &quot;organize&quot;,
          &quot;options&quot;: {
            &quot;excludeByName&quot;: {
              &quot;Time&quot;: true,
              &quot;Time 1&quot;: true,
              &quot;Time 2&quot;: true,
              &quot;Time 3&quot;: true
            },
            &quot;indexByName&quot;: {
              &quot;bucket&quot;: 0,
              &quot;Value #A&quot;: 1,
              &quot;Value #B&quot;: 2,
              &quot;Value #C&quot;: 3
            },
            &quot;renameByName&quot;: {
              &quot;Value #A&quot;: &quot;Objects Count&quot;,
              &quot;Value #B&quot;: &quot;Versions Count&quot;,
              &quot;Value #C&quot;: &quot;Total Bytes&quot;
            }
          }
        }
      ],
      &quot;title&quot;: &quot;High-Risk Buckets Ranking Table (List Scan Bottleneck by Objects/Versions)&quot;,
      &quot;type&quot;: &quot;table&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;custom&quot;: {
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;lineInterpolation&quot;: &quot;smooth&quot;,
            &quot;lineWidth&quot;: 2
          },
          &quot;unit&quot;: &quot;s&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 8,
        &quot;x&quot;: 16,
        &quot;y&quot;: 1
      },
      &quot;id&quot;: 2,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [
            &quot;mean&quot;,
            &quot;max&quot;
          ],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;multi&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;topk(10, sum by (name) (rate(minio_api_requests_duration_total{namespace=</del>&quot;$namespace&quot;, name=<del>&quot;(?i).<em>(put|multipart|delete|complete).</em>&quot;}[5m])) / clamp_min(sum by (name) (rate(minio_api_requests_total{namespace=</del>&quot;$namespace&quot;, name=<del>&quot;(?i).<em>(put|multipart|delete|complete).</em>&quot;}[5m])), 0.001))&quot;,
          &quot;legendFormat&quot;: &quot;{{name}}&quot;,
          &quot;refId&quot;: &quot;A&quot;
        }
      ],
      &quot;title&quot;: &quot;Lock-Holding APIs Duration (Put/CompleteMPU/Delete)&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;custom&quot;: {
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;lineInterpolation&quot;: &quot;smooth&quot;,
            &quot;lineWidth&quot;: 2
          },
          &quot;unit&quot;: &quot;s&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 12,
        &quot;x&quot;: 0,
        &quot;y&quot;: 9
      },
      &quot;id&quot;: 3,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [
            &quot;mean&quot;,
            &quot;max&quot;
          ],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;multi&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;minio_locks_dist_avg_latency&quot;,
          &quot;legendFormat&quot;: &quot;Cluster Avg Lock Latency&quot;,
          &quot;refId&quot;: &quot;A&quot;
        },
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;sum(minio_locks_waiting_total{namespace=</del>&quot;$namespace&quot;}) / clamp_min(count(count by (instance) (minio_api_requests_total{namespace=<del>&quot;$namespace&quot;})), 1)&quot;,
          &quot;legendFormat&quot;: &quot;Avg Waiting Locks / Pod&quot;,
          &quot;refId&quot;: &quot;B&quot;
        }
      ],
      &quot;title&quot;: &quot;Cluster Lock Latency &amp; Waiting Queue&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;custom&quot;: {
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;lineInterpolation&quot;: &quot;linear&quot;,
            &quot;lineWidth&quot;: 2
          },
          &quot;unit&quot;: &quot;reqps&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 12,
        &quot;x&quot;: 12,
        &quot;y&quot;: 9
      },
      &quot;id&quot;: 4,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [
            &quot;mean&quot;,
            &quot;max&quot;
          ],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;multi&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;sum(rate(minio_api_requests_total{namespace=</del>&quot;$namespace&quot;, name=<del>&quot;(?i).<em>(list).</em>&quot;}[5m]))&quot;,
          &quot;legendFormat&quot;: &quot;Cluster Total List req/s&quot;,
          &quot;refId&quot;: &quot;A&quot;
        },
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;sum(rate(minio_api_requests_total{namespace=</del>&quot;$namespace&quot;, name=<del>&quot;(?i).<em>(list).</em>&quot;}[5m])) / clamp_min(count(count by (instance) (minio_api_requests_total{namespace=</del>&quot;$namespace&quot;})), 1)&quot;,
          &quot;legendFormat&quot;: &quot;Per-Pod Avg List req/s&quot;,
          &quot;refId&quot;: &quot;B&quot;
        },
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;sum(rate(minio_api_requests_duration_total{namespace=<del>&quot;$namespace&quot;, name=</del>&quot;(?i).<em>(list).*&quot;}[5m])) / clamp_min(sum(rate(minio_api_requests_total{namespace=<del>&quot;$namespace&quot;, name=</del>&quot;(?i).</em>(list).<em>&quot;}[5m])), 0.001)&quot;,
          &quot;legendFormat&quot;: &quot;List Avg Duration (s)&quot;,
          &quot;refId&quot;: &quot;C&quot;
        }
      ],
      &quot;title&quot;: &quot;List Request Rate &amp; Duration (Cluster Total vs Per-Pod Avg)&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;collapsed&quot;: false,
      &quot;gridPos&quot;: {
        &quot;h&quot;: 1,
        &quot;w&quot;: 24,
        &quot;x&quot;: 0,
        &quot;y&quot;: 17
      },
      &quot;id&quot;: 200,
      &quot;title&quot;: &quot;2. Internal Goroutine Amplification &amp; Disk Metadata Contention&quot;,
      &quot;type&quot;: &quot;row&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;custom&quot;: {
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;lineInterpolation&quot;: &quot;linear&quot;,
            &quot;lineWidth&quot;: 2
          },
          &quot;unit&quot;: &quot;short&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 8,
        &quot;x&quot;: 0,
        &quot;y&quot;: 18
      },
      &quot;id&quot;: 5,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [
            &quot;max&quot;,
            &quot;lastNotNull&quot;
          ],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;multi&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;topk(5, go_goroutines{namespace=<del>&quot;$namespace&quot;} or minio_process_goroutines{namespace=</del>&quot;$namespace&quot;})&quot;,
          &quot;legendFormat&quot;: &quot;Pod Goroutines: {{instance}}&quot;,
          &quot;refId&quot;: &quot;A&quot;
        },
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;avg(go_goroutines{namespace=<del>&quot;$namespace&quot;} or minio_process_goroutines{namespace=</del>&quot;$namespace&quot;})&quot;,
          &quot;legendFormat&quot;: &quot;Cluster Avg Goroutines / Pod&quot;,
          &quot;refId&quot;: &quot;B&quot;
        }
      ],
      &quot;title&quot;: &quot;Pod Goroutine Spike (Internal Parallel Scan Amplification)&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;custom&quot;: {
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;lineInterpolation&quot;: &quot;linear&quot;,
            &quot;lineWidth&quot;: 2
          },
          &quot;unit&quot;: &quot;iops&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 8,
        &quot;x&quot;: 8,
        &quot;y&quot;: 18
      },
      &quot;id&quot;: 6,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [
            &quot;mean&quot;,
            &quot;max&quot;
          ],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;multi&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;topk(10, rate(node_disk_reads_completed_total{namespace=<del>&quot;$namespace&quot;}[5m]) or rate(minio_node_drive_reads_total{namespace=</del>&quot;$namespace&quot;}[5m]))&quot;,
          &quot;legendFormat&quot;: &quot;Read IOPS: {{instance}} - {{device}}&quot;,
          &quot;refId&quot;: &quot;A&quot;
        },
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;topk(10, rate(node_disk_writes_completed_total{namespace=<del>&quot;$namespace&quot;}[5m]) or rate(minio_node_drive_writes_total{namespace=</del>&quot;$namespace&quot;}[5m]))&quot;,
          &quot;legendFormat&quot;: &quot;Write IOPS: {{instance}} - {{device}}&quot;,
          &quot;refId&quot;: &quot;B&quot;
        }
      ],
      &quot;title&quot;: &quot;Drive IOPS (Random Small Read Spike by List Scan)&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;datasource&quot;: {
        &quot;type&quot;: &quot;prometheus&quot;,
        &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
      },
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;custom&quot;: {
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;lineInterpolation&quot;: &quot;linear&quot;,
            &quot;lineWidth&quot;: 2
          },
          &quot;unit&quot;: &quot;s&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 8,
        &quot;w&quot;: 8,
        &quot;x&quot;: 16,
        &quot;y&quot;: 18
      },
      &quot;id&quot;: 7,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [
            &quot;mean&quot;,
            &quot;max&quot;
          ],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;bottom&quot;
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;multi&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;topk(10, rate(minio_node_drive_total_waiting_time_seconds{namespace=<del>&quot;$namespace&quot;}[5m]) / clamp_min(rate(minio_node_drive_total_waiting{namespace=</del>&quot;$namespace&quot;}[5m]), 0.001))&quot;,
          &quot;legendFormat&quot;: &quot;Drive Wait Time: {{instance}} - {{drive}}&quot;,
          &quot;refId&quot;: &quot;A&quot;
        },
        {
          &quot;datasource&quot;: {
            &quot;type&quot;: &quot;prometheus&quot;,
            &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
          },
          &quot;expr&quot;: &quot;topk(10, rate(node_disk_io_time_seconds_total{namespace=~&quot;$namespace&quot;}[5m]))&quot;,
          &quot;legendFormat&quot;: &quot;Disk Saturation: {{instance}} - {{device}}&quot;,
          &quot;refId&quot;: &quot;B&quot;
        }
      ],
      &quot;title&quot;: &quot;Drive Waiting Time &amp; Saturation (Delaying Commit fsync)&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    }
  ],
  &quot;refresh&quot;: &quot;30s&quot;,
  &quot;schemaVersion&quot;: 38,
  &quot;style&quot;: &quot;dark&quot;,
  &quot;tags&quot;: [
    &quot;minio&quot;,
    &quot;aistor&quot;,
    &quot;v3&quot;,
    &quot;duration_total&quot;,
    &quot;locks&quot;
  ],
  &quot;templating&quot;: {
    &quot;list&quot;: [
      {
        &quot;current&quot;: {},
        &quot;hide&quot;: 0,
        &quot;label&quot;: &quot;Data Source&quot;,
        &quot;name&quot;: &quot;DS_PROMETHEUS&quot;,
        &quot;options&quot;: [],
        &quot;query&quot;: &quot;prometheus&quot;,
        &quot;refresh&quot;: 1,
        &quot;regex&quot;: &quot;&quot;,
        &quot;type&quot;: &quot;datasource&quot;
      },
      {
        &quot;allValue&quot;: &quot;.</em>&quot;,
        &quot;current&quot;: {
          &quot;selected&quot;: true,
          &quot;text&quot;: &quot;All&quot;,
          &quot;value&quot;: &quot;$<strong>all&quot;
        },
        &quot;datasource&quot;: {
          &quot;type&quot;: &quot;prometheus&quot;,
          &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
        },
        &quot;definition&quot;: &quot;label_values(minio_api_requests_total, namespace)&quot;,
        &quot;hide&quot;: 0,
        &quot;includeAll&quot;: true,
        &quot;label&quot;: &quot;Namespace&quot;,
        &quot;multi&quot;: false,
        &quot;name&quot;: &quot;namespace&quot;,
        &quot;options&quot;: [],
        &quot;query&quot;: {
          &quot;query&quot;: &quot;label_values(minio_api_requests_total, namespace)&quot;,
          &quot;refId&quot;: &quot;Prometheus-Namespace-Variable&quot;
        },
        &quot;refresh&quot;: 2,
        &quot;regex&quot;: &quot;&quot;,
        &quot;type&quot;: &quot;query&quot;
      },
      {
        &quot;allValue&quot;: &quot;.*&quot;,
        &quot;current&quot;: {
          &quot;selected&quot;: true,
          &quot;text&quot;: &quot;All&quot;,
          &quot;value&quot;: &quot;$</strong>all&quot;
        },
        &quot;datasource&quot;: {
          &quot;type&quot;: &quot;prometheus&quot;,
          &quot;uid&quot;: &quot;${DS_PROMETHEUS}&quot;
        },
        &quot;definition&quot;: &quot;label_values(minio_api_requests_total, bucket)&quot;,
        &quot;hide&quot;: 0,
        &quot;includeAll&quot;: true,
        &quot;label&quot;: &quot;Bucket&quot;,
        &quot;multi&quot;: true,
        &quot;name&quot;: &quot;bucket&quot;,
        &quot;options&quot;: [],
        &quot;query&quot;: {
          &quot;query&quot;: &quot;label_values(minio_api_requests_total, bucket)&quot;,
          &quot;refId&quot;: &quot;Prometheus-Bucket-Variable&quot;
        },
        &quot;refresh&quot;: 2,
        &quot;regex&quot;: &quot;&quot;,
        &quot;type&quot;: &quot;query&quot;
      }
    ]
  },
  &quot;time&quot;: {
    &quot;from&quot;: &quot;now-24h&quot;,
    &quot;to&quot;: &quot;now&quot;
  },
  &quot;timepicker&quot;: {
    &quot;refresh_intervals&quot;: [
      &quot;5s&quot;,
      &quot;10s&quot;,
      &quot;30s&quot;,
      &quot;1m&quot;,
      &quot;5m&quot;
    ]
  },
  &quot;timezone&quot;: &quot;browser&quot;,
  &quot;title&quot;: &quot;MinIO AIStor v3 - Bucket Metadata &amp; Lock Bottleneck Diagnostic&quot;,
  &quot;version&quot;: 8
}</p>
]]></description>
        </item>
    </channel>
</rss>