api倉(cāng)庫(kù)和唯一標(biāo)準(zhǔn)協(xié)議共識(shí)

背景

目前公司采用 protocol buffer 作為 IDL,雖然可以根據(jù) API 定義,輕松生成客戶端和服務(wù)端的代碼。但是對(duì)于跨項(xiàng)目的接口,會(huì)增加項(xiàng)目之間的耦合性。例如 A 服務(wù)對(duì)外提供了一個(gè)接口,B 服務(wù)去調(diào)用。那么就需要根據(jù) A 服務(wù)的 proto 文件,生成客戶端代碼,并拷貝給 B。如果聯(lián)調(diào)期間,A 服務(wù)改動(dòng)了該接口,還需重復(fù)前面的步驟,非常繁瑣。

由此引出兩個(gè)問(wèn)題,proto 文件放在哪合適?調(diào)用方如何獲取生成的接口客戶端代碼?

如何解決

常見(jiàn)的幾種解決方案,煎魚(yú)大佬已經(jīng)描述得很詳細(xì)了(真是頭疼,Proto 代碼到底放哪里?),這里不再贅述。

經(jīng)過(guò)查閱資料,總結(jié)出適用于我們項(xiàng)目的幾種方案。

方案一:api 大倉(cāng) + git submodule(b 站)

  • Proto 文件只有一份,沒(méi)有拷貝。
  • pr 和發(fā)布解耦,修改 api 后,不用完成 pr,他人切換到對(duì)應(yīng)分支,就能使用。

存在的問(wèn)題

  • build 時(shí)需要將整個(gè) api 大倉(cāng)都生成中間代碼。
    • java 項(xiàng)目可通過(guò) maven 指定部分 api 文件。
    • go 項(xiàng)目需要新增 yaml 文件描述當(dāng)前項(xiàng)目依賴哪些 pb 文件,再通過(guò)腳本去生成中間代碼。
  • 維護(hù) Makefile,使用 protoc + go build 統(tǒng)一處理。
    • 腳本難寫(xiě)。
    • 每個(gè)項(xiàng)目都得維護(hù)相同功能的 Makefile。重復(fù)代碼,想修改、優(yōu)化腳本就很難。新項(xiàng)目新同事不清楚參考哪個(gè)老項(xiàng)目來(lái)寫(xiě)腳本,可能抄了一個(gè)存在已發(fā)現(xiàn)缺陷但當(dāng)前項(xiàng)目未修改的老版本腳本。
    • 和 Gitlab CI\CD 流水線腳本一個(gè)道理,最終不得不抽取公共腳本到一個(gè)專屬倉(cāng)庫(kù),其他項(xiàng)目采用引入的形式來(lái)做。但是非流水線腳本,沒(méi)有引入操作。
    • 個(gè)人項(xiàng)目、單一項(xiàng)目可采用這種方案,企業(yè)級(jí)的就得寫(xiě)復(fù)雜腳本了。

方案二:api 大倉(cāng) + git submodule + 每個(gè)項(xiàng)目生成代碼專有倉(cāng)庫(kù)

  • 生成代碼交給 ci。
  • 使用時(shí)通過(guò) go 依賴引入,無(wú)需編寫(xiě)生成代碼的腳本。
  • 依賴服務(wù) A 的接口,只需 go get 服務(wù) A 的接口文件生成的代碼。

存在的問(wèn)題

  • 每個(gè) go 項(xiàng)目都要去創(chuàng)建一個(gè)存放跟進(jìn) api 定義生成的代碼的倉(cāng)庫(kù)

方案三:每個(gè)項(xiàng)目都有一個(gè) api 倉(cāng)庫(kù),包含生成的代碼

  • 和方案二類似,只是把 api 大倉(cāng)拆了。

存在的問(wèn)題

  • 和方案二一樣。
  • api 代碼提 pr 時(shí),會(huì)展示 api 生成的代碼,非源碼,影響 CodeReview。
  • api 文件分散,不好集中管理、查看。特別是企業(yè)里,還得給新人配置多個(gè) api 倉(cāng)庫(kù)的權(quán)限。

