|
先看google cloud的api设计指南
https://cloud.google.com/apis/design/directory_structure
API 服务通常使用 .proto 文件来定义 API 接口,并使用 .yaml 文件来配置 API 服务。每个 API 服务必须在 API 代码库中有一个 API 目录。API 目录应该包含所有 API 定义文件和构建脚本。
每个 API 目录应该具有以下标准布局:
代码库必要条件
BUILD:构建文件。METADATA:构建元数据文件。OWNERS:API 目录所有者。README.md:有关 API 服务的常规信息。
配置文件
{service}.yaml:基准服务配置文件,google.api.Service proto 消息的 YAML 表示法。prod.yaml:生产环境增量服务配置文件。staging.yaml:模拟环境增量服务配置文件。test.yaml:测试环境增量服务配置文件。local.yaml:本地环境增量服务配置文件。
文档文件
doc/*:技术文档文件。它们应采用 Markdown 格式。
接口定义
- v[0-9]/:每个这样的目录都包含 API 的主要版本,主要是 proto 文件和构建脚本。
{subapi}/v[0-9]/:每个 {subapi} 目录都包含子 API 的接口定义。每个子 API 可以有自己的独立主要版本。type/:proto 文件,包含在不同 API 之间、同一 API 的不同版本之间或 API 与服务实现之间共享的类型。type/ 下的类型定义一旦发布就不应该有破坏性更改。
看一个例子:
# 留意这里的v1目录中为proto文件googleapis/google/example/endpointsapis$ tree -A.├── BUILD.bazel├── endpointsapis.yaml├── goapp│ ├── app.yaml│ └── main.go├── prod.yaml├── README.md├── staging.yaml└── v1 └── workspace.proto再看 Uber Protobuf Style Guide V2
https://github.com/uber/prototool/blob/dev/style/README.md
Files should be stored in a directory structure that matches their package sub-names. All files in a given directory should be in the same package.
The following is an example of this in practice.
.└── uber ├── finance │ ├── ccap │ │ └── v1 │ │ └── ccap.proto // package uber.finance.ccap.v1 │ └── payment │ ├── v1 │ │ └── payment.proto // package uber.finance.payment.v1 │ └── v1beta1 │ └── payment.proto // package uber.finance.payment.v1beta1 └── trip ├── v1 │ ├── trip_api.proto // package uber.trip.v1 │ └── trip.proto // package uber.trip.v1 └── v2 ├── trip_api.proto // package uber.trip.v2初步结论:按照package + version的层次组织目录
综合上面google和uber的规范,可以得出结论,应该按照package + version的层次来组织protobuf文件的目录结构
举例我们新建一个项目:styx
那么protobuf应该是如下的结构
$ tree -A.└── styx └── v1 └── service.protopython项目目录
一个python项目的目录结构,按照标准,以styx为例,应该如下所示:
styx$ tree -A$ tree -A styxstyx├── README.md├── setup.py└── styx ├── front │ ├── index.py │ └── __init__.py ├── __int__.py └── logic └── user.py
项目目录为styx, 内层再有一个styx作为python的package, package作为整理部署,可以pip install,或放到指定的目录运行
那么styx项目中加入grpc服务,我们上面定义的protobuf文件的目录树应该放到哪里呢,生成的stub文件又该如何引用呢?
参考下envoy
envoy/api$ tree -A -L 2....├── CONTRIBUTING.md├── envoy│ ├── admin│ ├── annotations│ ├── api│ ├── config│ ├── data│ ├── extensions│ ├── service│ ├── type│ └── watchdog├── examples│ └── service_envoy├── README.md...
如上所示, envoy在项目目录下有个子目录api, 下面放所有的proto文件,也是按照package + version的结构,如:envoy/service/health/v3/hds.proto
api目录还会mirror到另外一个独立的仓库: https://github.com/envoyproxy/data-plane-api.git
参考这个方式,styx的完整结构应该是
$ tree styx -Astyx├── api│ └── styx│ └── v1│ └── service.proto├── README.md├── setup.py└── styx ├── front │ ├── index.py │ └── __init__.py ├── __int__.py └── logic └── user.py确定proto文件目录结构
不同于envoy,业务项目可能会对外提供不同的接口,可能有针对前端的restful接口,有针对内部的grpc接口,api这个目录名意义不明
可以改为protos,目录结构如下:
$ tree styx -Astyx├── protos│ └── styx│ └── v1│ └── service.proto├── README.md├── setup.py└── styx ├── front │ ├── index.py │ └── __init__.py ├── __int__.py └── logic └── user.pyprotobuf生成的stub文件
接下里的问题是,protobuf生成的stub文件应该如何引用? 如果作为一个独立pip包发布,那么引用的方式应该是什么?
styx本身部署的package是styx, 那stub代码应该要区分下才比较合适
envoy的api在pypi上有2个package,如下:
pip install python-envoy-protobuf-installer
from envoy_proto.envoy.config.filter.accesslog.v2.accesslog_pb2 import AccessLogfrom envoy_proto.envoy.config.accesslog.v2.file_pb2 import FileAccessLogfrom envoy_proto.envoy.api.v2.core.config_source_pb2 import ConfigSourcefrom envoy_proto.envoy.api.v2.listener.listener_pb2 import ( Filter, FilterChain,)
pip install envoy-data-plane
import envoy_data_plane.envoy.api.v2 as envoy
可以看到都额外包了个不同的package name
参考这个内容, 我们可以部门内部定个标准,所有stub代码放在统一的namespace下, 比如baorpc, 以styx为例:
import baorpc.styx.v1.service_pb2_grpc
假如内部的grpc接口使用package: internal, 那么styx的的结构如下:
$ tree styx -Astyx├── protos│ └── styx│ └── v1│ └── service.proto├── README.md├── setup.py└── styx ├── front │ ├── index.py │ └── __init__.py ├── internal │ └── server.py ├── __int__.py └── logic └── user.py
style/internal/server.py的内容示例:
#-*- coding: utf8 -*-from baogrpc.styx.v1.service_pb2_gprc import StyxServicerdef serve(): server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) # add servicer to server server.add_insecure_port('[::]:50051') server.start() server.wait_for_termination()if __name__ == '__main__': serve()ci自动push protos目录的变动到中央仓库
ci的实现参考envoy的做法 https://github.com/envoyproxy/envoy/blob/main/ci/api_mirror.sh:
#!/bin/bashset -eCHECKOUT_DIR=../data-plane-apiMAIN_BRANCH="refs/heads/main"API_MAIN_BRANCH="main"if [[ "${AZP_BRANCH}" == "${MAIN_BRANCH}" ]]; then echo "Cloning..." git clone git@github.com:envoyproxy/data-plane-api "$CHECKOUT_DIR" -b "${API_MAIN_BRANCH}" git -C "$CHECKOUT_DIR" config user.name "data-plane-api(Azure Pipelines)" git -C "$CHECKOUT_DIR" config user.email data-plane-api@users.noreply.github.com # Determine last envoyproxy/envoy SHA in envoyproxy/data-plane-api MIRROR_MSG="Mirrored from https://github.com/envoyproxy/envoy" LAST_ENVOY_SHA=$(git -C "$CHECKOUT_DIR" log --grep="$MIRROR_MSG" -n 1 | grep "$MIRROR_MSG" | \ tail -n 1 | sed -e "s#.*$MIRROR_MSG @ ##") echo "Last mirrored envoyproxy/envoy SHA is $LAST_ENVOY_SHA" # Compute SHA sequence to replay in envoyproxy/data-plane-api SHAS=$(git rev-list --reverse "$LAST_ENVOY_SHA"..HEAD api/) # For each SHA, hard reset, rsync api/ and generate commit in # envoyproxy/data-plane-api API_WORKING_DIR="../envoy-api-mirror" git worktree add "$API_WORKING_DIR" for sha in $SHAS do git -C "$API_WORKING_DIR" reset --hard "$sha" COMMIT_MSG=$(git -C "$API_WORKING_DIR" log --format=%B -n 1) QUALIFIED_COMMIT_MSG=$(echo -e "$COMMIT_MSG\n\n$MIRROR_MSG @ $sha") rsync -acv --delete --exclude "ci/" --exclude ".*" --exclude LICENSE \ "$API_WORKING_DIR"/api/ "$CHECKOUT_DIR"/ git -C "$CHECKOUT_DIR" add . git -C "$CHECKOUT_DIR" commit -m "$QUALIFIED_COMMIT_MSG" done echo "Pushing..." git -C "$CHECKOUT_DIR" push origin "${API_MAIN_BRANCH}" echo "Done"fi
不同项目在中央仓库的结构,可以使用项目的仓库名+branch组合作为中央仓库的branch,可以再讨论
ci也可以考虑自动生成pip包 |
|