Something in gRPC

常用的rpc框架

整体模型

就是常见的一种Reactor模型,

listen线程和处理线程是不同的

连接池

服务发现和服务注册

很遗憾,grpc只提供了接口,这些是要自己用其他服务发现的中间件来实现

一般的设计

集中式LB

就是在server和client端中间加一个proxy来记录注册的服务(一些地址映射表);

  • 单点问题(proxy改成分布式组件即可)
  • 增加了额外中间件,复杂度提高

进程内LB(Balancing-aware client)

因为第一个问题,那么我们把这个proxy放到消费方进程里,这个可以叫软负载或者客户端负载; 定期用心跳来同步服务注册表来表明服务存活状态

  • LB和服务发现分散到每一个服务消费者进程内部

  • 同时,消费方和提供方是直接连接,无消耗

  • 开发成本高,不同语言的调用方要有不同语言版本的LB

  • 升级后,每个都要重新发布

独立进程LB

同进程内LB差不多,都是在消费方进程中,只不过这个进程是独立出来专门负责服务发现和LB的

  • 因为不同进程,简化服务调用方,不需要重新开发不同语言客户端
  • 升级不用服务调用方改代码

grpc的负载均衡设计

grpc就属于这种: 有通过第三方proxy获得负载均衡列表的方式(grpclb),也有通过Name Resolver(dns)的方式(都是旧版本):

新版本中(1.26以上)

主要通过 atrributes.Attributes来实现:

type Address struct {
	...
	// Attributes contains arbitrary data about this address intended for
	// consumption by the load balancing policy.
	Attributes *attributes.Attributes

}

Atrributes包中我们可以看到,其为一个map,存储:

// Attributes is an immutable struct for storing and retrieving generic
// key/value pairs.  Keys must be hashable, and users should define their own
// types for keys.
type Attributes struct {
	m map[interface{}]interface{}
}

grpclb

主要属于在客户端处实行负载均衡:

  • 首先client会发起服务器名称解析请求,即把服务器名称解析成若干个IP,这些IP可能是负载均衡器的地址,也可能是实际服务器地址,同时可以设置是使用grpclb还是直接用roundRobin,roundRobinWeighted

  • 然后就要实例化负载均衡策略,如果client知道某个ip是负载均衡地址,则client会使用grpclb策略,其他的设置为配置中要求的策略

  • 负载均衡策略为每个服务建立一个subChannel(除了grpclb) 这里要注意,grpclb下,会在resolver返回的负载均衡器IP上打开一个流连接,客户端会通过这个连接,根据名称获得需要的服务器IP 但是,如果是非负载均衡器IP的话,会以回调的方式进行,以免没有均衡器

  • 当有rpc请求时,就用负载均衡决定的Subchannel接收请求,当可用服务器为空时会被阻塞;

在注释中有一个图描述的比较清楚

// The parent ClientConn should re-resolve when grpclb loses connection to the
// remote balancer. When the ClientConn inside grpclb gets a TransientFailure,
// it calls lbManualResolver.ResolveNow(), which calls parent ClientConn's
// ResolveNow, and eventually results in re-resolve happening in parent
// ClientConn's resolver (DNS for example).
//
//                          parent
//                          ClientConn
//  +-----------------------------------------------------------------+
//  |             parent          +---------------------------------+ |
//  | DNS         ClientConn      |  grpclb                         | |
//  | resolver    balancerWrapper |                                 | |
//  | +              +            |    grpclb          grpclb       | |
//  | |              |            |    ManualResolver  ClientConn   | |
//  | |              |            |     +              +            | |
//  | |              |            |     |              | Transient  | |
//  | |              |            |     |              | Failure    | |
//  | |              |            |     |  <---------  |            | |
//  | |              | <--------------- |  ResolveNow  |            | |
//  | |  <---------  | ResolveNow |     |              |            | |
//  | |  ResolveNow  |            |     |              |            | |
//  | |              |            |     |              |            | |
//  | +              +            |     +              +            | |
//  |                             +---------------------------------+ |
//  +-----------------------------------------------------------------+
// lbManualResolver is used by the ClientConn inside grpclb. It's a manual
// resolver with a special ResolveNow() function.
//
// When ResolveNow() is called, it calls ResolveNow() on the parent ClientConn,
// so when grpclb client lose contact with remote balancers, the parent
// ClientConn's resolver will re-resolve.

Name Resolver

grpc服务发现

/resolver/文件夹下(在/naming/文件夹下实现的接口已经报废):


http2

见我自己的http2的文章,随便记了一点;

server端针对不同的帧进行处理

每个Stream有唯一的ID标识,如果是客户端创建的则ID是奇数,服务端创建的ID则是偶数。 如果一条连接上的ID使用完了,Client会新建一条连接,Server也会给Client发送一个GOAWAY Frame强制让Client新建一条连接;

一条grpc连接允许并发的发送和接收多个Stream,而控制的参数便是MaxConcurrentStreams,Golang的服务端默认是100。

超时问题

可以带上一个timeout Context,但是里面实际会转换成header frame中的 grpc-timeout ???

在grpc中,header frame会带上不少信息,比如grpc-status状态等等, server端的处理可以看到processHeaderField方法中解析的多种header: 比较重要的有:

  • grpc-timeout:长连接超时控制
func (d *decodeState) processHeaderField(f hpack.HeaderField) {
	switch f.Name {
		...
		case "grpc-timeout":
			d.data.timeoutSet = true
			var err error
			if d.data.timeout, err = decodeTimeout(f.Value); err != nil {
				d.data.grpcErr = status.Errorf(codes.Internal, "transport: malformed time-out: %v", err)
			}
		case ":path":
			d.data.method = f.Value
		case ":status":
			code, err := strconv.Atoi(f.Value)
			if err != nil {
				d.data.httpErr = status.Errorf(codes.Internal, "transport: malformed http-status: %v", err)
				return
			}
			d.data.httpStatus = &code
		case ....
	}
}

如何识别服务以及解析

识别服务

识别服务方面比较直接,就是用string判等服务名字即可;

解析数据主要是借用了protogenerate工具,生成了服务端和客户端的代码,里面使用了自带的解码器,跟pb文件结合起来,避免了语言层面上的反射的消耗;

我们这里只讨论go语法,语言方面其他都是大同小异 创建proto文件 data.proto

syntax = "proto2";

service Authenticate{
	rpc login(toServerData) returns (ResponseFromServer){}
	rpc home(toServerData) returns(ResponseFromServer){}
	rpc logout(toServerData) returns(ResponseFromServer){}
}

message toServerData{
	required int32 ctype = 1;
	required string name =2;
    optional bytes httpdata=3;
}
message ResponseFromServer {
	required bool Success=1;
	optional bytes tcpData=2;
	// Errcode int
}

用安装的protoc插件生成

protoc --proto_path=/mypath --go_out=plugins=grpc:. *.proto //当前在mypath路径下,用grpc模式生成proto文件

proto解析

生成 data.pb.proto

-----------这些就直接参照手册理解吧-----------------
这里还是记录下这厮的解码方式吧,参考

《数据密集型系统设计》

protobuf的的编码其实跟 thrift的BinaryCompact编码有点相似 就拿上面那个例子:

message toServerData{
	required int32 ctype = 1;
	required string name =2;
    optional bytes httpdata=3;
	repeated string data =4;
}
  • 有个flag开头表明类型
  • 紧接着数据长度
  • 紧接着数据本身
  • 数组类型protobuf不提供,只是在二进制中连续排序(在第一个元素flag+length+data后面每个元素都是length+data);

提供的一些连接方式

单向stream 双向stream


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!