星澜

天接云涛连晓雾,星河欲转千帆舞

Proto 大仓模式


项目介绍

本项目采用 Proto 大仓 + 生成产物集中仓库 模式,统一管理全公司微服务的 Protocol Buffer(IDL)接口定义,通过 Buf 工具链 + Drone CI/CD 实现「语法校验 → 代码生成 → 文档同步 → 通知告警」全链路自动化,解决跨项目 API 协作的耦合、一致性、效率问题,适用于 200+ 人规模的中小团队微服务架构。

一、背景与痛点

在微服务架构推广过程中,我们采用 Proto 作为统一 IDL,但很快遇到了行业共性问题:

为解决这些问题,我们开始调研行业方案,而煎鱼大佬的《真是头疼,Proto 代码到底放哪里?》一文,为我们梳理了核心选型方向,也让我们陷入了方案纠结。

二、方案选型的纠结之路

参考煎鱼大佬对行业主流方案的拆解,我们逐一分析了 4 种核心方案,经历了从「觉得都可行」到「排除不合适」的纠结过程:

2.1 煎鱼大佬提出的 4 种核心方案

方案 核心设计 我们的纠结点(优点 vs 缺点)
方案一:API 大仓 + Git Submodule(B 站方案) Proto 集中存储,依赖方通过 Git Submodule 引入 Proto 或生成代码 ✅ 优点:无需额外维护生成仓库,架构简洁;
❌ 缺点:Submodule 更新繁琐(依赖方需手动执行 git submodule update),团队协作成本高,200+ 人团队易出现同步遗漏
方案二:API 大仓 + Submodule + 项目专有生成仓库 Proto 集中管理,每个业务项目对应独立的生成代码仓库 ✅ 优点:产物隔离性好,单个项目变更不影响其他项目;
❌ 缺点:仓库数量爆炸(20+ 业务线就需要 20+ 生成仓库),运维维护成本高,不符合「轻量化管理」需求
方案三:每个项目独立 API 仓库 每个服务自带 Proto 及生成代码,自主管理接口 ✅ 优点:自主性强,无跨项目依赖,单个项目迭代灵活;
❌ 缺点:Proto 规范无法统一(如公共枚举、消息类型重复定义),接口复用性差,跨项目联调时仍需手动拷贝,回到最初的痛点
方案四:API 大仓 + 生成产物集中仓库 Proto 集中存储,生成的代码 / 文档统一同步到 1-2 个专用仓库(如 apis-go、apis-swagger),依赖方直接引入专用仓库 ✅ 优点:规范统一(Proto 集中校验)、自动化程度高(提交即生成同步)、依赖方接入简单(go get 直接拉取);
❌ 缺点:需维护 3 个核心仓库(大仓 + 2 个产物仓库),初期配置稍复杂

2.2 最终决策:选择方案四

纠结了近 1 周后,我们最终选择了「API 大仓 + 生成产物集中仓库」,核心原因:

  1. 解决核心痛点:完全规避了「手动同步」「规范混乱」「协同成本高」的问题,契合我们的核心需求;

  2. 平衡维护成本:虽然需要维护 3 个仓库,但仓库分工明确(大仓管 Proto、产物仓库管代码 / 文档),后续运维成本远低于方案二;

  3. 适配团队规模:200+ 人团队需要「标准化流程」而非「个性化自主管理」,方案四能通过自动化减少人为干预,降低协作摩擦;

  4. 可扩展性强:支持新增语言生成(如未来扩展 Java 服务,可新增 apis-java 仓库)、扩展文档平台(如 YAPI、Swagger UI)。

可以说,煎鱼大佬的《真是头疼,Proto 代码到底放哪里?》帮我们理清了选型边界,而团队规模、维护成本、自动化需求,让我们最终敲定了方案四。

三、最终架构设计

3.1 核心仓库分工

基于方案四,我们设计了 3 个核心仓库,职责清晰、无冗余:

