English
← 返回 Agent Rule

AI Agent 服务的零停机蓝绿部署 ✓ 已验证

2026-08-17 · 16 分钟 · Docker · sun-port · PostgreSQL · Git · Hermes · im-bot

一个 Web 服务器可以在眨眼间重启,而没有人察觉。一个 AI Agent 服务却做不到——它持有状态:一个正做到一半的 Hermes 会话、一个有三个 Agent 在协商的 im-bot 房间、一个发布到一半的 cron 任务、一个刚写完四行中三行的 PostgreSQL 事务。用一句朴素的 docker compose up --force-recreate 部署新版本,你就会在用户面前把这一切统统拆掉。蓝绿部署就是答案:并排运行两套完全相同的栈,用 sun-port 在它们之间切换流量,如果新版本行为异常,一条命令即可回滚。本教程端到端地搭建这条流水线。

本文中的每一条命令都在本站所记录的 Agent 基础设施上运行过——两套 Docker Compose 栈、一张 sun-port 路由表、PostgreSQL 迁移和一个打上 Git 标签的发布——✓ 已验证 徽章意味着真实的执行,而非从 README 里复制粘贴。

1. 为什么 Agent 让部署变得更难

一个无状态 API 通过增加实例来扩展,通过替换实例来重新部署。而 Agent 服务有四个特性,会打破这个模型:

蓝绿部署通过从不拆掉正在运行的系统来解决这四个问题。你在旧版本旁边立起新版本,验证它,然后拨动开关。旧版本一直保持温热,直到你确认无误。

2. 蓝绿模型

两套环境,一个数据库,一个入口:

让这一切成立的纪律是:绿必须在蓝被动到之前就部署好,并且在切换窗口期内,数据库必须同时接受两个版本

3. 前置条件

$ docker --version
Docker version 27.3.1, build ce12230
$ docker compose version
Docker Compose version v2.29.7
$ psql --version
psql (PostgreSQL) 16.4
$ git --version
git version 2.46.0

你还需要一个运行中的 sun-port(见我们的反向代理教程),以及一个两套栈都能访问的 PostgreSQL 实例。如果你还没把 Agent 本身搭起来,先从Ubuntu 上手教程开始。

4. 把 Agent 容器化

只有当每个版本都是一个你能启动、停止和回滚的自包含镜像时,这套栈才成立。一个面向 Hermes 或 im-bot 服务的最小 Dockerfile,会在构建时钉死运行时并把代码烧进去:

# Dockerfile
FROM node:20-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
CMD ["node", "src/index.js"]

构建绿镜像,并用 Git 发布标签为它打标,这样每一个部署产物都能追溯到一个提交:

$ git tag v1.4.0 && git push origin v1.4.0
$ docker build -t agent-service:v1.4.0 .
$ docker tag agent-service:v1.4.0 agent-service:green
为什么用一个浮动的 green 标签?你的 Compose 文件引用的是 green,所以你部署时无需编辑 Compose——只需给刚构建的镜像重新打标。回滚同样简单:把 green 重新指向上一个可用的 v1.3.0,再把流量切回去。标签就是你的部署历史。

5. 两套 Compose 栈,一个数据库

Docker Compose 部署两套环境,它们只在名称、端口和镜像标签上不同:

# docker-compose.yml — 两套栈定义在同一个文件里
services:
  agent-blue:
    image: agent-service:v1.3.0
    container_name: agent-blue
    restart: unless-stopped
    environment:
      DATABASE_URL: postgres://agent:${DB_PASSWORD}@postgres:5432/agentdb
      RELEASE_COLOR: blue
    ports:
      - "127.0.0.1:8001:8001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8001/health"]
      interval: 10s
      timeout: 5s
      retries: 5

  agent-green:
    image: agent-service:green
    container_name: agent-green
    restart: unless-stopped
    environment:
      DATABASE_URL: postgres://agent:${DB_PASSWORD}@postgres:5432/agentdb
      RELEASE_COLOR: green
    ports:
      - "127.0.0.1:8002:8002"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8002/health"]
      interval: 10s
      timeout: 5s
      retries: 5

  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: agentdb
      POSTGRES_USER: agent
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

有三点要注意。端口绑定到 127.0.0.1——两套栈都无法从公网访问,只有 sun-port 可以。一个共享的 postgres 服务意味着两种颜色读写同一份数据,所以切换对状态是透明的。此外,RELEASE_COLOR 被传入应用,让应用自身的日志和健康页能报告自己是哪种颜色——你稍后就会用到它。

$ docker compose up -d
$ docker compose ps
NAME          STATUS
agent-blue    Up (healthy)
agent-green   Up (healthy)
postgres      Up (healthy)

6. 用 sun-port 路由流量

sun-port 就是真正发生切换的地方。定义一个指向蓝的 upstream,等准备好后再把它翻到绿——这是一次重载,而不是重新部署:

# sun-port 配置
upstream agent_backend {
  server 127.0.0.1:8001;   # 蓝
}

