K8s Sidecar AI推論プロキシ:トラフィックインターセプトからモデルルーティングまでの7つのプロダクションパターン

DevOps

AI推論サービスをそのまま裸で実行していませんか?モデルのアップグレードのたびにビジネスコードを変更していますか?複数のモデルバージョンが共存する際のトラフィック管理が混乱していませんか?GPU利用率が30%未満なのにスケールアウトを続けていますか?2026年、Kubernetes SidecarパターンはAI推論プロキシの標準アーキテクチャとなりました。Sidecarコンテナで推論ロジックとビジネスロジックを分離する時が来ました。


主要なポイント

  • K8s Sidecar AI推論プロキシのアーキテクチャ原理と適用シナリオの理解
  • 7つのプロダクショングレードSidecarプロキシパターンの完全な実装を習得
  • トラフィックインターセプト、モデルルーティング、A/BテストのYAML設定を学習
  • GPUリソースプーリング、バッチマージなどの高度な最適化テクニックを理解
  • 5つのよくある落とし穴と10の高頻度エラーを回避

目次

  1. Sidecar AIプロキシアーキテクチャ概要
  2. パターン1:Envoyトラフィックインターセプトとリライト
  3. パターン2:スマートモデルルーティング
  4. パターン3:A/Bテストとカナリアデプロイ
  5. パターン4:マルチモデルサービングとバージョン管理
  6. パターン5:GPUリソースプーリング
  7. パターン6:バッチ処理とリクエストマージ
  8. パターン7:オブザーバビリティと分散トレーシング
  9. 5つのよくある落とし穴と解決策
  10. 10のよくあるエラートラブルシューティング
  11. 高度な最適化テクニック
  12. 比較分析:Sidecarプロキシ vs Service Mesh vs Gateway
  13. オンラインツール推奨

Sidecar AIプロキシアーキテクチャ概要

AI推論にSidecarプロキシが必要な理由

従来のAI推論デプロイメントでは、モデル読み込み、推論実行、トラフィック管理がすべてビジネスコンテナ内に結合されています。モデルバージョンの反復、ルーティングポリシーの変更、リソース制限の調整時に、サービス全体を再構築・再デプロイする必要があります。Sidecarプロキシパターンは関心の分離を実現します:

┌─────────────────────────────────────────────────┐
│                   Pod                            │
│                                                  │
│  ┌──────────────┐      ┌──────────────────────┐ │
│  │  Business    │      │  Sidecar Proxy       │ │
│  │  Container   │─────▶│  (AI Inference)      │ │
│  │              │      │                      │ │
│  │  - API論理   │      │  - トラフィック傍受  │ │
│  │  - ビジネス   │      │  - モデルルーティング│ │
│  │  - 結果集約   │      │  - ロードバランシング│ │
│  │              │      │  - バッチマージ      │ │
│  │  :8080       │      │  - メトリクス収集    │ │
│  └──────────────┘      │                      │ │
│         │              │  :15001(inbound)     │ │
│         │              │  :15006(outbound)    │ │
│         ▼              └──────────────────────┘ │
│  ┌──────────────┐               │               │
│  │  Model       │◀──────────────┘               │
│  │  Server      │                               │
│  │  (vLLM/Triton│                               │
│  │   /Ollama)   │                               │
│  │  :8000       │                               │
│  └──────────────┘                               │
└─────────────────────────────────────────────────┘

Sidecarプロキシのコア責務

責務 説明 メリット
トラフィックインターセプト ビジネスコンテナの推論リクエストを傍受 ビジネスコードの変更ゼロ
モデルルーティング リクエスト特性に基づき異なるモデルにルーティング マルチモデルバージョン共存
ロードバランシング 複数レプリカ間で推論リクエストを分配 スループット向上
バッチ処理 複数リクエストをマージしてバッチ推論 GPU利用率3-5倍向上
サーキットブレーカー モデルサービス異常時の高速デグラデーション システム安定性保護
オブザーバビリティ 推論レイテンシ、スループット等のメトリクス収集 フルチェーン可観測性

パターン1:Envoyトラフィックインターセプトとリライト

アーキテクチャ原理

EnvoyがSidecarプロキシとして機能し、iptablesを介してビジネスコンテナのアウトバウンドトラフィックを傍受し、推論リクエストをターゲットモデルサービスにリライトします。これはK8s Sidecarパターンで最も古典的なトラフィックインターセプト方式です。

Client Request
     │
     ▼
┌─────────┐    iptables    ┌──────────────┐
│ Business │───redirect───▶│    Envoy     │
│ Container│    :8080      │   Sidecar    │
│          │               │   :15001     │
└─────────┘               └──────┬───────┘
                                  │
                    ┌─────────────┼─────────────┐
                    ▼             ▼             ▼
              ┌─────────┐  ┌─────────┐  ┌─────────┐
              │ Model A │  │ Model B │  │ Model C │
              │ :8000   │  │ :8001   │  │ :8002   │
              └─────────┘  └─────────┘  └─────────┘

完全設定

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ai-inference-app
  namespace: ai-serving
spec:
  replicas: 3
  selector:
    matchLabels:
      app: ai-inference
  template:
    metadata:
      labels:
        app: ai-inference
      annotations:
        sidecar.istio.io/inject: "true"
        traffic.sidecar.istio.io/includeOutboundIPRanges: "10.0.0.0/8"
        traffic.sidecar.istio.io/excludeInboundPorts: "9090"
    spec:
      containers:
        - name: business-app
          image: myregistry/ai-business-app:v2.1.0
          ports:
            - containerPort: 8080
          env:
            - name: INFERENCE_ENDPOINT
              value: "http://localhost:15001/v1/completions"
          resources:
            requests:
              cpu: "500m"
              memory: "512Mi"
            limits:
              cpu: "2000m"
              memory: "2Gi"
        - name: model-server
          image: vllm/vllm-openai:v0.6.0
          ports:
            - containerPort: 8000
          env:
            - name: MODEL_NAME
              value: "Qwen/Qwen2.5-72B-Instruct"
            - name: GPU_MEMORY_UTILIZATION
              value: "0.9"
          resources:
            requests:
              nvidia.com/gpu: "1"
            limits:
              nvidia.com/gpu: "1"

