背景
随着云原生技术的日益发展和普及,微服务架构下多语言(python,go,php....)多系统之间通过rpc来通信,一旦接口需要升级,协调 server 端有发布和升级,IDL 文件仓库的变更,client 端变更发布就变成了一个很麻烦的事情。我们需要一个方便透明的规范来协调各端,一个更加自动化的工具来协同。以主流的grpc为例来探讨下, 这个 proto 及代码到底放在哪里?应该如何做到自动化版本控制?
方案一:放在各自的代码仓库
如下图所示,直接将项目所依赖到的以及自己的所有 proto 文件和client代码都存放在 protobuf/
目录下,不借助外部工具。
很明显该方案有以下优缺点。
缺点:
1️⃣ 项目所有依赖的 proto 都存储在各自项目的代码仓库下,因此所有依赖 proto 都需要人工的向其它业务组 “要” 来,再放到
protobuf/
目录下,人工介入极度麻烦。依赖多了拷贝会成为一种负担。2️⃣ 版本的控制和更新没有办法去维护,沟通成本太高。
3️⃣ 代码的复用性比较差。
优点:
- 简单易用,每个项目的proto依赖一目了然。不需要看这看那。
方案二:独立proto仓库
每个项目都有自己的proto仓库。需要依赖的都从proto仓库拉(包括服务自己)。
不过这种方式也是有比较明显的缺点:
缺点:
1️⃣ 各服务需要同时关注自己的服务本身还有服务的proto仓库,开发时需要先在proto仓库定义好proto和生成好client代码,所以在开发时需要在服务仓库和proto仓库切换。
2️⃣ 如果服务依赖的太多,也有可能跨业务组,例如下图,如果跨组之后,可能需求找业务组负责人开多个proto仓库权限。还需要一个一个地去依赖。
- 3️⃣ 每次有新的服务都需要对应去申请一个proto仓库比较麻烦。
优点:
1️⃣ 各自服务有各自的proto仓库,方便版本维护和升级。
2️⃣ 依赖时可以按需拉取。
方案三:集中仓库
按业务组维度来管理proto仓库,这样的话如果依赖某个业务组多个依赖时只需要拉取一个proto仓库即可。同时每次新起一个服务时,只需要在各自业务组的prooto仓库加上自己服务的,不需要单独去申请一个自己服务的proto仓库。
优点:
1️⃣ 依赖多个服务的proto时只需要依赖中央仓库即可。
2️⃣ 新增服务时不需要单独去申请proto仓库,只需要在各自业务组的proto仓库添加即可。
缺点:
1️⃣ 各服务开发时还是需要关注两个仓库,需要切来切去。
2️⃣ 每个中央仓库下不能独立的对各服务的proto进行版本管理。
3️⃣ 可能只依赖某个svc的proto,而多引入了一些其他不必要的。
方案四:镜像仓库+git branch
为解决上述方案的一些痛点,综合各方案的优点我们在中央仓库的基础之上采取了镜像仓库结合git branch的方式。 说明:
- 1️⃣ 各业务组的中央仓库的master的readme维护了各分支和各服务的对应关系,如下图。
2️⃣ 这样主要是为了新增新的svc时可以拿到下一个分支的分支名(由于go.mod的分支版本管理限制,只能以V2,V3这种命名),同时方便人们去辨识自己依赖的svc在哪个模块。common表示是所有svc proto的集合分支。每次新增一个服务时便会在cicd的过程中往这个reame添加说明。
3️⃣ 中央仓库的分支的readme维护了各自自己的版本情况。
4️⃣ 使用时可以根据自己的需要去依赖对应的分支的版本即可。
5️⃣ python cleint版本和go的版本都是一一对应的。
6️⃣ 各svc只需要依赖一个.gitlab.yml文件即可实现。具体的gitlab cicd的job如下:
.push_tmpl: &push_proto script: - echo "push test" - echo $CI_PROJECT_NAME - | userMail=$GITLAB_USER_EMAIL git config --global user.email "$GITLAB_USER_EMAIL" userName=${userMail%@*} echo "$userMail" git config --global user.name "$userName" git clone -v https://xxxxx/proto-center.git cd proto-center if [ `grep -c $CI_PROJECT_NAME README.md` -eq '0' ];then echo "- $CI_PROJECT_NAME-->v$(sed -n '$p' README.md | awk -F "-->v" '{print $2+1}' | head)" >>README.md export branchName=$(sed -n '$p' README.md | awk -F "-->" '{print $2}') echo $branchName git add . && git commit -m "add $CI_PROJECT_NAME " git push https://xxxxxxxx/proto-center.git master git checkout -b $branchName mkdir -p $CI_PROJECT_NAME && cp -r ../protobuf/* $CI_PROJECT_NAME/ go mod init xxxxxxxx/proto-center/$branchName go mod tidy git add . git commit -m "add $CI_PROJECT_NAME proto" git push --set-upstream https:/xxxxxxxx/proto-center.git $branchName git checkout v2 mkdir -p $CI_PROJECT_NAME && cp -r ../protobuf/* $CI_PROJECT_NAME/ git add . git commit -m "add $CI_PROJECT_NAME proto" git push https://xxxxxxxx/proto-center.git v2 else export branchName=$(grep $CI_PROJECT_NAME README.md | awk -F "-->" '{print $2}' | head) git checkout $branchName rm -rf $CI_PROJECT_NAME/ mkdir -p $CI_PROJECT_NAME cp -r ../protobuf/* $CI_PROJECT_NAME/ && git add . git commit -m "update $CI_PROJECT_NAME proto" git push https://xxxxxxxx/proto-center.git $branchName; git checkout v2 rm -rf $CI_PROJECT_NAME/ mkdir -p $CI_PROJECT_NAME cp -r ../protobuf/* $CI_PROJECT_NAME/ && git add . git commit -m "update $CI_PROJECT_NAME proto" git push https://xxxxxxxx/proto-center.git v2; fi
7️⃣ 中央仓库的gitlab的cicd的job如下:
.buld_tmpl: &tag_proto script: - echo "tag proto " - | userMail=$GITLAB_USER_EMAIL userName=${userMail%@*} echo "$userMail" git config --global user.email $userMail git config --global user.name $userName branch=$CI_BUILD_REF_NAME isnottag="false" git describe --tag || isnottag="true" git clone -b $branch -v https://xxxxx/proto-center.git cd proto-center userMail=$( git log --pretty=format:%ae ${CI_COMMIT_SHA} -1) userName=${userMail%@*} echo "$userMail" git config --global user.email $userMail git config --global user.name $userName version="" if [ $isnottag = "true" ];then echo $isnottag version=$branch.0.1 echo $version git tag $version git push https://xxxxx/proto-center.git --tags echo "- $version" >> README.md git add . git commit -m "add $version" git push https://xxxxx/proto-center.git $branch else echo $isnottag version_ref=$(git describe --tags | awk -F "-" '{print $1}' | head) echo $version_ref version=`echo $version_ref | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}'` echo $version git tag $version git push https://xxxxx/proto-center.git --tags echo "- $version" >> README.md git add . git commit -m "add $version" git push https://xxxxx/proto-center.git $branch fi mkdir -p python cp -r **/*.proto python pkgName=$( grep "\\-->" README.md | tail -1 | awk -F "-->v" '{print $1}' | awk -F " " '{print $2}') pkgName=${pkgName//-/_} echo $pkgName export pip_pkg_name=$pkgName echo $version export pip_tag_name=$version pip install grpcio-tools==1.4.0 pip install protobuf==3.3.0 echo "#!/bin/env python # -*- encoding=utf8 -*- import os from setuptools import (setup, find_packages) version = os.getenv('pip_tag_name') name = os.getenv('pip_pkg_name') setup( name=name, version=version, description='LLS grpc protocol', packages=find_packages(exclude=[]), include_package_data=True, author='LLS DEV Team', author_email='', package_data={'': ['*.*']}, install_requires=[ 'grpcio==1.18.0', 'protobuf==3.3.0' ], zip_safe=False, classifiers=[ 'Programming Language :: python :: 2.7', ], )" > python/setup.py echo " [distutils] index-servers = internal [internal] repository: https://xxxxx.com/ username: xxxxx password: xxxxx " > ~/.pypirc mkdir -p python/$pkgName echo "" >> python/$pkgName/__init__.py python -m grpc_tools.protoc -I python/ --python_out=python/$pkgName --grpc_python_out=python/$pkgName/ python/*.proto cd python ls python setup.py bdist_wheel upload -r internal cd .. mkdir -p python3 cp -r -n **/*.proto python3 echo "#!/bin/env python # -*- encoding=utf8 -*- import os from setuptools import (setup, find_packages) version = os.getenv('pip_tag_name') name = os.getenv('pip_pkg_name') setup( name=name, version=version, description='LLS grpc protocol', packages=find_packages(exclude=[]), include_package_data=True, author='LLS DEV Team', author_email='', package_data={'': ['*.*']}, install_requires=[ 'grpcio==1.18.0', 'protobuf==3.12.4' ], zip_safe=False, classifiers=[ 'Programming Language :: python :: 3.7', ], )" > python3/setup.py python3 -m pip install grpcio-tools==1.4.0 python3 -m pip install protobuf mkdir -p python3/$pkgName echo "" >> python3/$pkgName/__init__.py python3 -m grpc_tools.protoc -I python3/ --python_out=python3/$pkgName --grpc_python_out=python3/$pkgName/ python3/*.proto cd python3/$pkgName ls pb_files=`ls | grep -v '__init__' | grep -v 'grpc.py'` echo $pb_files need_replace_strs=() for pb_file in ${pb_files[@]} do prefix=${pb_file/.py/} after_handle_package_name=${prefix//_/__} need_replace_str="import $prefix as $after_handle_package_name" echo $need_replace_str need_replace_strs[${#need_replace_strs[@]}]="$need_replace_str" #echo ${need_replace_strs[0]} done need_replace_str_num=${#need_replace_strs[@]} all_files=`ls | grep -v '__init__'` echo $all_files for file in ${all_files[@]} do for ((i=0;i<$need_replace_str_num;i++)) do need_replace_str=${need_replace_strs[${i}]} echo $need_replace_str sed -i "s/^$need_replace_str$/from . $need_replace_str/" $file done done echo "finished" cd .. python3 setup.py bdist_wheel upload -r internal echo ${CI_COMMIT_SHA} noticeMail=$userMail content="\nversion: $version\nproject: $pkgName\ncommit:\nhttps://xxxxx/proto-center/commit/${CI_COMMIT_SHA}\n" curl --location --request POST 'https://xxxxx/webhookurl' \ --header 'Content-Type: application/json' \ --data-raw '{ "msg": { "content":"'"${content}"'" }'
8️⃣ 构建完通知,在中央仓库的job里面可以添加钉钉,微信,slack这种类似的机器人🤖通知方便我们知道proto cicd流程的情况,可以带上我们的commit信息以及对应的版本。
总结
总的来说,还有很多方案,每种方案都各有利弊,适合业务系统架构和公司组织机构的才是最好的方案,不过尽量通过工具来减少人肉的过程,提高效率。
参考链接
- https://eddycjy.com/posts/where-is-proto
- https://segmentfault.com/a/1190000022532645