Golang进阶4-Go 工程化实践和配置中心

3,988 阅读21分钟

建议

  • 视频看归看 书还是要去看 要体系化学习
  • 系统性的总结和思考
  • 学习方法论
  • margin note 推荐软件

工程项目结构

  • 目录组织

  • 命名

  • 分层

  • 资源初始化

  • 依赖注入

  • 控制反转

    毛剑看了20几个refences和youtube的4个视频

Standard Go Project Layout

github.com/golang-stan…

工程化的必要性

  • 自己搞的话一个main.go
    • 从一些非常简单 的事情开始(一个 main.go 文件绰绰有余)
    • 如果你尝试学习 Go,或者你正在为自己建立一个 PoC 或一 个玩具项目,这个项目布局是没啥必要的
  • 越来越多的人参与项目的时候 需要结构化和工程化 协同方便
  • 你将需要更多的结构,包括需要一个 toolkit 来方便生成项目的模板,尽可能大家统一的工程目录布局。

/cmd

  • 存放本项目的主干文件
  • 不适合放太多的代码 main写一些初始化的操作
  • 每个应用程序的目录名应该与你想要的可执行文件的名称相匹配
    • (例如,/cmd/myapp)。
    • 不要在这个目录中放置太多代码。
  • 如果你认为代码可以导入并在其 他项目中使用,那么它应该位于 /pkg 目录中。
  • 如果代码不是可重用 的,或者你不希望其他人重用它,请将该代码放到 /internal 目录中。

main要做的事情 lifecycle的管理 例如数据库链接的初始化 应该放到上层来做 传参进来

/internal

  • 只能本项目导入 使用
  • 例如 internal/demo
  • 私有应用程序和库代码。这是你不希望其他人在其应用程序或库 中导入代码。
  • 这个布局模式是由 Go 编译器本身执行的。
    • 有关更多细节,请参阅Go 1.4 release notes。

注意

你并不局限于顶级 internal 目录。在项目树的任何级别上都可以有多个内部目录。 你可以选择向 internal 包中添加一些额外的结构,以分隔共享和非 共享的内部代码。这不是必需的(特别是对于较小的项目),但是最 好有有可视化的线索来显示预期的包的用途。

  • 实际应用程序代码可以放在 /internal/app 目录下(例如 /internal/app/myapp),
  • 应用程序共享的代码可以放在 /internal/pkg 目录下(例如 /internal/pkg/myprivlib)。

因为我们习惯把相关的服务,比如账号服务,内部有 rpc、job、 admin 等,相关的服务整合一起后,需要区分 app。单一的服务, 可以去掉 /internal/myapp。

也可以在每个app下面创建pkg 这个只供该app小 复用

/pkg

  • 可复用的代码 utils、common之类的代码

  • 外部应用程序可以使用的库代码(例如 /pkg/mypubliclib)。其他项 目会导入这些库,所以在这里放东西之前要三思:-),

    • internal 目录是确保私有包不可导入的更好方法,因为它是由 Go 强制执行 的。
  • /pkg 目录仍然是一种很好的方式,可以显式地表示该目录中的 代码对于其他人来说是安全使用的好方法。

  • /pkg 目录内,可以参考 go 标准库的组织方式,按照功能分类。

    • string net http
  • /internla/pkg 一般用于项目内的 跨多个应用的公共共享代码,但其作用域仅在单个项目工程内。

  • 由 Travis Jeffery 撰写的 I'll take pkg over internal 博客文章(travisjeffery.com/b/2019/11/i… pkg 和 internal 目录的一个很好的概述,以及什么时候使用它们是 有意义的。 当根目录包含大量非 Go 组件和目录时,这也是一种将 Go 代码分 组到一个位置的方法,这使得运行各种 Go 工具变得更加容易组织。

