CLI 部署:生产级命令行工具
详解如何通过命令行方式在生产环境部署和运维 ADK Go Agent——包括进程管理、日志、自启动等生产级实践。
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
解决:
- 降低
MaxSessions或缩短SessionTTL - 增加
MemoryMax限制(如果物理内存充足) - 启用历史摘要,减少单 Session 内存占用
- 使用 Redis 外部存储 Session
Q:磁盘满了
诊断:
# 查看磁盘使用
df -h
# 查看日志占用
du -sh /var/log/journal/
du -sh /var/log/my-agent/
# 查看 journal 使用
journalctl --disk-usage
解决:
- 配置 journal 大小限制:
/etc/systemd/journald.conf中设置SystemMaxUse=500M - 清理旧日志:
sudo journalctl --vacuum-time=7d - 降低应用日志级别(从 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 编程实战干货。