Envoyリライトルール

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: inference-rewrite
  namespace: ai-serving
spec:
  hosts:
    - inference-internal
  http:
    - match:
        - uri:
            prefix: "/v1/completions"
        - headers:
            x-model-type:
              exact: "chat"
      rewrite:
        uri: "/v1/chat/completions"
      route:
        - destination:
            host: model-server
            port:
              number: 8000
    - match:
        - uri:
            prefix: "/v1/embeddings"
      route:
        - destination:
            host: embedding-server
            port:
              number: 8001

パターン2:スマートモデルルーティング

リクエスト特性に基づく動的ルーティング

AI推論シナリオでは、異なるリクエストが異なる仕様のモデルにルーティングされる必要があります。Sidecarプロキシはリクエストヘッダー、ペイロード内容、トークン数などの特徴に基づいてスマートルーティングを行います。

┌──────────────────────────────────────────────────────┐
│                 Smart Model Router                    │
│                                                      │
│  Request ──▶ [Token Counter] ──▶ [Model Selector]   │
│                  │                    │               │
│                  │     ┌──────────────┼──────────┐   │
│                  │     ▼              ▼          ▼   │
│                  │  ┌───────┐  ┌─────────┐ ┌──────┐ │
│                  │  │Small  │  │Medium   │ │Large │ │
│                  │  │Model  │  │Model    │ │Model │ │
│                  │  │<1K tok│  │1K-8K tok│ │>8K   │ │
│                  │  │Qwen2.5│  │Qwen2.5  │ │Qwen2.│ │
│                  │  │-7B    │  │-32B     │ │5-72B │ │
│                  │  └───────┘  └─────────┘ └──────┘ │
└──────────────────────────────────────────────────────┘

ルーティング設定

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: smart-model-route
  namespace: ai-serving
spec:
  hosts:
    - ai-router
  http:
    - match:
        - headers:
            x-token-range:
              exact: "small"
          uri:
            prefix: "/v1"
      route:
        - destination:
            host: qwen-7b-service
            port:
              number: 8000
          weight: 100
    - match:
        - headers:
            x-token-range:
              exact: "medium"
          uri:
            prefix: "/v1"
      route:
        - destination:
            host: qwen-32b-service
            port:
              number: 8000
          weight: 100
    - match:
        - headers:
            x-token-range:
              exact: "large"
          uri:
            prefix: "/v1"
      route:
        - destination:
            host: qwen-72b-service
            port:
              number: 8000
          weight: 100
    - route:
        - destination:
            host: qwen-32b-service
            port:
              number: 8000

Goルーティングプロキシ実装

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"go.uber.org/zap"
)

type ModelRouteConfig struct {
	SmallModelEndpoint  string `json:"smallModelEndpoint"`
	MediumModelEndpoint string `json:"mediumModelEndpoint"`
	LargeModelEndpoint  string `json:"largeModelEndpoint"`
	SmallTokenThreshold int    `json:"smallTokenThreshold"`
	LargeTokenThreshold int    `json:"largeTokenThreshold"`
}

type InferenceRequest struct {
	Model     string    `json:"model"`
	Messages  []Message `json:"messages,omitempty"`
	Prompt    string    `json:"prompt,omitempty"`
	MaxTokens int       `json:"max_tokens,omitempty"`
}

type Message struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type SmartRouter struct {
	config  *ModelRouteConfig
	logger  *zap.Logger
	metrics *RouterMetrics
}

type RouterMetrics struct {
	routeDecision  *prometheus.CounterVec
	requestLatency *prometheus.HistogramVec
}

func NewSmartRouter(cfg *ModelRouteConfig, logger *zap.Logger) *SmartRouter {
	metrics := &RouterMetrics{
		routeDecision: prometheus.NewCounterVec(
			prometheus.CounterOpts{Name: "ai_router_route_decision_total", Help: "Model route decision counter"},
			[]string{"model_size", "model_name"},
		),
		requestLatency: prometheus.NewHistogramVec(
			prometheus.HistogramOpts{Name: "ai_router_request_latency_seconds", Help: "Request routing latency", Buckets: prometheus.DefBuckets},
			[]string{"model_size"},
		),
	}
	prometheus.MustRegister(metrics.routeDecision, metrics.requestLatency)
	return &SmartRouter{config: cfg, logger: logger, metrics: metrics}
}

func (r *SmartRouter) estimateTokenCount(req *InferenceRequest) int {
	totalChars := 0
	if req.Prompt != "" {
		totalChars += len(req.Prompt)
	}
	for _, msg := range req.Messages {
		totalChars += len(msg.Content)
	}
	return totalChars / 4
}

func (r *SmartRouter) selectModel(tokenCount int) (string, string) {
	if tokenCount <= r.config.SmallTokenThreshold {
		return "small", r.config.SmallModelEndpoint
	}
	if tokenCount <= r.config.LargeTokenThreshold {
		return "medium", r.config.MediumModelEndpoint
	}
	return "large", r.config.LargeModelEndpoint
}

func (r *SmartRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	start := time.Now()
	body, err := io.ReadAll(req.Body)
	if err != nil {
		http.Error(w, "failed to read request body", http.StatusBadRequest)
		return
	}
	defer req.Body.Close()

	var inferReq InferenceRequest
	if err := json.Unmarshal(body, &inferReq); err != nil {
		http.Error(w, "invalid request format", http.StatusBadRequest)
		return
	}

	tokenCount := r.estimateTokenCount(&inferReq)
	modelSize, endpoint := r.selectModel(tokenCount)
	r.logger.Info("routing decision", zap.Int("token_count", tokenCount), zap.String("model_size", modelSize))
	r.metrics.routeDecision.WithLabelValues(modelSize, inferReq.Model).Inc()

	target, _ := url.Parse(endpoint)
	proxy := httputil.NewSingleHostReverseProxy(target)
	req.Body = io.NopCloser(strings.NewReader(string(body)))
	req.ContentLength = int64(len(body))
	proxy.ServeHTTP(w, req)
	r.metrics.requestLatency.WithLabelValues(modelSize).Observe(time.Since(start).Seconds())
}

