Web 界面部署与维护:让用户通过浏览器使用

将 Agent 部署为 Web 服务意味着要面对真实的互联网环境——HTTPS 加密、跨域请求、WebSocket 长连接、CDN 加速、DDoS 防护等。与纯 API 部署不同,Web 界面部署还需要考虑前端资源托管、SEO、首屏加载速度等用户体验问题。

本文将深入讲解生产级 Web 部署的完整技术栈,从反向代理配置到高可用架构,从性能优化到安全防护。


启动 Web 服务:分离 API 与 WebUI

ADK Go 支持将 API 服务和 WebUI 分离部署,这在生产环境中是最佳实践:

// 生产环境推荐:API 和 WebUI 分离
func main() {
    ctx := context.Background()
    
    // 创建 Agent
    agent, err := createAgent(ctx)
    if err != nil {
        log.Fatal(err)
    }
    
    // API 服务(面向程序调用)
    apiRuntime, err := agentruntime.New(ctx,
        agentruntime.WithAgent(agent),
        agentruntime.WithPort(8080),
        agentruntime.WithCORS(agentruntime.CORSConfig{
            AllowedOrigins:   []string{"https://my-agent.example.com"},
            AllowedMethods:   []string{"GET", "POST", "OPTIONS"},
            AllowedHeaders:   []string{"Content-Type", "Authorization", "X-Request-ID"},
            AllowCredentials: true,
            MaxAge:           86400,
        }),
    )
    if err != nil {
        log.Fatal(err)
    }
    
    // WebUI 服务(面向浏览器用户)
    webUIRuntime, err := agentruntime.New(ctx,
        agentruntime.WithAgent(agent),
        agentruntime.WithPort(8081),
        agentruntime.WithWebUI(agentruntime.WebUIConfig{
            StaticPath:     "./webui/dist",  // 前端构建产物
            IndexPath:      "index.html",
            APIProxyTarget: "http://localhost:8080",  // 代理到 API 服务
        }),
    )
    if err != nil {
        log.Fatal(err)
    }
    
    // 启动两个服务
    errCh := make(chan error, 2)
    go func() { errCh <- apiRuntime.Serve() }()
    go func() { errCh <- webUIRuntime.Serve() }()
    
    if err := <-errCh; err != nil {
        log.Fatal(err)
    }
}

分离部署的优势

  1. 独立扩展:API 和 WebUI 的负载模式不同,可以分别扩容
  2. 独立维护:前端更新不需要重启 API 服务
  3. 安全隔离:WebUI 可以放在 CDN 后,API 可以限制内网访问
  4. 技术栈独立:前端可以用 Vite/Webpack 构建,与后端解耦

HTTPS 配置:生产环境的安全基线

使用 Nginx 反向代理(推荐)

Nginx 作为反向代理是生产环境的标准做法,它提供 SSL 终止、负载均衡、静态文件缓存等功能:

# /etc/nginx/sites-available/my-agent

# HTTP 重定向到 HTTPS
server {
    listen 80;
    server_name my-agent.example.com;
    return 301 https://$server_name$request_uri;
}

# HTTPS 服务
server {
    listen 443 ssl http2;
    server_name my-agent.example.com;

    # SSL 证书配置
    ssl_certificate /etc/ssl/certs/my-agent.example.com.crt;
    ssl_certificate_key /etc/ssl/private/my-agent.example.com.key;
    
    # SSL 安全加固
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
    
    # HSTS(强制 HTTPS)
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    
    # 安全响应头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    
    # 日志配置
    access_log /var/log/nginx/my-agent.access.log;
    error_log /var/log/nginx/my-agent.error.log;
    
    # Gzip 压缩
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;
    
    # API 代理(代理到后端服务)
    location /api/ {
        proxy_pass http://localhost:8080/;
        proxy_http_version 1.1;
        
        # 请求头转发
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Request-ID $request_id;
        
        # 超时配置(Agent 响应可能较慢)
        proxy_connect_timeout 30s;
        proxy_send_timeout 60s;
        proxy_read_timeout 300s;  # 5 分钟,适应长推理
        
        # SSE(Server-Sent Events)支持
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 86400;
        
        # 错误处理
        proxy_intercept_errors on;
        error_page 502 503 504 /50x.html;
    }
    
    # WebSocket 支持(如果需要实时通信)
    location /ws/ {
        proxy_pass http://localhost:8080/ws/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 86400;
    }
    
    # WebUI 静态文件
    location / {
        root /opt/my-agent/webui/dist;
        try_files $uri $uri/ /index.html;
        
        # 静态文件缓存
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
            access_log off;
        }
        
        # HTML 文件不缓存(支持前端路由)
        location ~* \.html$ {
            expires -1;
            add_header Cache-Control "no-cache, no-store, must-revalidate";
        }
    }
    
    # 错误页面
    location = /50x.html {
        root /usr/share/nginx/html;
        internal;
    }
    
    # 限制请求体大小(防止大文件上传导致内存问题)
    client_max_body_size 10m;
    
    # 限流配置(防止 DDoS)
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
    location /api/ {
        limit_req zone=api_limit burst=20 nodelay;
        # ... 其他配置
    }
}