仓库名称 定位 核心内容
APIS 仓库(Proto 大仓) 接口定义中心 + 自动化调度中心 1. 各业务模块的 Proto 文件(如 auth-center、order-service);
  1. Buf 配置(语法校验、依赖管理、生成规则);
  2. 自动化脚本(代码生成、仓库同步);
  3. Drone CI/CD 流水线配置| |apis-go 仓库|Go 代码生成产物中心|1. 自动化生成的 Go 代码(含 Proto 基础代码、GRPC 代码、HTTP 网关代码);
  4. 独立 Drone 配置(代码同步后发送钉钉通知,方便依赖方接入)| |apis-swagger 仓库|接口文档生成产物中心|1. 自动化生成的 Swagger 文档(JSON 格式);
  5. YAPI 上传脚本(同步文档到 YAPI 平台);
  6. 独立 Drone 配置(文档同步后自动上传 YAPI,失败时告警)|

3.2 技术栈选型

类别 工具 / 技术 选型原因
接口定义 Protocol Buffer(Proto3) 行业标准 IDL,跨语言、序列化效率高
编译与校验 Buf(buf lint、buf generate) 替代传统 protoc,统一校验规则、生成流程,支持插件化扩展
CI/CD 流水线 Drone 轻量、容器原生,与我们现有工具链兼容(团队已熟练使用 Drone)
构建镜像 自定义 apis-generate-go:1.0.0 封装 Go、Buf、protoc 及插件,避免本地环境差异
代码仓库 Gogs 私有化部署,适配内网环境,权限管理灵活
文档管理 YAPI 支持 Swagger 导入、在线调试,契合团队接口管理习惯
通知工具 钉钉机器人 团队日常沟通工具,消息触达率高
HTTP 网关插件 go-kratos protoc-gen-go-http 与 Kratos 框架深度集成,生成代码可直接适配框架中间件、错误处理机制

3.3 整体流程图

flowchart TD
    A[开发者] -->|1. 提交 Proto 到 APIS 仓库分支| B[APIS 仓库(Proto 大仓)]
    B -->|2. 触发 Drone 流水线| C[Step 1: buf lint 语法校验]
    C -->|不通过| D[钉钉通知失败原因(如 Proto 命名不规范)]
    C -->|通过| E[Step 2: 执行 apis-go.sh 脚本]
    E -->|a. buf generate 生成 Go 代码| F[同步代码到 apis-go 仓库]
    F -->|触发 apis-go 仓库 Drone| G[钉钉发送代码就绪通知(含 go get 快捷命令)]
    E -->|b. 并行执行 apis-swagger.sh 脚本| H[生成 Swagger 文档并同步到 apis-swagger 仓库]
    H -->|触发 apis-swagger 仓库 Drone| I[自动上传 Swagger 到 YAPI]
    I -->|失败| J[钉钉通知上传失败]
    I -->|成功| K[YAPI 文档更新完成(支持在线调试)]
    G -->|依赖方接收通知| L[下游服务执行 go get 拉取最新代码]
    K -->|测试/开发使用| M[在线查看接口文档、调试接口]

四、核心配置说明

4.1 Proto 大仓(APIS 仓库)核心配置

(1)buf.yml(全局规范与依赖配置)

version: v1
# 模块名(标识当前 Proto 大仓,用于 Buf 依赖管理)
name: buf.build/maizuo/apis
# 依赖管理(引入外部公共 Proto,如 Google 公共类型、GRPC 网关定义)
deps:
  - buf.build/googleapis/googleapis
  - buf.build/grpc-ecosystem/grpc-gateway
  - buf.build/envoyproxy/protoc-gen-validate
# 语法校验规则(使用 Buf 默认规则,确保团队 Proto 规范统一)
lint:
  use:
    - DEFAULT
# 兼容性检查规则(按文件维度检测,避免接口变更不兼容)
breaking:
  use:
    - FILE

(2)apis-go.gen.yml(Go 代码生成规则,明确插件来源)

version: v1
managed:
  enabled: true
