微服务是一种非常热门的架构设计理念,其主张将单个应用程序拆分为一组小型服务,每个服务都单独部署运行,并且这些服务之间通过轻量级的方式进行通信。
1. Go 语言 RPC 标准库
Go 语言自带一个 RPC 标准库 ,通过该标准库,我们可以很方便地实现 RPC 服务端与客户端程序。
1.1 使用入门
Go 语言原生的 RPC 标准库在 rpc 包中,该包定义了 RPC 相关的结构体。 其中,rpc.Server 表示 RPC 服务端,rpc.Client 表示 RPC 客户端。我们基于这两个结构体实现一个 RPC 服务。不过,Go 语言对自定义的 RPC 服务有一些约束,其要求 RPC 服务提供的每一个方法都必须满足一定条件,参考 Go 语言源码中的注释:
- the method's type is exported.
- the method is exported.
- the method has two arguments,both exported (or builtin) types.
- the method has return type error.
In effect,the method must look schematically like
func (t *T) MethodName(argType T1,replyType *T2) error
参考上面的注释,RPC 服务提供的每一个方法都必须满足上述 5 个条件:方法的类型必须是可导出的,即方法接收者的名称必须首字目大写;方法必须是可导出的,即方法名称必须首字母大写;方法必须有两个输入参数,并且这两个参数的类型必须是可导出的,或者是 Go 语言内置类型;方法的第二个参数必须是指针类型,这是因为第二个参数实际上是作为 RPC 请求的返回值使用的;方法必须返回一个 error 类型的返回值。接下来看一下 Go 语言官方给出的 RPC 服务测试用例,代码如下所示:
// RPC 请求参数
type Args struct {
A,B int
}
// RPC 请求返回值
type Reply struct {
C int
}
// 自定义类型,用于定义 RPC 服务
type Arith int
// RPC 方法
func (t *Arith) Div(args *Args,reply *Reply) error {
if args.B == 0 {
return errors.New("divide by zero")
}
reply.C = args.A / args.B
return nil
}
在上面的代码中,结构体 Args 定义了 RPC 请求的参数,结构体 Reply 定义了 RPC 请求的返回值。Arith 是自定义类型,用于定义 RPC 服务,它声明了多个方法,分别用于计算两个数的加减乘除。定义好 RPC服务之后,接下来就可以注册并启动 RPC 服务了,代码如下所示:
err := rpc.Register(new(Arith))
l,e := net.Listen("tcp","127.0.0.1:9090")
for {
conn,err := 1.Accept()
//使用JSON 实现序列化
go jsonrpc.ServerConn(conn)
}
在上面的代码中,函数 rpc.Register 用于注册 RPC 服务。需要说明的是,这里只是将该 RPC 服务的相关数据存储在本地内存,并没有注册到远端注册中心。接下来就是创建并监听套接字,等待客户端建立 TCP 连接,并处理客户端发起的 RPC 请求。另外可以看到,这里使用 JSON(可读性更好)实现 RPC 请求与响应的序列化。
那么,如何访问上述 RPC 服务呢?需要说明的是,普通的工具如 curl 是无法访问上述 RPC 服务的,这是因为 curl 命令是基于 HTTP 协议发起的请求,而上述 RPC 服务使用的是私有协议。我们可以使用 Go 语言提供的 rpc.Client 访问上述 RPC 服务,代码如下所示:
在上面的代码中,首先客户端需要与 RPC 服务端建立 TCP 连接,接下来就能够通过方法 client.Call 发起 RPC 请求。可以看到我们传递了三个参数,第一个参数表示需要访问的 RPC 方法,第二个参数是请求参数,第三个参数用于接收 RPC 返回值。另外,我们还可以通过方法 client.Go 以协程方式发起 RPC 请求,该方法的返回值是一个自定义结构体,其中字段 mulCall.Done 的类型是管道,我们可以通过该管道监听 RPC 请求的处理结果。
最后,我们可以通过 tcpdump 工具抓包,查看 RPC 请求与响应的数据格式,如下所示:
{"method":"Arith.Div","params":[{"A":80,"B":10}],"id":0} //请求1
{"id":0,"result":{"C":8},"error":null} //响应1
{"method":"Arith.Mul","params":[{"A":80,"B":10}],"id":1} //请求2
{"id":1,"result":{"C":800},"error":null} //响应2
参考上面的输出结果,在 RPC 请求中,字段 method 表示请求方法,字段 params 表示请求参数,字段 id 表示请求序列号。在 RPC 响应中,字段 id 表示响应序列号,字段 result 表示返回值,字段 error 用于存储错误信息。
1.2 原理浅析
当客户端发起一个 RPC 请求时,底层是如何将该请求发送到服务端呢?服务端又是如何解析并处理该请求呢?首先客户端需要将本次请求进行序列化,包括请求方法名称、请求参数等,接着再通过 TCP 连接将该请求发送到服务端。当服务端接收到请求时,首先需要将请求进行反序列化,解析出请求方法名称、请求参数,接着查找并调用该请求对应的实现方法,最后再将返回值进行序列化并通过 TCP 连接返回给客户端。
上述流程涉及两个核心点:
1)序列化与反序列化。
2)服务端需要查找并调用该请求对应的实现方法,也就是说服务端需要存储每一个请求方法名称对应的实现方法,只是如何调用对应的实现方法呢?这就不得不提一下 Go 语言中的反射了,RPC 标准库在注册服务以及调用请求对应的实现方法时,都用到了反射。我们先看一下注册服务的逻辑,代码如下所示:
在上面的代码中,结构体 service 定义了一个 RPC 服务,其中包含多个字段,用于存储该 RPC 服务的相关信息。函数 reflect.TypeOf 的返回值类型是 reflect.Type, 这是一个接口,表示 Go 语言类型,该接口定义了很多方法,可以帮助我们获取到任意数据类型的所有信息,包括结构体类型的方法、字段、函数类型的输入参数、返回值等。前面提到,RPC 服务提供的每一个方法都必须满足一定条件,这些条件都是基于 reflect.Type 判断的,参考函数 suitableMethods 的代码逻辑,如下所示:
2. 微服务框架 Kitex
Kitex 是字节跳动开源的一款微服务框架,具有高性能、强可扩展性的特点,目前在字节跳动内部广泛使用。Kitex 使用的是自研网络库,性能比 Go 语言原生的网络库更高;Kitex 提供了较多的扩展接口,以便使用者能够对其功能进行扩展;Kitex 还支持完善的服务治理功能,如服务注册/发现、负载均衡、熔断限流等。
2.1 使用入门
首先需要说明的是,Kitex 内置了代码生成工具,可以帮助我们自动生成一些代码,其安装方式如下:
2.2 可扩展性
前面提到,Kitex 最核心的特性之一就是可扩展性,其很多功能比如服务注册与发现、负载均衡、请求追踪、中间件等都是可扩展的。
2.2.1 中间件扩展
Kitex 的中间件与 Gin 框架中间件非常相似,我们可以基于中间件实现一些通用功能,如记录请求日志,校验权限等。不过与 Gin 框架不同的是,Kitex 将中间件分为了 3 种,分别是客户端中间件、上下文中间件、服务端中间件,这 3种类型中间件的注册方式如下所示:
client.WithMiddleware(mw) // 客户端中间件
ctx = client.WithContextMiddlewares(ctx,mw) // 上下文中间件
server.WithMiddleware(mw) // 服务端中间件
2.2.2 Suite 扩展
Suite (套件)是对选项以及中间件的组合和封装。需要说明的是,Suite 只允许在初始化 RPC 服务端对象和客户端对象时设置。另外,Suite 是按照设置顺序执行的(在客户端先设置先执行,在服务端先设置后执行)。Suite 的定义如下:
type Suite interface {
Options()[]Option
}
2.2.3 服务注册与服务发现扩展
扩展服务注册与服务发现其实非常简单,只需要实现 Kitex 定义的接口,并通过 WithXXX 之类的函数注入自定义的服务注册与服务发现即可。服务注册与服务发现的接口定义如下所示:
// 服务注册接口
type Registry interface {
Register(info *Info) error
Deregister(info *Info) error
}
//服务发现接口定义
type Resolver interface {
Target(ctx context.Context,target rpcinfo.EndpointInfo) string
Resolve(ctx context.Context,key string)(Result,error)
Diff(key string,prev,next Result)(Change,bool)
Name() string
}
2.3 服务治理
Kitex 提供了比较完善的服务治理能力,包括熔断、限流、请求重试、访问控制等,而且这些能力大多数都是可扩展的。
2.3.1 熔断
Kitex 实现的熔断器与 Sentinel 的熔断器其实是比较类似的,同样是基于滑动窗口统计请求指标,并且内部同样维护了一个熔断状态机。
2.3.2 限流
限流是一种保护 RPC 服务端的措施,用于防止突发流量导致 RPC 服务端过载。Kitex 目前提供了两种方式的限流:基于 QPS 的限流(基于令牌桶算法实现)以及基于连接数的限流(基于计数器算法实现)。