CLI 部署:生产级命令行工具

将 Agent 编译为静态二进制文件并通过 CLI 部署,是 Go 语言最具优势的生产交付方式。相比 Python、Node.js 等需要运行时环境的语言,Go 的静态编译特性使得部署过程极其简洁——单个二进制文件加上配置文件即可运行。但"能跑"和"跑得稳"之间,隔着一整套运维体系的差距。

本文将从编译优化、进程管理、日志体系、监控接入到自动化运维,系统性地讲解 CLI 部署的完整生产实践。


编译优化:从 go build 到生产级二进制

基础编译

# 开发环境编译(含调试信息)
go build -o my-agent .

# 生产环境编译(优化版)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -ldflags="-s -w -X main.version=$(git describe --tags) -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
    -trimpath \
    -o my-agent .

参数详解:

参数作用生产建议
CGO_ENABLED=0禁用 CGO,静态链接必须。避免 glibc 版本依赖问题
GOOS=linux目标操作系统根据部署环境选择
GOARCH=amd64目标架构主流云服务器架构
-ldflags="-s -w"去除符号表和调试信息必须。减少 30% 二进制体积
-ldflags="-X main.version=..."注入版本信息建议。便于问题追踪
-trimpath去除构建路径信息建议。增强安全性

多架构编译(云原生必备)

现代云环境往往需要支持多种 CPU 架构(x86_64、ARM64):

#!/bin/bash
# build.sh - 多架构编译脚本

VERSION=$(git describe --tags --always)
BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS="-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}"

# 定义目标架构
PLATFORMS=(
    "linux/amd64"
    "linux/arm64"
    "darwin/amd64"
    "darwin/arm64"
)

for PLATFORM in "${PLATFORMS[@]}"; do
    GOOS=${PLATFORM%/*}
    GOARCH=${PLATFORM#*/}
    OUTPUT="dist/my-agent-${GOOS}-${GOARCH}"
    
    echo "Building for ${PLATFORM}..."
    CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} \
        go build -ldflags="${LDFLAGS}" -trimpath -o "${OUTPUT}" .
    
    # 计算并输出文件哈希(用于验证)
    sha256sum "${OUTPUT}" > "${OUTPUT}.sha256"
done

echo "Build complete. Artifacts in dist/"

二进制体积优化

生产环境二进制越小,部署传输越快,冷启动时间越短:

# 使用 upx 压缩(可选,但会增加启动时的解压时间)
upx --best dist/my-agent-linux-amd64

# 验证压缩效果
ls -lh dist/

体积对比

编译方式典型体积适用场景
默认编译~50MB开发调试
-ldflags="-s -w"~35MB生产环境
+ upx 压缩~12MB带宽受限环境

运行与配置管理

命令行参数设计

生产级 CLI 应该支持多种配置来源(优先级从高到低):

// main.go - 配置解析
package main

import (
    "flag"
    "os"
    "github.com/spf13/viper"
)

type Config struct {
    Port           int           `mapstructure:"port"`
    LogLevel       string        `mapstructure:"log_level"`
    LogFormat      string        `mapstructure:"log_format"`
    MaxConcurrent  int           `mapstructure:"max_concurrent"`
    MaxSessions    int           `mapstructure:"max_sessions"`
    SessionTTL     time.Duration `mapstructure:"session_ttl"`
    RedisURL       string        `mapstructure:"redis_url"`
    MetricsPort    int           `mapstructure:"metrics_port"`
    EnablePprof    bool          `mapstructure:"enable_pprof"`
}

func loadConfig() (*Config, error) {
    viper.SetEnvPrefix("AGENT")  // 环境变量前缀:AGENT_PORT
    viper.AutomaticEnv()
    
    // 默认值
    viper.SetDefault("port", 8080)
    viper.SetDefault("log_level", "info")
    viper.SetDefault("log_format", "json")
    viper.SetDefault("max_concurrent", 100)
    viper.SetDefault("max_sessions", 10000)
    viper.SetDefault("session_ttl", "24h")
    viper.SetDefault("metrics_port", 9090)
    
    // 配置文件
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath(".")
    viper.AddConfigPath("/etc/my-agent/")
    
    if err := viper.ReadInConfig(); err != nil {
        if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
            return nil, err
        }
    }
    
    // 命令行参数(最高优先级)
    flag.Int("port", viper.GetInt("port"), "HTTP server port")
    flag.String("log-level", viper.GetString("log_level"), "Log level")
    flag.Parse()
    
    // 绑定命令行参数到 viper
    viper.BindPFlags(flag.CommandLine)
    
    var cfg Config
    if err := viper.Unmarshal(&cfg); err != nil {
        return nil, err
    }
    
    return &cfg, nil
}

