Go + Kubernetes 1.30 実践:AI推論プラットフォームをゼロから構築

技术架构本番環境の AI Agent

なぜAI推論プラットフォームにKubernetesが必要か?

AI推論サービスには独自の運用課題があります:GPUリソースが高価、モデルロードが遅い、トラフィック変動が激しい、コールドスタートコストが高い。Kubernetes 1.30 + Go はこれらの課題を解決する最適な組み合わせです。

┌──────────────────────────────────────────────────┐
│              AI推論プラットフォームアーキテクチャ        │
│                                                  │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐          │
│  │ Go API  │  │ Goスケ  │  │ Go監視  │          │
│  │ Gateway │  │ジューラ │  │ Monitor │          │
│  └────┬────┘  └────┬────┘  └────┬────┘          │
│       │            │            │                 │
│  ┌────▼────────────▼────────────▼────┐           │
│  │        Kubernetes 1.30            │           │
│  │  ┌──────┐ ┌──────┐ ┌──────┐     │           │
│  │  │ Pod  │ │ Pod  │ │ Pod  │     │           │
│  │  │GPU 0 │ │GPU 1 │ │GPU 2 │     │           │
│  │  │Model │ │Model │ │Model │     │           │
│  │  │ A    │ │ B    │ │ A    │     │           │
│  │  └──────┘ └──────┘ └──────┘     │           │
│  └──────────────────────────────────┘           │
└──────────────────────────────────────────────────┘

Goスケジューラ:GPUアウェアスケジューリング

カスタムスケジューラ

package scheduler

import (
    "context"
    "fmt"
    "sort"

    v1 "k8s.io/api/core/v1"
    "k8s.io/kubernetes/pkg/scheduler/framework"
)

type GPUScheduler struct {
    handle framework.Handle
}

func (s *GPUScheduler) Filter(ctx context.Context, state *framework.CycleState,
    pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {

    gpuRequest := getGPURequest(pod)
    if gpuRequest == 0 {
        return nil
    }

    availableGPU := getAvailableGPU(nodeInfo)
    if availableGPU < gpuRequest {
        return framework.NewStatus(framework.Unschedulable,
            fmt.Sprintf("insufficient GPU: need %d, available %d", gpuRequest, availableGPU))
    }

    return nil
}

func (s *GPUScheduler) Score(ctx context.Context, state *framework.CycleState,
    pod *v1.Pod, nodeName string) (int64, *framework.Status) {

    nodeInfo, _ := s.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)
    gpuUtilization := getGPUUtilization(nodeInfo)

    // GPU使用率の低いノードを優先(負荷分散)
    score := int64(100 - gpuUtilization)
    return score, nil
}

func getGPURequest(pod *v1.Pod) int64 {
    for _, container := range pod.Spec.Containers {
        if limit, ok := container.Resources.Limits[v1.ResourceName("nvidia.com/gpu")]; ok {
            return limit.Value()
        }
    }
    return 0
}

Filter 段階でGPUリソースが足りないノードを除外し、Score 段階でGPU使用率が最も低いノードに高い優先度を割り当てます。この「Filter + Score」二段階方式はKubernetesスケジューラの標準アーキテクチャに完全準拠しています。


モデルサービング:vLLM + Kubernetes

Deployment設定

apiVersion: apps/v1
kind: Deployment
metadata:
  name: llm-inference
  labels:
    app: llm-inference
    model: qwen2.5-72b
spec:
  replicas: 3
  selector:
    matchLabels:
      app: llm-inference
  template:
    metadata:
      labels:
        app: llm-inference
    spec:
      schedulerName: gpu-scheduler
      containers:
      - name: vllm
        image: vllm/vllm-openai:latest
        resources:
          limits:
            nvidia.com/gpu: "2"
          requests:
            nvidia.com/gpu: "2"
            memory: "32Gi"
        env:
        - name: MODEL_NAME
          value: "Qwen/Qwen2.5-72B-Instruct"
        - name: GPU_MEMORY_UTILIZATION
          value: "0.90"
        - name: MAX_NUM_SEQS
          value: "256"
        ports:
        - containerPort: 8000
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 120
          periodSeconds: 10
        volumeMounts:
        - name: model-cache
          mountPath: /models
      volumes:
      - name: model-cache
        persistentVolumeClaim:
          claimName: model-cache-pvc
      tolerations:
      - key: nvidia.com/gpu
        operator: Exists
        effect: NoSchedule

readinessProbeinitialDelaySeconds: 120 に注目——大規模モデル(72B)のロードには2分程度かかるため、起動猶予を十分に確保します。