func main() {
	logger, _ := zap.NewProduction()
	defer logger.Sync()
	cfg := &ModelRouteConfig{
		SmallModelEndpoint:  "http://qwen-7b-service:8000",
		MediumModelEndpoint: "http://qwen-32b-service:8000",
		LargeModelEndpoint:  "http://qwen-72b-service:8000",
		SmallTokenThreshold: 1000,
		LargeTokenThreshold: 8000,
	}
	router := NewSmartRouter(cfg, logger)
	mux := http.NewServeMux()
	mux.Handle("/v1/", router)
	mux.Handle("/metrics", promhttp.Handler())
	mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, "ok") })
	server := &http.Server{Addr: ":15001", Handler: mux, WriteTimeout: 120 * time.Second}
	logger.Info("smart router starting", zap.String("addr", server.Addr))
	server.ListenAndServe()
}

パターン3:A/Bテストとカナリアデプロイ

重みベースのカナリアデプロイ

AIモデルのリリース時、カナリアデプロイは不可欠なプロセスです。Sidecarプロキシは重みベースのトラフィック分配を実現し、旧モデルから新モデルへ段階的にトラフィックを移行します。

┌────────────────────────────────────────────┐
│           Canary Deployment Flow            │
│                                            │
│  Traffic ──▶ [Sidecar Proxy] ──┬── 90% ──▶│──▶ Model v1 (Stable)
│                                │           │
│                                └── 10% ──▶│──▶ Model v2 (Canary)
│                                            │
│  Metrics:                                   │
│  ┌──────────────────────────────────────┐  │
│  │ v1: latency_p99=120ms  error=0.1%    │  │
│  │ v2: latency_p99=95ms   error=0.05%   │  │
│  └──────────────────────────────────────┘  │
│                                            │
│  Decision: Promote v2 ──▶ Shift to 50/50  │
└────────────────────────────────────────────┘

カナリアデプロイ設定

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: model-canary
  namespace: ai-serving
spec:
  hosts:
    - model-service
  http:
    - route:
        - destination:
            host: model-v1
            port:
              number: 8000
          weight: 90
        - destination:
            host: model-v2
            port:
              number: 8000
          weight: 10
      retries:
        attempts: 3
        perTryTimeout: 30s
        retryOn: 5xx,reset

ヘッダーベースのA/Bテスト

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: model-ab-test
  namespace: ai-serving
spec:
  hosts:
    - model-service
  http:
    - match:
        - headers:
            x-experiment:
              exact: "model-v2-creative"
      route:
        - destination:
            host: model-v2-creative
            port:
              number: 8000
    - match:
        - headers:
            x-experiment:
              exact: "model-v2-precise"
      route:
        - destination:
            host: model-v2-precise
            port:
              number: 8000
    - route:
        - destination:
            host: model-v1
            port:
              number: 8000

Argo Rollouts自動カナリア

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: model-rollout
  namespace: ai-serving
spec:
  replicas: 4
  strategy:
    canary:
      canaryService: model-v2
      stableService: model-v1
      trafficRouting:
        istio:
          virtualServices:
            - name: model-canary
              routes:
                - primary
      steps:
        - setWeight: 10
        - pause: { duration: 5m }
        - setWeight: 25
        - pause: { duration: 10m }
        - setWeight: 50
        - pause: { duration: 15m }
        - setWeight: 75
        - pause: { duration: 10m }
        - setWeight: 100
      analysis:
        templates:
          - templateName: model-quality-check
        startingStep: 2
        args:
          - name: canary-service
            value: model-v2
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: model-quality-check
  namespace: ai-serving
spec:
  args:
    - name: canary-service
  metrics:
    - name: error-rate
      interval: 30s
      count: 10
      successCondition: result[0] <= 0.01
      provider:
        prometheus:
          query: |
            sum(rate(http_requests_total{service="{{args.canary-service}}",code=~"5xx"}[1m]))
            /
            sum(rate(http_requests_total{service="{{args.canary-service}}"}[1m]))
    - name: latency-p99
      interval: 30s
      count: 10
      successCondition: result[0] <= 500
      provider:
        prometheus:
          query: |
            histogram_quantile(0.99,
              sum(rate(http_request_duration_seconds_bucket{service="{{args.canary-service}}"}[1m]))
              by (le)
            ) * 1000

パターン4:マルチモデルサービングとバージョン管理

マルチバージョンモデル共存アーキテクチャ

本番環境では、異なるビジネスラインが異なるモデルバージョンに依存する場合があります。Sidecarプロキシは複数のモデルバージョンを同時に管理し、バージョン共存とスムーズな移行を実現します。

┌─────────────────────────────────────────────────────┐
│              Multi-Model Serving Pod                 │
│                                                     │
│  ┌──────────┐     ┌────────────────────────────┐   │
│  │ Business  │     │  Model Router Sidecar      │   │
│  │ App       │────▶│                            │   │
│  │           │     │  /v1/chat ──▶ v2.5 model   │   │
│  │           │     │  /v1/embed ──▶ embed model │   │
│  │           │     │  /v1/rerank─▶ rerank model │   │
│  └──────────┘     └────────────┬───────────────┘   │
│                                  │                  │
│              ┌───────────────────┼───────────────┐  │
│              ▼                   ▼               ▼  │
│        ┌──────────┐      ┌──────────┐     ┌────────┐│
│        │ vLLM     │      │ TEI      │     │ TEI    ││
│        │ Chat     │      │ Embed    │     │ Rerank ││
│        │ :8000    │      │ :8080    │     │ :8081  ││
│        └──────────┘      └──────────┘     └────────┘│
└─────────────────────────────────────────────────────┘

マルチモデルDeployment設定