方案四:api 大倉(cāng) + api 生成代碼的集中倉(cāng)庫(kù)

  • 將方案二里的每個(gè)項(xiàng)目都創(chuàng)建一個(gè) api 生成代碼的倉(cāng)庫(kù),改成一個(gè)整合的大倉(cāng)庫(kù)。
  • 使用時(shí) go get 依賴一個(gè)大倉(cāng)庫(kù)即可

存在的問(wèn)題

  • 依賴服務(wù) A 的接口,需要通過(guò) go get 引入所有服務(wù)的接口文件生成的代碼
    • 不過(guò)這個(gè)問(wèn)題不嚴(yán)重
      1. 這個(gè)倉(cāng)庫(kù)體積不大,因?yàn)榻涌诙x文件,整個(gè)公司也沒(méi)多少,一個(gè)項(xiàng)目才幾個(gè)文本文件,生成的代碼也不多。
      2. Java 不同,go build 不會(huì)將依賴包全部構(gòu)建到二進(jìn)制文件里,只會(huì)構(gòu)建項(xiàng)目里實(shí)際用到的文件。

權(quán)衡了下,最終選擇方案四。

具體實(shí)現(xiàn)

image

API 大倉(cāng):xxxapis

這里主要的工作就是 API 大倉(cāng)的 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: git@xxx.com: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,對(duì) proto 文件 lint 檢查,以及生成 go 代碼。之前有寫(xiě)文章介紹過(guò):Protocol Buffers 的擴(kuò)展工具:Buf

  • BUF_CACHE_DIR:buf 會(huì)產(chǎn)生緩存文件,可自定義路徑。這里放到了 /cache 下,這個(gè)目錄是掛載的,具體請(qǐng)看:Gitlab CI/CD 實(shí)踐三:Docker 安裝 Gitlab Runner。之所以采用掛載目錄來(lái)達(dá)到緩存的效果,是因?yàn)?gitlab 流水線的 cache 性能太差,而緩存的文件大多是小文件,數(shù)量幾千上萬(wàn),每次使用緩存都需要解壓、壓縮。

  • 172.x.x.x/common/buf:1.6.0:封裝的一個(gè)包含 buf 命令的鏡像,通過(guò) cicd 自動(dòng)構(gòu)建的:Gitlab CI/CD 實(shí)踐五:基礎(chǔ)鏡像 Dcokerfile 倉(cāng)庫(kù) 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 # 設(shè)置私服RUN go env -w GOPRIVATE=xxx.com # 設(shè)置忽略私服的https證書(shū)校驗(yàn)RUN go env -w GOINSECURE=xxx.comRUN git config --global http.sslverify falseRUN git config --global https.sslverify false # 設(shè)置代理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 "-------------------- 根據(jù) proto 文件生成go代碼 --------------------";buf mod update;buf generate --timeout 5m; echo "-------------------- 同步 proto 文件生成的go代碼到 ${TARGET_REPOSITORY} 倉(cāng)庫(kù) --------------------"; echo "-------------------- 配置 git ssh,實(shí)現(xiàn)免密提交到 ${TARGET_REPOSITORY} 倉(cāng)庫(kù) --------------------";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 用戶信息為當(dāng)前觸發(fā)流水線的用戶 --------------------";git config --global user.email "$GITLAB_USER_EMAIL";git config --global user.name "$GITLAB_USER_LOGIN"; echo "-------------------- git clone ${TARGET_REPOSITORY} 倉(cāng)庫(kù),只拉取指定分支的最后一次 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} 倉(cāng)庫(kù) --------------------";rm -rf /tmp/${TARGET_REPOSITORY}/xxxmv $CI_PROJECT_DIR/apigen/xxx /tmp/${TARGET_REPOSITORY};cd /tmp/${TARGET_REPOSITORY}go mod tidy; echo "-------------------- 提交到 ${TARGET_REPOSITORY} 倉(cāng)庫(kù) --------------------";cd /tmp/$TARGET_REPOSITORY;git add .;git commit -m "sync: 通過(guò) ${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
# 配置模塊信息,包括依賴項(xiàng)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生成規(guī)則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 倉(cāng)庫(kù):xxx-api-go