配置文件示例

# config.yaml - 生产环境配置
port: 8080
log_level: info
log_format: json  # json 格式便于日志收集系统解析

# 并发控制
max_concurrent: 200
max_sessions: 50000
session_ttl: 24h

# Redis 持久化
redis_url: "redis://localhost:6379/0"

# 监控
metrics_port: 9090
enable_pprof: false  # 生产环境关闭,排查问题时临时开启

# TLS(生产环境必需)
tls:
  enabled: true
  cert_file: "/etc/my-agent/server.crt"
  key_file: "/etc/my-agent/server.key"

进程管理:systemd 深度配置

systemd 是现代 Linux 系统的标准进程管理器,正确配置可以大幅提升服务的稳定性。

基础服务配置

# /etc/systemd/system/my-agent.service
[Unit]
Description=ADK Go Agent Runtime
Documentation=https://docs.example.com/my-agent
After=network-online.target redis.service
Wants=network-online.target
Requires=redis.service  # 如果依赖 Redis

[Service]
Type=notify
User=agent
Group=agent

# 工作目录
WorkingDirectory=/opt/my-agent

# 执行命令
ExecStart=/opt/my-agent/my-agent --config /etc/my-agent/config.yaml
ExecReload=/bin/kill -HUP $MAINPID

# 重启策略
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=3

# 资源限制
# 内存:根据 Agent 复杂度调整,建议预留 50% 余量
MemoryMax=2G
MemorySwapMax=0  # 禁用 swap,防止性能抖动

# CPU:限制为 2 核
CPUQuota=200%

# 文件描述符:高并发需要调高
LimitNOFILE=65535

# 进程数限制
LimitNPROC=10000

# 日志输出到 journal
StandardOutput=journal
StandardError=journal
SyslogIdentifier=my-agent

# 优雅关闭
TimeoutStopSec=30
KillSignal=SIGTERM
KillMode=mixed

# 安全加固
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/my-agent/data
PrivateTmp=true

[Install]
WantedBy=multi-user.target

启用与运维

# 重新加载 systemd 配置
sudo systemctl daemon-reload

# 启用开机自启
sudo systemctl enable my-agent

# 启动服务
sudo systemctl start my-agent

# 查看状态
sudo systemctl status my-agent

# 查看详细日志
sudo journalctl -u my-agent -f

# 优雅重启(如果应用支持 HUP 信号重载配置)
sudo systemctl reload my-agent

# 查看资源使用
systemctl show my-agent --property=MemoryCurrent,CPUUsageNSec

高级:通知模式(Type=notify)

Go 1.16+ 支持 systemd 的 notify 协议,可以让服务启动完成后才通知 systemd:

import (
    "github.com/coreos/go-systemd/v22/daemon"
)

func main() {
    // ... 初始化代码 ...
    
    // 通知 systemd:服务已就绪
    if _, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil {
        log.Printf("failed to notify systemd: %v", err)
    }
    
    // ... 启动服务器 ...
}

这样配置 Type=notify 后,systemd 会等待服务真正就绪后才标记为 active,避免"服务启动了但还没准备好接收请求"的竞态条件。


日志体系:从打印到可观测

结构化日志配置

生产环境必须使用结构化日志(JSON 格式),便于日志收集系统(如 ELK、Loki、Fluentd)解析:

import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
    "os"
    "time"
)

func setupLogger(level, format string) {
    // 设置日志级别
    switch level {
    case "debug":
        zerolog.SetGlobalLevel(zerolog.DebugLevel)
    case "info":
        zerolog.SetGlobalLevel(zerolog.InfoLevel)
    case "warn":
        zerolog.SetGlobalLevel(zerolog.WarnLevel)
    case "error":
        zerolog.SetGlobalLevel(zerolog.ErrorLevel)
    }
    
    if format == "json" {
        // JSON 格式(生产环境)
        log.Logger = zerolog.New(os.Stdout).
            With().
            Timestamp().
            Caller().
            Str("service", "my-agent").
            Str("version", version).
            Logger()
    } else {
        // 控制台格式(开发环境)
        log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}).
            With().
            Timestamp().
            Caller().
            Logger()
    }
}

// 使用示例
func handleRequest(ctx context.Context, req *Request) {
    logger := log.Ctx(ctx).With().
        Str("request_id", req.ID).
        Str("user_id", req.UserID).
        Logger()
    
    logger.Info().Str("input", req.Input).Msg("processing request")
    
    // ... 处理逻辑 ...
    
    if err != nil {
        logger.Error().Err(err).Int("latency_ms", latency).Msg("request failed")
        return
    }
    
    logger.Info().Int("latency_ms", latency).Msg("request completed")
}

journalctl 日志查询

