霞的云原生之旅: 后端工程化
说明
这里后端指Golang
使用了参考DDD的Kratos框架库作为project kit, 在学习了毛剑老师的教程之后的感受
设计
表设计
- 区分经常更新的字段与不经常更新的字段, 例如投稿, 它有名字, 投稿时间, 更新时间,分类, 状态, 标签. 这些都是不经常更新的. 而它的稿件的播放量, 投币数量, 点赞, 收藏, 这些都是频繁更新的, 所以就把它们进行分表设计, 对这个高频更新的表进行缓存, 减少穿透和雪崩, 当频繁更新这些热点表时, MySQL数据库就会缓存这些表的行
配置管理
最佳实践思想
- 区分必选值与可选择
- 配置文件与选项解耦
- 避免复杂
- 简单化, 除了必选的值, 默认值应该是科学的, 合理的, 最佳实践的值
- 防御编程, 防止其他人乱改和运行时修改配置, 如果用户手一抖, 一秒钟填写成1分钟, 对不合理的配置直接panic
- 配置版本和应用对齐, 回滚应用时, 应用和配置文件应该一一对应, 一起回滚, 在配置中心或者k8s配置
最佳实践
proto + 配置语言(json/yaml/toml)
proto可以进行高亮 yaml编写配置就可以与proto进行解耦
演进
函数配置
防止在运行时修改DailOption
配置
优点:
- 告诉你可选和必选值
- 函数内方便使用默认值
扩展:
如果要扩展, 必须新增函数, 例如
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模式
将应用划分为两部分, 一个查询端和变更端, 例如稿件服务, 流程是: 这类服务通常都是有大量的变更, 但是用户只想知道最终结果, 但是这里查询和变更强耦合的话, 后续审核等越来越复杂时,判断的变量越来越多, 代码就更加耦合, 就把查询和变更进行拆分, 把业务划分为: 稿件审核负责业务变更, 投稿结果负责业务查询
微服务安全
外网信息安全强调信息安全, 内网的安全是认证和授权, 一般在内网的服务都是要做身份认证和授权
外网信息安全认证流程:
从前端登录, 使用账号密码登录, 然后经过网关, 网关为了防止恶意携带用户ID的攻击,需要踢掉这个Head头, 然后注入新的用户ID, 然后下发给BFF, BFF再head头或者gRPC的metadata重新获取用户的信息, 然后下游服务在rpc接口定义的用户ID, 根据用户的ID进行请求
内网信息安全
内网信息安全分为三种:
- Full Trust, 完全信任机制. 没有任何安全机制, 微服务之间可以随意调用
- Half Trust, 微服务知道是谁在调用, 但rpc流量没有任何安全的处理, 抓包时都是明文
- Zero Trust, 0信任机制, 微服务之间既要做身份认证, 也要做, 同时也要做通讯的加解密(gRPC(基于HTTP2.0)支持TLS证书)
授权
顾名思义, 标志这个用户是谁, 就是你是否可以调用这个接口, 可以对这个接口可以做什么操作, 不可以做什么操作
身份认证
基于Token的认证
- 判断你是谁: A服务通过颁发token给B服务, B服务在访问A服务的时候就知道是A服务在访问
- 判断你可以请求什么:
基于证书的认证
- 容器里需要有一个统一的根证书
- 基于根证书再颁发新的证书, 例如颁发给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
先发布, 后续补充
转载自:https://juejin.cn/post/7346519210724278306