apiVersion: apps/v1
kind: Deployment
metadata:
  name: multi-model-serving
  namespace: ai-serving
spec:
  replicas: 2
  selector:
    matchLabels:
      app: multi-model
  template:
    metadata:
      labels:
        app: multi-model
    spec:
      containers:
        - name: chat-model
          image: vllm/vllm-openai:v0.6.0
          ports:
            - containerPort: 8000
          env:
            - name: MODEL_NAME
              value: "Qwen/Qwen2.5-32B-Instruct"
            - name: PORT
              value: "8000"
          resources:
            requests:
              nvidia.com/gpu: "1"
            limits:
              nvidia.com/gpu: "1"
          volumeMounts:
            - name: model-cache
              mountPath: /root/.cache/huggingface
        - name: embedding-model
          image: ghcr.io/huggingface/text-embeddings-inference:latest
          ports:
            - containerPort: 8080
          env:
            - name: MODEL_ID
              value: "BAAI/bge-large-zh-v1.5"
            - name: PORT
              value: "8080"
          resources:
            requests:
              cpu: "2"
              memory: "4Gi"
        - name: rerank-model
          image: ghcr.io/huggingface/text-embeddings-inference:latest
          ports:
            - containerPort: 8081
          env:
            - name: MODEL_ID
              value: "BAAI/bge-reranker-v2-m3"
            - name: PORT
              value: "8081"
            - name: RERANK
              value: "true"
          resources:
            requests:
              cpu: "2"
              memory: "4Gi"
      volumes:
        - name: model-cache
          persistentVolumeClaim:
            claimName: model-cache-pvc

モデルバージョン管理CRD

apiVersion: ai.toolsku.dev/v1alpha1
kind: ModelVersion
metadata:
  name: qwen-chat-v2-5
  namespace: ai-serving
spec:
  modelName: qwen-chat
  version: "2.5"
  framework: vllm
  source:
    huggingFace:
      modelId: Qwen/Qwen2.5-32B-Instruct
      revision: main
  serving:
    port: 8000
    maxBatchSize: 32
    gpuMemoryUtilization: 0.9
  routing:
    weight: 80
    canary: false
  healthCheck:
    endpoint: /health
    interval: 10s
    timeout: 5s
    unhealthyThreshold: 3
---
apiVersion: ai.toolsku.dev/v1alpha1
kind: ModelVersion
metadata:
  name: qwen-chat-v2-6-canary
  namespace: ai-serving
spec:
  modelName: qwen-chat
  version: "2.6"
  framework: vllm
  source:
    huggingFace:
      modelId: Qwen/Qwen2.6-32B-Instruct
      revision: main
  serving:
    port: 8000
    maxBatchSize: 32
    gpuMemoryUtilization: 0.9
  routing:
    weight: 20
    canary: true
  healthCheck:
    endpoint: /health
    interval: 10s
    timeout: 5s
    unhealthyThreshold: 3

パターン5:GPUリソースプーリング

GPU共有とタイムスライシング

GPUはAI推論で最も高価なリソースです。SidecarプロキシはGPUリソースプーリングを実現し、複数の推論サービスが同じGPUをタイムスライシングで共有し、利用率を向上させます。

┌─────────────────────────────────────────────────────┐
│               GPU Resource Pooling                   │
│                                                     │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐            │
│  │ Pod A   │  │ Pod B   │  │ Pod C   │            │
│  │ Sidecar │  │ Sidecar │  │ Sidecar │            │
│  └────┬────┘  └────┬────┘  └────┬────┘            │
│       │            │            │                  │
│       ▼            ▼            ▼                  │
│  ┌──────────────────────────────────────────┐      │
│  │         GPU Scheduler Sidecar            │      │
│  │                                          │      │
│  │  Time-Slicing:                           │      │
│  │  GPU 0: [A][A][B][B][C][A][A][B][B][C]  │      │
│  │  GPU 1: [C][C][A][B][C][C][A][B][C][C]  │      │
│  │                                          │      │
│  │  Memory Partitioning:                    │      │
│  │  GPU 0: 40% A | 35% B | 25% C           │      │
│  │  GPU 1: 30% A | 40% B | 30% C           │      │
│  └──────────────────────────────────────────┘      │
│       │            │                               │
│       ▼            ▼                               │
│  ┌──────────┐ ┌──────────┐                        │
│  │  GPU 0   │ │  GPU 1   │                        │
│  │ A100 80G │ │ A100 80G │                        │
│  └──────────┘ └──────────┘                        │
└─────────────────────────────────────────────────────┘

GPUタイムスライシング設定

apiVersion: v1
kind: ConfigMap
metadata:
  name: gpu-scheduler-config
  namespace: ai-serving
data:
  scheduler.yaml: |
    scheduling:
      strategy: time-slicing
      gpuGroups:
        - name: inference-pool
          gpuIds: [0, 1, 2, 3]
          timeSliceInterval: 100ms
          maxSharesPerGpu: 4
          memoryLimitPerShare: 20Gi
        - name: embedding-pool
          gpuIds: [4, 5]
          timeSliceInterval: 50ms
          maxSharesPerGpu: 8
          memoryLimitPerShare: 10Gi
    policies:
      - modelType: chat
        gpuGroup: inference-pool
        minShares: 1
        maxShares: 2
        priority: high
      - modelType: embedding
        gpuGroup: embedding-pool
        minShares: 1
        maxShares: 4
        priority: medium

GPUリソースクォータ管理

apiVersion: v1
kind: ResourceQuota
metadata:
  name: gpu-quota
  namespace: ai-serving
spec:
  hard:
    requests.nvidia.com/gpu: "8"
    limits.nvidia.com/gpu: "8"
    requests.nvidia.com/gpu-share: "32"
    limits.nvidia.com/gpu-share: "32"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: gpu-high-priority
value: 1000000
globalDefault: false
description: "High priority for latency-sensitive AI inference"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: gpu-low-priority
value: 100000
globalDefault: false
description: "Low priority for batch inference jobs"