plugins:
  # 1. Buf 远程插件:生成基础 Go 代码(proto -> .pb.go)
  - plugin: buf.build/protocolbuffers/go:v1.33.0
    out: apis-go
    opt: paths=source_relative
  # 2. Buf 远程插件:生成 GRPC 代码(proto -> _grpc.pb.go)
  - plugin: buf.build/grpc/go:v1.4.0
    out: apis-go
    opt: paths=source_relative,require_unimplemented_servers=false
  # 3. Buf 远程插件:生成数据校验代码(proto -> _validate.pb.go)
  - plugin: buf.build/bufbuild/validate-go:v1.0.4
    out: apis-go
    opt: paths=source_relative
  # 4. 本地 protoc 插件(kratos 内置):生成 HTTP 网关代码
  - plugin: go-http
    out: apis-go
    opt:
      - paths=source_relative

插件来源说明

选择 go-kratos 本地插件的原因

  1. 框架深度集成:生成的 HTTP 代码直接适配 Kratos 框架的 http.Handler 接口、路由注册逻辑,可无缝对接框架的中间件(熔断、限流)、错误处理机制,无需额外编写适配代码;

  2. 配置简洁统一:通过 Proto 自定义选项(如 google.api.http)即可定义 HTTP 映射规则,与 Kratos 生态工具(kratos newkratos proto add)兼容;

  3. 降低学习成本:团队已深度使用 Kratos 框架,复用其原生插件可减少新工具的学习和适配成本,避免引入通用插件(如 grpc-gateway)后的胶水代码开发。

(3)Drone 流水线配置(.drone.yml)

kind: pipeline
type: docker
name: apis-auto-pipeline
workspace:
  base: /app
  path: ${DRONE_REPO_NAME}

# 挂载 Buf 缓存目录,加速生成流程
volumes:
  - name: buf-cache
    host:
      path: /home/docker/drone/buf/.cache

steps:
  # 步骤1:Proto 语法校验(buf lint)
  - name: proto-lint
    image: apis-generate-go:1.0.0
    pull: if-not-exists
    volumes:
      - name: buf-cache
        path: /app/buf/.cache
    commands:
      - buf lint

  # 步骤2:生成 Go 代码并同步到 apis-go 仓库
  - name: generate-go-code
    image: apis-generate-go:1.0.0
    pull: if-not-exists
    volumes:
      - name: buf-cache
        path: /app/buf/.cache
    environment:
      BUF_CACHE_DIR: /app/buf/.cache
      TARGET_REPO: apis-go
      TARGET_REPO_ADDR: git@gogs.maizuo.com:fino/apis-go.git
      SSH_PRIVATE_KEY:
        from_secret: ssh_private_key # Drone 保密变量(Base64 编码的 Git 私钥)
    commands:
      - sh ./apis-go.sh
    retry:
      limit: 3 # 失败最多重试 3 次

  # 步骤3:生成 Swagger 文档并同步到 apis-swagger 仓库
  - name: generate-swagger-doc
    image: apis-generate-go:1.0.0
    pull: if-not-exists
    volumes:
      - name: buf-cache
        path: /app/buf/.cache
    environment:
      BUF_CACHE_DIR: /app/buf/.cache
      TARGET_REPO: apis-swagger
      TARGET_REPO_ADDR: git@gogs.maizuo.com:fino/apis-swagger.git
      SSH_PRIVATE_KEY:
        from_secret: ssh_private_key
    commands:
      - sh ./apis-swagger.sh
    retry:
      limit: 3

  # 步骤4:流水线失败通知
  - name: notify-failure
    image: plugins/webhook
    pull: if-not-exists
    settings:
      urls: https://oapi.dingtalk.com/robot/send?access_token=7f2deb3091c46ed9e77103ecdbfce2ec29385527c75059bf689b5a6af0eeb92e
      content_type: application/json
      template: |
        {
            "msgtype": "text",
            "text": {
                "content": "Proto 大仓流水线失败 \n > 分支: {{ build.branch }} \n > 提交者: {{ build.author }} \n > 提交信息: {{ build.message }} \n > 详情链接: {{ build.link }}"
            }
        }
    when:
      status:
        - failure

