0%

Go微服务实战05-工程化项目实践API设计和配置管理

1. 工程项目

1.1 项目结构

可以参考:https://github.com/golang-standards/project-layout/blob/master/README_zh.md

  1. /cmd
    本项目的主干,cmd应用目录负责程序的:启动、关闭、配置初始化等。

    每个应用程序的目录名应该与你想要的可执行文件的名称相匹配(例如,/cmd/myapp)。

    不要在这个目录中放置太多代码。如果你认为代码可以导入并在其他项目中使用,那么它应该位于 /pkg 目录中。如果代码不是可重用的,或者你不希望其他人重用它,请将该代码放到 /internal 目录中。

  1. /pkg

    希望被别人重用,放在 pkg。

    外部应用程序可以使用的库代码(如,/pkg/mypubliclib),pkg目录内,可以参考go标准库的组织方式,按照功能分类。

  2. /internal

    私有应用程序和库代码,这是你不希望其他人在其应用程序或库中导入代码。

1.2 kit 基础库

每个公司都应当为不同的微服务建立一个统一的 kit 工具包项目(基础库/框架)和app项目。

基础库kit为独立项目,公司级建议只有一个,按照功能目录来拆分会带来不少的管理工作,因此建议合并整合。

kit项目必须具备的特点:

  • 统一
  • 标准库方式布局
  • 高度抽象
  • 支持插件

1.3 服务项目结构

  1. /api

    API协议定义目录,xxapi.proto protobuf文件,以及生成的go 文件。我们通常把api文档直接proto文件中描述。

  2. /config

    配置文件模板或默认配置。

  3. /test

    额外的外部测试应用程序和测试数据。你可以随时根据需求构造 /test目录。

    对于较大的顶目,有一个数据子目录是有意义的。例如, 你可以使用test/data或test/testdata(如果你需要忽略目录中的内

    容)。

你不应该拥有的目录

  1. /src

    有些 Go 项目确实有一个 src 文件夹,但这通常发生在开发人员有 Java 背景,在那里它是一种常见的模式。如果可以的话,尽量不要采用这种 Java 模式。你真的不希望你的 Go 代码或 Go 项目看起来像 Java:-)

1.4 服务应用项目

一个gitlab的project里可以放置多个微服务的 app(类似monorepo)。也可以按照gitlab的 group里建立多个project,每个project对应一个 app。

微服务中的app服务类型分为几类:interface、 service、job、admin、task。

  • interface: 对外的 BFF 服务,接受来自用户的请求,比如暴露了 HTTP/gRPC 接口。
  • service: 对内的微服务,仅接受来自内部其他服务或者网关的请求,比如暴露了 gRPC 接口只对内服务。
  • admin:区别于 service,更多是面向运营测的服务,通常数据权限更高,隔离带来更好的代码级别安全。
  • job: 流式任务处理的服务,上游一般依赖 message broker(kafka)。
  • task: 定时任务,类似 cronjob,部署到 task 托管平台中(可以用k8s的cronjob)。

1.5 示例

image.png
  • app 目录下有 api、cmd、configs、internal 目录,目录里一般还会放置 README、CHANGELOG、OWNERS。
  • internal: 是为了避免有同业务下有人跨目录引用了内部的 biz、data、service 等内部 struct。
    • 如果存在一个仓库多个应用,那么可以在 internal 里面进行分层,例如 /internal/app , /internal/job
    • biz: 业务逻辑的组装层,类似 DDD 的 domain 层,data 类似 DDD 的 repo,repo 接口在这里定义,使用依赖倒置的原则
    • data: 业务数据访问,包含 cache、db 等封装,实现了 biz 的 repo 接口。我们可能会把 data 与 dao 混淆在一起,data 偏重业务的含义,它所要做的是将领域对象重新拿出来,我们去掉了 DDD 的 infra 层。
    • service: 实现了 api 定义的服务层,类似 DDD 的 application 层,处理 DTO 到 biz 领域实体的转换(DTO -> DO),同时协同各类 biz 交互,但是不应处理复杂逻辑。

2. 模型设计

2.1 数据模型

  • DTO(Data Transfer Object)

    数据传输对象。这个概念来源于J2EE的设计模式。但在这里,泛指用于展示层AP1层与服务层业务逻辑层)之间的数据传输对象。

    就是数据库映射的类型和展示的类型,要进行一些裁剪。

  • DO(Domain Obiect)

    领域对象,就是从现实世界中抽象出来的有形或无形的业务实体,第一版缺乏DTO->DO的对象转换,DO理解成以前的model。

  • PO(Persistent Object)

    持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么数据表中的每个字段(或若干个)就对应 PO 的一个(或若干个)属性。

image-20240628115807850

2.2 Biz 领域模型

image-20240628132416429

3. API设计

3.1 API 项目

https://github.com/envoyproxy/data-plane-api