Python GPUスケジューラ

import asyncio
import time
import logging
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from enum import Enum

logger = logging.getLogger(__name__)

class TaskPriority(Enum):
    HIGH = 1
    MEDIUM = 2
    LOW = 3

@dataclass
class InferenceTask:
    taskId: str
    modelId: str
    gpuMemoryRequired: int
    priority: TaskPriority
    maxLatencyMs: int
    submittedAt: float = field(default_factory=time.time)
    assignedGpu: Optional[int] = None

@dataclass
class GpuSlot:
    gpuId: int
    totalMemory: int
    usedMemory: int = 0
    currentModel: Optional[str] = None
    lastUsed: float = field(default_factory=time.time)

    @property
    def availableMemory(self) -> int:
        return self.totalMemory - self.usedMemory

    @property
    def utilization(self) -> float:
        return self.usedMemory / self.totalMemory if self.totalMemory > 0 else 0.0

class GpuScheduler:
    def __init__(self, gpuSlots: List[GpuSlot], maxQueueSize: int = 1000):
        self.gpuSlots = {slot.gpuId: slot for slot in gpuSlots}
        self.taskQueue: List[InferenceTask] = []
        self.maxQueueSize = maxQueueSize
        self._lock = asyncio.Lock()
        self._stats = {"totalScheduled": 0, "totalRejected": 0, "totalEvicted": 0}

    async def submitTask(self, task: InferenceTask) -> Optional[int]:
        async with self._lock:
            if len(self.taskQueue) >= self.maxQueueSize:
                self._stats["totalRejected"] += 1
                return None
            gpuId = self._findBestGpu(task)
            if gpuId is not None:
                slot = self.gpuSlots[gpuId]
                slot.usedMemory += task.gpuMemoryRequired
                slot.currentModel = task.modelId
                slot.lastUsed = time.time()
                task.assignedGpu = gpuId
                self._stats["totalScheduled"] += 1
                return gpuId
            self.taskQueue.append(task)
            self.taskQueue.sort(key=lambda t: t.priority.value)
            return None

    def _findBestGpu(self, task: InferenceTask) -> Optional[int]:
        candidates = [(gid, s) for gid, s in self.gpuSlots.items() if s.availableMemory >= task.gpuMemoryRequired]
        if not candidates:
            return self._tryEvict(task)
        candidates.sort(key=lambda x: x[1].utilization)
        return candidates[0][0]

    def _tryEvict(self, task: InferenceTask) -> Optional[int]:
        if task.priority != TaskPriority.HIGH:
            return None
        for gpuId, slot in self.gpuSlots.items():
            if slot.currentModel and slot.lastUsed < time.time() - 300:
                slot.usedMemory = 0
                slot.currentModel = None
                self._stats["totalEvicted"] += 1
                return gpuId
        return None

    async def releaseGpu(self, gpuId: int, memoryFreed: int):
        async with self._lock:
            slot = self.gpuSlots.get(gpuId)
            if slot:
                slot.usedMemory = max(0, slot.usedMemory - memoryFreed)
                if slot.usedMemory == 0:
                    slot.currentModel = None
                if self.taskQueue:
                    nextTask = self.taskQueue[0]
                    if slot.availableMemory >= nextTask.gpuMemoryRequired:
                        self.taskQueue.pop(0)
                        slot.usedMemory += nextTask.gpuMemoryRequired
                        slot.currentModel = nextTask.modelId
                        nextTask.assignedGpu = gpuId
                        self._stats["totalScheduled"] += 1

    def getStats(self) -> Dict:
        return {
            **self._stats,
            "queueSize": len(self.taskQueue),
            "gpuUtilization": {
                gid: {"utilization": f"{s.utilization:.1%}", "availableMemory": f"{s.availableMemory}MB", "currentModel": s.currentModel}
                for gid, s in self.gpuSlots.items()
            },
        }

パターン6:バッチ処理とリクエストマージ

動的バッチングアーキテクチャ

LLM推論のGPU利用率は通常低く(10-30%)、各リクエストが個別に処理されるためです。Sidecarプロキシは短時間ウィンドウ内の複数リクエストを収集し、単一のバッチ推論にマージすることでスループットを大幅に向上させます。

┌─────────────────────────────────────────────────────┐
│           Dynamic Batching Sidecar                   │
│                                                     │
│  Request 1 ──▶ ┐                                    │
│  Request 2 ──▶ │  ┌──────────────────────────┐     │
│  Request 3 ──▶ ├─▶│  Batch Window (50ms)     │     │
│  Request 4 ──▶ │  │                          │     │
│  Request 5 ──▶ ┘  │  Collect → Merge → Send  │     │
│                   │                          │     │
│                   │  Batch Size: 4-32        │     │
│                   │  Max Wait: 50ms          │     │
│                   └──────────┬───────────────┘     │
│                              │                     │
│                              ▼                     │
│                   ┌──────────────────────┐         │
│                   │  Model Server        │         │
│                   │  (vLLM with          │         │
│                   │   continuous         │         │
│                   │   batching)          │         │
│                   └──────────────────────┘         │
│                                                     │
│  Throughput: 1x → 5-8x                             │
│  Latency overhead: +5-15ms                         │
└─────────────────────────────────────────────────────┘

バッチプロキシ設定

apiVersion: v1
kind: ConfigMap
metadata:
  name: batch-proxy-config
  namespace: ai-serving
data:
  proxy.yaml: |
    server:
      port: 15001
      readTimeout: 30s
      writeTimeout: 120s
    batching:
      enabled: true
      maxBatchSize: 32
      maxWaitTimeMs: 50
      maxRequestTokens: 8192
      strategy: dynamic
    routing:
      defaultEndpoint: http://localhost:8000
      endpoints:
        - path: /v1/chat/completions
          model: chat
          batchEnabled: true
        - path: /v1/embeddings
          model: embedding
          batchEnabled: true
          maxBatchSize: 64
    circuitBreaker:
      enabled: true
      failureThreshold: 5
      recoveryTimeout: 30s
      halfOpenRequests: 3

