微服务概率与治理

服务角色划分

角色可分3种

  • Api-gateway
    • 账号鉴权、限流、统一升级等
  • BFF
    • 聚合service的数据给网关
  • service

服务拆分问题

grpc 和服务发现

grpc 为啥选这个?

  • 单tcp连接多路复用
  • http1.1缺陷 请求必须等reponse ,等待中,啥也不能干
  • go net/rpc 单连接复用
  • 连接池,协议失败才搞出来的
  • grpc 可能要考虑做降级(http2.0 不行 ,降级成1.1)

gRPC 有一个标准的健康检测协议,在gRPC 的所有语言实现中基本都提供了生成代码和用于设置运行状态的功能。 主动健康检查health check,可以在服务提供者服务不稳定时,被消费者所感知,临时从负载均衡中摘除,减少错误请求。当服务提供者重新稳定后,health check成功,重新加入到消费者的负载均衡,恢复请求。 health check,同样也被用于外挂方式的容器健康检测,或者流量检测(k8s liveness & readiness)。

gRPC - HealthCheck

服务外挂注册到 服务发现 (不在代码层做),因为直接启动 监听端口等都需要时间。在docker entrypoint 中写脚本,向服务注册注册服务。

下线:代码监听信号,收到kill 信号 ,优雅退出(先告诉服务提供者下线,其他consumer要陆续下线这个服务,把自己的 health check标记为失败), 等待health check 2个心跳周期(利用grcp 的shutdown接口 传context timeout), 还要做兜底,如果容器一直不下线,超过多少时间,强制下线

服务发现

微服务去中心化,选择客户端发现

客户端发现

一个服务实例被启动时,它的网络地址会被写到注册表上;当服务实例终止时,再从注册表中 删除;这个服务实例的注册表通过心跳机制动态刷新;客户端使用一个负载均衡算法,去选择 一个可用的服务实例,来响应这个请求。

服务端服务发现

lvs 4层负载均衡, 暴露一个虚拟ip ,lb (consul+nginx ) -> service

多一次网络跳转

多集群

多集群,多套redis-cluster,冗余,db一个,cache 有几份,然后通过订阅db 的binlog 去更新cache

cache集群根据业务拆分

业务隔离集群带来的问题是cache hit ratio 下降,不同业务形态数据正交(如用户信息,几乎所有服务都利用用户信息,那喜欢游戏的人,去其他频道,用户信息cache就不热了)我们推而求其次整个集群全部连接。

连接cache,采用负载均衡算法,让cache 热起来。

多集群 取子集连接, 解决心跳 cpu问题

多租户

测试环境问题。

带有特点头的(染色),代码中维护map[string]connpool ,那么就可以打到自己想要到服务去了, 实现复杂发布测试需求。

header 头 , go context,grpc meta 头,

多租户架构本质上描述为:

跨服务传递请求携带上下文,数据隔离的流量路由方案。

利用服务发现注册租户信息,注册为特定的租户。

其他

bff层一般不会相互调用

Bff, gateway 用 grpc 通信

面向后台管理和面向用户 共占db

一般一个服务使用一个db

发送时要保守(字段要少),接受要冗余(做版本兼容)

ssr服务端渲染(不需要再操作dom)

复杂业务做精简 cqrs

前端用多个域名去优化图片加载,浏览器是针对域名有连接数限制的。

error处理

Go error 就是普通的一个接口,普通的值。

errors.New() 返回的是内部 errorString 对象的指针

返回指针,判断地址是否相同,而不是结构体的内部字段值相同

错误是一层一层往上抛的 dao->serivice->controller,在最上层处理log。

panic 只在服务启动的时候,做强依赖判断的时候才用,或者直接log.Fatal 停止进程了。 别用panic 然后recover去处理业务逻辑。(之前公司就这么干过,不太好)。

error are values

error type

  • Sentinel Error (哨兵error)
  • Error types (自定义错误类型)
  • Opaque error (不透明错误处理)

哨兵error,预定义好的error,需要等于判断是否是这个错误. 最不灵活的错误处理策略

Error types (自定义错误类型),相比哨兵error 可以带更多信息在自定义类型中。

推荐 不透明错误处理方式, 就判断是否是err !=nil 进行处理,这是最灵活的错误处理策略,因为它要求代码和调用者之间的耦合最少。

不透明的error ,就是error !=nil ,但是无法携带上下文信息

在少数情况下,这种二分错误处理方法是不够的。例如,与进程外的世界进行交互(如网络活动),需要调用方调查错误的性质,以确定重试该操作是否合理。在这种情况下,我们可以断言错误实现了特定的行为,而不是断言错误是特定的类型或值。考虑这个例子:

 1package net
 2type Error interface {
 3	error 
 4	Timeout() bool   //是否是 Timeout错误
 5	Temporary() bool   //是否错误是Temporary
 6}
 7
 8if nerr,ok :=err.(net.Error);ok && net.Temporary() {
 9	time.Sleep(1e9)
10	continue
11}
12if err !=nil {
13	log.Fatal(err)
14}

这个逻辑可以在不导入定义错误的包或者实际上不了解 err的底层类型的情况下实现

Warp error包装错误

使用这个库