https://github.com/istio/api

为了统一检索和规范API,我们内部建立了一个统一的bapis仓库,整合所有对内对外API。

  • API仓库,方便跨部门协作。
  • 版本管理,基于gt控制。
  • 规范化检查,API lint。
  • API design review,变更diff。
  • 权限管理,目录OWNERS。

3.2 API 兼容性设计

  1. 向下兼容的变更
  • 新增接口
  • 新增参数字段
  • 新增返回字段
  1. 向下不兼容的变更(破坏性变更)
  • 删除或重命名服务,字段,方法或枚举值
    • 如果只有很少的 api 变动可以创建一个 XXXV2 的方法
    • 如果变动的 api 比较多,可以直接新启一个 v2 的包
  • 修改字段的类型
  • 修改现有请求的可见行为
  • 给资源消息添加 读取/写入 字段

3.3 API 命名规范

  • 包名
产品名product
应用名app
版本号v1
包名product.app.v1
目录结构api/product/app/v1/xx.proto

3.4 API Protobuf 字段

  • oneof 最多同时设置其中一个字段

    如果你有一条包含多个字段的消息,并且最多同时设置其中一个字段,那么你可以通过使用oneof来实现并节省内存。

    1
    2
    3
    4
    5
    6
    7
    8
    // 通知读者的消息
    message NoticeReaderRequest{
    string msg = 1;
    oneof notice_way{
    string email = 2;
    string phone = 3;
    }
    }
  • WrapValue 默认值问题

    gRPC默认使用Protobuf v3格式,因为去除了 required和optional关键字,默认全部都是 optional字段。如果没有赋值的字段,默认会基础类型字段的默认值,比如0或者””。(v3.15.0又加回来了)

    可以通过包裹一下解决,这样就有指针类型了。

1
2
3
message DoubleValue {
double value = 1;
}
  • FieldMask 只更新个别字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
message UpdateBookRequest {
// 操作人
string op = 1;
// 要更新的书籍信息
Book book = 2;
}


message UpdateBookRequest {
// 操作人
string op = 1;
// 要更新的书籍信息
Book book = 2;
// 要更新的字段
google.protobuf.FieldMask update_mask = 3;
}

3.5 API Errors

  1. 需要细节错误的再定义小错误。一般直接返回一个大错误。
  2. 错误传播:隐藏机密信息,感兴趣的可以把错误转换成自己的错误码,翻译了错误码。
  3. 错误码的唯一性,一般和服务绑定的。全局的错误码比较松散。
1
2
3
4
5
{
Code int // 大类错误
Reason string // 小类错误
Message string
}

4. 配置管理

4.1 分类

  • 环境配置
    • 环境配置,应该是应用部署时就已经确定好的信息,这些信息不应该写在我们的配置文件或者是放到配置中心,而是应该由我们的部署平台,例如 K8s 直接在容器启动时候就注入好。
    • region: 区域信息
    • env: 环境信息,例如 prod, test
    • zone: 可用区
    • host: 机器名
    • appid: 应用 id
    • color: 流量染色信息,用来做流量分发的
  • 静态配置【固定不reload】
    • 资源需要初始化的配置信息,比如 http/gRPC server、redis、mysql 等。
    • 这类资源在线变更配置的风险非常大,尽量不要在线动态变更,很可能会导致业务出现不可预期的事故。
    • 变更静态配置和发布 bianry app 没有区别,应该走一次迭代发布的流程。
  • 动态配置
    • 应用程序可能需要一些在线的开关,来控制业务的一些简单策略,会频繁的调整和使用,我们把这类是基础类型(int, bool)等配置,用于可以动态变更业务流的收归一起。可以结合:https://pkg.go.dev/expvar 使用。
  • 全局配置
    • 我们依赖的各类组件、中间件都有大量的默认配置或者指定配置,在各个项目里大量拷贝复制,容易出现意外,所以我们使用全局配置模板来定制化常用的组件,然后再特化的应用里进行局部替换。

4.2 函数参数配置

  • 函数可选方案
1
2
3
4
5
6
7
8
9
10
type DialOption func(*dialOptions)

func Dial(network, address string, options ...DialOption) (Conn, error) {
do := dialOptions{
dial: net.Dial,
}
for _, option := range options {
option.f(&do)
} // ...
}

4.3 最佳实践

  • 避免复杂
  • 多样的配置
  • 简单化努力
  • 以基础设施(越多越好) -> 面向用户进行转变
  • 配置的必选项和可选项
  • 配置的防御编程(直接panic)
  • 权限和变更跟踪
  • 配置的版本和应用对齐,这个很多都没做到,经常应用回滚了配置没回滚,就出事故了
  • 安全的配置变更:逐步部署、回滚更改、自动回滚

5. 参考资料

可以加首页作者微信,咨询相关问题!