gRPC学习
概述
gRPC 一开始由 google 开发,是一款语言中立、平台中立、开源的远程过程调用(RPC)系统。
在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。
特性
基于HTTP/2
HTTP/2 提供了连接多路复用、双向流、服务器推送、请求优先级、首部压缩等机制。可以节省带宽、降低TCP链接次数、节省CPU,帮助移动设备延长电池寿命等。gRPC 的协议设计上使用了HTTP2 现有的语义,请求和响应的数据使用HTTP Body 发送,其他的控制信息则用Header 表示。
IDL使用ProtoBuf
gRPC使用ProtoBuf来定义服务,ProtoBuf是由Google开发的一种数据序列化协议(类似于XML、JSON、hessian)。ProtoBuf能够将数据进行序列化,并广泛应用在数据存储、通信协议等方面。压缩和传输效率高,语法简单,表达力强。
多语言支持(C, C++, Python, PHP, Nodejs, C#, Objective-C、Golang、Java)
gRPC支持多种语言,并能够基于语言自动生成客户端和服务端功能库。目前已提供了C版本grpc、Java版本grpc-java 和 Go版本grpc-go,其它语言的版本正在积极开发中,其中,grpc支持C、C++、Node.js、Python、Ruby、Objective-C、PHP和C#等语言,grpc-java已经支持Android开发。
gRPC已经应用在Google的云服务和对外提供的API中,其主要应用场景如下:
- 低延迟、高扩展性、分布式的系统
- 同云服务器进行通信的移动应用客户端
- 设计语言独立、高效、精确的新协议
- 便于各方面扩展的分层设计,如认证、负载均衡、日志记录、监控等
HTTP2.0 特性
HTTP/2,也就是超文本传输协议第2版,不论是1还是2,HTTP的基本语义是不变的,比如方法语义(GET/PUST/PUT/DELETE),状态码(200/404/500等),Range Request,Cacheing,Authentication、URL路径, 不同的主要是下面几点:
多路复用 (Multiplexing)
在 HTTP/1.1 协议中 「浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞」。
HTTP/2 的多路复用(Multiplexing) 则允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。
因此 HTTP/2 可以很容易的去实现多流并行而不用依赖建立多个 TCP 连接,HTTP/2 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。并行地在同一个 TCP 连接上双向交换消息。
二进制帧
HTTP/2 传输的数据是二进制的。相比 HTTP/1.1 的纯文本数据,二进制数据一个显而易见的好处是:更小的传输体积。这就意味着更低的负载。二进制的帧也更易于解析而且不易出错,纯文本帧在解析的时候还要考虑处理空格、大小写、空行和换行等问题,而二进制帧就不存在这个问题。
首部压缩(Header Compression)
HTTP是无状态协议。简而言之,这意味着每个请求必须要携带服务器需要的所有细节,而不是让服务器保存住之前请求的元数据。因为http2没有改变这个范式,所以它也需要这样(携带所有细节),因此 HTTP 请求的头部需要包含用于标识身份的数据比如 cookies,而这些数据的量也在随着时间增长。每一个请求的头部都包含这些大量的重复数据,无疑是一种很大的负担。对请求头部进行压缩,将会大大减轻这种负担,尤其对移动端来说,性能提高非常明显。
HTTP/2 使用的压缩方式是 HPACK。 http://http2.github.io/http2-spec/compression.html
HTTP2.0在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只需发送一次。
事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。
如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP2.0的连接存续期内始终存在,由客户端和服务器共同渐进地更新。
服务端推送(Server Push)
HTTP/2 的服务器推送所作的工作就是,服务器在收到客户端对某个资源的请求时,会判断客户端十有八九还要请求其他的什么资源,然后一同把这些资源都发送给客户端,即便客户端还没有明确表示它需要这些资源。
客户端可以选择把额外的资源放入缓存中(所以这个特点也叫 Cache push),也可以选择发送一个 RST_STREAM frame 拒绝任何它不想要的资源。
主动重置链接
Length的HTTP消息被送出之后,我们就很难中断它了。当然,通常我们可以断开整个TCP链接(但也不总是可以这样),但这样导致的代价就是需要重新通过三次握手建立一个新的TCP连接。
HTTP/2 引入了一个 RST_STREAM frame 来让客户端在已有的连接中发送重置请求,从而中断或者放弃响应。当浏览器进行页面跳转或者用户取消下载时,它可以防止建立新连接,避免浪费所有带宽。
与其他rpc比较
与thrift,dubbo,motan等比较
- Motan Dubbox thrift gRPC rpcx
开发语言 Java Java 跨语言 跨语言 go
分布式服务治理 Y Y 可以配合zookeeper, Eureka等实现 可以配合etcd(go),zookeeper,consul等实现 自带服务注册中心,也支持zookerper,etcd等发现方式
底层协议 motan协议,使用tcp长连接 Dubbo 协议、 Rmi 协议、 Hessian 协议、 HTTP 协议、 WebService 协议、Dubbo Thrift 协议、Memcached 协议 tpc/http/frame http2 tcp长链接
消息序列化 hessian2,json hessian2,json,resr,kyro,FST等,可扩展protobuf等 thrift protobuf Gob、Json、MessagePack、gencode、ProtoBuf等
跨语言编程 N(支持php client和c server) N Y Y N
负载均衡 ActiveWeight 、Random 、 RoundRobin 、LocalFirst 、 Consistent 、ConfigurableWeight Random 、RoundRobin 、ConsistentHash 、 LeastActive Haproxy, zookerper+客户端负载均衡等方案 负载均衡软件HaProxy等 支持随机请求、轮询、低并发优先、一致性 Hash等
容错 Failover 失效切换、Failfast 快速失败 Failover 、 Failfast 、Failsafe 、 Failback 、 Forking、 Broadcast Failover 具有 Failover 失效切换的容错策略 失败重试(Failover)、快速失败(Failfast)
注册中心 consul zookeeper zookeeper etcd,zookeeper,consul zookerper,etcd
性能 ★★ ★★ ★★★★ 比grpc快2-5倍 ★★★ 比dubbox,motan快 ★★★★★ 比thrift快1-1.5倍
侧重优势 服务管理 服务管理 跨语言,性能++ 跨语言,性能 性能++,服务治理
客户端异步调用方案
使用thrift IDL “oneway” 关键字(无返回结果),+callback
tcp异步请求
- thrift IDL参数不支持函数或服务
stream传输,双向通信
服务端异步处理 1、TNonblockingServer(java/c++,php); THsHaServer(java/c++); TThreadpoolServer(java/c++); TThreadSelectorServer(java/c++)
2、结合消息队列或中间件
3、swoole/goroutine等多任务支持 同上,使用stream传输。Stream对象在传输过程中会被当做集合,用Iterator来遍历处理
grpc vs thrift:
使用gRPC的公司或项目:
Google
Mochi中国
阿里OTS
腾讯部分部门
Tensorflow项目中使用了grpc
CoreOS — Production API for etcd v3 is entirely gRPC. etcd v3的接口全部使用grpc
Square — replacement for all of their internal RPC. one of the very first adopters and contributors to gRPC.
ngrok — all 20+ internal services communicate via gRPC 一个内网转发产品
Netflix
Yik Yak
VSCO
Cockroach
使用Thrift的公司或项目:
Facebook
雪球
饿了么
今日头条
evernote
友盟
小米
美团
Quora
Twitter
Pinterest
Foursquare
Maxeler Technologies
gRPC优缺点:
优点:
protobuf二进制消息,性能好/效率高(空间和时间效率都很不错)
proto文件生成目标代码,简单易用
序列化反序列化直接对应程序中的数据类,不需要解析后在进行映射(XML,JSON都是这种方式)
支持向前兼容(新加字段采用默认值)和向后兼容(忽略新加字段),简化升级
支持多种语言(可以把proto文件看做IDL文件)
Netty等一些框架集成
缺点:
1)GRPC尚未提供连接池,需要自行实现
2)尚未提供“服务发现”、“负载均衡”机制
3)因为基于HTTP2,绝大部多数HTTP Server、Nginx都尚不支持,即Nginx不能将GRPC请求作为HTTP请求来负载均衡,而是作为普通的TCP请求。(nginx1.9版本已支持)
4) Protobuf二进制可读性差(貌似提供了Text_Fromat功能)
默认不具备动态特性(可以通过动态定义生成消息类型或者动态编译支持)
grpc坑:
来自https://news.ycombinator.com/item?id=12345223的网友:
http2只允许单个链接传输10亿流数据。原因在于:
htt2使用31位整形标示流,服务端使用奇数,客户端使用偶数,所以总共10亿可用。
HTTP/2.0 uses an unsigned 31-bit integer to identity individual streams over a connection.
Server-initiated streams must use even identifiers.
Client-initiated streams must use odd identifiers.
解决思路:超过一定数量的流,需要重启链接。
gRPC通信方式
gRPC有四种通信方式:
1、 Simple RPC
简单rpc
这就是一般的rpc调用,一个请求对象对应一个返回对象
proto语法:
rpc simpleHello(Person) returns (Result) {}
1
2、 Server-side streaming RPC
服务端流式rpc
一个请求对象,服务端可以传回多个结果对象
proto语法
rpc serverStreamHello(Person) returns (stream Result) {}
1
3、 Client-side streaming RPC
客户端流式rpc
客户端传入多个请求对象,服务端返回一个响应结果
proto语法
rpc clientStreamHello(stream Person) returns (Result) {}
1
4、 Bidirectional streaming RPC
双向流式rpc
结合客户端流式rpc和服务端流式rpc,可以传入多个对象,返回多个响应对象
proto语法
rpc biStreamHello(stream Person) returns (stream Result) {}
1
服务定义及ProtoBuf
gRPC使用ProtoBuf定义服务, 我们可以一次性的在一个 .proto 文件中定义服务并使用任何支持它的语言去实现客户端和服务器,反过来,它们可以在各种环境中,从云服务器到你自己的平板电脑—— gRPC 帮你解决了不同语言及环境间通信的复杂性。使用 protocol buffers 还能获得其他好处,包括高效的序列号,简单的 IDL 以及容易进行接口更新。
protoc编译工具
protoc工具可在https://github.com/google/protobuf/releases 下载到源码。
linux下安装
protobuf语法
1、syntax = “proto3”;
文件的第一行指定了你使用的是proto3的语法:如果你不指定,protocol buffer 编译器就会认为你使用的是proto2的语法。这个语句必须出现在.proto文件的非空非注释的第一行。
2、message SearchRequest {……}
message 定义实体,c/c++/go中的结构体,php中类
3、基本数据类型
4、注释符号: 双斜线,如://xxxxxxxxxxxxxxxxxxx
5、字段唯一数字标识(用于在二进制格式中识别各个字段,上线后不宜再变动):Tags
1到15使用一个字节来编码,包括标识数字和字段类型(你可以在Protocol Buffer 编码中查看更多详细);16到2047占用两个字节。因此定义proto文件时应该保留1到15,用作出现最频繁的消息类型的标识。记得为将来会继续增加并可能频繁出现的元素留一点儿标识区间,也就是说,不要一下子把1—15全部用完,为将来留一点儿。
标识数字的合法范围:最小是1,最大是 229 - 1,或者536,870,911。
另外,不能使用19000 到 19999之间的数字(FieldDescriptor::kFirstReservedNumber through FieldDescriptor::kLastReservedNumber),因为它们被Protocol Buffers保留使用
6、字段修饰符:
required:值不可为空
optional:可选字段
singular:符合语法规则的消息包含零个或者一个这样的字段(最多一个)
repeated:一个字段在合法的消息中可以重复出现一定次数(包括零次)。重复出现的值的次序将被保留。在proto3中,重复出现的值类型字段默认采用压缩编码。你可以在这里找到更多关于压缩编码的东西: Protocol Buffer Encoding。
默认值: optional PhoneType type = 2 [default = HOME];
proto3中,省略required,optional,singular,由protoc自动选择。
7、代理类生成
1)、C++, 每一个.proto 文件可以生成一个 .h 文件和一个 .cc 文件
2)、Java, 每一个.proto文件可以生成一个 .java 文件
3)、Python, 每一个.proto文件生成一个模块,其中为每一个消息类型生成一个静态的描述器,在运行时,和一个metaclass一起使用来创建必要的Python数据访问类
4)、Go, 每一个.proto生成一个 .pb.go 文件
5)、Ruby, 每一个.proto生成一个 .rb 文件
6)、Objective-C, 每一个.proto 文件可以生成一个 pbobjc.h 和一个pbobjc.m 文件
7)、C#, 每一个.proto文件可以生成一个.cs文件.
8)、php, 每一个message消息体生成一个.php类文件,并在GPBMetadata目录生成一个对应包名的.php类文件,用于保存.proto的二进制元数据。
8、字段默认值
- strings, 默认值是空字符串(empty string)
- bytes, 默认值是空bytes(empty bytes)
- bools, 默认值是false
- numeric, 默认值是0
- enums, 默认值是第一个枚举值(value必须为0)
- message fields, the field is not set. Its exact value is langauge-dependent. See the generated code guide for details.
- repeated fields,默认值为empty,通常是一个空list
9、枚举
// 枚举类型,必须从0开始,序号可跨越。同一包下不能重名,所以加前缀来区别
enum WshExportInstStatus {
INST_INITED = 0;
INST_RUNNING = 1;
INST_FINISH = 2;
INST_FAILED = 3;
}
10、Maps字段类型
map<key_type, value_type> map_field = N;
1
其中key_type可以是任意Integer或者string类型(所以,除了floating和bytes的任意标量类型都是可以的)value_type可以是任意类型。
例如,如果你希望创建一个project的映射,每个Projecct使用一个string作为key,你可以像下面这样定义:
map<string, Project> projects = 3;
1
Map的字段可以是repeated。
序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理Map
当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序。
从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用,当从文本格式中解析map时,如果存在重复的key。
11、默认值
字符串类型默认为空字符串
字节类型默认为空字节
布尔类型默认false
数值类型默认为0值
enums类型默认为第一个定义的枚举值,必须是0
12、服务
服务使用service{}包起来,每个方法使用rpc起一行申明,一个方法包含一个请求消息体和一个返回消息体
service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string greeting = 1;
}
message HelloResponse {
string reply = 1;
}
更多protobuf参考(google)
更多protobuf参考(csdn)
golang中使用gRPC
前期准备
go get -u "google.golang.org/grpc"
go get -u "google.golang.org/grpc/reflection"
1
2
例如我定义的文件exporttask.proto:
// 微生活导出服务
// 导出功能接口
/*
1、 创建任务模板
2、 查询、列出任务模板
3、 删除任务模板
4、 添加导出任务实例
5、 查询任务状态
*/
syntax = "proto3";
// java 语法特别选项
option java_multiple_files = true;
option java_package = "io.grpc.welife.WshExportTask";
option java_outer_classname = "WshExportTask";
// 包名 golang包名,php中namespase,
package exporttask;
// 导出任务服务定义
service ExportTask {
// 创建任务模板
rpc CreateTpl(WshExportTaskCreateTplReq) returns (WshExportTaskCreateTplRes) {}
// 查询任务模板
rpc ListTpl(WshExportTaskListTplReq) returns (WshExportTaskListTplRes) {}
}
// 枚举类型,必须从0开始,序号可跨越。同一包下不能重名,所以加前缀来区别
enum WshExportTplStatus {
TPL_INITED = 0;
TPL_NORMAL = 1;
TPL_DELETED = 9;
}
enum WshExportFormat {
FMT_DEFAULT = 0;
FMT_CSV = 1;
FMT_XLS = 2;
}
message WshExportTpl {
string etplName = 1;
string etplTag = 2;
WshExportFormat etplOutputFormat = 3;
string etplOutputColumns = 4;
string etplExpr = 5;
int32 etplId = 6;
int32 etplExecTimes = 7;
int32 etplExecOkTimes = 8;
int32 etplStatus = 9;
string etplCreated = 10;
string etplUpdated = 11;
string etplDeleted = 12;
int32 operatorId = 13;
}
message WshExportTaskCreateTplReq {
string etplName = 1;
string etplTag = 2;
string etplExpr = 3;
string etplOutputColumns = 4;
WshExportFormat etplOutputFormat = 5;
int32 operatorId = 6;
}
message WshExportTaskCreateTplRes {
string errCode = 1;
string errMsg = 2;
WshExportTpl data = 3;
}
message WshExportTaskListTplReq {
int32 etplId = 1;
string etplName = 2;
string etplTag = 3;
}
// repeated 表示数组
message WshExportTaskListTplRes {
string errCode = 1;
string errMsg = 2;
repeated WshExportTpl data = 3;
}
使用protoc命令生成golang对应的rpc代码:
格式 protoc --go_out=plugins=grpc:{go代码输出路径}
protoc --go_out=plugins=grpc:./ ./exporttask.proto
1
2
生成对应当exporttask.pb.go
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: exporttask.proto
/*
Package exporttask is a generated protocol buffer package.
包名 golang包名,php中namespase,
It is generated from these files:
exporttask.proto
It has these top-level messages:
WshExportTpl
WshExportTaskCreateTplReq
WshExportTaskCreateTplRes
WshExportTaskListTplReq
WshExportTaskListTplRes
*/
package exporttask
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
// 枚举类型,必须从0开始,序号可跨越。同一包下不能重名,所以加前缀来区别
type WshExportTplStatus int32
const (
WshExportTplStatus_TPL_INITED WshExportTplStatus = 0
WshExportTplStatus_TPL_NORMAL WshExportTplStatus = 1
WshExportTplStatus_TPL_DELETED WshExportTplStatus = 9
)
var WshExportTplStatus_name = map[int32]string{
0: "TPL_INITED",
1: "TPL_NORMAL",
9: "TPL_DELETED",
}
var WshExportTplStatus_value = map[string]int32{
"TPL_INITED": 0,
"TPL_NORMAL": 1,
"TPL_DELETED": 9,
}
func (x WshExportTplStatus) String() string {
return proto.EnumName(WshExportTplStatus_name, int32(x))
}
func (WshExportTplStatus) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
type WshExportFormat int32
const (
WshExportFormat_FMT_DEFAULT WshExportFormat = 0
WshExportFormat_FMT_CSV WshExportFormat = 1
WshExportFormat_FMT_XLS WshExportFormat = 2
)
var WshExportFormat_name = map[int32]string{
0: "FMT_DEFAULT",
1: "FMT_CSV",
2: "FMT_XLS",
}
var WshExportFormat_value = map[string]int32{
"FMT_DEFAULT": 0,
"FMT_CSV": 1,
"FMT_XLS": 2,
}
func (x WshExportFormat) String() string {
return proto.EnumName(WshExportFormat_name, int32(x))
}
func (WshExportFormat) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
type WshExportTpl struct {
EtplName string protobuf:"bytes,1,opt,name=etplName" json:"etplName,omitempty"
EtplTag string protobuf:"bytes,2,opt,name=etplTag" json:"etplTag,omitempty"
EtplOutputFormat WshExportFormat protobuf:"varint,3,opt,name=etplOutputFormat,enum=exporttask.WshExportFormat" json:"etplOutputFormat,omitempty"
EtplOutputColumns string protobuf:"bytes,4,opt,name=etplOutputColumns" json:"etplOutputColumns,omitempty"
EtplExpr string protobuf:"bytes,5,opt,name=etplExpr" json:"etplExpr,omitempty"
EtplId int32 protobuf:"varint,6,opt,name=etplId" json:"etplId,omitempty"
EtplExecTimes int32 protobuf:"varint,7,opt,name=etplExecTimes" json:"etplExecTimes,omitempty"
EtplExecOkTimes int32 protobuf:"varint,8,opt,name=etplExecOkTimes" json:"etplExecOkTimes,omitempty"
EtplStatus int32 protobuf:"varint,9,opt,name=etplStatus" json:"etplStatus,omitempty"
EtplCreated string protobuf:"bytes,10,opt,name=etplCreated" json:"etplCreated,omitempty"
EtplUpdated string protobuf:"bytes,11,opt,name=etplUpdated" json:"etplUpdated,omitempty"
EtplDeleted string protobuf:"bytes,12,opt,name=etplDeleted" json:"etplDeleted,omitempty"
OperatorId int32 protobuf:"varint,13,opt,name=operatorId" json:"operatorId,omitempty"
}
func (m *WshExportTpl) Reset() { m = WshExportTpl{} }
func (m WshExportTpl) String() string { return proto.CompactTextString(m) }
func (WshExportTpl) ProtoMessage() {}
func (WshExportTpl) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *WshExportTpl) GetEtplName() string {
if m != nil {
return m.EtplName
}
return ""
}
func (m *WshExportTpl) GetEtplTag() string {
if m != nil {
return m.EtplTag
}
return ""
}
func (m *WshExportTpl) GetEtplOutputFormat() WshExportFormat {
if m != nil {
return m.EtplOutputFormat
}
return WshExportFormat_FMT_DEFAULT
}
func (m *WshExportTpl) GetEtplOutputColumns() string {
if m != nil {
return m.EtplOutputColumns
}
return ""
}
func (m *WshExportTpl) GetEtplExpr() string {
if m != nil {
return m.EtplExpr
}
return ""
}
func (m *WshExportTpl) GetEtplId() int32 {
if m != nil {
return m.EtplId
}
return 0
}
func (m *WshExportTpl) GetEtplExecTimes() int32 {
if m != nil {
return m.EtplExecTimes
}
return 0
}
func (m *WshExportTpl) GetEtplExecOkTimes() int32 {
if m != nil {
return m.EtplExecOkTimes
}
return 0
}
func (m *WshExportTpl) GetEtplStatus() int32 {
if m != nil {
return m.EtplStatus
}
return 0
}
func (m *WshExportTpl) GetEtplCreated() string {
if m != nil {
return m.EtplCreated
}
return ""
}
func (m *WshExportTpl) GetEtplUpdated() string {
if m != nil {
return m.EtplUpdated
}
return ""
}
func (m *WshExportTpl) GetEtplDeleted() string {
if m != nil {
return m.EtplDeleted
}
return ""
}
func (m *WshExportTpl) GetOperatorId() int32 {
if m != nil {
return m.OperatorId
}
return 0
}
type WshExportTaskCreateTplReq struct {
EtplName string protobuf:"bytes,1,opt,name=etplName" json:"etplName,omitempty"
EtplTag string protobuf:"bytes,2,opt,name=etplTag" json:"etplTag,omitempty"
EtplExpr string protobuf:"bytes,3,opt,name=etplExpr" json:"etplExpr,omitempty"
EtplOutputColumns string protobuf:"bytes,4,opt,name=etplOutputColumns" json:"etplOutputColumns,omitempty"
EtplOutputFormat WshExportFormat protobuf:"varint,5,opt,name=etplOutputFormat,enum=exporttask.WshExportFormat" json:"etplOutputFormat,omitempty"
OperatorId int32 protobuf:"varint,6,opt,name=operatorId" json:"operatorId,omitempty"
}
func (m *WshExportTaskCreateTplReq) Reset() { m = WshExportTaskCreateTplReq{} }
func (m WshExportTaskCreateTplReq) String() string { return proto.CompactTextString(m) }
func (WshExportTaskCreateTplReq) ProtoMessage() {}
func (WshExportTaskCreateTplReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func (m *WshExportTaskCreateTplReq) GetEtplName() string {
if m != nil {
return m.EtplName
}
return ""
}
func (m *WshExportTaskCreateTplReq) GetEtplTag() string {
if m != nil {
return m.EtplTag
}
return ""
}
func (m *WshExportTaskCreateTplReq) GetEtplExpr() string {
if m != nil {
return m.EtplExpr
}
return ""
}
func (m *WshExportTaskCreateTplReq) GetEtplOutputColumns() string {
if m != nil {
return m.EtplOutputColumns
}
return ""
}
func (m *WshExportTaskCreateTplReq) GetEtplOutputFormat() WshExportFormat {
if m != nil {
return m.EtplOutputFormat
}
return WshExportFormat_FMT_DEFAULT
}
func (m *WshExportTaskCreateTplReq) GetOperatorId() int32 {
if m != nil {
return m.OperatorId
}
return 0
}
type WshExportTaskCreateTplRes struct {
ErrCode string protobuf:"bytes,1,opt,name=errCode" json:"errCode,omitempty"
ErrMsg string protobuf:"bytes,2,opt,name=errMsg" json:"errMsg,omitempty"
Data *WshExportTpl protobuf:"bytes,3,opt,name=data" json:"data,omitempty"
}
func (m *WshExportTaskCreateTplRes) Reset() { m = WshExportTaskCreateTplRes{} }
func (m WshExportTaskCreateTplRes) String() string { return proto.CompactTextString(m) }
func (WshExportTaskCreateTplRes) ProtoMessage() {}
func (WshExportTaskCreateTplRes) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
func (m *WshExportTaskCreateTplRes) GetErrCode() string {
if m != nil {
return m.ErrCode
}
return ""
}
func (m *WshExportTaskCreateTplRes) GetErrMsg() string {
if m != nil {
return m.ErrMsg
}
return ""
}
func (m *WshExportTaskCreateTplRes) GetData() *WshExportTpl {
if m != nil {
return m.Data
}
return nil
}
type WshExportTaskListTplReq struct {
EtplId int32 protobuf:"varint,1,opt,name=etplId" json:"etplId,omitempty"
EtplName string protobuf:"bytes,2,opt,name=etplName" json:"etplName,omitempty"
EtplTag string protobuf:"bytes,3,opt,name=etplTag" json:"etplTag,omitempty"
}
func (m *WshExportTaskListTplReq) Reset() { m = WshExportTaskListTplReq{} }
func (m WshExportTaskListTplReq) String() string { return proto.CompactTextString(m) }
func (WshExportTaskListTplReq) ProtoMessage() {}
func (WshExportTaskListTplReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} }
func (m *WshExportTaskListTplReq) GetEtplId() int32 {
if m != nil {
return m.EtplId
}
return 0
}
func (m *WshExportTaskListTplReq) GetEtplName() string {
if m != nil {
return m.EtplName
}
return ""
}
func (m *WshExportTaskListTplReq) GetEtplTag() string {
if m != nil {
return m.EtplTag
}
return ""
}
// repeated 表示数组
type WshExportTaskListTplRes struct {
ErrCode string protobuf:"bytes,1,opt,name=errCode" json:"errCode,omitempty"
ErrMsg string protobuf:"bytes,2,opt,name=errMsg" json:"errMsg,omitempty"
Data []*WshExportTpl protobuf:"bytes,3,rep,name=data" json:"data,omitempty"
}
func (m *WshExportTaskListTplRes) Reset() { m = WshExportTaskListTplRes{} }
func (m WshExportTaskListTplRes) String() string { return proto.CompactTextString(m) }
func (WshExportTaskListTplRes) ProtoMessage() {}
func (WshExportTaskListTplRes) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} }
func (m *WshExportTaskListTplRes) GetErrCode() string {
if m != nil {
return m.ErrCode
}
return ""
}
func (m *WshExportTaskListTplRes) GetErrMsg() string {
if m != nil {
return m.ErrMsg
}
return ""
}
func (m WshExportTaskListTplRes) GetData() []WshExportTpl {
if m != nil {
return m.Data
}
return nil
}
func init() {
proto.RegisterType((WshExportTpl)(nil), "exporttask.WshExportTpl")
proto.RegisterType((WshExportTaskCreateTplReq)(nil), "exporttask.WshExportTaskCreateTplReq")
proto.RegisterType((WshExportTaskCreateTplRes)(nil), "exporttask.WshExportTaskCreateTplRes")
proto.RegisterType((WshExportTaskListTplReq)(nil), "exporttask.WshExportTaskListTplReq")
proto.RegisterType((*WshExportTaskListTplRes)(nil), "exporttask.WshExportTaskListTplRes")
proto.RegisterEnum("exporttask.WshExportTplStatus", WshExportTplStatus_name, WshExportTplStatus_value)
proto.RegisterEnum("exporttask.WshExportFormat", WshExportFormat_name, WshExportFormat_value)
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for ExportTask service
type ExportTaskClient interface {
// 创建任务模板
CreateTpl(ctx context.Context, in WshExportTaskCreateTplReq, opts ...grpc.CallOption) (WshExportTaskCreateTplRes, error)
// 查询任务模板
ListTpl(ctx context.Context, in WshExportTaskListTplReq, opts ...grpc.CallOption) (WshExportTaskListTplRes, error)
}
type exportTaskClient struct {
cc *grpc.ClientConn
}
func NewExportTaskClient(cc *grpc.ClientConn) ExportTaskClient {
return &exportTaskClient{cc}
}
func (c *exportTaskClient) CreateTpl(ctx context.Context, in WshExportTaskCreateTplReq, opts ...grpc.CallOption) (WshExportTaskCreateTplRes, error) {
out := new(WshExportTaskCreateTplRes)
err := grpc.Invoke(ctx, "/exporttask.ExportTask/CreateTpl", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *exportTaskClient) ListTpl(ctx context.Context, in WshExportTaskListTplReq, opts ...grpc.CallOption) (WshExportTaskListTplRes, error) {
out := new(WshExportTaskListTplRes)
err := grpc.Invoke(ctx, "/exporttask.ExportTask/ListTpl", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for ExportTask service
type ExportTaskServer interface {
// 创建任务模板
CreateTpl(context.Context, WshExportTaskCreateTplReq) (WshExportTaskCreateTplRes, error)
// 查询任务模板
ListTpl(context.Context, WshExportTaskListTplReq) (WshExportTaskListTplRes, error)
}
func RegisterExportTaskServer(s *grpc.Server, srv ExportTaskServer) {
s.RegisterService(&_ExportTask_serviceDesc, srv)
}
func _ExportTask_CreateTpl_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(WshExportTaskCreateTplReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ExportTaskServer).CreateTpl(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/exporttask.ExportTask/CreateTpl",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ExportTaskServer).CreateTpl(ctx, req.(*WshExportTaskCreateTplReq))
}
return interceptor(ctx, in, info, handler)
}
func _ExportTask_ListTpl_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(WshExportTaskListTplReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ExportTaskServer).ListTpl(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/exporttask.ExportTask/ListTpl",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ExportTaskServer).ListTpl(ctx, req.(*WshExportTaskListTplReq))
}
return interceptor(ctx, in, info, handler)
}
var _ExportTask_serviceDesc = grpc.ServiceDesc{
ServiceName: "exporttask.ExportTask",
HandlerType: (*ExportTaskServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CreateTpl",
Handler: _ExportTask_CreateTpl_Handler,
},
{
MethodName: "ListTpl",
Handler: _ExportTask_ListTpl_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "exporttask.proto",
}
func init() { proto.RegisterFile("exporttask.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 550 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xa4, 0x54, 0x5f, 0x8f, 0xd2, 0x4e,
0x14, 0xa5, 0xb0, 0xc0, 0x72, 0x59, 0x96, 0xfe, 0xe6, 0xe1, 0xe7, 0x88, 0xc6, 0x90, 0xaa, 0x09,
0xd9, 0x6c, 0x30, 0xc1, 0x57, 0x5f, 0x76, 0xa1, 0x18, 0x92, 0x02, 0x1b, 0x28, 0x6a, 0xe2, 0xc3,
0xa6, 0x6e, 0x47, 0x24, 0x14, 0x5b, 0x67, 0x86, 0xb8, 0xc6, 0x57, 0x3f, 0x98, 0x5f, 0xc6, 0xef,
0x61, 0x66, 0xda, 0x61, 0x5a, 0xfe, 0xc4, 0x75, 0x7d, 0x9b, 0x73, 0xe6, 0x94, 0x7b, 0xef, 0x39,
0x77, 0x00, 0x93, 0xdc, 0x46, 0x21, 0xe5, 0xdc, 0x63, 0xcb, 0x76, 0x44, 0x43, 0x1e, 0x22, 0xd0,
0x8c, 0xf5, 0xab, 0x00, 0x27, 0x6f, 0xd9, 0x27, 0x5b, 0x32, 0x6e, 0x14, 0xa0, 0x06, 0x1c, 0x13,
0x1e, 0x05, 0x23, 0x6f, 0x45, 0xb0, 0xd1, 0x34, 0x5a, 0x95, 0xc9, 0x06, 0x23, 0x0c, 0x65, 0x71,
0x76, 0xbd, 0x39, 0xce, 0xcb, 0x2b, 0x05, 0xd1, 0x6b, 0x30, 0xc5, 0x71, 0xbc, 0xe6, 0xd1, 0x9a,
0xf7, 0x43, 0xba, 0xf2, 0x38, 0x2e, 0x34, 0x8d, 0xd6, 0x69, 0xe7, 0x51, 0x3b, 0x55, 0x7f, 0x53,
0x29, 0x96, 0x4c, 0x76, 0x3e, 0x42, 0xe7, 0xf0, 0x9f, 0xe6, 0xba, 0x61, 0xb0, 0x5e, 0x7d, 0x66,
0xf8, 0x48, 0x16, 0xdb, 0xbd, 0x50, 0xcd, 0xda, 0xb7, 0x11, 0xc5, 0x45, 0xdd, 0xac, 0xc0, 0xe8,
0x7f, 0x28, 0x89, 0xf3, 0xc0, 0xc7, 0xa5, 0xa6, 0xd1, 0x2a, 0x4e, 0x12, 0x84, 0x9e, 0x41, 0x2d,
0xd6, 0x90, 0x1b, 0x77, 0xb1, 0x22, 0x0c, 0x97, 0xe5, 0x75, 0x96, 0x44, 0x2d, 0xa8, 0x2b, 0x62,
0xbc, 0x8c, 0x75, 0xc7, 0x52, 0xb7, 0x4d, 0xa3, 0x27, 0x00, 0x82, 0x9a, 0x72, 0x8f, 0xaf, 0x19,
0xae, 0x48, 0x51, 0x8a, 0x41, 0x4d, 0xa8, 0x0a, 0xd4, 0xa5, 0xc4, 0xe3, 0xc4, 0xc7, 0x20, 0xdb,
0x4c, 0x53, 0x4a, 0x31, 0x8b, 0x7c, 0xa9, 0xa8, 0x6a, 0x45, 0x42, 0x29, 0x45, 0x8f, 0x04, 0x44,
0x28, 0x4e, 0xb4, 0x22, 0xa1, 0x44, 0x17, 0x61, 0x44, 0xa8, 0xc7, 0x43, 0x3a, 0xf0, 0x71, 0x2d,
0xee, 0x42, 0x33, 0xd6, 0x8f, 0x3c, 0x3c, 0xd4, 0x39, 0x7b, 0x6c, 0x19, 0x17, 0x77, 0xa3, 0x60,
0x42, 0xbe, 0xdc, 0x33, 0xf4, 0xb4, 0xfb, 0x85, 0x2d, 0xf7, 0xff, 0x2e, 0xc7, 0x7d, 0xeb, 0x53,
0xbc, 0xcf, 0xfa, 0x64, 0x6d, 0x28, 0xed, 0xd8, 0xf0, 0xfd, 0xb0, 0x0b, 0x4c, 0x4e, 0x4a, 0x69,
0x37, 0xf4, 0x95, 0x09, 0x0a, 0xca, 0x5d, 0xa2, 0x74, 0xc8, 0x94, 0x05, 0x09, 0x42, 0xe7, 0x70,
0xe4, 0x7b, 0xdc, 0x93, 0xd3, 0x57, 0x3b, 0x78, 0x6f, 0xaf, 0xe2, 0xc7, 0xa5, 0xca, 0x9a, 0xc3,
0x83, 0x4c, 0x71, 0x67, 0xc1, 0x78, 0x12, 0x80, 0x5e, 0x56, 0x23, 0xb3, 0xac, 0xe9, 0x60, 0xf2,
0x87, 0x83, 0x29, 0x64, 0x82, 0xb1, 0xbe, 0x1d, 0x2a, 0xf4, 0x6f, 0x33, 0x16, 0xfe, 0x3c, 0xe3,
0x99, 0x0d, 0x28, 0xcd, 0x26, 0x6f, 0xe0, 0x14, 0xc0, 0xbd, 0x72, 0xae, 0x07, 0xa3, 0x81, 0x6b,
0xf7, 0xcc, 0x9c, 0xc2, 0xa3, 0xf1, 0x64, 0x78, 0xe1, 0x98, 0x06, 0xaa, 0x43, 0x55, 0xe0, 0x9e,
0xed, 0xd8, 0x42, 0x50, 0x39, 0x7b, 0x05, 0xf5, 0xad, 0xb0, 0x85, 0xa6, 0x3f, 0x74, 0xaf, 0x7b,
0x76, 0xff, 0x62, 0xe6, 0xb8, 0x66, 0x0e, 0x55, 0xa1, 0x2c, 0x88, 0xee, 0xf4, 0x8d, 0x69, 0x28,
0xf0, 0xce, 0x99, 0x9a, 0xf9, 0xce, 0x4f, 0x03, 0x40, 0x4f, 0x8f, 0xde, 0x43, 0x65, 0x93, 0x33,
0x7a, 0xbe, 0x7f, 0x80, 0xad, 0x17, 0xd1, 0xb8, 0x93, 0x8c, 0x59, 0x39, 0x34, 0x83, 0x72, 0x62,
0x2f, 0x7a, 0x7a, 0xf0, 0x1b, 0x9d, 0x74, 0xe3, 0x0e, 0x22, 0x66, 0xe5, 0x2e, 0x5f, 0xc0, 0xe3,
0x45, 0xd8, 0x9e, 0xd3, 0xe8, 0xa6, 0xfd, 0x95, 0x04, 0x8b, 0x8f, 0x24, 0xab, 0xbd, 0xac, 0x65,
0xe0, 0x95, 0xf1, 0xa1, 0x24, 0xff, 0xdb, 0x5f, 0xfe, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x60, 0x9b,
0xec, 0x89, 0xef, 0x05, 0x00, 0x00,
}
服务端实现proto中的接口:
package main
import (
"log"
//"net"
svcExport "../../common/exporttask" // 包含上面的pb生成的go包
"./model"
_ "github.com/go-sql-driver/mysql"
"golang.org/x/net/context"
//"google.golang.org/grpc"
//"google.golang.org/grpc/reflection"
)
// server 这个对象来实现 exporttask 包中的pb定义的rpc服务
// 实现的方式是将服务转化成本地的数据库操作
type server struct{}
func (s *server) CreateTpl(ctx context.Context, in *svcExport.WshExportTaskCreateTplReq) (res *svcExport.WshExportTaskCreateTplRes, err error) {
res = new(svcExport.WshExportTaskCreateTplRes)
res.Data = new(svcExport.WshExportTpl)
var etplId int32 = 0
etplId, err = model.CreateTpl(in.EtplName, in.EtplTag, in.EtplExpr, in.EtplOutputColumns, int32(in.EtplOutputFormat), in.OperatorId)
//res.Data, err = model.GetTpl(etplId)
res.Data.EtplId = etplId
return res, err
}
func (s *server) ListTpl(ctx context.Context, in svcExport.WshExportTaskListTplReq) (svcExport.WshExportTaskListTplRes, error) {
res := new(svcExport.WshExportTaskListTplRes)
entList, err := model.ListTpl(in.EtplId, in.EtplName, in.EtplTag)
if err != nil {
res.ErrMsg = err.Error()
res.ErrCode = "2"
}
for _, ent := range entList {
t := new(svcExport.WshExportTpl)
ent.CopyToPb(t)
res.Data = append(res.Data, t)
}
return res, err
}
服务端main启动服务main.go
/**
- exporttask server main
- $ go build exporttask.go
*/
package main
import (
"log"
"net"
svcExport "../../common/exporttask"
//"./model"
_ "github.com/go-sql-driver/mysql"
//"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
const (
port = ":50051"
)
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 生成一个rpc服务器
s := grpc.NewServer()
// 使用pb包调用注册已实现的rpc接口类server
svcExport.RegisterExportTaskServer(s, &server{})
// Register reflection service on gRPC server.
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Golang gRPC客户端
客户端的代码相对比较简单
package main
import (
"flag"
"log"
"os"
svcExport "../../common/exporttask"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
const (
address = "127.0.0.1:50052"
defaultName = "world"
)
func main() {
// 发起链接
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// 创建pb包的客户端
c := svcExport.NewExportTaskClient(conn)
name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
// 发起请求
var r2 *svcExport.WshExportTaskCreateTplRes
req := svcExport.WshExportTaskCreateTplReq{EtplName: name, EtplTag: "mall", EtplExpr: "select EtplName from welife_export_tpl", EtplOutputFormat: svcExport.WshExportFormat_FMT_CSV, EtplOutputColumns: ""}
r2, err = c.CreateTpl(context.Background(), &req)
// 打印结果
log.Println("create tpl: r=", r2, err)
}
php中使用gRPC
php需要安装grpc扩展。
使用protoc命令生成对应的php代码:
protoc --php_out=plugins=grpc:./ exporttask.proto
1
2
生成代码包括:
Exporttask/
|-WshExportFormat.php
|-WshExportTaskCreateTplReq.php
|-WshExportTaskCreateTplRes.php
|-WshExportTaskListTplReq.php
|-WshExportTaskListTplRes.php
|-WshExportTpl.php
-WshExportTplStatus.php
GPBMetadata/
-Exporttask.php
每一个message对应生成一个类,在Exporttask命名空间下。
这里就不都贴出来了,只贴一个WshExportTpl.php:
string etplName = 1;
*/
private $etplName = '';
/**
* string etplTag = 2;
*/
private $etplTag = '';
/**
* .exporttask.WshExportFormat etplOutputFormat = 3;
*/
private $etplOutputFormat = 0;
/**
* string etplOutputColumns = 4;
*/
private $etplOutputColumns = '';
/**
* string etplExpr = 5;
*/
private $etplExpr = '';
/**
* int32 etplId = 6;
*/
private $etplId = 0;
/**
* int32 etplExecTimes = 7;
*/
private $etplExecTimes = 0;
/**
* int32 etplExecOkTimes = 8;
*/
private $etplExecOkTimes = 0;
/**
* int32 etplStatus = 9;
*/
private $etplStatus = 0;
/**
* string etplCreated = 10;
*/
private $etplCreated = '';
/**
* string etplUpdated = 11;
*/
private $etplUpdated = '';
/**
* string etplDeleted = 12;
*/
private $etplDeleted = '';
/**
* int32 operatorId = 13;
*/
private $operatorId = 0;
public function __construct() {
\GPBMetadata\Exporttask::initOnce();
parent::__construct();
}
/**
* string etplName = 1;
*/
public function getEtplName()
{
return $this->etplName;
}
/**
* string etplName = 1;
*/
public function setEtplName($var)
{
GPBUtil::checkString($var, True);
$this->etplName = $var;
}
// ... 其他省略
\Grpc\ChannelCredentials::createInsecure(),
]);
$request = new Exporttask\WshExportTaskCreateTplReq();
$request->setEtplName($name);
list($reply, $status) = $client->createTpl($request)->wait();
$message = $reply->getMessage();
var_dump($message);
gRPC服务发现与服务治理的方案
目前gRPC主流分布式方案有这么几种: etcd, zookeeper, consul.
1、集中式LB(Proxy Model)
在服务消费者和服务提供者之间有一个独立的LB,通常是专门的硬件设备如 F5,或者基于软件如 LVS,HAproxy等实现。LB上有所有服务的地址映射表,通常由运维配置注册,当服务消费方调用某个目标服务时,它向LB发起请求,由LB以某种策略,比如轮询(Round-Robin)做负载均衡后将请求转发到目标服务。LB一般具备健康检查能力,能自动摘除不健康的服务实例。 该方案主要问题:
1、 单点问题,所有服务调用流量都经过LB,当服务数量和调用量大的时候,LB容易成为瓶颈,且一旦LB发生故障影响整个系统;
2、服务消费方、提供方之间增加了一级,有一定性能开销。
2、进程内LB(Balancing-aware Client)
针对第一个方案的不足,此方案将LB的功能集成到服务消费方进程里,也被称为软负载或者客户端负载方案。服务提供方启动时,首先将服务地址注册到服务注册表,同时定期报心跳到服务注册表以表明服务的存活状态,相当于健康检查,服务消费方要访问某个服务时,它通过内置的LB组件向服务注册表查询,同时缓存并定期刷新目标服务地址列表,然后以某种负载均衡策略选择一个目标服务地址,最后向目标服务发起请求。LB和服务发现能力被分散到每一个服务消费者的进程内部,同时服务消费方和服务提供方之间是直接调用,没有额外开销,性能比较好。该方案主要问题:
1、开发成本,该方案将服务调用方集成到客户端的进程里头,如果有多种不同的语言栈,就要配合开发多种不同的客户端,有一定的研发和维护成本;
2、另外生产环境中,后续如果要对客户库进行升级,势必要求服务调用方修改代码并重新发布,升级较复杂。
3、独立 LB 进程(External Load Balancing Service)
该方案是针对第二种方案的不足而提出的一种折中方案,原理和第二种方案基本类似。
不同之处是将LB和服务发现功能从进程内移出来,变成主机上的一个独立进程。主机上的一个或者多个服务要访问目标服务时,他们都通过同一主机上的独立LB进程做服务发现和负载均衡。该方案也是一种分布式方案没有单点问题,一个LB进程挂了只影响该主机上的服务调用方,服务调用方和LB之间是进程内调用性能好,同时该方案还简化了服务调用方,不需要为不同语言开发客户库,LB的升级不需要服务调用方改代码。
该方案主要问题:部署较复杂,环节多,出错调试排查问题不方便。
服务发现负载均衡实现
gRPC开源组件官方并未直接提供服务注册与发现的功能实现,但其设计文档已提供实现的思路,并在不同语言的gRPC代码API中已提供了命名解析和负载均衡接口供扩展。
其基本实现原理:
1、服务启动后gRPC客户端向命名服务器发出名称解析请求,名称将解析为一个或多个IP地址,每个IP地址标示它是服务器地址还是负载均衡器地址,以及标示要使用那个客户端负载均衡策略或服务配置。
2、客户端实例化负载均衡策略,如果解析返回的地址是负载均衡器地址,则客户端将使用grpclb策略,否则客户端使用服务配置请求的负载均衡策略。
3、负载均衡策略为每个服务器地址创建一个子通道(channel)。
4、当有rpc请求时,负载均衡策略决定那个子通道即grpc服务器将接收请求,当可用服务器为空时客户端的请求将被阻塞。
根据gRPC官方提供的设计思路,基于进程内LB方案(即第2个案,阿里开源的服务框架 Dubbo 也是采用类似机制),结合分布式一致的组件(如Zookeeper、Consul、Etcd),可找到gRPC服务发现和负载均衡的可行解决方案。接下来以GO语言为例,简单介绍下基于Etcd3的关键代码实现:
1)命名解析实现:resolver.go
package etcdv3
import (
"errors"
"fmt"
"strings"
etcd3 "github.com/coreos/etcd/clientv3"
"google.golang.org/grpc/naming"
)
// resolver is the implementaion of grpc.naming.Resolver
type resolver struct {
serviceName string // service name to resolve
}
// NewResolver return resolver with service name
func NewResolver(serviceName string) *resolver {
return &resolver{serviceName: serviceName}
}
// Resolve to resolve the service from etcd, target is the dial address of etcd
// target example: "http://127.0.0.1:2379,http://127.0.0.1:12379,http://127.0.0.1:22379"
func (re *resolver) Resolve(target string) (naming.Watcher, error) {
if re.serviceName == "" {
return nil, errors.New("grpclb: no service name provided")
}
// generate etcd client
client, err := etcd3.New(etcd3.Config{
Endpoints: strings.Split(target, ","),
})
if err != nil {
return nil, fmt.Errorf("grpclb: creat etcd3 client failed: %s", err.Error())
}
// Return watcher
return &watcher{re: re, client: *client}, nil
}
2)服务发现实现:watcher.go
package etcdv3
import (
"fmt"
etcd3 "github.com/coreos/etcd/clientv3"
"golang.org/x/net/context"
"google.golang.org/grpc/naming"
"github.com/coreos/etcd/mvcc/mvccpb"
)
// watcher is the implementaion of grpc.naming.Watcher
type watcher struct {
re *resolver // re: Etcd Resolver
client etcd3.Client
isInitialized bool
}
// Close do nothing
func (w *watcher) Close() {
}
// Next to return the updates
func (w *watcher) Next() ([]*naming.Update, error) {
// prefix is the etcd prefix/value to watch
prefix := fmt.Sprintf("/%s/%s/", Prefix, w.re.serviceName)
// check if is initialized
if !w.isInitialized {
// query addresses from etcd
resp, err := w.client.Get(context.Background(), prefix, etcd3.WithPrefix())
w.isInitialized = true
if err == nil {
addrs := extractAddrs(resp)
//if not empty, return the updates or watcher new dir
if l := len(addrs); l != 0 {
updates := make([]*naming.Update, l)
for i := range addrs {
updates[i] = &naming.Update{Op: naming.Add, Addr: addrs[i]}
}
return updates, nil
}
}
}
// generate etcd Watcher
rch := w.client.Watch(context.Background(), prefix, etcd3.WithPrefix())
for wresp := range rch {
for _, ev := range wresp.Events {
switch ev.Type {
case mvccpb.PUT:
return []*naming.Update{{Op: naming.Add, Addr: string(ev.Kv.Value)}}, nil
case mvccpb.DELETE:
return []*naming.Update{{Op: naming.Delete, Addr: string(ev.Kv.Value)}}, nil
}
}
}
return nil, nil
}
func extractAddrs(resp *etcd3.GetResponse) []string {
addrs := []string{}
if resp == nil || resp.Kvs == nil {
return addrs
}
for i := range resp.Kvs {
if v := resp.Kvs[i].Value; v != nil {
addrs = append(addrs, string(v))
}
}
return addrs
}
3)服务注册实现:register.go
package etcdv3
import (
"fmt"
"log"
"strings"
"time"
etcd3 "github.com/coreos/etcd/clientv3"
"golang.org/x/net/context"
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
)
// Prefix should start and end with no slash
var Prefix = "etcd3_naming"
var client etcd3.Client
var serviceKey string
var stopSignal = make(chan bool, 1)
// Register
func Register(name string, host string, port int, target string, interval time.Duration, ttl int) error {
serviceValue := fmt.Sprintf("%s:%d", host, port)
serviceKey = fmt.Sprintf("/%s/%s/%s", Prefix, name, serviceValue)
// get endpoints for register dial address
var err error
client, err := etcd3.New(etcd3.Config{
Endpoints: strings.Split(target, ","),
})
if err != nil {
return fmt.Errorf("grpclb: create etcd3 client failed: %v", err)
}
go func() {
// invoke self-register with ticker
ticker := time.NewTicker(interval)
for {
// minimum lease TTL is ttl-second
resp, _ := client.Grant(context.TODO(), int64(ttl))
// should get first, if not exist, set it
_, err := client.Get(context.Background(), serviceKey)
if err != nil {
if err == rpctypes.ErrKeyNotFound {
if _, err := client.Put(context.TODO(), serviceKey, serviceValue, etcd3.WithLease(resp.ID)); err != nil {
log.Printf("grpclb: set service '%s' with ttl to etcd3 failed: %s", name, err.Error())
}
} else {
log.Printf("grpclb: service '%s' connect to etcd3 failed: %s", name, err.Error())
}
} else {
// refresh set to true for not notifying the watcher
if _, err := client.Put(context.Background(), serviceKey, serviceValue, etcd3.WithLease(resp.ID)); err != nil {
log.Printf("grpclb: refresh service '%s' with ttl to etcd3 failed: %s", name, err.Error())
}
}
select {
case <-stopSignal:
return
case <-ticker.C:
}
}
}()
return nil
}
// UnRegister delete registered service from etcd
func UnRegister() error {
stopSignal <- true
stopSignal = make(chan bool, 1) // just a hack to avoid multi UnRegister deadlock
var err error;
if _, err := client.Delete(context.Background(), serviceKey); err != nil {
log.Printf("grpclb: deregister '%s' failed: %s", serviceKey, err.Error())
} else {
log.Printf("grpclb: deregister '%s' ok.", serviceKey)
}
return err
}
4)接口描述文件:helloworld.proto
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.midea.jr.test.grpc";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {
}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
5)实现服务端接口:helloworldserver.go
package main
import (
"flag"
"fmt"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
"golang.org/x/net/context"
"google.golang.org/grpc"
grpclb "com.midea/jr/grpclb/naming/etcd/v3"
"com.midea/jr/grpclb/example/pb"
)
var (
serv = flag.String("service", "hello_service", "service name")
port = flag.Int("port", 50001, "listening port")
reg = flag.String("reg", "http://127.0.0.1:2379", "register etcd address")
)
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", *port))
if err != nil {
panic(err)
}
err = grpclb.Register(*serv, "127.0.0.1", *port, *reg, time.Second*10, 15)
if err != nil {
panic(err)
}
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL, syscall.SIGHUP, syscall.SIGQUIT)
go func() {
s := <-ch
log.Printf("receive signal '%v'", s)
grpclb.UnRegister()
os.Exit(1)
}()
log.Printf("starting hello service at %d", *port)
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
s.Serve(lis)
}
// server is used to implement helloworld.GreeterServer.
type server struct{}
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
fmt.Printf("%v: Receive is %s\n", time.Now(), in.Name)
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
6)实现客户端接口:helloworldclient.go
package main
import (
"flag"
"fmt"
"time"
grpclb "com.midea/jr/grpclb/naming/etcd/v3"
"com.midea/jr/grpclb/example/pb"
"golang.org/x/net/context"
"google.golang.org/grpc"
"strconv"
)
var (
serv = flag.String("service", "hello_service", "service name")
reg = flag.String("reg", "http://127.0.0.1:2379", "register etcd address")
)
func main() {
flag.Parse()
r := grpclb.NewResolver(*serv)
b := grpc.RoundRobin(r)
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
conn, err := grpc.DialContext(ctx, *reg, grpc.WithInsecure(), grpc.WithBalancer(b))
if err != nil {
panic(err)
}
ticker := time.NewTicker(1 * time.Second)
for t := range ticker.C {
client := pb.NewGreeterClient(conn)
resp, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "world " + strconv.Itoa(t.Second())})
if err == nil {
fmt.Printf("%v: Reply is %s\n", t, resp.Message)
}
}
}