likes
comments
collection
share

霞的云原生之旅: 后端工程化

作者站长头像
站长
· 阅读数 3

说明

这里后端指Golang

使用了参考DDD的Kratos框架库作为project kit, 在学习了毛剑老师的教程之后的感受

设计

表设计

  1. 区分经常更新的字段与不经常更新的字段, 例如投稿, 它有名字, 投稿时间, 更新时间,分类, 状态, 标签. 这些都是不经常更新的. 而它的稿件的播放量, 投币数量, 点赞, 收藏, 这些都是频繁更新的, 所以就把它们进行分表设计, 对这个高频更新的表进行缓存, 减少穿透和雪崩, 当频繁更新这些热点表时, MySQL数据库就会缓存这些表的行

配置管理

最佳实践思想

  1. 区分必选值与可选择
  2. 配置文件与选项解耦
  3. 避免复杂
  4. 简单化, 除了必选的值, 默认值应该是科学的, 合理的, 最佳实践的值
  5. 防御编程, 防止其他人乱改和运行时修改配置, 如果用户手一抖, 一秒钟填写成1分钟, 对不合理的配置直接panic
  6. 配置版本和应用对齐, 回滚应用时, 应用和配置文件应该一一对应, 一起回滚, 在配置中心或者k8s配置

最佳实践

proto + 配置语言(json/yaml/toml)

proto可以进行高亮 yaml编写配置就可以与proto进行解耦

演进

函数配置

防止在运行时修改DailOption配置 优点:

  1. 告诉你可选和必选值
  2. 函数内方便使用默认值 扩展: 如果要扩展, 必须新增函数, 例如DailDatabase等才可以扩展
type DailOption struct {
	// dailOption, 小写, 意味着没有任何人有修改里面的值
	f func(*dailOption)
}

func Dial(network, address string, options ...DailOption) (Conn, error){
	// 默认值
	do := dailOption {
		dial: net.Dial,
	}
	// 如果没有传递options, 也不会造成影响
	for _, option := range options {
		option.f(&do)
	}
}

或者:

type DailOption func(*dailOption)

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

函数配置, 包含返回值

用于单元测试等, 需要切换参数配置时 改之前与改之后

type option func(f *Foo) option
func Verbosity(v int) option {
	return func(f *Foo) option {
		prev := f.verbostiy
		f.verbostiy = v
		return Verbosity(prev)
	}
}

func DoSomethingVerbosity(foo *Foo, verbosity int) {
	prev := foo.Option(pkg.Verbosity(verbostiy))
	defer foo.Option(prev)
}

包含interface的函数配置

type GreeterClient interface {
SayHello(ctx context.Context, in *HelloReq, opts ...grpc.CallOption) (*SayHelloReply, error)
}

type CallOption interface {
	before(*callInfo) error
	after(*callInfo)
}
// EmptyCallOption 不改变 CallCallOption
type EmptyCallOption struct{}

// 对原有的grpc.CallOption进行扩展
type TimeoutCallOption struct {
	grpc.EmptyCallOption
	Timeout time.Duration
}

其它配置的缺点

传统结构体(不加指针)

缺点: 不加指针是copy该结构体, 无法使用默认值, 以及0值和设定的值无法区分

func Dail(c Config) (cn Conn, err error)

配置文件解析

如果是把配置文件解析到go结构体, 那么会产生以下问题: 例如Redis, 如果你使用了配置文件映射到结构体, 那么如果别人使用的是redis.NewConn, 与你的配置文件冲突, 那么副作用是什么?

// config.yaml
server:
	address: xxx

// struct
type Config struct {
	Address string
}

func main() {
	c := Config {Address: xxx}
	// 正常使用
	c := Redis.Config {
		Address: c.Address
	}
	// 其它使用
	r, _ := &redis.NewConn(c)
	c.Addr = "xxx" // 副作用是什么? 
}

参数扩展

这样写不知道有多少个的方法

func Dial(network, address, timeout string)
func DialDatabase(network, address, timeout, database string) (Conn, error)

函数可选参数

缺点: 参数不清晰, 如果填多个Config, 哪个生效呢? 有什么副作用?

func Dail(c ...Config) (cn Conn, err error)

指针默认配置

缺点: 使用nil来表示使用默认的值, 这个nil作为默认值不是特别好. nil不应该作为公共函数的参数

func Dail(c *Config) (cn Conn, err error)

func main() {
	Dail(nil)
}

微服务划分与安全

Business Capacity职能划分

对于对业务模型不了熟悉的情况选择, 这种比较简单, 由公司不同部分进行划分的, 例如客服是提供客户服务的职能, 财务部们提供财务的相关服务