Kit Project Layout

  • 每个公司都应当为不同的微服务建立一个统一的 kit 工具包项目(基础库/框架) 和 app 项目。
    • 公共的代码 都放到一起
  • 基础库 kit 为独立项目,公司级建议只有一个,按照功能目录来拆
    • 分会带来不少的管理工作,因此建议合并整合。
    • 用行政手段 不要自己搞小灶 你写的好就拿出来

参考文章 Package Oriented Design(www.ardanlabs.com/blog/2017/0…)

“To this end, the Kit project is not allowed to have a vendor folder. If any of packages are dependent on 3rd party packages, they must always build against the latest version of those dependences.”

  • 不允许用第三方的包 因为存在依赖关系
  • kit包依赖第三方 随着依赖包的变动 会出问题

kratos库

kit 项目必须具备的特点:

  • 统一 规范性的东西,尽早统一
  • 标准库方式布局
    • package 的命名 《effectice go》有讲
  • 高度抽象
  • 支持插件
  • go-kit 基础库

最小化依赖 不应该依赖某一个实现

Service Application Project Layout

/api

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

  • 先把API的定义放到项目里面 这样别人调用你的代码时 方便别人找
    • 权限 不能每个人都去看
  • 之前的做法是 别人写API文档 然后别人去看文档

/configs

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

  • 建议YAML

/test

额外的外部测试应用程序和测试数据。你可以随时根据需求构造 /test 目录。对于较大的项目,有一个数据子目录是有意义的。 例如,你可以使用 /test/data 或 /test/testdata (如果你需要忽略目录中的内 容)。 请注意,Go 还会忽略以“.”或“_”开头的目录或文件,因此在如 何命名测试数据目录方面有更大的灵活性。

重点 不应该包含:/src

  • 有些 Go 项目确实有一个 src 文件夹,但这通常发生在开发人员有 Java 背景,在那里它是一种常见的模式。
  • 不要将项目级别 src 目录 与 Go 用于其工作空间的 src 目录。
  • go的路径(package) 一等公民, 不需要src
  • 区分GOPATH(workspace)

