Blazor Pod Scheduling

C# Blazor Pod Scheduling Kubernetes Docker SignalR WebSocket Sticky Session HPA Health Check .NET 8 Razor Components

AspectBlazor ServerBlazor WASMBlazor United (.NET 8)
RenderServer (SignalR)Browser (WASM)ทั้งสอง (per component)
Sticky Sessionต้องใช้ไม่ต้องขึ้นกับ Component
Image Size80-120MB20-30MB (Nginx)80-120MB
ResourceCPU+RAM มากน้อย (Static)ปานกลาง
Scaleซับซ้อน (Session)ง่าย (Stateless)ปานกลาง
Offlineไม่ได้ได้บางส่วน

Docker Configuration

# === Blazor Docker Setup ===

# Dockerfile (Blazor Server)
# FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
# WORKDIR /src
# COPY *.csproj ./
# RUN dotnet restore
# COPY . .
# RUN dotnet publish -c Release -o /app/publish
#
# FROM mcr.microsoft.com/dotnet/aspnet:8.0
# WORKDIR /app
# COPY --from=build /app/publish .
# ENV ASPNETCORE_URLS=http://+:8080
# ENV ASPNETCORE_ENVIRONMENT=Production
# EXPOSE 8080
# HEALTHCHECK --interval=30s --timeout=3s \
#   CMD curl -f http://localhost:8080/health || exit 1
# ENTRYPOINT ["dotnet", "BlazorApp.dll"]

# Program.cs Health Checks
# var builder = WebApplication.CreateBuilder(args);
# builder.Services.AddHealthChecks()
#     .AddDbContextCheck("database")
#     .AddRedis(builder.Configuration["Redis:Connection"], "redis")
#     .AddUrlGroup(new Uri("https://api.example.com/health"), "external-api");
#
# var app = builder.Build();
# app.MapHealthChecks("/health");
# app.MapHealthChecks("/health/ready", new HealthCheckOptions {
#     Predicate = check => check.Tags.Contains("ready")
# });

from dataclasses import dataclass

@dataclass
class DockerImage:
    variant: str
    base_image: str
    size: str
    use_case: str

images = [
    DockerImage("Blazor Server",
        "mcr.microsoft.com/dotnet/aspnet:8.0",
        "80-120MB",
        "Server-side rendering SignalR WebSocket"),
    DockerImage("Blazor WASM (Nginx)",
        "nginx:alpine",
        "20-30MB",
        "Client-side WASM Static Files"),
    DockerImage("Blazor United",
        "mcr.microsoft.com/dotnet/aspnet:8.0",
        "80-120MB",
        "Mixed render modes Server + WASM"),
    DockerImage("SDK (Build Stage)",
        "mcr.microsoft.com/dotnet/sdk:8.0",
        "~700MB (build only)",
        "Compile Publish ไม่อยู่ใน Final"),
]

print("=== Docker Images ===")
for d in images:
    print(f"  [{d.variant}] {d.base_image}")
    print(f"    Size: {d.size} | Use: {d.use_case}")

Kubernetes Deployment

# === Blazor Server Kubernetes Config ===

# apiVersion: apps/v1
# kind: Deployment
# metadata:
#   name: blazor-server
# spec:
#   replicas: 3
#   template:
#     spec:
#       containers:
#         - name: blazor
#           image: registry.example.com/blazor-app:v1.0
#           ports:
#             - containerPort: 8080
#           env:
#             - name: ConnectionStrings__Default
#               valueFrom:
#                 secretKeyRef:
#                   name: blazor-secrets
#                   key: db-connection
#           resources:
#             requests:
#               cpu: 250m
#               memory: 256Mi
#             limits:
#               cpu: "1"
#               memory: 512Mi
#           livenessProbe:
#             httpGet:
#               path: /health
#               port: 8080
#             initialDelaySeconds: 10
#             periodSeconds: 15
#           readinessProbe:
#             httpGet:
#               path: /health/ready
#               port: 8080
#             initialDelaySeconds: 5
#             periodSeconds: 10
# ---
# # Ingress with Sticky Session (for SignalR)
# apiVersion: networking.k8s.io/v1
# kind: Ingress
# metadata:
#   annotations:
#     nginx.ingress.kubernetes.io/affinity: "cookie"
#     nginx.ingress.kubernetes.io/session-cookie-name: "blazor-affinity"
#     nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
#     nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"