node:
  env: aliyun-dev # 执行节点环境

(4)apis-go.sh 脚本(Go 代码生成与仓库同步)

文件路径:APIS 仓库根目录 /apis-go.sh 核心作用:连接 Proto 大仓与 apis-go 仓库,完成代码生成、仓库同步、提交推送全流程。


#!/bin/sh
set -e # 命令执行失败时立即退出,避免后续步骤在异常状态下执行

# ==================== 依赖环境变量说明 ====================
# TARGET_REPO: 目标仓库名(apis-go),由 .drone.yml 定义
# TARGET_REPO_ADDR: 目标仓库 Git SSH 地址,由 .drone.yml 定义
# SSH_PRIVATE_KEY: Git 免密提交私钥(Base64 编码),来自 Drone Secrets
# DRONE_COMMIT_AUTHOR: 流水线触发者姓名,由 Drone 自动注入
# DRONE_COMMIT_AUTHOR_EMAIL: 流水线触发者邮箱,由 Drone 自动注入
# DRONE_BRANCH: 当前流水线分支,由 Drone 自动注入
# DRONE_COMMIT_MESSAGE: 提交信息,由 Drone 自动注入
# DRONE_REPO_NAME: 当前仓库名,由 Drone 自动注入
# =========================================================

# 打印调试信息,便于问题追踪
echo "===== 开始同步 Go 代码到 ${TARGET_REPO} 仓库 ====="
echo "目标仓库地址: ${TARGET_REPO_ADDR}"
echo "当前分支: ${DRONE_BRANCH}"
echo "提交者: ${DRONE_COMMIT_AUTHOR} <${DRONE_COMMIT_AUTHOR_EMAIL}>"

# ------------------- 步骤1:配置 Git SSH 免密登录 -------------------
echo ">>> 配置 Git SSH 环境"
eval "$(ssh-agent -s)" # 启动 SSH Agent
ssh-add <(echo "${SSH_PRIVATE_KEY}" | base64 -d) # 解密私钥并添加到 Agent
mkdir -p ~/.ssh
# 配置 SSH 连接规则:指定内部 Git 仓库的连接参数,关闭严格主机密钥检查(避免 CI 环境手动确认)
cat > ~/.ssh/config <<EOF
Host 内部Git仓库域名
HostName 内部Git仓库IP
User git
Port 内部Git仓库端口
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
EOF

# ------------------- 步骤2:配置 Git 用户信息 -------------------
echo ">>> 配置 Git 用户信息"
if [ -z "${DRONE_COMMIT_AUTHOR}" ]; then
  echo "错误:无法获取提交者信息,退出流程"
  exit 1
fi
git config --global user.name "${DRONE_COMMIT_AUTHOR}"
git config --global user.email "${DRONE_COMMIT_AUTHOR_EMAIL}"

# ------------------- 步骤3:克隆/创建目标仓库分支 -------------------
echo ">>> 克隆 ${TARGET_REPO} 仓库"
mkdir -p /app && cd /app || exit 1
# 尝试克隆指定分支,若分支不存在则基于 master 创建
if git clone --branch "${DRONE_BRANCH}" "${TARGET_REPO_ADDR}" >/dev/null 2>&1; then
  echo "✅ 目标分支 ${DRONE_BRANCH} 已存在,直接拉取"
else
  echo "ℹ️  目标分支不存在,基于 master 分支创建"
  git clone "${TARGET_REPO_ADDR}" || exit 1
  cd "${TARGET_REPO}" || exit 1
  git checkout -b "${DRONE_BRANCH}" || exit 1
fi

# ------------------- 步骤4:生成 Go 代码 -------------------
echo ">>> 执行 buf generate 生成代码"
cd /app/"${DRONE_REPO_NAME}" || exit 1
buf generate --template apis-go.gen.yml --timeout 5m || exit 1
echo "✅ Go 代码生成成功"

