背景

随着云原生技术的日益发展和普及,微服务架构下多语言(python,go,php…)多系统之间通过rpc来通信,一旦接口需要升级,协调 server 端有发布和升级,IDL 文件仓库的变更,client 端变更发布就变成了一个很麻烦的事情。我们需要一个方便透明的规范来协调各端,一个更加自动化的工具来协同。以主流的grpc为例来探讨下, 这个 proto 及代码到底放在哪里?应该如何做到自动化版本控制?

微服务架构下RPC IDL及代码如何统一管理?_rpc

方案一:放在各自的代码仓库

如下图所示,直接将项目所依赖到的以及自己的所有 proto 文件和client代码都存放在 protobuf/ 目录下,不借助外部工具。

微服务架构下RPC IDL及代码如何统一管理?_IDL_02
很明显该方案有以下优缺点。

缺点:

  • 1️⃣ 项目所有依赖的 proto 都存储在各自项目的代码仓库下,因此所有依赖 proto 都需要人工的向其它业务组 “要” 来,再放到 protobuf/ 目录下,人工介入极度麻烦。依赖多了拷贝会成为一种负担。

  • 2️⃣ 版本的控制和更新没有办法去维护,沟通成本太高。

  • 3️⃣ 代码的复用性比较差。

优点:

  • 简单易用,每个项目的proto依赖一目了然。不需要看这看那。

方案二:独立proto仓库

每个项目都有自己的proto仓库。需要依赖的都从proto仓库拉(包括服务自己)。

微服务架构下RPC IDL及代码如何统一管理?_微服务_03
不过这种方式也是有比较明显的缺点:

缺点:

  • 1️⃣ 各服务需要同时关注自己的服务本身还有服务的proto仓库,开发时需要先在proto仓库定义好proto和生成好client代码,所以在开发时需要在服务仓库和proto仓库切换。

  • 2️⃣ 如果服务依赖的太多,也有可能跨业务组,例如下图,如果跨组之后,可能需求找业务组负责人开多个proto仓库权限。还需要一个一个地去依赖。

微服务架构下RPC IDL及代码如何统一管理?_rpc_04

  • 3️⃣ 每次有新的服务都需要对应去申请一个proto仓库比较麻烦。

优点:

  • 1️⃣ 各自服务有各自的proto仓库,方便版本维护和升级。

  • 2️⃣ 依赖时可以按需拉取。

方案三:集中仓库

按业务组维度来管理proto仓库,这样的话如果依赖某个业务组多个依赖时只需要拉取一个proto仓库即可。同时每次新起一个服务时,只需要在各自业务组的prooto仓库加上自己服务的,不需要单独去申请一个自己服务的proto仓库。

微服务架构下RPC IDL及代码如何统一管理?_rpc_05

优点:

  • 1️⃣ 依赖多个服务的proto时只需要依赖中央仓库即可。

  • 2️⃣ 新增服务时不需要单独去申请proto仓库,只需要在各自业务组的proto仓库添加即可。

缺点:

  • 1️⃣ 各服务开发时还是需要关注两个仓库,需要切来切去。

  • 2️⃣ 每个中央仓库下不能独立的对各服务的proto进行版本管理。

  • 3️⃣ 可能只依赖某个svc的proto,而多引入了一些其他不必要的。

方案四:镜像仓库+git branch

为解决上述方案的一些痛点,综合各方案的优点我们在中央仓库的基础之上采取了镜像仓库结合git branch的方式。
微服务架构下RPC IDL及代码如何统一管理?_微服务_06
说明:

  • 1️⃣ 各业务组的中央仓库的master的readme维护了各分支和各服务的对应关系,如下图。

微服务架构下RPC IDL及代码如何统一管理?_微服务_07

  • 2️⃣ 这样主要是为了新增新的svc时可以拿到下一个分支的分支名(由于go.mod的分支版本管理限制,只能以V2,V3这种命名),同时方便人们去辨识自己依赖的svc在哪个模块。common表示是所有svc proto的集合分支。每次新增一个服务时便会在cicd的过程中往这个reame添加说明。

  • 3️⃣ 中央仓库的分支的readme维护了各自自己的版本情况。

微服务架构下RPC IDL及代码如何统一管理?_rpc_08

  • 4️⃣ 使用时可以根据自己的需要去依赖对应的分支的版本即可。

  • 5️⃣ python cleint版本和go的版本都是一一对应的。

微服务架构下RPC IDL及代码如何统一管理?_rpc_09

  • 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 '{
              "text": {
                  "msg":"'"${content}"'"
              }
          }'
    
  • 8️⃣ 构建完通知,在中央仓库的job里面可以添加钉钉,微信,slack这种类似的机器人????通知方便我们知道proto cicd流程的情况,可以带上我们的commit信息以及对应的版本。

微服务架构下RPC IDL及代码如何统一管理?_微服务_10

总结

总的来说,还有很多方案,每种方案都各有利弊,适合业务系统架构和公司组织机构的才是最好的方案,不过尽量通过工具来减少人肉的过程,提高效率。

参考链接

  • https://eddycjy.com/posts/where-is-proto