1go get github.com/pkg/errors 
  • 减少err !=nil 的代码

  • 在你的应用代码中,使用 errors.New 或者 errors.Errorf 返回错误。

    1func parseArgs(args []string) error {
    2	   if len(args) < 3 {
    3         return errors.Errorf("args error ")
    4   }
    5}
    
  • 如果调用其他的函数,通常简单的直接返回。

    1if err !=nil {
    2      return err
    3}
    
  • 如果和其他库进行协作,考虑使用 errors.Wrap 或者 errors.Wrapf保存堆栈信息。同样适用于和标准库协作的时候。

    1f, err := os.Open(path)
    2if err != nil {
    3	return nil, errors.Wrap(err, "open failed")
    4}
    
  • 直接返回错误,而不是每个错误产生的地方到处打日志。

  • 在程序的顶部或者是工作的 goroutine 顶部(请求入口),使用%+v 把堆栈详情记录。

  • 使用 errors.Cause 获取root error,再进行和sentinel error 判定。

 1package main
 2
 3import (
 4	"fmt"
 5	"io/ioutil"
 6	"os"
 7	"path/filepath"
 8  
 9   "github.com/pkg/errors"
10)
11
12
13func ReadFile(path string) ([]byte, error) {
14	f, err := os.Open(path)
15	if err != nil {
16		return nil, errors.Wrap(err, "open failed")
17	}
18	defer f.Close()
19	buf, err := ioutil.ReadAll(f)
20	if err != nil {
21		return nil, errors.Wrap(err, "read failed")
22	}
23	return buf, nil
24}
25
26
27func ReadConfig()([]byte,error) {
28	home :=os.Getenv("HOME")
29	config,err :=ReadFile(filepath.Join(home,".setting.xml"))
30	return config,errors.WithMessage(err,"could not read config")
31}
32
33func main(){
34	_,err :=ReadConfig()
35	if err !=nil {
36		fmt.Println("original error:%T %v\n",errors.Cause(err),errors.Cause(err))
37		fmt.Printf("stack stace :\n %+v\n",err)
38		os.Exit(1)
39	}
40}

总结

  • 一层一层调用, 统一地方记录error

  • 官方返回的是 errstring结构体的指针

  • 每一个error.New() 都返回新的地址

  • 函数返回最后一个是error, 推荐做法

  • panic 等于挂了

  • 配置强依赖的,panic掉

  • 用bool和error 做返回值

  • 尽量避免 预定义错误,error types

  • Packages that are reusable across many projects only return root error values.

选择 wrap error 是只有 applications 可以选择应用的策略 。具有最高可重用性的包(工具包,第三包)只能返回根错误值。此机制与 Go 标准库中使用的相同(kit 库的 sql.ErrNoRows).

wrap error 用在 业务中, 别用在工具包,第三包中 (不然 错误的堆栈信息可能会出现多次)。

  • If the error is not going to be handled, wrap and return up the call stack.

    这是关于函数/方法调用返回的每个错误的基本问题。如果函数/方法不打算处理错误,那么用足够的上下文 wrap errors 并将其返回到调用堆栈中。 (可以不处理 return error,也可以wrap error)

可以通过观察日志 来判断 上下文是否足够多,或者说太多了。

  • Once an error is handled, it is not allowed to be passed up the call stack any longer.

    一旦确定函数/方法将处理错误,错误就不再是错误。如果函数/方法仍然需要发出返回,则它不能返回错误值。它应该只返回零(比如降级处理中,你返回了降级数据,然后需要 return nil

  • 通过使用 pkg/errors 包,您可以向错误值添加上下文,这种方式既可以由人也可以由机器检查。

  • Go 中的错误处理契约规定,在出现错误的情况下,不能对其他返回值的内容做出任何假设

    错误要被日志记录。应用程序处理错误,保证100%完整性。之后不再报告当前错误。
    

go 1.13 errors ,UnWrap error

is和as方法判断错误 golang 1.13 新增 Is() 函数,它可以顺着错误链(error chain)上所有被包装的错误(wrapped error)的值做比较,直到找到一个匹配的错误。

 1package main
 2
 3import (
 4	"fmt"
 5	"github.com/pkg/errors"
 6
 7	e "errors"
 8)
 9
10var myerror = errors.New("name is empty")
11
12func Service(name string) error {
13	if name == "" {
14		return myerror
15	}
16	return nil
17}
18
19func Controller() error {
20	err := Service("")
21	if err != nil {
22		return errors.Wrap(err, "Service has error")
23	}
24	return nil
25}
26
27func main() {
28	err := Controller()
29	//fmt.Printf("%+v", err)
30	// 	if errors.Is(err, myerror) {
31	if e.Is(err, myerror) {
32		fmt.Println("is myerror value")
33	}
34}

在 golang 1.13 中,新增 As() 函数,当 error 类型的变量是一个包装错误(wrap error)时,它可以顺着错误链(error chain)上所有被包装的错误(wrapped error)的类型做比较,直到找到一个匹配的错误类型,并返回 true,如果找不到,则返回 false。

通常,我们会使用 As() 函数判断一个 error 类型的变量是否为特定的自定义错误类型。

 1package main
 2
 3import (
 4	"fmt"
 5	"github.com/pkg/errors"
 6)
 7
 8// 自定义的错误类型
 9type DefineError struct {
10	msg string
11}
12
13func (d *DefineError) Error() string {
14	return d.msg
15}
16
17func Service(name string) error {
18	if name == "" {
19		return &DefineError{msg: "error 1"}
20	}
21	return nil
22}
23
24func Controller() error {
25	err := Service("")
26	if err != nil {
27		return errors.Wrap(err, "Service has error")
28	}
29	return nil
30}
31
32func main() {
33	err := Controller()
34	//fmt.Printf("%+v", err)
35	// 	if errors.Is(err, myerror) {
36
37	var myerror *DefineError
38	if errors.As(err, &myerror) {
39		fmt.Println("AS error")
40	}
41}

并发编程

https://github.com/cr-mao/go-concurrency

内存模型

Go如何保证并发读写的顺序