# 实时跟踪日志
sudo journalctl -u my-agent -f

# 最近 1 小时日志
sudo journalctl -u my-agent --since "1 hour ago"

# 按时间范围查询
sudo journalctl -u my-agent --since "2026-05-29 10:00:00" --until "2026-05-29 12:00:00"

# 查看错误级别日志
sudo journalctl -u my-agent -p err

# 导出日志到文件
sudo journalctl -u my-agent --since "today" > /tmp/my-agent.log

日志轮转配置

虽然 systemd journal 有内置的日志管理,但如果应用直接写日志文件,需要配置 logrotate:

# /etc/logrotate.d/my-agent
/var/log/my-agent/*.log {
    daily
    rotate 30          # 保留 30 天
    compress           # 压缩旧日志
    delaycompress      # 延迟一天压缩
    missingok          # 日志不存在不报错
    notifempty         # 空日志不轮转
    create 0640 agent agent
    
    # 轮转后发送 HUP 信号让应用重新打开日志文件
    postrotate
        systemctl reload my-agent
    endscript
}

健康检查与自动恢复

HTTP 健康检查端点

func healthHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    
    checks := map[string]interface{}{
        "status": "healthy",
        "timestamp": time.Now().UTC(),
        "version": version,
    }
    
    // 检查关键依赖
    if err := checkRedis(ctx); err != nil {
        checks["status"] = "unhealthy"
        checks["redis"] = map[string]string{"status": "down", "error": err.Error()}
        w.WriteHeader(http.StatusServiceUnavailable)
    } else {
        checks["redis"] = map[string]string{"status": "up"}
    }
    
    // 检查 LLM API 连通性(轻量级检查,不要真调用)
    if err := checkLLMConnectivity(ctx); err != nil {
        checks["llm"] = map[string]string{"status": "degraded", "error": err.Error()}
    } else {
        checks["llm"] = map[string]string{"status": "up"}
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(checks)
}

// 轻量级连通性检查(只检查 DNS 解析和 TCP 连接)
func checkLLMConnectivity(ctx context.Context) error {
    dialer := &net.Dialer{Timeout: 5 * time.Second}
    conn, err := dialer.DialContext(ctx, "tcp", "generativelanguage.googleapis.com:443")
    if err != nil {
        return err
    }
    conn.Close()
    return nil
}

高级 systemd 健康检查

[Service]
# 启动限制:5 分钟内最多重启 3 次
StartLimitIntervalSec=300
StartLimitBurst=3

# 服务健康检查(systemd 245+)
ExecStartPost=/usr/bin/curl -f -s http://localhost:8080/health || exit 1
WatchdogSec=30
NotifyAccess=all

常见问题排查指南

Q:进程 OOM 被 killed

诊断

# 查看系统日志中的 OOM 记录
sudo dmesg | grep -i "killed process"
sudo journalctl -k | grep -i "oom"

# 查看进程内存使用趋势
systemctl show my-agent --property=MemoryCurrent

解决

  1. 降低 MaxSessions 或缩短 SessionTTL
  2. 增加 MemoryMax 限制(如果物理内存充足)
  3. 启用历史摘要,减少单 Session 内存占用
  4. 使用 Redis 外部存储 Session

Q:磁盘满了

诊断

# 查看磁盘使用
df -h

# 查看日志占用
du -sh /var/log/journal/
du -sh /var/log/my-agent/

# 查看 journal 使用
journalctl --disk-usage

解决

  1. 配置 journal 大小限制:/etc/systemd/journald.conf 中设置 SystemMaxUse=500M
  2. 清理旧日志:sudo journalctl --vacuum-time=7d
  3. 降低应用日志级别(从 debug 改为 info)

Q:更新 Agent 后需要重启

零停机更新方案

# 方案 1:使用 systemd reload(如果应用支持配置热重载)
sudo systemctl reload my-agent

# 方案 2:蓝绿部署(推荐)
# 1. 在另一个端口启动新版本
sudo cp my-agent /opt/my-agent/my-agent.new
sudo /opt/my-agent/my-agent.new --port 8081 &

# 2. 切换负载均衡器指向新端口
sudo sed -i 's/8080/8081/' /etc/nginx/conf.d/my-agent.conf
sudo systemctl reload nginx

# 3. 停止旧版本
sudo systemctl stop my-agent
sudo mv /opt/my-agent/my-agent.new /opt/my-agent/my-agent
sudo systemctl start my-agent

# 4. 切回负载均衡器到原端口
sudo sed -i 's/8081/8080/' /etc/nginx/conf.d/my-agent.conf
sudo systemctl reload nginx

下一步

CLI 部署搞定了,接下来看 Web 界面部署。

Agent Runtime 架构 | Web 界面部署 →


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