Pythonバッチプロキシ

import asyncio
import time
import uuid
import logging
from dataclasses import dataclass, field
from typing import Dict, List, Any, Optional
from collections import defaultdict

logger = logging.getLogger(__name__)

@dataclass
class BatchRequest:
    requestId: str
    payload: Dict[str, Any]
    future: asyncio.Future
    submittedAt: float = field(default_factory=time.time)

@dataclass
class BatchConfig:
    maxBatchSize: int = 32
    maxWaitTimeMs: int = 50
    maxRequestTokens: int = 8192

class DynamicBatcher:
    def __init__(self, config: BatchConfig, inferenceFn):
        self.config = config
        self.inferenceFn = inferenceFn
        self.pendingRequests: Dict[str, List[BatchRequest]] = defaultdict(list)
        self._running = False
        self._stats = {"totalBatches": 0, "totalRequests": 0, "avgBatchSize": 0.0, "avgWaitTimeMs": 0.0}

    async def start(self):
        self._running = True
        asyncio.create_task(self._batchLoop())

    async def stop(self):
        self._running = False

    async def submit(self, model: str, payload: Dict[str, Any]) -> Any:
        future = asyncio.get_event_loop().create_future()
        request = BatchRequest(requestId=str(uuid.uuid4()), payload=payload, future=future)
        self.pendingRequests[model].append(request)
        self._stats["totalRequests"] += 1
        if len(self.pendingRequests[model]) >= self.config.maxBatchSize:
            asyncio.create_task(self._processBatch(model))
        return await future

    async def _batchLoop(self):
        while self._running:
            for model in list(self.pendingRequests.keys()):
                if self.pendingRequests[model]:
                    oldest = self.pendingRequests[model][0]
                    waitTime = (time.time() - oldest.submittedAt) * 1000
                    if waitTime >= self.config.maxWaitTimeMs:
                        await self._processBatch(model)
            await asyncio.sleep(0.005)

    async def _processBatch(self, model: str):
        requests = self.pendingRequests[model][:self.config.maxBatchSize]
        self.pendingRequests[model] = self.pendingRequests[model][len(requests):]
        if not requests:
            return
        batchSize = len(requests)
        self._stats["totalBatches"] += 1
        waitTimes = [(time.time() - r.submittedAt) * 1000 for r in requests]
        avgWait = sum(waitTimes) / len(waitTimes)
        self._stats["avgWaitTimeMs"] = self._stats["avgWaitTimeMs"] * 0.95 + avgWait * 0.05
        self._stats["avgBatchSize"] = self._stats["avgBatchSize"] * 0.95 + batchSize * 0.05
        try:
            batchPayload = self._mergePayloads([r.payload for r in requests])
            results = await self.inferenceFn(model, batchPayload)
            for i, request in enumerate(requests):
                if not request.future.done():
                    request.future.set_result(results[i])
        except Exception as e:
            for request in requests:
                if not request.future.done():
                    request.future.set_exception(e)

    def _mergePayloads(self, payloads: List[Dict]) -> Dict:
        messages = []
        for payload in payloads:
            if "messages" in payload:
                messages.append(payload["messages"])
            elif "prompt" in payload:
                messages.append([{"role": "user", "content": payload["prompt"]}])
        return {"model": payloads[0].get("model", "default"), "messages": messages, "stream": False, "batch_size": len(payloads)}

パターン7:オブザーバビリティと分散トレーシング

フルチェーントレーシングアーキテクチャ

AI推論チェーンは通常、複数のコンポーネント(APIゲートウェイ → Sidecarプロキシ → モデルサーバー → GPUスケジューラ)で構成されます。OpenTelemetryはフルチェーントレーシングを実現し、パフォーマンスボトルネックの特定を支援します。

┌─────────────────────────────────────────────────────────────┐
│                    Observability Stack                        │
│                                                             │
│  Request ──▶ [Gateway] ──▶ [Sidecar] ──▶ [Model Server]   │
│      │           │            │              │              │
│      ▼           ▼            ▼              ▼              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              OpenTelemetry Collector                 │   │
│  │                                                     │   │
│  │  Traces ──▶ Jaeger/Tempo                            │   │
│  │  Metrics ──▶ Prometheus                             │   │
│  │  Logs   ──▶ Loki/Elasticsearch                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Key Metrics:                                               │
│  - inference_latency_ms (P50/P95/P99)                      │
│  - model_tokens_per_second                                 │
│  - gpu_utilization_percent                                 │
│  - batch_size_avg                                          │
│  - request_queue_depth                                     │
│  - model_load_time_seconds                                 │
└─────────────────────────────────────────────────────────────┘

OpenTelemetry Sidecar設定

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ai-inference-obs
  namespace: ai-serving
spec:
  replicas: 2
  selector:
    matchLabels:
      app: ai-inference-obs
  template:
    metadata:
      labels:
        app: ai-inference-obs
      annotations:
        sidecar.opentelemetry.io/inject: "true"
        instrumentation.opentelemetry.io/inject-go: "true"
    spec:
      containers:
        - name: business-app
          image: myregistry/ai-business-app:v2.1.0
          ports:
            - containerPort: 8080
          env:
            - name: OTEL_SERVICE_NAME
              value: "ai-inference-app"
            - name: OTEL_EXPORTER_OTLP_ENDPOINT
              value: "http://otel-collector:4317"
            - name: OTEL_TRACES_SAMPLER
              value: "parentbased_traceidratio"
            - name: OTEL_TRACES_SAMPLER_ARG
              value: "0.1"
        - name: otel-sidecar
          image: otel/opentelemetry-collector-contrib:latest
          ports:
            - containerPort: 4317
            - containerPort: 4318
          volumeMounts:
            - name: otel-config
              mountPath: /etc/otelcol-contrib/config.yaml
              subPath: config.yaml
      volumes:
        - name: otel-config
          configMap:
            name: otel-sidecar-config
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-sidecar-config
  namespace: ai-serving