使用 Let’s Encrypt 免费证书

# 安装 certbot
sudo apt-get install certbot python3-certbot-nginx

# 自动获取并配置证书
sudo certbot --nginx -d my-agent.example.com

# 测试自动续期
sudo certbot renew --dry-run

# 查看证书信息
sudo certbot certificates

自动续期配置

Let’s Encrypt 证书有效期 90 天,需要设置自动续期:

# 添加 crontab 任务
sudo crontab -e

# 添加以下行(每天凌晨 3 点尝试续期)
0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"

使用 Cloudflare 代理(增强安全)

如果域名使用 Cloudflare DNS,可以开启其代理服务获得额外保护:

  1. SSL/TLS 模式:设置为 “Full (strict)”
  2. Always Use HTTPS:开启
  3. Security Level:根据需求设置
  4. Bot Fight Mode:开启,防止恶意爬虫
  5. Rate Limiting Rules:配置 API 限流

高可用方案:从单点到集群

多实例 + 负载均衡架构

                              ┌─────────────┐
                              │   CDN /     │
                              │ Cloudflare  │
                              └──────┬──────┘
                              ┌──────▼──────┐
                              │   Nginx     │
                              │ (负载均衡)   │
                              │  + SSL 终止  │
                              └──────┬──────┘
                    ┌────────────────┼────────────────┐
                    ▼                ▼                ▼
             ┌──────────┐     ┌──────────┐     ┌──────────┐
             │ Runtime  │     │ Runtime  │     │ Runtime  │
             │ 实例 1   │     │ 实例 2   │     │ 实例 3   │
             │ :8080    │     │ :8081    │     │ :8082    │
             └────┬─────┘     └────┬─────┘     └────┬─────┘
                  │                │                │
                  └────────────────┼────────────────┘
                            ┌─────────────┐
                            │    Redis    │
                            │ (Session    │
                            │  共享存储)   │
                            └─────────────┘

Nginx 负载均衡配置

upstream runtime_backend {
    # 加权轮询(根据实例性能调整权重)
    server 127.0.0.1:8080 weight=3 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8081 weight=3 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8082 weight=3 max_fails=3 fail_timeout=30s;
    
    # 健康检查(需要 nginx-plus 或第三方模块)
    # health_check interval=5s fails=3 passes=2;
    
    # 连接池配置
    keepalive 32;
    keepalive_timeout 60s;
    keepalive_requests 1000;
}

server {
    location /api/ {
        proxy_pass http://runtime_backend/;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        
        # 启用连接复用
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # 超时配置
        proxy_connect_timeout 30s;
        proxy_send_timeout 60s;
        proxy_read_timeout 300s;
    }
}

Session 共享:Redis 集群配置

多实例部署必须解决 Session 共享问题,Redis 是最常用的方案:

import (
    "github.com/redis/go-redis/v9"
)

func createRedisClient() *redis.Client {
    return redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "",  // 生产环境必须设置密码
        DB:       0,
        
        // 连接池配置
        PoolSize:     100,              // 连接池大小
        MinIdleConns: 10,               // 最小空闲连接
        MaxRetries:   3,                // 最大重试次数
        DialTimeout:  5 * time.Second,  // 连接超时
        ReadTimeout:  3 * time.Second,  // 读取超时
        WriteTimeout: 3 * time.Second,  // 写入超时
        PoolTimeout:  4 * time.Second,  // 从池获取连接超时
        
        // 健康检查
        ConnMaxIdleTime: 30 * time.Minute,
        ConnMaxLifetime: 1 * time.Hour,
    })
}