Service Application Project

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

  • 多app的方式,app目录内的每个微服务按照自己 的全局唯一名称,比如 “account.service.vip” 来建立 目录,如: account/vip/*。
  • 和app平级的目录pkg存放业务有关的公共库(非基础框架库)。如果应用不希望导出这些目录,可以 放置到 myapp/internal/pkg 中。

企业微信截图_9c8020ae-abc9-4e9d-b445-7990df4e61d6.png

image.png

biz不依赖任何基础设施

image.png

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

interface:

  • 对外的BFF服务,接受来自用户的请求, 比如暴露了 HTTP/gRPC 接口。
  • 就是BFF层 上层是APIGateway
  • xxxinterface

service:

  • 对内的微服务,仅接受来自内部其他服务或 者网关的请求,比如暴露了gRPC 接口只对内服务。
  • xxxservice

admin:

  • 区别于service,更多是面向运营侧的服务,通常数据权限更高,隔离带来更好的代码级别安全。

job:

  • 偏向 常驻执行
  • 流式任务处理的服务,上游一般依赖message broker。
  • 例如:binlog订阅等

task:

  • 偏向 定时执行的
  • 定时任务,类似cronjob,部署到task托管平台中。
  • 托管到定时任务平台
    • gocron
    • airflow
    • cobra CMD库

cmd 应用目录负责程序的: 启动、关闭、配置初始化等。 资源初始化 配置 监听 日志

演进 Service Application Project-v1

我们老的布局,app目录下有 api、cmd、configs、 internal 目录,目录里一般还会放置 README、 CHANGELOG、OWNERS。

  • api:放置API定义(protobuf),以及对应的生成的client代 码,基于 pb 生成的 swagger.json。
  • configs:放服务所需要的配置文件,比如database.yaml、 redis.yaml、application.yaml。
  • internal:是为了避免有同业务下有人跨目录引用了内部的
    • model 结构体 映射MYSQL结构表
    • dao 等内部 struct。 (data acess object)
      • 访问DB, 面向表 一个表对应一个方法
    • service 业务逻辑 会依赖dao
    • server
  • server:放置HTTP/gRPC的路由代码,以及DTO转换的 代码。
  • cmd 初始化的

model示例

  • dao 依赖 model
  • service 依赖 dao
  • server 依赖 service
  • restful ---> server
    • 服务和监听绑定的写法

痛点

model和数据库表一一对应 - 密码敏感字段 希望忽略掉 json: "-" - josn序列化的时候 int64不支持 - mysql层的数据 抛到表现层 - 复杂的表现层的数据 需要耦合 数据层的代码

解决

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

依赖导致

项目的依赖路径为: model -> dao -> service -> api, model struct 串联各个层,直到 api 需要做 DTO 对象 转换。

  • model:放对应“存储层”的结构体,是对存储的一一隐射。
  • dao:数据读写层,数据库和缓存全部在这层统一处理,
    • 包括 cache miss 处理。
    • 一开始在业务逻辑层进行处理 后来演进到了dao层
    • 业务层 专注于业务
  • service:组合各种数据访问来构建业务逻辑。
  • server:依赖proto定义的服务作为入参,提供快捷的启 动服务全局方法。
  • api:定义了APIproto文件,和生成的stub代码,它生成 的 interface,其实现者在 service 中。

service 的方法签名因为实现了 API 的 接口定义,DTO 直接 在业务逻辑层直接使用了,更有 dao 直接使用,最简化代码。

贫血模型

DO(Domain Object): 领域对象,

  • 就是从现实世界中抽象出来 的有形或无形的业务实体。缺乏 DTO -> DO 的对象转换。
  • 面向业务的

演进 Service Application Project-v2

app 目录下有 api、cmd、configs、internal 目录,目录里一般还会 放置 README、CHANGELOG、OWNERS。

internal:

  • 是为了避免有同业务下有人跨目录引用了内部的 biz、 data、service 等内部 struct。

biz:

  • 业务逻辑的组装层,类似DDD的domain层,data类似DDD 的 repo,repo 接口在这里定义,使用依赖倒置的原则。
  • 持久化的interface定义在业务逻辑层
  • 业务逻辑层 依赖 持久化曾(依赖倒置)

data:

  • 业务数据访问,包含cache、db等封装,实现了biz的repo 接口。我们可能会把 data 与 dao 混淆在一起,data 偏重业务的含义, 它所要做的是将领域对象重新拿出来,我们去掉了 DDD 的 infra层。

service:

  • 实现了api定义的服务层,类似DDD的application层,处 理 DTO 到 biz 领域实体的转换(DTO -> DO),同时协同各类 biz 交互, 但是不应处理复杂逻辑。
  • 注重grpc的 interface层的实现

PO(Persistent Object): 持久化对象,它跟持久层(通常是关系型数据库) 的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那 么数据表中的每个字段(或若干个)就对应 PO 的一个(或若干个)属 性。github.com/facebook/en… (gorm不推荐)

Lifecycle

Lifecycle 需要考虑服务应用的对象初始化以及生命周期的 管理,所有 HTTP/gRPC 依赖的前置资源初始化,包括 data、biz、service,之后再启动监听服务。

我们使用 github.com/google/wire ,来管理所有资源的依赖 注入。

为何需要依赖注入?

核心是为了:

  • 1、方便测试;
  • 2、单次初始化和复用;

控制反转

不在内部初始化 如果多个人需要的话 需要创建多个链接 内部初始化后 不方便但愿测试 在外层初始化 然后分给不同的有需要的人 可以复用 也方便单元测试

Wire

blog.golang.org/wire 手撸资源的初始化和关闭是非常繁琐,容易出错的。上面 提到我们使用依赖注入的思路 DI,结合 google wire,静 态的 go generate 生成静态的代码,可以在很方便诊断和 查看,不是在运行时利用 reflection 实现。

API设计

  • 重点将gRPC
  • protobuff 定义message
  • 传输层(tpc/http/rpc)和服务实现 隔离开

gRPC

gRPC是什么可以用官网的一句话来概括

“A high-performance, open-source universal RPC framework”

  • 多语言:语言中立,支持多种语言。
  • 轻量级、高性能:序列化支持 PB(Protocol Buffer)和 JSON,PB 是一种语言无关的高性能序列化框架。
  • 可插拔
  • IDL:基于文件定义服务,通过 proto3 工具生成指定语言的数据结构、服务端接口以及客户端 Stub。
  • 设计理念
  • 移动端:基于标准的 HTTP2 设计,支持双向流、消 息头压缩、单 TCP 的多路复用、服务端推送等特性, 这些特性使得 gRPC 在移动端设备上更加省电和节 省网络流量。
  • 服务而非对象、消息而非引用:促进微服务的系统 间粗粒度消息交互设计理念。
  • 负载无关的:不同的服务需要使用不同的消息类型 和编码,例如 protocol buffers、JSON、XML和 Thrift。
  • 流: Streaming API。
  • 阻塞式和非阻塞式:支持异步和同步处理在客户端和服务端间交互的消息序列。
  • 元数据交换:常见的横切关注点,如认证或跟踪, 依赖数据交换。
  • 标准化状态码:客户端通常以有限的方式响应 API 调用返回的错误。

看一下gprc的文档 使用一下

不要过早关注性能问题,先标准化。

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative helloworld/helloworld.proto

API Project

github.com/googleapis/… github.com/envoyproxy/… github.com/istio/api

自动更新项目的API目录 然后自动更新到另外项目对应的专门对外的一个目录

  • 隐藏源码 只能看到API的代码
  • 参考了Gogole的思路
  • 要有一个好的目录结构
  • 方便做API 的版本管理
    • code review 、版本控制

如何避免误删别人的文件

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

  • API仓库,方便跨部门协作。
  • 版本管理,基于git控制。
  • 规范化检查,APIlint。
  • APIdesignreview,变更diff。
  • 权限管理,目录OWNERS。
    • 每个目录会定义个OWNERS文件 标注作者

API Project Layout

项目中定义 proto,以 api 为包名根目录: 在统一仓库中管理 proto ,以仓库为包名根目 录:

API Compatibility

向后兼容(非破坏性)的修改

  • 给 API 服务定义添加 API 接口, 从协议的角度来看,这始终是安全的。
  • 给请求消息添加字段 只要客户端在新版和旧版中对该字段的处理不保持一致,添加请求字段就是兼容的。
  • 给响应消息添加字段

在不改变其他响应字段的行为的前提下,非资源(例如,ListBooksResponse)的响应消息可以扩展而不 必破坏客户端的兼容性。即使会引入冗余,先前在响应中填充的任何字段应继续使用相同的语义填充。

向后不兼容(破坏性)的修改

  • 删除或重命名服务,字段,方法或枚举值
    • 从根本上说,如果客户端代码可以引用某些东西,那么删除或重命名它都是不兼容 的变化,这时必须修改major 版本号。
  • 修改字段的类型 即使新类型是传输格式兼容的,这也可能会导致客户端库生成的代码发生变化,因此必须增加
    • major版本号。 对于编译型静态语言来说,会容易引入编译错误。 • 修改现有请求的可见行为
    • 客户端通常依赖于 API 行为和语义,即使这样的行为没有被明确支持或记录。 因此,在大多 数情况下,修改 API 数据的行为或语义将被消费者视为是破坏性的。如果行为没有加密隐藏,您 应该假设用户已经发现它,并将依赖于它。
  • 给资源消息添加 读取/写入 字段

API Naming Conventions

包名为应用的标识(APP_ID),用于生成 gRPC 请求路径,或者 proto 之间进行引用 Message。 文件中声明的包名称应该与产品和服务名称保 持一致。带有版本的 API 的软件包名称必须以 此版本结尾。

  • my.package.v1,为API目录,定义service相关 接口,用于提供业务使用。

// RequestURL: /<package_name>..<service_name>/{method} package <package_name>.;

参考googleAPI设计规范

输入的参数 设计成一个对象 方便以后扩展

API Primitive Fields

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

Protobuf v3 中,建议使用: github.com/protocolbuf… Warpper 类型的字段,即包装一个 message,使用时变 为指针。 Protobuf 作为强 schema 的描述文件,也可以方便扩展, 是不是用于配置文件定义也可?

API Errors

按照Google的规范grpc也使用标准状态码

使用一小组标准错误配合大量资源

例如,服务器没有定义不同类型的“找不到”错误,而是使用一个标准 google.rpc.Code.NOT_FOUND 错误代码并告诉客户端找不到哪个特定资源。状态空间变小降低了文档的复杂性,在客户端库中提供了更好的惯用映射,并降低了客户端的逻辑复杂性,同时不限制是否包含可操作信息(/google/rpc/error_details)。

错误传播

如果您的 API 服务依赖于其他服务,则不应盲目地将这些服务的错误传播到您的客户端。在翻译错误时,我们建议执行以下操作: 隐藏实现详细信息和机密信息。 调整负责该错误的一方。例如,从另一个服务接收 INVALID_ARGUMENT 错误的服务器应该将 INTERNAL 传播给它自己的调用者。

全局错误码

全局错误码,是松散、易被破坏契约的,基于我们上述 讨论的,在每个服务传播错误的时候,做一次翻译,这 样保证每个服务 + 错误枚举,应该是唯一的,而且在 proto 定义中是可以写出来文档的。

API Design

FieldMask 部分更新的方案: • 客户端可以执行需要更新的字段信息: paths: "author" paths: "submessage.submessage.field" 空 FieldMask 默认应用到 “所有字段”

配置管理

  • 完成的解决方案
  • 怎么生成、管理配置文件
  • 初始化

环境变量(配置)

Region、Zone、Cluster、Environment、Color、Discovery、AppID、Host,等之类的环境信息,都是通过在线运行时平台打入到容器或者物理机,供 kit 库读取使用。

静态配置

资源需要初始化的配置信息,比如 http/gRPC server、redis、mysql 等,这类资源在线变更配置的风险非常大,我通常不鼓励 on-the-fly 变更,很可能会导致业务出现不可预期的事故,变更静态配置和发布 bianry app 没有区别,应该走一次迭代发布的流程。

动态配置

应用程序可能需要一些在线的开关,来控制业务的一些简单策略,会频繁的调整和使用,我们把这类是基础类型(int, bool)等配置,用于可以动态变更业务流的收归一起,同时可以考虑结合类似 官方标准库 pkg.go.dev/expvar 来结合使用。

全局配置

通常,我们依赖的各类组件、中间件都有大量的默认配置或者指定配置,在各个项目里大量拷贝复制,容易出现意外,所以我们使用全局配置模板来定制化常用的组件,然后再特化的应用里进行局部替换。

Configuration Best Pratice

代码更改系统功能是一个冗长且复杂的过程,往往还涉及Review、测试等流程,但更改单个配置选项可能会对功能产生重大影响,通常配置还未经测试。配置的目标:

  • 避免复杂
  • 多样的配置
  • 简单化努力
  • 以基础设施 -> 面向用户进行转变
    • 面向用户的不要复杂
    • 中间件的可以复杂
  • 配置的必选项和可选项
  • 配置的防御编程
  • 权限和变更跟踪
  • 配置的版本和应用对齐
  • 安全的配置变更:逐步部署、回滚更改、自动回滚

包管理

  • gomod

github.com/gomods/athe… goproxy.cn blog.golang.org/modules2019 blog.golang.org/using-go-mo… blog.golang.org/migrating-t… blog.golang.org/module-mirr… blog.golang.org/publishing-… blog.golang.org/v2-go-modul… blog.golang.org/module-comp…

测试

  • 小型测试带来优秀的代码质量、良好的异常处理、优雅的错误报告;大中型测试会带来整体产品质量和数据验证。
  • 不同类型的项目,对测试的需求不同,总体上有一个经验法则,即70/20/10原则:70%是小型测试,20%是中型测试,10%是大型测试。
  • 如果一个项目是面向用户的,拥有较高的集成度,或者用户接口比较复杂,他们就应该有更多的中型和大型测试;如果是基础平台或者面向数据的项目,例如索引或网络爬虫,则最好有大量的小型测试,中型测试和大型测试的数量要求会少很多。

“自动化实现的,用于验证一个单独函数或独立功能模块的代码是否按照预期工作,着重于典型功能性问题、数据损坏、错误条件和大小差一错误(译注:大小差一(off-by-one)错误是一类常见的程序设计错误)等方面的验证” - 《Google软件测试之道》

单元测试的基本要求:

  • 快速
  • 环境一致
  • 任意顺序
  • 并行

基于 docker-compose 实现跨平台跨语言环境的容器依赖管理方案,以解决运行 unittest 场景下的(mysql, redis, mc)容器依赖问题:

  • 本地安装 Docker。
  • 无侵入式的环境初始化。
  • 快速重置环境。
  • 随时随地运行(不依赖外部服务)。
  • 语义式 API 声明资源。
  • 真实外部依赖,而非 in-process 模拟。

正确的对容器内服务进行健康检测,避免unittest 启动时候资源还未 ready。 应该交由 app 自己来初始化数据,比如 db 的scheme,初始的 sql 数据等,为了满足测试的一致性,在每次结束后,都会销毁容器。

在单元测试开始前,导入封装好的 testing 库,方便启动和销毁容器。 对于 service 的单元测试,使用 gomock 等库把 dao mock 掉,所以在设计包的时候,应该面向抽象编程。 在本地执行依赖 Docker,在 CI 环境里执行Unittest,需要考虑在物理机里的 Docker 网络,或者在 Docker 里再次启动一个 Docker。

利用 go 官方提供的: Subtests + Gomock 完成整个单元测试。 /api 比较适合进行集成测试,直接测试 API,使用 API 测试框架(例如: yapi),维护大量业务测试 case。 /data docker compose 把底层基础设施真实模拟,因此可以去掉 infra 的抽象层。 /biz 依赖 repo、rpc client,利用 gomock 模拟 interface 的实现,来进行业务单元测试。 /service 依赖 biz 的实现,构建 biz 的实现类传入,进行单元测试。 基于 git branch 进行 feature 开发,本地进行 unittest,之后提交 gitlab merge request 进行 CI 的单元测试,基于 feature branch 进行构建,完成功能测试,之后合并 master,进行集成测试,上线后进行回归测试。

  • API 测试
  • DAO 测试
  • server层 测试
  • 项目结构搞得好 测试高效和方便
  • 单元测试
    • 技术调整 不改不错
    • 技术债

References

配置中心

应用构成

image.png

配置管理

image.png

配置视图

image.png

image.png

  • 动态配置 例如根据cpu的数量 确定工作线程的数量

配置机制

image.png

控制(UI界面、API) 和 基础设施 分离

控制端 可以依赖一个原始的yml 或者依赖一个接口、portal可以修改

image.png 语法验证 提前检查是否合法有效 降低运维成本

怎么填写这些配置呢?

  • 看文档?
  • 配置的元数据 声明

image.png

思路 - window分辨率修改 10秒后自动恢复成上个版本 - 危险的操作 多次确认 避免出现事故

配置分类

image.png

配置场景-业务应用

image.png

配置场景-自建基础设施

image.png

image.png