只是存放通過(guò) xxxapis 倉(cāng)庫(kù) ci 同步過(guò)來(lái)的文件。

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,這兩個(gè)倉(cāng)庫(kù)放到 gitlab 的 xxxapis 組下。

如何使用

這里就直接貼上 xxxapis 項(xiàng)目的 readme。

xxxapis

公司所有 API 定義文件(protocol buffer)統(tǒng)一存放到此倉(cāng)庫(kù)。

image
一、如何使用
1. 其他項(xiàng)目如何通過(guò) git submodule 的方式引入 API 大倉(cāng)?
git submodule add https://xxx.com/xxxapis/xxxapis.git
  • 提交代碼時(shí),需要提交.gitmodules 文件和 xxxapis 文件夾。
  • 通過(guò) git submodule 的方式引入 API 大倉(cāng)僅僅是為了在一個(gè)工作空間里,能同時(shí)修改代碼和接口定義,非必要。你完全可以 IDE 打開(kāi)兩個(gè)工作空間,一個(gè)其他項(xiàng)目,一個(gè) API 大倉(cāng)。

每個(gè)項(xiàng)目都引入 API 大倉(cāng),會(huì)不會(huì)浪費(fèi)空間?

API 大倉(cāng)體積很小的,一個(gè)項(xiàng)目的接口定義就幾個(gè)文本文件。

2. 如何下載 git submodule 的代碼?
  • 方案一、拉取主倉(cāng)庫(kù)以及子倉(cāng)

    git clone --recurse-submodules https://xxx.com/xxx
    
  • 方案二、已經(jīng)拉取主倉(cāng)庫(kù),手動(dòng)拉取子倉(cāng)

    git submodule update --init --recursive
    

    注意:首次拉取子倉(cāng),子倉(cāng)分支應(yīng)該是處于游離狀態(tài),可進(jìn)入子倉(cāng)目錄,通過(guò) git branch 查看。并不在任何分支,需要切換分支 git checkout main。

3. 如何更新、提交 git submodule 的代碼?

進(jìn)入子倉(cāng)目錄,和正常的倉(cāng)庫(kù)一樣,運(yùn)行 git pull,git submit,切記要檢查當(dāng)前所在分支是不是游離的。

4. 提交 proto 文件到 API 大倉(cāng)后,如何使用根據(jù) proto 文件生成的客戶端、服務(wù)端代碼?

go

提交 proto 文件后,會(huì)通過(guò)流水線生成對(duì)應(yīng)的 go 代碼,并上傳到 xxx-api-go。時(shí)間目前測(cè)試為半分鐘,流水線跑完會(huì)有郵件提醒。

go get xxx.com/xxxapis/xxx-api-go@main
  • 如果只是提交到 feature 分支,還未合并到 main,上訴命令需要修改末尾的分支名。

  • 跨項(xiàng)目聯(lián)調(diào)時(shí),可使用相同的分支。

  • 依賴包里還有 swagger 接口文檔

java

可使用 maven 插件,具體請(qǐng)參考 maven + protobuf + gRPC + gitlab CI

其他語(yǔ)言

暫未考慮,需要時(shí)再擴(kuò)展吧。

二、項(xiàng)目結(jié)構(gòu)

存放 proto 文件的目錄:

  • 一級(jí)目錄:公司名稱
  • 二級(jí)目錄:項(xiàng)目所在 gitlab 里的組
  • 三級(jí)目錄:項(xiàng)目所在 gitlab 里的項(xiàng)目名
  • 四級(jí)目錄:如果該項(xiàng)目只有一個(gè)服務(wù),四級(jí)目錄為接口版本號(hào)。如果項(xiàng)目包含多個(gè)服務(wù),四級(jí)目錄為服務(wù)名。

三、分支管理

此項(xiàng)目采用 Github Flow,持續(xù)發(fā)布。只有一個(gè)長(zhǎng)期分支:main,新功能基于 main 打 feature 分支,格式為 feature/xxx 功能,不用帶版本號(hào),因?yàn)榇隧?xiàng)目目前沒(méi)有使用版本號(hào)管理,接口版本通過(guò)目錄來(lái)體現(xiàn)。最后提合并請(qǐng)求到 main 分支,成功合并后就代表發(fā)布了。

參考

git submodule 使用方法

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容