// 生产环境:Redis Cluster
func createRedisCluster() *redis.ClusterClient {
    return redis.NewClusterClient(&redis.ClusterOptions{
        Addrs: []string{
            "redis-node-1:6379",
            "redis-node-2:6379",
            "redis-node-3:6379",
        },
        Password: "your-strong-password",
        
        // 集群配置
        PoolSize:        100,
        MinIdleConns:    10,
        MaxRetries:      3,
        DialTimeout:     5 * time.Second,
        ReadTimeout:     3 * time.Second,
        WriteTimeout:    3 * time.Second,
        
        // 路由配置
        RouteRandomly:   true,  // 读操作随机路由到从节点
        RouteByLatency:  false,
    })
}

// Runtime 配置 Redis Session 存储
runtime, err := agentruntime.New(ctx,
    agentruntime.WithAgent(agent),
    agentruntime.WithRedisStore(redisClient),
    agentruntime.WithSessionSerializer(agentruntime.JSONSerializer),  // JSON 序列化
)

Session 序列化性能对比

序列化方式速度体积可读性建议
JSON中等较大通用,调试方便
MessagePack高性能场景
Protobuf最快最小极致性能,但增加复杂度

运维监控:可观测性三支柱

健康检查端点

// 分层健康检查
func healthHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    
    health := HealthStatus{
        Status:    "healthy",
        Timestamp: time.Now().UTC(),
        Version:   version,
        Uptime:    time.Since(startTime).String(),
    }
    
    // 依赖检查
    checks := make(map[string]DependencyStatus)
    
    // Redis 检查
    if err := redisClient.Ping(ctx).Err(); err != nil {
        checks["redis"] = DependencyStatus{Status: "down", Error: err.Error()}
        health.Status = "unhealthy"
    } else {
        checks["redis"] = DependencyStatus{Status: "up", Latency: "2ms"}
    }
    
    // LLM API 检查(轻量级)
    if err := checkLLMHealth(ctx); err != nil {
        checks["llm"] = DependencyStatus{Status: "degraded", Error: err.Error()}
        if health.Status == "healthy" {
            health.Status = "degraded"
        }
    } else {
        checks["llm"] = DependencyStatus{Status: "up", Latency: "150ms"}
    }
    
    health.Checks = checks
    
    // 根据状态设置 HTTP 状态码
    switch health.Status {
    case "healthy":
        w.WriteHeader(http.StatusOK)
    case "degraded":
        w.WriteHeader(http.StatusOK)  // 降级仍返回 200,但标记状态
    case "unhealthy":
        w.WriteHeader(http.StatusServiceUnavailable)
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(health)
}

Prometheus 指标接入

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    // 请求计数器
    requestCounter = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "agent_requests_total",
            Help: "Total number of requests",
        },
        []string{"method", "endpoint", "status"},
    )
    
    // 请求延迟直方图
    requestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "agent_request_duration_seconds",
            Help:    "Request duration in seconds",
            Buckets: prometheus.DefBuckets,
        },
        []string{"method", "endpoint"},
    )
    
    // 活跃 Session 数
    activeSessions = prometheus.NewGauge(
        prometheus.GaugeOpts{
            Name: "agent_active_sessions",
            Help: "Number of active sessions",
        },
    )
    
    // LLM API 调用指标
    llmCalls = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "agent_llm_calls_total",
            Help: "Total number of LLM API calls",
        },
        []string{"model", "status"},
    )
    
    llmLatency = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "agent_llm_latency_seconds",
            Help:    "LLM API latency in seconds",
            Buckets: []float64{0.5, 1, 2, 5, 10, 30, 60},
        },
        []string{"model"},
    )
)

func init() {
    prometheus.MustRegister(requestCounter, requestDuration, activeSessions, llmCalls, llmLatency)
}