HPA自動スケーリング

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: llm-inference-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: llm-inference
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Pods
    pods:
      metric:
        name: vllm_num_requests_running
      target:
        type: AverageValue
        averageValue: "128"
  - type: Resource
    resource:
      name: nvidia.com/gpu-utilization
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Pods
        value: 2
        periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Pods
        value: 1
        periodSeconds: 120

重要な設計ポイント

  • スケールアップは速く(60秒の安定化ウィンドウ)、スケールダウンは遅く(300秒)——トラフィックスパイクによる頻繁な変動を防止
  • GPU使用率と実行中リクエスト数の2つの指標でトリガー
  • GPUは高価なので、maxReplicas: 10 でコスト上限を設定

Go APIゲートウェイ

モデルルーティングとロードバランシング

package gateway

import (
    "net/http"
    "net/http/httputil"
    "net/url"
    "sync/atomic"

    "github.com/gin-gonic/gin"
)

type ModelRouter struct {
    endpoints map[string][]*url.URL
    counters  map[string]*atomic.Uint64
}

func NewModelRouter() *ModelRouter {
    return &ModelRouter{
        endpoints: make(map[string][]*url.URL),
        counters:  make(map[string]*atomic.Uint64),
    }
}

func (r *ModelRouter) RegisterModel(model string, urls ...string) {
    for _, u := range urls {
        parsed, _ := url.Parse(u)
        r.endpoints[model] = append(r.endpoints[model], parsed)
    }
    r.counters[model] = &atomic.Uint64{}
}

func (r *ModelRouter) ProxyHandler(c *gin.Context) {
    model := c.Param("model")
    endpoints, ok := r.endpoints[model]
    if !ok {
        c.JSON(http.StatusNotFound, gin.H{"error": "model not found"})
        return
    }

    counter := r.counters[model]
    idx := counter.Add(1) % uint64(len(endpoints))
    target := endpoints[idx]

    proxy := httputil.NewSingleHostReverseProxy(target)
    proxy.ServeHTTP(c.Writer, c.Request)
}

atomic.Uint64 を使用したラウンドロビン負荷分散で、ロックなしの軽量なルーティングを実現。Ginフレームワークのパフォーマンスは数百万 QPS を処理可能です。


カナリーデプロイ

apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: llm-inference
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: llm-inference
  service:
    port: 8000
  analysis:
    interval: 1m
    threshold: 5
    maxWeight: 50
    stepWeight: 10
    metrics:
    - name: request-success-rate
      thresholdRange:
        min: 99
      interval: 1m
    - name: request-duration
      thresholdRange:
        max: 500
      interval: 1m
    - name: vllm-num-requests-waiting
      thresholdRange:
        max: 50
      interval: 1m

Flaggerを使用したカナリーデプロイ:新バージョンは10%から始め、5回の分析間隔を経て徐々に50%まで拡大。成功率99%以上、レイテンシ500ms以内、待機リクエスト50未満を維持できなければ自動ロールバック。

モデル更新のゼロダウンタイム戦略

  1. 新モデルバージョンのPodを起動(既存Podはそのまま)
  2. トラフィックの10%を新バージョンにルーティング
  3. 5分間メトリクスを監視(成功率、レイテンシ、GPUメモリ)
  4. 問題なければさらに10%ずつ増量
  5. 異常検出時に即時ロールバック

監視とオブザーバビリティ

# Prometheusカスタムメトリクス
apiVersion: v1
kind: ConfigMap
metadata:
  name: vllm-metrics-config
data:
  vllm-rules.yml: |
    groups:
    - name: vllm
      rules:
      - alert: HighQueueLength
        expr: vllm_num_requests_waiting > 100
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "LLM推論キューが過剰"
      - alert: HighGPUUtilization
        expr: nvidia_gpu_utilization > 95
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "GPU使用率が高すぎる"

まとめ

Go + KubernetesでAI推論プラットフォームを構築するコアバリュー:

  1. Goスケジューラ:GPUアウェアスケジューリングでGPUリソース使用率を最適化
  2. K8s HPA:推論キュー長とGPU使用率に基づく自動スケーリング
  3. カナリーデプロイ:モデル更新のゼロダウンタイム、自動ロールバック
  4. オブザーバビリティ:GPU使用率、キュー長、推論レイテンシのフルチェーン監視

AI推論プラットフォームは「モデルをデプロイする」ことではなく「システムを運用する」こと——Kubernetesが基盤で、Goが接着剤です。

ブラウザローカルツールを無料で試す →

#Go#Kubernetes#AI推理#GPU调度#云原生