背景
目前公司采用 protocol buffer 作为 IDL,虽然可以根据 API 定义,轻松生成客户端和服务端的代码。但是对于跨项目的接口,会增加项目之间的耦合性。例如 A 服务对外提供了一个接口,B 服务去调用。那么就需要根据 A 服务的 proto 文件,生成客户端代码,并拷贝给 B。如果联调期间,A 服务改动了该接口,还需重复前面的步骤,非常繁琐。
由此引出两个问题,proto 文件放在哪合适?调用方如何获取生成的接口客户端代码?
如何解决
常见的几种解决方案,煎鱼大佬已经描述得很详细了(真是头疼,Proto 代码到底放哪里?),这里不再赘述。
经过查阅资料,总结出适用于我们项目的几种方案。
方案一:api 大仓 + git submodule(b 站)
- Proto 文件只有一份,没有拷贝。
- pr 和发布解耦,修改 api 后,不用完成 pr,他人切换到对应分支,就能使用。
存在的问题
- build 时需要将整个 api 大仓都生成中间代码。
- java 项目可通过 maven 指定部分 api 文件。
- go 项目需要新增 yaml 文件描述当前项目依赖哪些 pb 文件,再通过脚本去生成中间代码。
- 维护 Makefile,使用 protoc + go build 统一处理。
- 脚本难写。
- 每个项目都得维护相同功能的 Makefile。重复代码,想修改、优化脚本就很难。新项目新同事不清楚参考哪个老项目来写脚本,可能抄了一个存在已发现缺陷但当前项目未修改的老版本脚本。
- 和 Gitlab CI\CD 流水线脚本一个道理,最终不得不抽取公共脚本到一个专属仓库,其他项目采用引入的形式来做。但是非流水线脚本,没有引入操作。
- 个人项目、单一项目可采用这种方案,企业级的就得写复杂脚本了。
方案二:api 大仓 + git submodule + 每个项目生成代码专有仓库
- 生成代码交给 ci。
- 使用时通过 go 依赖引入,无需编写生成代码的脚本。
- 依赖服务 A 的接口,只需 go get 服务 A 的接口文件生成的代码。
存在的问题
- 每个 go 项目都要去创建一个存放跟进 api 定义生成的代码的仓库
方案三:每个项目都有一个 api 仓库,包含生成的代码
- 和方案二类似,只是把 api 大仓拆了。
存在的问题
- 和方案二一样。
- api 代码提 pr 时,会展示 api 生成的代码,非源码,影响 CodeReview。
- api 文件分散,不好集中管理、查看。特别是企业里,还得给新人配置多个 api 仓库的权限。
方案四:api 大仓 + api 生成代码的集中仓库
- 将方案二里的每个项目都创建一个 api 生成代码的仓库,改成一个整合的大仓库。
- 使用时 go get 依赖一个大仓库即可
存在的问题
- 依赖服务 A 的接口,需要通过 go get 引入所有服务的接口文件生成的代码
- 不过这个问题不严重
- 这个仓库体积不大,因为接口定义文件,整个公司也没多少,一个项目才几个文本文件,生成的代码也不多。
- 和
Java
不同,go build 不会将依赖包全部构建到二进制文件里,只会构建项目里实际用到的文件。
- 不过这个问题不严重
权衡了下,最终选择方案四。
具体实现
API 大仓:xxxapis
这里主要的工作就是 API 大仓的 CI 脚本.gitlab-ci.yml
stages: - lint - generate variables: BUF_CACHE_DIR: /cache/${CI_PROJECT_PATH}/buf-cachebefore_script: - mkdir -p $BUF_CACHE_DIR buf_lint: stage: lint image: 172.x.x.x/common/buf:1.6.0 tags: - 172.x.x.x-runner interruptible: true script: - buf mod update - buf lint --error-format=json --timeout 5m generate_go_file: stage: generate image: 172.x.x.x/common/buf:1.6.0 tags: - 172.x.x.x-runner interruptible: true variables: TARGET_REPOSITORY_ADDR: [email protected]:xxxapis/xxx-api-go.git TARGET_REPOSITORY: xxx-api-go script: - chmod +x ./script/generate-go-file-to-xxx-api-go.sh - ./script/generate-go-file-to-xxx-api-go.sh
使用 buf,对 proto 文件 lint 检查,以及生成 go 代码。之前有写文章介绍过:Protocol Buffers 的扩展工具:Buf
BUF_CACHE_DIR
:buf 会产生缓存文件,可自定义路径。这里放到了/cache
下,这个目录是挂载的,具体请看:Gitlab CI/CD 实践三:Docker 安装 Gitlab Runner。之所以采用挂载目录来达到缓存的效果,是因为 gitlab 流水线的 cache 性能太差,而缓存的文件大多是小文件,数量几千上万,每次使用缓存都需要解压、压缩。-
172.x.x.x/common/buf:1.6.0
:封装的一个包含 buf 命令的镜像,通过 cicd 自动构建的:Gitlab CI/CD 实践五:基础镜像 Dcokerfile 仓库 CI 流水线配置common/buf/1.6.0/Dockerfile
FROM 172.x.x.x/common/golang:1.17.9 RUN BIN="/usr/local/bin" && \ VERSION="1.6.0" && \ curl -sSL \ "https://ghproxy.com/https://github.com/bufbuild/buf/releases/download/v${VERSION}/buf-$(uname -s)-$(uname -m)" \ -o "${BIN}/buf" && \ chmod +x "${BIN}/buf" && \ sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list && \ apt-get install apt-transport-https ca-certificates -y && \ apt-get update && \ apt-get install -y --no-install-recommends \ vim openssh-server && \ touch ~/.vimrc && \ echo "set fileencodings=ucs-bom,utf-8,gbk,gb2312,cp936,gb18030,big5,latin-1" >> ~/.vimrc && \ echo "set encoding=utf-8" >> ~/.vimrc && \ echo "set termencoding=utf-8" >> ~/.vimrc && \ echo "set fileencoding=utf-8" >> ~/.vimrc && \ echo "set number" >> ~/.vimrc && \ 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/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
172.x.x.x/common/golang/1.17.9/Dockerfile
FROM golang:1.17.9 # 设置私服RUN go env -w GOPRIVATE=xxx.com # 设置忽略私服的https证书校验RUN go env -w GOINSECURE=xxx.comRUN git config --global http.sslverify falseRUN git config --global https.sslverify false # 设置代理RUN go env -w GOPROXY="https://goproxy.cn,direct" RUN go env -w GO111MODULE=on
generate-go-file-to-xxx-api-go.sh
#!/bin/bash echo "-------------------- 根据 proto 文件生成go代码 --------------------";buf mod update;buf generate --timeout 5m; echo "-------------------- 同步 proto 文件生成的go代码到 ${TARGET_REPOSITORY} 仓库 --------------------"; echo "-------------------- 配置 git ssh,实现免密提交到 ${TARGET_REPOSITORY} 仓库 --------------------";eval $(ssh-agent -s)ssh-add <(echo "$CI_AUTO_SYNC_SSH_PRIVATE_KEY");mkdir -p ~/.ssh;touch ~/.ssh/config;echo "StrictHostKeyChecking no" >> ~/.ssh/config;echo "UserKnownHostsFile /dev/null" >> ~/.ssh/config; echo "-------------------- 配置 git 用户信息为当前触发流水线的用户 --------------------";git config --global user.email "$GITLAB_USER_EMAIL";git config --global user.name "$GITLAB_USER_LOGIN"; echo "-------------------- git clone ${TARGET_REPOSITORY} 仓库,只拉取指定分支的最后一次 commit --------------------";cd /tmp;BRANCH=$CI_COMMIT_BRANCH;if ! (git clone --depth 1 --branch $BRANCH $TARGET_REPOSITORY_ADDR); then echo "新建分支:$BRANCH" git clone --depth 1 --branch main $TARGET_REPOSITORY_ADDR; cd ${TARGET_REPOSITORY}; git checkout -b $BRANCH;fi echo "-------------------- 拷贝go文件到 ${TARGET_REPOSITORY} 仓库 --------------------";rm -rf /tmp/${TARGET_REPOSITORY}/xxxmv $CI_PROJECT_DIR/apigen/xxx /tmp/${TARGET_REPOSITORY};cd /tmp/${TARGET_REPOSITORY}go mod tidy; echo "-------------------- 提交到 ${TARGET_REPOSITORY} 仓库 --------------------";cd /tmp/$TARGET_REPOSITORY;git add .;git commit -m "sync: 通过 ${CI_PROJECT_PATH} gitlab ci 同步 proto 文件生成的go代码" || true;git push --set-upstream origin $BRANCH;echo "-------------------- 同步成功 --------------------";
-
CI_AUTO_SYNC_SSH_PRIVATE_KEY
:在 gitlab 配置的变量,具体谷歌 gitlab 配置 ssh
buf 配置
buf.yaml
# 配置模块信息,包括依赖项version: v1deps: - buf.build/googleapis/googleapislint: use: - DEFAULT except: - FIELD_LOWER_SNAKE_CASE - RPC_REQUEST_STANDARD_NAME - RPC_RESPONSE_STANDARD_NAME - RPC_REQUEST_RESPONSE_UNIQUEbreaking: use: - FILE
buf.gen.yaml
# 配置protoc生成规则version: v1managed: enabled: true go_package_prefix: default: xxx.com/xxxapis/xxx-api-go except: - buf.build/googleapis/googleapis plugins: - name: go out: apigen opt: paths=source_relative - name: go-grpc out: apigen opt: - paths=source_relative - require_unimplemented_servers=false - name: grpc-gateway out: apigen opt: paths=source_relative - name: openapiv2 out: apigen opt: - allow_repeated_fields_in_body=true
API-GO 仓库:xxx-api-go
只是存放通过 xxxapis
仓库 ci 同步过来的文件。
go.mod
module xxx.com/xxxapis/xxx-api-go go 1.17 require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.3 google.golang.org/genproto v0.0.0-20220707150051-590a5ac7bee1 google.golang.org/grpc v1.47.0 google.golang.org/protobuf v1.28.0) require ( github.com/golang/protobuf v1.5.2 // indirect golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect golang.org/x/text v0.3.7 // indirect)
参照 google,这两个仓库放到 gitlab 的 xxxapis 组下。
如何使用
这里就直接贴上 xxxapis 项目的 readme。
xxxapis
公司所有 API 定义文件(protocol buffer)统一存放到此仓库。
一、如何使用
1. 其他项目如何通过 git submodule
的方式引入 API 大仓?
git submodule add https://xxx.com/xxxapis/xxxapis.git
- 提交代码时,需要提交
.gitmodules
文件和xxxapis
文件夹。 - 通过
git submodule
的方式引入 API 大仓仅仅是为了在一个工作空间里,能同时修改代码和接口定义,非必要。你完全可以 IDE 打开两个工作空间,一个其他项目,一个 API 大仓。
每个项目都引入 API 大仓,会不会浪费空间?
API 大仓体积很小的,一个项目的接口定义就几个文本文件。
2. 如何下载 git submodule
的代码?
-
方案一、拉取主仓库以及子仓
git clone --recurse-submodules https://xxx.com/xxx
-
方案二、已经拉取主仓库,手动拉取子仓
git submodule update --init --recursive
注意:首次拉取子仓,子仓分支应该是处于游离状态,可进入子仓目录,通过
git branch
查看。并不在任何分支,需要切换分支git checkout main
。
3. 如何更新、提交 git submodule
的代码?
进入子仓目录,和正常的仓库一样,运行 git pull
,git submit
,切记要检查当前所在分支是不是游离的。
4. 提交 proto 文件到 API 大仓后,如何使用根据 proto 文件生成的客户端、服务端代码?
go
提交 proto 文件后,会通过流水线生成对应的 go 代码,并上传到 xxx-api-go
。时间目前测试为半分钟,流水线跑完会有邮件提醒。
go get xxx.com/xxxapis/xxx-api-go@main
如果只是提交到 feature 分支,还未合并到 main,上诉命令需要修改末尾的分支名。
跨项目联调时,可使用相同的分支。
依赖包里还有 swagger 接口文档
java
可使用 maven 插件,具体请参考 maven + protobuf + gRPC + gitlab CI
其他语言
暂未考虑,需要时再扩展吧。
二、项目结构
存放 proto 文件的目录:
- 一级目录:公司名称
- 二级目录:项目所在 gitlab 里的组
- 三级目录:项目所在 gitlab 里的项目名
- 四级目录:如果该项目只有一个服务,四级目录为接口版本号。如果项目包含多个服务,四级目录为服务名。
三、分支管理
此项目采用 Github Flow,持续发布。只有一个长期分支:main,新功能基于 main 打 feature 分支,格式为 feature/xxx 功能,不用带版本号,因为此项目目前没有使用版本号管理,接口版本通过目录来体现。最后提合并请求到 main
分支,成功合并后就代表发布了。
参考
git submodule 使用方法
参考资料
- 真是头疼,Proto 代码到底放哪里?
- Protobuf 规范
- googleapis
- 微服务架构下 RPC IDL 及代码如何统一管理?
- API 工程化分享
- API 工程化分享 - 毛剑
- maven + protobuf + gRPC + gitlab CI
- 使用 gitlab 实现 proto 文件的 semantic version 管理(1) - 使用规范
- 使用 gitlab 实现 proto 文件的 semantic version 管理(2) - 配置篇
- git submodule 使用方法