// 在 main 中启动 metrics server
func startMetricsServer(port int) {
    mux := http.NewServeMux()
    mux.Handle("/metrics", promhttp.Handler())
    
    server := &http.Server{
        Addr:    fmt.Sprintf(":%d", port),
        Handler: mux,
    }
    
    go func() {
        log.Printf("Metrics server starting on :%d", port)
        if err := server.ListenAndServe(); err != nil {
            log.Printf("Metrics server error: %v", err)
        }
    }()
}

Grafana 监控面板

关键监控指标:

指标告警阈值说明
请求 QPS-了解负载趋势
P99 延迟> 10s用户体验关键指标
错误率> 1%服务健康度
活跃 Session 数> MaxSessions * 0.8内存压力预警
LLM API 错误率> 5%依赖服务异常
Goroutine 数量> 10000可能的泄漏
内存使用> 80%OOM 风险
CPU 使用> 80%性能瓶颈

常见问题深度排查

Q:Nginx 代理后 WebSocket 不工作

根本原因:Nginx 默认不会转发 WebSocket 的 Upgrade 头。

完整解决方案

location /ws/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    
    # WebSocket 必需的头
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    
    # 其他头
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    
    # 长连接超时
    proxy_read_timeout 86400;
    proxy_send_timeout 86400;
}

验证方法

# 测试 WebSocket 连接
curl -i -N \
    -H "Connection: Upgrade" \
    -H "Upgrade: websocket" \
    -H "Host: my-agent.example.com" \
    -H "Origin: https://my-agent.example.com" \
    https://my-agent.example.com/ws/

Q:多实例下 Session 不一致

根本原因:默认 Session 存储在内存中,多实例间不共享。

解决方案

  1. 使用 Redis 共享 Session(推荐)
  2. 使用 Sticky Session(会话保持)
# Sticky Session 配置(基于 IP 哈希)
upstream runtime_backend {
    ip_hash;  # 同一 IP 总是路由到同一实例
    server 127.0.0.1:8080;
    server 127.0.0.1:8081;
    server 127.0.0.1:8082;
}

注意:Sticky Session 是临时方案,实例重启或扩缩容时会话会丢失。生产环境务必使用 Redis。

Q:CORS 问题

根本原因:浏览器的同源策略限制。

正确配置

// 后端配置
runtime, err := agentruntime.New(ctx,
    agentruntime.WithCORS(agentruntime.CORSConfig{
        // 不要在生产环境使用 "*"
        AllowedOrigins: []string{
            "https://my-agent.example.com",
            "https://app.example.com",
        },
        AllowedMethods: []string{"GET", "POST", "OPTIONS"},
        AllowedHeaders: []string{
            "Content-Type",
            "Authorization",
            "X-Request-ID",
            "X-CSRF-Token",
        },
        ExposedHeaders:   []string{"X-Request-ID"},
        AllowCredentials: true,  // 允许携带 Cookie
        MaxAge:           86400,
    }),
)
# Nginx 层也可以配置 CORS(作为兜底)
location /api/ {
    # CORS 预检请求
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'https://my-agent.example.com' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        add_header 'Access-Control-Max-Age' '86400' always;
        add_header 'Content-Length' '0';
        return 204;
    }
    
    add_header 'Access-Control-Allow-Origin' 'https://my-agent.example.com' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;
    
    proxy_pass http://backend;
}

性能优化清单

  • 启用 HTTP/2:Nginx 配置 listen 443 ssl http2
  • 启用 Gzip:压缩文本响应,减少 70%+ 传输量
  • 静态文件缓存:JS/CSS/图片设置 1 年缓存
  • CDN 加速:静态资源放到 CDN
  • 连接复用:Nginx keepalive 配置
  • 连接池:Redis 连接池、HTTP Client 连接池
  • 响应压缩:大 JSON 响应启用压缩
  • 请求合并:前端批量请求,减少连接数
  • 懒加载:WebUI 组件按需加载
  • 预连接<link rel="preconnect"> 加速关键资源

下一步

Web 部署搞定了,接下来看 Docker 容器化部署——构建镜像、部署到任何环境。

CLI 部署 | Docker 容器化部署 →


想跟着学更多 Go ADK 实战?关注「全栈之巅-梦兽编程」公众号,每周更新 Go / AI 编程实战干货。