Web 界面部署与维护:让用户通过浏览器使用
详解通过 ADK Go 的 Web 界面在生产环境部署 Agent——包括 HTTPS 配置、反向代理、高可用方案。
Table of Contents
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)
}
}
分离部署的优势:
- 独立扩展:API 和 WebUI 的负载模式不同,可以分别扩容
- 独立维护:前端更新不需要重启 API 服务
- 安全隔离:WebUI 可以放在 CDN 后,API 可以限制内网访问
- 技术栈独立:前端可以用 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,可以开启其代理服务获得额外保护:
- SSL/TLS 模式:设置为 “Full (strict)”
- Always Use HTTPS:开启
- Security Level:根据需求设置
- Bot Fight Mode:开启,防止恶意爬虫
- 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 存储在内存中,多实例间不共享。
解决方案:
- 使用 Redis 共享 Session(推荐)
- 使用 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 编程实战干货。
