grpc在微服务中的运行流程图
![3c8e009cd9e7c5487f8fc19ab0a222e1.png](https://i-blog.csdnimg.cn/blog_migrate/b49baaaf71e3f290b1647905b51feb0d.jpeg)
- grpc server将自己注册到注册中心
- grpc client从注册中心获得注册信息从而知道有多少个服务可使用
- grpc client选择一个grpc server发起请求,完成调用
从以上流程图中可以看到,主要的工作其实是在client进行,server只需要将自身注册到注册中心,然后等待请求到来就可以了
简单grpc例子
```proto代码```
syntax="proto3";
package api;
option go_package="mytest/api;api";
service Api {
rpc Ping(PingReq)returns(PingResp);
}
message PingReq{
int64 id=1;
}
message PingResp{
int64 status=2;
}
```go代码```
package main
import (
"context"
"flag"
"fmt"
"net"
"mytest/api"
"google.golang.org/grpc"
)
func main() {
t := flag.String("t", "", "s to start servernc to start client")
flag.Parse()
if *t == "" || (*t != "s" && *t != "c") {
panic("need flag")
}
if *t == "s" {
server()
} else {
client()
}
}
func client() {
c, e := grpc.Dial("127.0.0.1:9234", grpc.WithInsecure())
if e != nil {
panic(e)
}
cc := api.NewApiClient(c)
resp, e := cc.Ping(context.Background(), &api.PingReq{})
if e != nil {
panic(e)
}
fmt.Println(resp)
}
func server() {
l, e := net.Listen("tcp", "127.0.0.1:9234")
if e != nil {
panic(e)
}
s := grpc.NewServer()
api.RegisterApiService(s, &api.ApiService{
Ping: Ping,
})
s.Serve(l)
}
func Ping(ctx context.Context, in *api.PingReq) (*api.PingResp, error) {
fmt.Println(in)
return &api.PingResp{}, nil
}
从Dial创建连接开始看Client是如何解析服务端地址的:Resolver
//文件位置 grpc/clientconn.go
//代码位置 DialContext函数,这里有一大堆代码,我们只看主要的代码
//在该函数中有以下代码,其中target就是我们传入的地址,目前为127.0.0.1:9234
//Part 1 获取resolver,创建resolver
cc.parsedTarget = grpcutil.ParseTarget(cc.target)//改函数展开在后面
resolverBuilder := cc.getResolver(cc.parsedTarget.Scheme)//改函数展开在后面
if resolverBuilder == nil {//使用默认的scheme
cc.parsedTarget = resolver.Target{
Scheme: resolver.GetDefaultScheme(),
Endpoint: target,
}
resolverBuilder = cc.getResolver(cc.parsedTarget.Scheme)
if resolverBuilder == nil {
return nil, fmt.Errorf("could not get resolver for default scheme: %q", cc.parsedTarget.Scheme)
}
}
rWrapper, err := newCCResolverWrapper(cc, resolverBuilder)//改函数展开在后面
//函数展开
func split2(s, sep string) (string, string, bool) {
spl := strings.SplitN(s, sep, 2)
if len(spl) < 2 {
return "", "", false
}
return spl[0], spl[1], true
}
func ParseTarget(target string) (ret resolver.Target) {
var ok bool
ret.Scheme, ret.Endpoint, ok = split2(target, "://")
if !ok {
return resolver.Target{Endpoint: target}
}
ret.Authority, ret.Endpoint, ok = split2(ret.Endpoint, "/")
if !ok {
return resolver.Target{Endpoint: target}
}
return ret
}
//resolver.Target为以下格式
type Target struct {
Scheme string
Authority string
Endpoint string
}
unc (cc *ClientConn) getResolver(scheme string) resolver.Builder {
for _, rb := range cc.dopts.resolvers {
if scheme == rb.Scheme() {
return rb
}
}
return resolver.Get(scheme)
}
func newCCResolverWrapper(cc *ClientConn, rb resolver.Builder) (*ccResolverWrapper, error) {
ccr := &ccResolverWrapper{
cc: cc,
done: grpcsync.NewEvent(),
}
......//最终调用的是resolver的Build函数
ccr.resolver, err = rb.Build(cc.parsedTarget, ccr, rbo)
if err != nil {
return nil, err
}
return ccr, nil
}
从以上代码可以看到,当我们指定ip创建一个简单的client的时候,ParseTarget无法解析到scheme,所以getResolver也无法获得,因此就会使用默认scheme进行解析地址,该scheme的名字叫做passthrough。
//文件地址 grpc/internal/resolver/passthrough/passthrough.go
package passthrough
import "google.golang.org/grpc/resolver"
const scheme = "passthrough"
type passthroughBuilder struct{}
//这里的Build函数就是Part 1中创建resolver中调用的
//这将最终创建passthrough这个resolver
func (*passthroughBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
r := &passthroughResolver{
target: target,
cc: cc,
}
r.start()
return r, nil
}
func (*passthroughBuilder) Scheme() string {
return scheme
}
type passthroughResolver struct {
target resolver.Target
cc resolver.ClientConn
}
func (r *passthroughResolver) start() {
r.cc.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: r.target.Endpoint}}})
}
func (*passthroughResolver) ResolveNow(o resolver.ResolveNowOptions) {}
func (*passthroughResolver) Close() {}
func init() {
resolver.Register(&passthroughBuilder{})
}
//这里调用的Register,文件位置 grpc/resolver/resolver.go
var m = make(map[string]Builder)
func Register(b Builder) {
m[b.Scheme()] = b
}
//这里的Register正好呼应了Part 1中的getResolver
现在,我们已经创建了一个resolver了,我们也知道,resolver是用来解析服务端的地址的,接下来,我们就需要将resolver中解析到的地址告诉client,以便让client创建指向服务端的连接
//回到上面passthrough的代码中
func (*passthroughBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
r := &passthroughResolver{
target: target,
cc: cc,
}
r.start()//该函数开始解析地址
return r, nil
}
func (r *passthroughResolver) start() {
//该调用将解析到的地址往Client中抛
//因为passthrough是一个默认的指向固定地址的resolver,因此直接调用
r.cc.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: r.target.Endpoint}}})
}
//最终调用的函数 文件位置 grpc/resolver_conn_wrapper
func (ccr *ccResolverWrapper) UpdateState(s resolver.State) {
......
ccr.curState = s
//最终调用Client的updateResoverState执行创建
ccr.poll(ccr.cc.updateResolverState(ccr.curState, nil))
}
//文件位置 grpc/clientconn.go 该函数与Dail在一个文件,绕了一圈我们又回来了
//我们可以暂时认为连接在这里已经创建了,后续还会再进行展开
func (cc *ClientConn) updateResolverState(s resolver.State, err error) error {
}
至此,我们的resolver的工作就做完了,我们已经解析到了服务器的地址,并将地址通过UpdateState的方法传递给了Client进行创建到server的连接。我们现在可以和最开头的流程图结合,来思考一下,如何才能从discover server中动态的获得server的变动呢。
//回到passthrough的代码中
//我们知道,resolver的开始是通过Build
//Build中又调用了start
//那么我们可以改造一下start,然后给我们改造后的resolver重新起个名字,并且register自己的名字
//那么这个新的名字就变成了scheme,我们只需要在Dail的时候遵循ParseTarget的格式,就能使用我们自己的resolver了
package selfresolver
import "google.golang.org/grpc/resolver"
//改造成自己的scheme
const scheme = "selfresolver"
type selfbuilder struct{}
func (*selfbuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
r := &passthroughResolver{
target: target,
cc: cc,
}
//这里我们改造成使用go协程
go r.start()
return r, nil
}
func (*selfbuilder) Scheme() string {
return scheme
}
type selfresolver struct {
target resolver.Target
cc resolver.ClientConn
}
func (r *selfresolver) start() {
for {
//在这里写上从discover获取server更新的逻辑
......
//更新完成后
//state中更新serve的address
state := resolver.State{Addresses: []resolver.Address{}}
//使用updatestate去通知Client
r.cc.UpdateState(state)
}
}
func (*selfresolver) ResolveNow(o resolver.ResolveNowOptions) {}
func (*selfresolver) Close() {}
func init() {
resolver.Register(&selfbuilder{})
}
//这里调用的Register,文件位置 grpc/resolver/resolver.go
var (
m = make(map[string]Builder)
)
func Register(b Builder) {
m[b.Scheme()] = b
}
//这时候我们Dail的时候,就可以修改我们的target为 selfresolver://auth/endpoint
//其中endpoint可以是discovery的地址,然后在上面start函数中使用endpoint
至此,我们对服务端地址的解析以及动态变更已经完成
负载均衡:Balancer
在分布式程序中,最重要的一环应该就是负载均衡,它能使得请求流量均匀地分散到所有的服务器上,这样配合我们上面讲到的resolver,就能实现在高负载下的动态扩容,对server随时进行增加,只要server注册到discover上,那么client就能通过resolver获得新增加server,进而通过Balancer将流量分到新服务器中。
//balancer也是和resolver一样Register在一个地方的
//文件位置 grpc/balancer/balancer.go
var m = make(map[string]Builder)
func Register(b Builder) {
m[strings.ToLower(b.Name())] = b
}
func Get(name string) Builder {
if b, ok := m[strings.ToLower(name)]; ok {
return b
}
return nil
}
//我们先来看几个Dial时与balancer有关的选项
grpc.WithBalancerName()//该方法已经被标注为弃用
//该方法在Dail的时候就指定了一个需要使用的balancer
func WithBalancerName(balancerName string) DialOption {
builder := balancer.Get(balancerName)
if builder == nil {
panic(fmt.Sprintf("grpc.WithBalancerName: no balancer is registered for name %v", balancerName))
}
return newFuncDialOption(func(o *dialOptions) {
o.balancerBuilder = builder
})
}
grpc.WithDefaultServiceConfig()//该方法暂时被标注为试验,非稳定
//该方法在Dail的时候指定了一个配置文件,其中包括了balancer信息,名字,配置等
func WithDefaultServiceConfig(s string) DialOption {
return newFuncDialOption(func(o *dialOptions) {
o.defaultServiceConfigRawJSON = &s
})
}
grpc.WithDisableServiceConfig()//该方法禁用resolver的ServiceConfig
//resolver在调用UpdateState的时候
//state参数中不仅可以传入servers的address
//还可以传入一个ServiceConfig,其中包括了balancer信息,名字,配置等
//该方法将禁用resolver的config,强制使用defaultserviceconfig,如果defaultservice不存在就用空的
func WithDisableServiceConfig() DialOption {
return newFuncDialOption(func(o *dialOptions) {
o.disableServiceConfig = true
})
}
从上面选项中,废弃,以及实验等信息来看,使用resolver中的serviceconfig应该是一个比较合适的做法,这样也无须在Dail中增加额外的with选项,因此,在resolver中的UpdateState中,传入的state需要带上ServiceConfig,我们来看下state和ServiceConfig的结构
//这里只留下了暂时需要关心的部分字段,与流程无关的一些字段删除了
type State struct {
Addresses []Address
ServiceConfig *serviceconfig.ParseResult
}
type ServiceConfig struct {
LB *string
lbConfig *lbConfig
}
现在我们来看下,balancer到底是怎么运行的
//文件位置 grpc/clientconn.go
//代码位置 updateResolverState函数
//这个方法是不是感觉很眼熟,这不就是之前说resolver调用UpdateState之后创建连接的地方吗
//其实,连接不是在这里创建的,之前只是说暂定认为是在这里创建
//我们来想一下,负载均衡是干什么的,不就是分散流量吗
//那么流量往哪走是负载均衡说了算,连接的管理自然肯定也是在负载均衡里面
//因此,resolver的UpdateState只是触发了balancer的初始化/更新
func (cc *ClientConn) updateResolverState(s resolver.State, err error) error {
.......
var ret error
//这里就是上面说过的,grpc.WithDisableServiceConfig()生效的地方
//禁用了resolver来的ServiceConfig
if cc.dopts.disableServiceConfig || s.ServiceConfig == nil {
//cc.maybeApplyDefaultServiceConfig(s.Addresses)//将此方法展开
if cc.sc != nil {
cc.applyServiceConfigAndBalancer(cc.sc, addrs)
return
}
if cc.dopts.defaultServiceConfig != nil {
//使用grpc.WithDefaultServiceConfig()设置的
cc.applyServiceConfigAndBalancer(cc.dopts.defaultServiceConfig, addrs)
} else {
//使用空的
cc.applyServiceConfigAndBalancer(emptyServiceConfig, addrs)
}
} else {
if sc, ok := s.ServiceConfig.Config.(*ServiceConfig); s.ServiceConfig.Err == nil && ok {
//使用从resolver来的
cc.applyServiceConfigAndBalancer(sc, s.Addresses)
} else {
//出错,只有在首次才出错,因为首次需要初始化balancer
......
}
}
......//最后执行更新操作
bw.updateClientConnState(&balancer.ClientConnState{ResolverState: s, BalancerConfig: balCfg})
......
}
//调用的主要方法,用于创建balancer
func (cc *ClientConn) applyServiceConfigAndBalancer(sc *ServiceConfig, addrs []resolver.Address) {
cc.sc = sc
......
//grpc.WithBalancerName()会设置该值
//没有使用grpc.WithBalancerName()时该值为nil
//会从ServiceConfig中获取balancer名字
//最后通过switchBalancer获取到balancer builder
if cc.dopts.balancerBuilder == nil {
var newBalancerName string
......
newBalancerName = cc.sc.lbConfig.name
......
cc.switchBalancer(newBalancerName)
} else if cc.balancerWrapper == nil {
cc.curBalancerName = cc.dopts.balancerBuilder.Name()
cc.balancerWrapper = newCCBalancerWrapper(cc, cc.dopts.balancerBuilder, cc.balancerBuildOpts)
}
}
func (cc *ClientConn) switchBalancer(name string) {
......
builder := balancer.Get(name)
cc.curBalancerName = builder.Name()
cc.balancerWrapper = newCCBalancerWrapper(cc, builder, cc.balancerBuildOpts)
}
func newCCBalancerWrapper(cc *ClientConn, b balancer.Builder, bopts balancer.BuildOptions) *ccBalancerWrapper {
ccb := &ccBalancerWrapper{
cc: cc,
scBuffer: buffer.NewUnbounded(),
done: grpcsync.NewEvent(),
subConns: make(map[*acBalancerWrapper]struct{}),
}
//watcher用于监听连接的状态
go ccb.watcher()
//最终调用了balancer中的Build方法进行构造balancer,代码转到balancer中的实现
ccb.balancer = b.Build(ccb, bopts)
return ccb
}
func (ccb *ccBalancerWrapper) watcher() {
for {
select {
case t := <-ccb.scBuffer.Get():
//最终调用balancer中的UpdateSubConnState方法通知balancer更新连接状态
ccb.balancer.UpdateSubConnState(su.sc, balancer.SubConnState{ConnectivityState: su.state, ConnectionError: su.err})
case <-ccb.done.Done():
}
}
}
//调用的主要方法,用于更新balancer
unc (ccb *ccBalancerWrapper) updateClientConnState(ccs *balancer.ClientConnState) error {
//最终调用了balancer中的UpdateClientConnState的方法,代码转到balancer中的实现
return ccb.balancer.UpdateClientConnState(*ccs)
}
我们找一个最简单的balancer来分析下代码
//文件位置 grpc/balancer/base/balancer.go
package base
type baseBuilder struct {
name string
pickerBuilder PickerBuilder
config Config
}
//用于构建balancer
func (bb *baseBuilder) Build(cc balancer.ClientConn, opt balancer.BuildOptions) balancer.Balancer {
bal := &baseBalancer{
cc: cc,
pickerBuilder: bb.pickerBuilder,
subConns: make(map[resolver.Address]balancer.SubConn),
scStates: make(map[balancer.SubConn]connectivity.State),
csEvltr: &balancer.ConnectivityStateEvaluator{},
config: bb.config,
}
bal.picker = NewErrPicker(balancer.ErrNoSubConnAvailable)
return bal
}
//用于register
func (bb *baseBuilder) Name() string {
return bb.name
}
type baseBalancer struct {
cc balancer.ClientConn
pickerBuilder PickerBuilder
csEvltr *balancer.ConnectivityStateEvaluator
state connectivity.State
subConns map[resolver.Address]balancer.SubConn
scStates map[balancer.SubConn]connectivity.State
picker balancer.Picker
config Config
resolverErr error
connErr error
}
//从updateResolverState调用该函数
func (b *baseBalancer) UpdateClientConnState(s balancer.ClientConnState) error {
b.resolverErr = nil
//解析resolver中的server地址
addrsSet := make(map[resolver.Address]struct{})
for _, a := range s.ResolverState.Addresses {
addrsSet[a] = struct{}{}
if _, ok := b.subConns[a]; !ok {
//通知Client创建新连接,有新的server上线了
//最终Client创建完成后,通过balancerwrapper的watcher调用UpdateSubConnState
sc, err := b.cc.NewSubConn([]resolver.Address{a}, balancer.NewSubConnOptions{HealthCheckEnabled: b.config.HealthCheck})
......
}
}
//删除已经下线的server的连接
for a, sc := range b.subConns {
if _, ok := addrsSet[a]; !ok {
b.cc.RemoveSubConn(sc)
delete(b.subConns, a)
}
}
......
}
//刷新选择器,当一起请求发起时,用于选择向哪个server发起
func (b *baseBalancer) regeneratePicker() {
if b.state == connectivity.TransientFailure {
b.picker = NewErrPicker(b.mergeErrors())
return
}
readySCs := make(map[balancer.SubConn]SubConnInfo)
// Filter out all ready SCs from full subConn map.
for addr, sc := range b.subConns {
if st, ok := b.scStates[sc]; ok && st == connectivity.Ready {
readySCs[sc] = SubConnInfo{Address: addr}
}
}
b.picker = b.pickerBuilder.Build(PickerBuildInfo{ReadySCs: readySCs})
}
//在balancerwrapper的watcher方法中调用该函数,用于更新连接状体啊
func (b *baseBalancer) UpdateSubConnState(sc balancer.SubConn, state balancer.SubConnState) {
......
b.scStates[sc] = s
switch s {
case connectivity.Idle:
sc.Connect()
case connectivity.Shutdown:
delete(b.scStates, sc)
case connectivity.TransientFailure:
b.connErr = state.ConnectionError
}
if (s == connectivity.Ready) != (oldS == connectivity.Ready) ||
b.state == connectivity.TransientFailure {
b.regeneratePicker()
}
b.cc.UpdateState(balancer.State{ConnectivityState: b.state, Picker: b.picker})
}
//其中使用的主要方法
//文件位置 grpc/balancer_conn_wrappers.go
func (ccb *ccBalancerWrapper) NewSubConn(addrs []resolver.Address, opts balancer.NewSubConnOptions) (balancer.SubConn, error) {
//调用Client的newconn方法
ac, err := ccb.cc.newAddrConn(addrs, opts)
}
至于还剩下的,在发起请求时,如何选择server,就相对简单了,各位自己看一下吧,入口在grpc/call.go的Invoke函数,有空再补全