@dataclass
class K8sConfig:
    resource: str
    key_setting: str
    blazor_server: str
    blazor_wasm: str

configs = [
    K8sConfig("Deployment",
        "replicas",
        "3 (ต้อง Sticky Session)",
        "3 (Stateless Scale ง่าย)"),
    K8sConfig("Ingress",
        "affinity",
        "cookie (SignalR WebSocket)",
        "ไม่ต้อง (Static Files)"),
    K8sConfig("Ingress",
        "proxy-timeout",
        "3600s (WebSocket long-lived)",
        "60s (ปกติ)"),
    K8sConfig("Service",
        "sessionAffinity",
        "ClientIP (Sticky Session)",
        "None (Round Robin)"),
    K8sConfig("HPA",
        "metric",
        "CPU > 70% (Min 2 Max 10)",
        "CPU > 70% (Min 2 Max 20)"),
    K8sConfig("Resources",
        "request/limit",
        "CPU 250m-1 Memory 256Mi-512Mi",
        "CPU 50m-200m Memory 64Mi-128Mi"),
]

print("=== K8s Configuration ===")
for c in configs:
    print(f"  [{c.resource}] {c.key_setting}")
    print(f"    Server: {c.blazor_server}")
    print(f"    WASM: {c.blazor_wasm}")

Scaling & Monitoring

# === Blazor Scaling Strategy ===

@dataclass
class ScaleStrategy:
    scenario: str
    challenge: str
    solution: str
    config: str

scaling = [
    ScaleStrategy("Blazor Server Scale Out",
        "SignalR WebSocket ต้อง Sticky Session ย้าย Pod ไม่ได้",
        "ใช้ Azure SignalR Service หรือ Redis Backplane",
        "AddSignalR().AddRedis(redisConnectionString)"),
    ScaleStrategy("Session Affinity Failover",
        "ถ้า Pod ตาย Session หาย User ต้อง Reconnect",
        "ตั้ง Circuit Reconnect UI แจ้ง User + Auto-reconnect",
        "Blazor.start({reconnectionOptions})"),
    ScaleStrategy("WASM CDN Distribution",
        "WASM Files 5-20MB ต้อง Download ครั้งแรก",
        "ใช้ CDN Cache + Brotli Compression + Service Worker",
        "Cache-Control: public, max-age=31536000"),
    ScaleStrategy("Database Connection Pool",
        "Pod เพิ่ม Connection Pool หมด",
        "ใช้ PgBouncer / ProxySQL + Connection Pool Limit",
        "MaxPoolSize=50 per Pod"),
]

@dataclass
class MonitorMetric:
    metric: str
    source: str
    alert: str

monitoring = [
    MonitorMetric("SignalR Connection Count",
        "dotnet_signalr_connections_current",
        "> 1000 connections/pod → Scale out"),
    MonitorMetric("Request Duration p99",
        "http_request_duration_seconds",
        "> 500ms → Check DB/External API"),
    MonitorMetric("Memory Usage",
        "container_memory_usage_bytes",
        "> 80% limit → Scale up/out"),
    MonitorMetric("Health Check Status",
        "/health endpoint",
        "Unhealthy → Alert + Auto-restart"),
    MonitorMetric("WebSocket Errors",
        "signalr_errors_total",
        "> 1% error rate → Check Network"),
]

print("=== Scaling ===")
for s in scaling:
    print(f"  [{s.scenario}]")
    print(f"    Challenge: {s.challenge}")
    print(f"    Solution: {s.solution}")

print("\n=== Monitoring ===")
for m in monitoring:
    print(f"  [{m.metric}] {m.source}")
    print(f"    Alert: {m.alert}")

เคล็ดลับ

  • SignalR: ใช้ Redis Backplane สำหรับ Blazor Server Multi-pod
  • Sticky: ตั้ง Cookie Affinity สำหรับ Ingress WebSocket
  • WASM: ใช้ Brotli Compression ลด WASM Download Size 60%
  • Health: ตั้ง Health Check ตรวจ DB Redis External API
  • .NET 8: ใช้ Blazor United เลือก Render Mode ต่อ Component

Blazor คืออะไร

Web Framework C# Microsoft Razor Components Server SignalR WASM WebAssembly United .NET 8 NuGet Entity Framework Interactive UI