# ------------------- 步骤5:同步代码到目标仓库 -------------------
echo ">>> 同步生成的代码到 ${TARGET_REPO} 仓库"
cd /app/"${TARGET_REPO}" || exit 1
# 清理目标仓库原有目录(避免旧文件残留)
find ./* -type d -exec rm -rf {} + || exit 1
# 拷贝新生成的代码
cp -rf /app/"${DRONE_REPO_NAME}"/apis-go/* . || exit 1
# 同步 Drone 配置文件(用于 apis-go 仓库的通知流程)
cp /app/"${DRONE_REPO_NAME}"/.apis-go.drone.yml .drone.yml || exit 1
# 整理 Go 依赖
go mod tidy >/dev/null 2>&1 || exit 1

# ------------------- 步骤6:提交并推送代码 -------------------
echo ">>> 提交代码变更"
git fetch --unshallow || true # 确保仓库为完整克隆(避免浅克隆导致的提交失败)
git add . || exit 1
# 检查是否有实际变更(避免空提交)
if git diff --cached --quiet; then
  echo "ℹ️  无代码变更,无需提交"
  exit 0
fi
git commit -m "${DRONE_COMMIT_MESSAGE}" || exit 1
git push --set-upstream origin "${DRONE_BRANCH}" || exit 1
echo "✅ 代码同步到 ${TARGET_REPO} 仓库成功"

(5)apis-swagger.sh 脚本(Swagger 文档生成与仓库同步)

文件路径:APIS 仓库根目录 /apis-swagger.sh 核心作用:生成 Swagger 文档并同步到 apis-swagger 仓库,为后续 YAPI 上传做准备。


#!/bin/sh
set -e

# ==================== 依赖环境变量说明 ====================
# 同 apis-go.sh,核心变量一致
# =========================================================

echo "===== 开始生成 Swagger 文档并同步到 ${TARGET_REPO} 仓库 ====="

# ------------------- 步骤1:配置 Git SSH 与用户信息 -------------------
# 与 apis-go.sh 步骤1、2一致,省略重复代码
eval "$(ssh-agent -s)"
ssh-add <(echo "${SSH_PRIVATE_KEY}" | base64 -d)
mkdir -p ~/.ssh
# 配置 SSH 连接规则:指定内部 Git 仓库的连接参数,关闭严格主机密钥检查(避免 CI 环境手动确认)
cat > ~/.ssh/config <<EOF
Host 内部Git仓库域名
HostName 内部Git仓库IP
User git
Port 内部Git仓库端口
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
EOF

if [ -z "${DRONE_COMMIT_AUTHOR}" ]; then
  echo "错误:无法获取提交者信息,退出流程"
  exit 1
fi
git config --global user.name "${DRONE_COMMIT_AUTHOR}"
git config --global user.email "${DRONE_COMMIT_AUTHOR_EMAIL}"

# ------------------- 步骤2:克隆/创建目标仓库分支 -------------------
mkdir -p /app && cd /app || exit 1
if git clone --branch "${DRONE_BRANCH}" "${TARGET_REPO_ADDR}" >/dev/null 2>&1; then
  echo "✅ 目标分支 ${DRONE_BRANCH} 已存在,直接拉取"
else
  echo "ℹ️  目标分支不存在,基于 master 分支创建"
  git clone "${TARGET_REPO_ADDR}" || exit 1
  cd "${TARGET_REPO}" || exit 1
  git checkout -b "${DRONE_BRANCH}" || exit 1
fi

# ------------------- 步骤3:生成 Swagger 文档 -------------------
cd /app/"${DRONE_REPO_NAME}" || exit 1
buf generate --template apis-swagger.gen.yml || exit 1
echo "✅ Swagger 文档生成成功"

# ------------------- 步骤4:同步文档到目标仓库 -------------------
cd /app/"${TARGET_REPO}" || exit 1
# 清理原有目录
find ./* -type d -exec rm -rf {} + || exit 1
# 拷贝 Swagger 文档及相关文件
cp -rf /app/"${DRONE_REPO_NAME}"/apis-swagger/* . || exit 1
cp /app/"${DRONE_REPO_NAME}"/.apis-swagger.drone.yml .drone.yml || exit 1
cp /app/"${DRONE_REPO_NAME}"/.apis-swagger.sh apis-swagger.sh || exit 1
# 拷贝项目 token 文件(用于 YAPI 上传授权)
PROJECTS=$(find /app/"${DRONE_REPO_NAME}" -maxdepth 1 -type d ! -name ".*" -exec basename {} \;) || exit 1
for PROJECT in ${PROJECTS}; do
  if [ -f "/app/${DRONE_REPO_NAME}/${PROJECT}/token" ]; then
    cp /app/"${DRONE_REPO_NAME}"/"${PROJECT}"/token ./${PROJECT}/token || exit 1
    echo "✅ 拷贝 ${PROJECT} 项目 token 文件"
  fi
done

# ------------------- 步骤5:提交并推送代码 -------------------
git fetch --unshallow || true
git add . || exit 1
if git diff --cached --quiet; then
  echo "ℹ️  无文档变更,无需提交"
  exit 0
fi
git commit -m "${DRONE_COMMIT_MESSAGE}" || exit 1
git push --set-upstream origin "${DRONE_BRANCH}" || exit 1
echo "✅ Swagger 文档同步到 ${TARGET_REPO} 仓库成功"

4.2 自定义构建镜像(Dockerfile)

文件路径:APIS 仓库根目录 /Dockerfile 核心作用:封装统一的工具链环境,避免本地环境差异导致的编译失败。

FROM golang:1.21-alpine

# 替换 Alpine 镜像源为清华源,加速依赖安装
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
    apk add --no-cache git protobuf curl openssh-server openssh-client && \
    git --version && protoc --version && ssh-keygen -A

# 安装 Buf 工具(版本 1.26.1)
RUN BIN="/usr/local/bin" && \
    VERSION="1.26.1" && \
    curl -sSL "https://mirror.ghproxy.com/https://github.com/bufbuild/buf/releases/download/v${VERSION}/buf-$(uname -s)-$(uname -m)" \
    -o "${BIN}/buf" && \
    chmod +x "${BIN}/buf"

# 配置 Go 代理,安装本地 protoc 插件(含 Kratos HTTP 插件)
RUN go env -w GOPROXY=https://goproxy.cn,direct && \
    go install google.golang.org/protobuf/cmd/protoc-gen-go@latest && \
    go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest && \
    go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest && \
    go install github.com/go-kratos/kratos/cmd/protoc-gen-go-http/v2@latest && \
    go install buf.build/bufbuild/validate-go/cmd/protoc-gen-validate-go@latest

六、注意事项

  1. Proto 规范约束:提交的 Proto 必须通过 buf lint 校验(如字段命名、消息类型定义),否则流水线会直接失败,确保团队规范统一;

  2. 分支一致性:APIS 仓库、apis-go、apis-swagger 仓库的分支需保持一致(如 feature/user-login 分支同步),避免依赖方拉取错误版本;

  3. 密钥安全ssh_private_key 需配置为 Drone 保密变量,禁止明文存储在脚本或配置文件中;

  4. 兼容性保障:接口变更需遵循「向后兼容」原则(如新增字段而非修改 / 删除原有字段),buf breaking 会自动检查兼容性,不兼容变更会阻断流水线;

  5. 失败排查:流水线失败时,优先查看 Drone 构建日志,常见问题:Proto 语法错误、Git 权限不足、YAPI 服务不可用;

  6. 脚本幂等性apis-go.shapis-swagger.sh 已实现幂等性(重复执行不会导致异常),可放心重试失败的流水线。

七、参考链接

  1. 煎鱼大佬:《真是头疼,Proto 代码到底放哪里?》

  2. Buf 官方文档:https://buf.build/docs/

  3. GRPC 官方文档:https://grpc.io/docs/

  4. Go-Kratos 官方文档:https://go-kratos.dev/docs