Bounded Context 界限上下文

界限上下文是DDD中用来划分不同业务边界的元素, 是一种工程思路和架构思路. DDD对架构拆分, 软件工程组织有帮助 DDD用来划分不同业务边界的一种方式, 业务边界指的是解决不同业务问题的问题域和对应的解决方案域, 为了解决某种类型的业务问题, 贴近领域知识, 也就是业务, 解决一类业务场景, 解决某类业务场景, 它一定是贴近领域的一些知识, 也就是贴近业务维度的一个思考的划分方式 示例: 当用户需要投稿时: 会经过稿件API和视频API, 如果是按照职能划分, 那一定是稿件部分和视频部门两个来做, 如果是DDD, 就可以抽象为一个投稿服务, 对这个领域业务进行划分

CQRS模式

将应用划分为两部分, 一个查询端和变更端, 例如稿件服务, 流程是: 这类服务通常都是有大量的变更, 但是用户只想知道最终结果, 但是这里查询和变更强耦合的话, 后续审核等越来越复杂时,判断的变量越来越多, 代码就更加耦合, 就把查询和变更进行拆分, 把业务划分为: 稿件审核负责业务变更, 投稿结果负责业务查询

模拟成MySQL的slave,canal
canal
订阅
消费并写入
gRPC
稿件
创作
MySQLA
Kafka
稿件job
MySQLB
External
稿件结果

微服务安全

外网信息安全强调信息安全, 内网的安全是认证和授权, 一般在内网的服务都是要做身份认证和授权

外网信息安全认证流程:

从前端登录, 使用账号密码登录, 然后经过网关, 网关为了防止恶意携带用户ID的攻击,需要踢掉这个Head头, 然后注入新的用户ID, 然后下发给BFF, BFF再head头或者gRPC的metadata重新获取用户的信息, 然后下游服务在rpc接口定义的用户ID, 根据用户的ID进行请求

登录
踢掉用户ID的head
HTTP/gRPC获取用户ID
根据用户id请求数据
Web
APIGateway
BFF
microserver1
microserver2

内网信息安全

内网信息安全分为三种:

  1. Full Trust, 完全信任机制. 没有任何安全机制, 微服务之间可以随意调用
  2. Half Trust, 微服务知道是谁在调用, 但rpc流量没有任何安全的处理, 抓包时都是明文
  3. Zero Trust, 0信任机制, 微服务之间既要做身份认证, 也要做, 同时也要做通讯的加解密(gRPC(基于HTTP2.0)支持TLS证书)

授权

顾名思义, 标志这个用户是谁, 就是你是否可以调用这个接口, 可以对这个接口可以做什么操作, 不可以做什么操作

身份认证

基于Token的认证

  1. 判断你是谁: A服务通过颁发token给B服务, B服务在访问A服务的时候就知道是A服务在访问
  2. 判断你可以请求什么:

基于证书的认证

  1. 容器里需要有一个统一的根证书
  2. 基于根证书再颁发新的证书, 例如颁发给a, b

Kratos设计

internal包

internal在Golang中, 指对内的包

servicec层

kratos的service业务服务层与api层: 以下例子是常见的service层的定义,在定义biz(业务逻辑)层结构体, 而不是直接使用proto的message?, 为什么kratos官方要这样做?

答: 因为proto定义的字段不一定全部给前端, 例如密码字段, 在返回给前端的时候不需要携带, 而在存储到数据时就需要用到, 通过两个struct隔离, 可以很好的做到前端需要什么就才返回什么. 例如类型不一致的问题, 通过DTO来转换前端需要的类型

DTO: 数据传输对象, 这里泛指API层与service层直接的数据传输对象, 通过deep copy,引用类型拷贝到那个对象去, 然后序列到api或者前端

// CreateGoodsType 创建商品类型
func (g *GoodsServices) CreateGoodsType(ctx context.Context, r *v1.GoodsTypeRequest) (*v1.GoodsTypeResponse, error) {
	// 数据传输对象, 将proto转成go的结构体(deep copy)
	id, err := g.gu.GoosTypeCreate(ctx, &biz.GoodsType{
		ID:        r.Id,
		Name:      r.Name,
		TypeCode:  r.TypeCode,
		NameAlias: r.NameAlias,
		IsVirtual: r.IsVirtual,
		Desc:      r.Desc,
		Sort:      r.Sort,
	})
	if err != nil {
		return nil, err
	}
	return &v1.GoodsTypeResponse{
		Id: id,
	}, err
}

server层

需要运行的服务, 例如HTTP服务, gRPC服务, kafka, Consul服务等, 都在这层编写

TODO

先发布, 后续补充