server {
  listen 443 ssl;
  server_name agent.agent-rule.com;

  ssl_certificate     /etc/letsencrypt/live/agent.agent-rule.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/agent.agent-rule.com/privkey.pem;

  location / {
    proxy_pass http://agent_backend;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

切换就是一行编辑加一次重载:

# 蓝 -> 绿 切换
upstream agent_backend {
  server 127.0.0.1:8002;   # 绿
}
$ sun-port -t            # 校验配置
configuration file is valid
$ sun-port -s reload     # 热重载,不丢连接
reload signal sent
sun-port 重载路由表时不会丢弃已有连接,所以一个在蓝上开始的进行中请求会在蓝上完成——只有新的连接才去绿。正是这种优雅交接,让它成为「零停机」而非仅仅是「快」。

7. 向后兼容地迁移 PostgreSQL

数据库是两种颜色共享的唯一资源,所以 schema 变更必须同时兼容两个版本。规则是扩展-收缩(expand-contract):

  1. 扩展——部署一个只新增(带默认值的新列、新表)的迁移。蓝忽略它们,绿使用它们。在切换之前运行它。
  2. 切换——把 sun-port 翻到绿。绿现在在新 schema 上承载流量。
  3. 收缩——在之后的某个发布里,等蓝早已退场,再删掉旧列。

一个用 psql 执行的安全加法迁移:

$ psql "$DATABASE_URL" <<'SQL'
ALTER TABLE conversations
  ADD COLUMN IF NOT EXISTS model_provider TEXT NOT NULL DEFAULT 'openai';
CREATE INDEX IF NOT EXISTS idx_conversations_model
  ON conversations (model_provider);
SQL
ALTER TABLE
CREATE INDEX

ADD COLUMN IF NOT EXISTS 和默认值,是让迁移对任何一种颜色都可运行的两个习惯。一旦你需要删除一列或改一个类型,那就是横跨两个发布的两步舞步——绝不要在切换流量的同一次部署里做这件事。

绝不要在你切换的同一个发布里删除或重命名。如果绿删掉了一列而蓝还在写入,蓝会立刻报错,你的「零停机」部署就会变成两种颜色上的故障。切换窗口期内只做加法;破坏性变更留到后续发布。

8. 用健康检查为切换把关

在切换流量之前,先证明绿是健康的——不只是在运行,而是真正在工作。一次真实的检查直接命中绿容器,确认它能访问数据库并报告自己的颜色:

$ curl -sf http://127.0.0.1:8002/health
{"status":"ok","color":"green","db":"up"}

$ curl -sf http://127.0.0.1:8002/health | grep -q '"db":"up"' \
    && echo "green 已就绪" || echo "green 尚未就绪——不要切换"
green 已就绪

color 字段是你的安全网:如果 green 某天返回了 "color":"blue",说明你的浮动标签被指错了,你差点把旧版本当成新版本部署上去。切换要同时以 statusdb 为把关条件。

9. 自动化切换

把整个切换封装进一个脚本,让它可复现——也让凌晨两点疲惫的运维人员无法跳过某一步:

#!/usr/bin/env bash
# deploy.sh — 蓝绿切换
set -euo pipefail
COLOR=${1:-green}          # 把流量发给哪种颜色
PORT=$([ "$COLOR" = green ] && echo 8002 || echo 8001)

# 1. 对目标做健康把关
curl -sf "http://127.0.0.1:$PORT/health" | grep -q '"db":"up"' \
  || { echo "target $COLOR 不健康"; exit 1; }

# 2. 把 sun-port 指向它
sed -i "s/server 127.0.0.1:800[0-9];/server 127.0.0.1:$PORT;/" \
  /etc/sun-port/sun-port.conf

# 3. 校验并重载
sun-port -t
sun-port -s reload

echo "流量现在在 $COLOR 上"
$ ./deploy.sh green
流量现在在 green 上

回滚就是同一个脚本、换成另一种颜色——而且因为旧栈从未被拆掉,回滚是即时的:

$ ./deploy.sh blue      # 绿行为异常——一条命令回到蓝
流量现在在 blue 上

10. Git 标签与发布纪律

蓝绿是一种流程,而流程住在 Git 里。保持简单和线性:

$ git checkout -b release/v1.4.0
$ # ...代码改动、测试...
$ git tag v1.4.0
$ git push origin v1.4.0

三个习惯让这条流水线保持诚实:

11. Agent 特有的考量:Hermes 与 im-bot

蓝绿解决了基础设施问题,但 Agent 还带来两个值得提前规划的麻烦:

最干净的模式:在切换窗口期内(几分钟到一小时)让蓝保持完全存活,让进行中的工作自然排空,等它安静下来后,再 docker compose stop agent-blue。零丢弃工作,零停机。

12. 常见坑

13. 核心要点

蓝绿部署把运行 Agent 基础设施时最吓人的那部分——「新版本会不会把一切都搞垮?」——变成一次可逆的、一条命令的操作。对于一个通过 sun-port 编排 Hermes Agent、im-bot 房间、Docker 工作负载和 PostgreSQL 状态的集群来说,这正是「在周五部署」与「在周五信心满满地部署」之间的差别。