data:
  config.yaml: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318
    processors:
      batch:
        timeout: 5s
        send_batch_size: 1024
      memory_limiter:
        check_interval: 1s
        limit_mib: 512
      transform:
        error_mode: ignore
        trace_statements:
          - context: span
            statements:
              - set(attributes["ai.model.name"], attributes["model"]) where attributes["model"] != nil
              - set(attributes["ai.inference.latency_ms"], attributes["duration"]/1000000) where attributes["duration"] != nil
    exporters:
      otlp/jaeger:
        endpoint: jaeger-collector:4317
        tls:
          insecure: true
      prometheusremotewrite:
        endpoint: http://prometheus:9090/api/v1/write
      loki:
        endpoint: http://loki:3100/loki/api/v1/push
    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, transform, batch]
          exporters: [otlp/jaeger]
        metrics:
          receivers: [otlp]
          processors: [memory_limiter, transform, batch]
          exporters: [prometheusremotewrite]
        logs:
          receivers: [otlp]
          processors: [memory_limiter, batch]
          exporters: [loki]

カスタム推論メトリクス

apiVersion: v1
kind: ConfigMap
metadata:
  name: inference-metrics-rules
  namespace: ai-serving
data:
  rules.yaml: |
    groups:
      - name: ai_inference_metrics
        interval: 15s
        rules:
          - record: ai:inference:latency_p99
            expr: histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{job="ai-inference"}[5m])) by (le, model))
          - record: ai:inference:tokens_per_second
            expr: sum(rate(ai_inference_tokens_total{job="ai-inference"}[5m])) by (model)
          - record: ai:inference:gpu_utilization
            expr: avg(DCGM_FI_DEV_GPU_UTIL{gpu="*"}) by (instance, gpu)
          - alert: InferenceHighLatency
            expr: ai:inference:latency_p99 > 5
            for: 5m
            labels:
              severity: warning
            annotations:
              summary: "AI推論P99レイテンシが5秒を超過"
          - alert: GPUUtilizationLow
            expr: ai:inference:gpu_utilization < 30
            for: 10m
            labels:
              severity: info
            annotations:
              summary: "GPU利用率が30%未満"

5つのよくある落とし穴と解決策

落とし穴1:Sidecar起動順序によるリクエスト損失

現象:ビジネスコンテナがSidecarプロキシより先に起動し、初期推論リクエストがlocalhostに送信されるがSidecarが準備できておらず、リクエストが失敗する。

解決策postStartフックを使用してSidecarの準備完了後にビジネスコンテナを起動するか、ビジネスコンテナのreadinessProbeをSidecarのヘルスチェックに依存させる。

spec:
  containers:
    - name: business-app
      readinessProbe:
        httpGet:
          path: /health
          port: 15001
        initialDelaySeconds: 5
        periodSeconds: 5
    - name: sidecar-proxy
      lifecycle:
        postStart:
          exec:
            command: ["/bin/sh", "-c", "until curl -s http://localhost:15001/health; do sleep 1; done"]

落とし穴2:iptablesルールとGPUドライバの競合

現象:Sidecarのiptablesトラフィック傍受ルールがNVIDIA GPUドライバ通信エラーを引き起こし、モデル読み込みが失敗する。

解決策:GPU通信ポートとIPレンジをiptables傍受から除外する。

metadata:
  annotations:
    traffic.sidecar.istio.io/excludeOutboundIPRanges: "10.96.0.0/12"
    traffic.sidecar.istio.io/excludeOutboundPorts: "50051,50052"

落とし穴3:大規模モデルリクエストボディがEnvoyバッファを超過

現象:LLM推論リクエストのpromptが非常に長く(数十KB)、Envoyのデフォルトバッファサイズを超過し、リクエストが切り詰められたり413エラーが発生する。

解決策:Envoyのリクエストバッファサイズを増やすか、ストリーミング転送を設定する。

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: model-dest
  namespace: ai-serving
spec:
  host: model-service
  trafficPolicy:
    connectionPool:
      http:
        h2UpgradePolicy: UPGRADE
        maxRequestsPerConnection: 100

落とし穴4:Sidecarリソース競合による推論レイテンシスパイク

現象:SidecarプロキシとモデルサービスがPodを共有し、CPU/メモリリソースの競合により推論レイテンシにスパイクが発生する。

解決策:Sidecarに独立したリソース制限を設定し、cpumanagerのstaticポリシーでCPUピニングを使用する。

spec:
  containers:
    - name: sidecar-proxy
      resources:
        requests:
          cpu: "200m"
          memory: "256Mi"
        limits:
          cpu: "500m"
          memory: "512Mi"
  runtimeClassName: nvidia
  overhead:
    podFixed:
      cpu: "200m"
      memory: "256Mi"

落とし穴5:モデルホットロード時のSidecar接続プール枯渇

現象:モデルバージョン切り替え時に古い接続が閉じられず、新しい接続の作成に失敗し、接続プールが枯渇する。

解決策:適切な接続プールタイムアウトとアイドル接続リサイクルポリシーを設定する。

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: model-conn-pool
  namespace: ai-serving
spec:
  host: model-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 1000
        connectTimeout: 10s
        idleTimeout: 60s
      http:
        maxRequestsPerConnection: 50

10のよくあるエラートラブルシューティング

