learn-tech/专栏/趣谈网络协议/36跨语言类RPC协议:交流之前,双方先来个专业术语表.md
2024-10-16 11:00:45 +08:00

205 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
36 跨语言类RPC协议交流之前双方先来个专业术语表
到目前为止咱们讲了四种RPC分别是ONC RPC、基于XML的SOAP、基于JSON的RESTful和Hessian2。
通过学习,我们知道,二进制的传输性能好,文本类的传输性能差一些;二进制的难以跨语言,文本类的可以跨语言;要写协议文件的严谨一些,不写协议文件的灵活一些。虽然都有服务发现机制,有的可以进行服务治理,有的则没有。
我们也看到了RPC从最初的客户端服务器模式最终演进到微服务。对于RPC框架的要求越来越多了具体有哪些要求呢
首先,传输性能很重要。因为服务之间的调用如此频繁了,还是二进制的越快越好。
其次,跨语言很重要。因为服务多了,什么语言写成的都有,而且不同的场景适宜用不同的语言,不能一个语言走到底。
最好既严谨又灵活,添加个字段不用重新编译和发布程序。
最好既有服务发现也有服务治理就像Dubbo和Spring Cloud一样。
Protocol Buffers
这是要多快好省地建设社会主义啊。理想还是要有的嘛这里我就来介绍一个向“理想”迈进的GRPC。
GRPC首先满足二进制和跨语言这两条二进制说明压缩效率高跨语言说明更灵活。但是又是二进制又是跨语言这就相当于两个人沟通你不但说方言还说缩略语人家怎么听懂呢所以最好双方弄一个协议约定文件里面规定好双方沟通的专业术语这样沟通就顺畅多了。
对于GRPC来讲二进制序列化协议是Protocol Buffers。首先需要定义一个协议文件.proto。
我们还看买极客时间专栏的这个例子。
syntax = “proto3”;
package com.geektime.grpc
option java_package = “com.geektime.grpc”;
message Order {
required string date = 1;
required string classname = 2;
required string author = 3;
required int price = 4;
}
message OrderResponse {
required string message = 1;
}
service PurchaseOrder {
rpc Purchase (Order) returns (OrderResponse) {}
}
在这个协议文件中我们首先指定使用proto3的语法然后我们使用Protocol Buffers的语法定义两个消息的类型一个是发出去的参数一个是返回的结果。里面的每一个字段例如date、classname、author、price都有唯一的一个数字标识这样在压缩的时候就不用传输字段名称了只传输这个数字标识就行了能节省很多空间。
最后定义一个Service里面会有一个RPC调用的声明。
无论使用什么语言都有相应的工具生成客户端和服务端的Stub程序这样客户端就可以像调用本地一样调用远程的服务了。
协议约定问题
Protocol Buffers是一款压缩效率极高的序列化协议有很多设计精巧的序列化方法。
对于int类型32位的一般都需要4个Byte进行存储。在Protocol Buffers中使用的是变长整数的形式。对于每一个Byte的8位最高位都有特殊的含义。
如果该位为 1表示这个数字没完后续的Byte也属于这个数字如果该位为 0则这个数字到此结束。其他的7个Bit才是用来表示数字的内容。因此小于128的数字都可以用一个Byte表示大于128的数字比如130会用两个字节来表示。
对于每一个字段使用的是TLVTagLengthValue的存储办法。
其中Tag = (field_num << 3) | wire_typefield_num就是在proto文件中给每个字段指定唯一的数字标识而wire_type用于标识后面的数据类型
例如对于string author = 3在这里field_num为3string的wire_type为2于是 (field_num << 3) | wire_type = (11000) | 10 = 11010 = 26接下来是Length最后是Value为liuchao”,如果使用UTF-8编码长度为7个字符因而Length为7
可见在序列化效率方面Protocol Buffers简直做到了极致
在灵活性方面这种基于协议文件的二进制压缩协议往往存在更新不方便的问题例如客户端和服务器因为需求的改变需要添加或者删除字段
这一点上Protocol Buffers考虑了兼容性在上面的协议文件中每一个字段都有修饰符比如
required这个值不能为空一定要有这么一个字段出现
optional可选字段可以设置也可以不设置如果不设置则使用默认值
repeated可以重复0到多次
如果我们想修改协议文件对于赋给某个标签的数字例如string author=3这个就不要改变了改变了就不认了也不要添加或者删除required字段因为解析的时候发现没有这个字段就会报错。对于optional和repeated字段可以删除也可以添加。这就给了客户端和服务端升级的可能性。
例如我们在协议里面新增一个string recommended字段表示这个课程是谁推荐的就将这个字段设置为optional我们可以先升级服务端当客户端发过来消息的时候是没有这个值的将它设置为一个默认值我们也可以先升级客户端当客户端发过来消息的时候是有这个值的那它将被服务端忽略
至此我们解决了协议约定的问题
网络传输问题
接下来我们来看网络传输的问题
如果是Java技术栈GRPC的客户端和服务器之间通过Netty Channel作为数据通道每个请求都被封装成HTTP 2.0的Stream
Netty是一个高效的基于异步IO的网络传输框架这个上一节我们已经介绍过了HTTP 2.0在[第14讲]我们也介绍过HTTP 2.0协议将一个TCP的连接切分成多个流每个流都有自己的ID而且流是有优先级的流可以是客户端发往服务端也可以是服务端发往客户端它其实只是一个虚拟的通道
HTTP 2.0还将所有的传输信息分割为更小的消息和帧并对它们采用二进制格式编码
通过这两种机制HTTP 2.0的客户端可以将多个请求分到不同的流中然后将请求内容拆成帧进行二进制传输这些帧可以打散乱序发送 然后根据每个帧首部的流标识符重新组装并且可以根据优先级决定优先处理哪个流的数据
由于基于HTTP 2.0GRPC和其他的RPC不同可以定义四种服务方法
第一种也是最常用的方式是单向RPC即客户端发送一个请求给服务端从服务端获取一个应答就像一次普通的函数调用
rpc SayHello(HelloRequest) returns (HelloResponse){}
第二种方式是服务端流式RPC即服务端返回的不是一个结果而是一批客户端发送一个请求给服务端可获取一个数据流用来读取一系列消息客户端从返回的数据流里一直读取直到没有更多消息为止
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){}
第三种方式为客户端流式RPC也即客户端的请求不是一个而是一批客户端用提供的一个数据流写入并发送一系列消息给服务端一旦客户端完成消息写入就等待服务端读取这些消息并返回应答
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {}
第四种方式为双向流式 RPC即两边都可以分别通过一个读写数据流来发送一系列消息这两个数据流操作是相互独立的所以客户端和服务端能按其希望的任意顺序读写服务端可以在写应答前等待所有的客户端消息或者它可以先读一个消息再写一个消息或者读写相结合的其他方式每个数据流里消息的顺序会被保持
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){}
如果基于HTTP 2.0客户端和服务器之间的交互方式要丰富得多不仅可以单方向远程调用还可以实现当服务端状态改变的时候主动通知客户端
至此传输问题得到了解决
服务发现与治理问题
最后是服务发现与服务治理的问题
GRPC本身没有提供服务发现的机制需要借助其他的组件发现要访问的服务端在多个服务端之间进行容错和负载均衡
其实负载均衡本身比较简单LVSHAProxyNginx都可以做关键问题是如何发现服务端并根据服务端的变化动态修改负载均衡器的配置
在这里我们介绍一种对于GRPC支持比较好的负载均衡器Envoy其实Envoy不仅仅是负载均衡器它还是一个高性能的C++写的Proxy转发器可以配置非常灵活的转发规则
这些规则可以是静态的放在配置文件中的在启动的时候加载要想重新加载一般需要重新启动但是Envoy支持热加载和热重启这在一定程度上缓解了这个问题
当然最好的方式是将规则设置为动态的放在统一的地方维护这个统一的地方在Envoy眼中被称为服务发现Discovery Service过一段时间去这里拿一下配置就修改了转发策略
无论是静态的还是动态的在配置里面往往会配置四个东西
第一个是listenerEnvoy既然是Proxy专门做转发就得监听一个端口接入请求然后才能够根据策略转发这个监听的端口就称为listener
第二个是endpoint是目标的IP地址和端口这个是Proxy最终将请求转发到的地方
第三个是cluster一个cluster是具有完全相同行为的多个endpoint也即如果有三个服务端在运行就会有三个IP和端口但是部署的是完全相同的三个服务它们组成一个cluster从cluster到endpoint的过程称为负载均衡可以轮询
第四个是route有时候多个cluster具有类似的功能但是是不同的版本号可以通过route规则选择将请求路由到某一个版本号也即某一个cluster
如果是静态的则将后端的服务端的IP地址拿到然后放在配置文件里面就可以了
如果是动态的就需要配置一个服务发现中心这个服务发现中心要实现Envoy的APIEnvoy可以主动去服务发现中心拉取转发策略
看来Envoy进程和服务发现中心之间要经常相互通信互相推送数据所以Envoy在控制面和服务发现中心沟通的时候就可以使用GRPC也就天然具备在用户面支撑GRPC的能力
Envoy如果复杂的配置都能干什么事呢
一种常见的规则是配置路由策略例如后端的服务有两个版本可以通过配置Envoy的route来设置两个版本之间也即两个cluster之间的route规则一个占99%的流量一个占1%的流量
另一种常见的规则就是负载均衡策略对于一个cluster下的多个endpoint可以配置负载均衡机制和健康检查机制当服务端新增了一个或者挂了一个都能够及时配置Envoy进行负载均衡
所有这些节点的变化都会上传到注册中心所有这些策略都可以通过注册中心进行下发所以更严格的意义上讲注册中心可以称为注册治理中心
Envoy这么牛是不是能够将服务之间的相互调用全部由它代理如果这样服务也不用像Dubbo或者Spring Cloud一样自己感知到注册中心自己注册自己治理对应用干预比较大
如果我们的应用能够意识不到服务治理的存在就可以直接进行GRPC的调用
这就是未来服务治理的趋势Serivce Mesh也即应用之间的相互调用全部由Envoy进行代理服务之间的治理也被Envoy进行代理完全将服务治理抽象出来到平台层解决
-
至此RPC框架中有治理功能的DubboSpring CloudService Mesh就聚齐了
小结
好了这一节就到这里了我们来总结一下
GRPC是一种二进制性能好跨语言还灵活同时可以进行服务治理的多快好省的RPC框架唯一不足就是还是要写协议文件
GRPC序列化使用Protocol Buffers网络传输使用HTTP 2.0服务治理可以使用基于Envoy的Service Mesh
最后给你留一个思考题吧
在讲述Service Mesh的时候我们说了希望Envoy能够在服务不感知的情况下将服务之间的调用全部代理了你知道怎么做到这一点吗
我们趣谈网络协议专栏已经接近尾声了你还记得专栏开始我们讲过的那个双十一下单的故事吗
下节开始我会将这个过程涉及的网络协议细节全部串联起来给你还原一个完整的网络协议使用场景信息量会很大做好准备哦我们下期见