# エラーメッセージ 原因 解決策
1 Sidecar proxy not ready iptablesルールが注入されていないかSidecarコンテナが起動していない namespaceラベルistio-injection=enabledを確認、Sidecarイメージのプル成功を確認
2 upstream connect error or disconnect/reset before headers モデルサービスが準備できていないかポートが不一致 モデルコンテナの健全性を確認、ポートがVirtualServiceと一致するか確認
3 GPU out of memory モデル読み込みがGPUメモリを超過 gpu_memory_utilizationを下げる、量子化モデルを使用、またはGPUを追加
4 connection refused to 127.0.0.1:15001 Sidecarが期待されるポートでリッスンしていない Sidecar設定を確認、inboundポート設定が正しいか確認
5 request body too large リクエストボディがEnvoyバッファ制限を超過 max_request_sizeを増やすかストリーミングを有効化
6 model not found in registry モデル名がルーティング設定と一致しない モデル登録名とルーティングルールのmodelフィールドを確認
7 circuit breaker open バックエンドモデルサービスの連続失敗がサーキットブレーカーをトリガー モデルサービスの健全性を確認、サーキットブレーカー閾値を調整
8 timeout waiting for batch completion バッチ待機タイムアウト maxWaitTimeMsを増やすかmaxBatchSizeを減らす
9 CUDA error: no kernel image is available GPUドライバとCUDAバージョンの非互換 NVIDIAドライババージョンとコンテナCUDAバージョンの一致を確認
10 OOMKilled for sidecar container Sidecarのメモリ不足でK8sにキルされる Sidecarのmemory limitを増やす、メモリリークを確認

高度な最適化テクニック

1. アダプティブバッチングウィンドウ

リアルタイム負荷に基づいてバッチングウィンドウサイズを動的に調整:

class AdaptiveBatcher:
    def __init__(self, minWaitMs=10, maxWaitMs=100, targetBatchSize=16):
        self.minWaitMs = minWaitMs
        self.maxWaitMs = maxWaitMs
        self.targetBatchSize = targetBatchSize
        self.currentWaitMs = minWaitMs
        self._emaArrivalRate = 0.0

    def updateWaitTime(self, queueSize: int, intervalMs: float):
        if intervalMs > 0:
            arrivalRate = queueSize / (intervalMs / 1000.0)
            self._emaArrivalRate = 0.7 * self._emaArrivalRate + 0.3 * arrivalRate
        if self._emaArrivalRate > 0:
            optimalWait = (self.targetBatchSize / self._emaArrivalRate) * 1000
            self.currentWaitMs = max(self.minWaitMs, min(self.maxWaitMs, optimalWait))
        else:
            self.currentWaitMs = self.maxWaitMs

2. モデルウォームアップとコールドスタート最適化

apiVersion: v1
kind: ConfigMap
metadata:
  name: model-warmup-config
  namespace: ai-serving
data:
  warmup.yaml: |
    models:
      - name: qwen-chat-v2.5
        warmupRequests:
          - prompt: "Hello, how are you?"
            maxTokens: 32
          - prompt: "Explain quantum computing in one sentence."
            maxTokens: 64
        warmupInterval: 300s
        maxWarmupRetries: 3

3. 推論結果キャッシング

同じpromptの推論結果をキャッシュし、重複計算を回避:

apiVersion: v1
kind: ConfigMap
metadata:
  name: inference-cache-config
  namespace: ai-serving
data:
  cache.yaml: |
    enabled: true
    backend: redis
    redis:
      endpoint: redis://redis-cluster:6379
      ttl: 3600
      maxMemory: 2gb
    keyStrategy: prompt_hash
    cacheableModels:
      - qwen-chat-v2.5
      - bge-embedding-v1.5
    hitRateThreshold: 0.3

4. リクエスト優先度とプリエンプション

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: realtime-inference
value: 1000000
globalDefault: false
description: "厳格なレイテンシSLAを持つリアルタイム推論リクエスト"
preemptionPolicy: PreemptLowerPriority
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: batch-inference
value: 10000
globalDefault: false
description: "レイテンシSLAのないバッチ推論"
preemptionPolicy: Never

比較分析:Sidecarプロキシ vs Service Mesh vs Gateway

次元 Sidecarプロキシ Service Mesh (Istio) API Gateway
デプロイ位置 Pod内、ビジネスコンテナと共存 Pod内、メッシュ全体をカバー クラスタイングレス、独立デプロイ
トラフィック傍受 iptables/ebpf iptables/ztunnel DNS/仮想IP
モデルルーティング カスタムロジック、柔軟 VirtualService、宣言的 Routeルール、限定的
バッチ処理 ネイティブサポート、深いカスタマイズ可能 サポートなし サポートなし
GPU認識 GPUリソースを認識可能 認識なし 認識なし
パフォーマンスオーバーヘッド 低(5-15ms) 中(10-30ms) 低(2-5ms)
オブザーバビリティ カスタムメトリクス フルメッシュメトリクス イングレスメトリクス
運用複雑度
適用シナリオ AI推論専用プロキシ クラスタ全体のサービス通信 外部トラフィックイングレス
学習曲線

推奨戦略:AI推論シナリオでは専用Sidecarプロキシでモデルルーティングとバッチ処理を扱い、Service Meshでサービス間通信を、API Gatewayで外部イングレスを処理する。3層がそれぞれの責務を担い、相互に干渉しない。


オンラインツール推奨

  • YAML/JSONフォーマッター/ja/json/format — K8s YAML設定のフォーマット
  • Base64エンコード/デコード/ja/encode/base64 — Secretの証明書とキーの処理
  • curl to Code/ja/dev/curl-to-code — APIテストコードの迅速生成

関連記事


まとめ

K8s Sidecar AI推論プロキシは2026年にAI推論デプロイメントの標準アーキテクチャパターンとなりました。7つのプロダクションパターンは、トラフィックインターセプトからオブザーバビリティまでの完全なチェーンをカバーしています:Envoyトラフィックインターセプトでビジネスコードの変更ゼロを実現、スマートモデルルーティングでリクエスト特性に基づき動的にモデルを選択、A/Bテストとカナリアデプロイでモデルリリースの安全性を確保、マルチモデルサービングでバージョン共存を実現、GPUリソースプーリングで利用率を30%から80%以上に向上、バッチマージでスループットを5-8倍に向上、OpenTelemetryフルチェーントレーシングでパフォーマンスボトルネックを可視化。コア原則:Sidecarプロキシは推論ロジックに集中、ビジネスコンテナはビジネスロジックに集中、localhostで通信、ゼロ結合ゼロ侵入。


外部参考

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

#Kubernetes#Sidecar#AI推理#模型路由#流量拦截#K8s部署#2026#DevOps