步骤
- 创建 线程组
- 创建 压测用例
- 创建 监听器
由程序负责任务切换,可以减少线程/进程上下文切换的消耗。用户态实现任务切换,无需进入内核态。
虽然 Python 有多线程的概念,但是由于 GIL 的存在,并不能利用多核资源。如果易不能充分利用单进程资源,可能会带来严重的性能问题。
python 默认只为主线程创建 loop。如下 tornado 代码实现了自动为创建 loop 的功能,使用 asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
来生效。
1 | if sys.platform == "win32" and hasattr(asyncio, "WindowsSelectorEventLoopPolicy"): |
下面是使用协程实现的定时器。
1 | # coding: utf-8 |
下看下抛出异常的代码。
1 | def ensure_future(coro_or_future, *, loop=None): |
ServiceDiscovery
,创建 ServiceProvider
对象,首先需要有 ServiceDiscovery
;所有请求直接访问 zkServiceProvider
, 特定服务发现的封装,并集成了负载均衡策略;集成了 ServiceCache
,有节点监听和缓存ServiceCache
,会在本地内存缓存,并使用 watcher 来保持数据最新ServiceDiscovery
、ServiceProvider
需要调用 start
方法后可用。
ServiceDiscovery
的 registerService
注册服务后,只要 ServiceDiscovery
不 stop
,会一直保持节点注册stop
,没有及时调用 unregisterService
接口来取消注册,zk 节点会保存一段时间(几秒),然后由 zk 摘除ServiceProvider
的接口,会实时调用 zk 查询数据,ServiceCacheListener
有两个方法。
cacheChanged
当服务节点变化时,会调用该方法stateChanged
当 zk 连接状态变化时,会调用该方法gRPC 代码实现中,Channel 是一个虚拟类,是物理连接的逻辑概念。ManagedChannelImpl 和 ManagedChannelImpl2 继承了该类,并实现了 newCall 和 authority 接口。
SubChannel 是 LoadBalancer 的内部类,在了解 SubChannel 之前,需要先了解 SocketAddress 和 EquivalentAddressGroup。
SocketAddress 是一个虚拟类,代表一个不包含协议信息的 Socket 地址。
EquivalentAddressGroup 是一组 SocketAddress,在 Channel 创建连接时,其包含的 SocketAddress 被视为无差异的。
再回到 SubChannel,他表示和一个服务器或者 EquivalentAddressGroup 表示的一组等价服务器的 一个物理连接,这里需要注意的是,他至多有一个物理连接。在发起新的 RPCs 时,如果没有激活的 transport,在被安排 RPC 请求时,会创建 transport。调用 requestConnection ,会请求 Subchannel 在没有 transport 的情况下创建 transport。
SubChannel 有一个 List<EquivalentAddressGroup>
属性,可以通过 setAddresses(List<EquivalentAddressGroup> addrs)
和 setAddresses(EquivalentAddressGroup addrs)
设置。
InternalSubchannel 表示一个 SocketAddress 的 Transports 。他实现了 TransportProvider 接口,定义了 obtainActiveTransport
方法,该方法如果没有激活的 transports,则会调用 startNewTransport
进行创建。
在创建 transports 时,需要先获取 SocketAddress。在创建 InternalSubChannel 时,会传入 List<EquivalentAddressGroup>
。需要注意的是,InternalSubChannel 默认使用 第一个 EquivalentAddressGroup 的 第一个 SocketAddress ,只有在 transport 被关闭时,才会尝试下一个服务地址。
尝试完所有的地址,全部失败后,此时 InternalSubChannel 处于 TRANSIENT_FAILURE 状态,等待一个 delay 时间后,重新尝试。
NameResolver 是一个可插拔的组件(pluggable component),代码层面是一个接口,用来解析一个 target(URI),并返回给调用方一个地址列表,gRPC 内置了 DnsNameResolver。
地址的返回是基于 Listener 机制,NameResolver 的实现类,需要定义 start 方法,方法会传入一个 Listener,当服务列表发生变化时,调用 Listener 的 onResult 方法,通知 NameResolver 的持有方。
LoadBalancer 是一个可插拔的组件,接受 NameResolver 拿到的服务方列表,提供可用的 SubChannel。
从 RoundRobinLoadBalancer 的 handleResolvedAddresses 实现可以发现。
每次刷新时
Channel 去执行一次远程调用,是通过 newCall 方法,传入 **MethodDescriptor ** 和 CallOptions。对于 ManagedChannelImpl,其实现 Channel 接口,newCall 方法转而调用 InterceptorChannel 的 newCall,先简单说下 InterceptorChannel。
managedChannelImpl 字段是调用
ClientInterceptors.intercept(channel, interceptors)
构造,先说 InterceptorChannel 再说 ClientInterceptors。InterceptorChannel 将 Interceptor 和 Channel 的结合,由 channel + interceptor 构造,调用 channel 的 newCall 时,会执行 interceptor 的 interceptCall,该调用会传入 channel。对于一个原始 channel 和 多个 interceptor,先将 interceptor 倒序,然后依次创建 InterceptorChannel,进行包装。
1
2
3 for (ClientInterceptor interceptor : interceptors) {
channel = new InterceptorChannel(channel, interceptor);
}相比之下,ClientInterceptors 只是一个工具类。
接着,怎么用上 NameSolver 的。在构造 interceptorChannel 时,传入一个 channel。这个channel 是一个 RealChannel 对象。
RealChannel 实现了 Channel 接口。
这里需要再提一下,Channel 是逻辑连接,SubChannel 是物理连接。ManagedChannelImpl 实现了 Channel 接口,同时,有一个内部类 SubchannelImpl 实现 SubChannel。
首先调用 SubchannelImpl 的 requestConnection
,在方法内会调用 InternalSubchannel
的 obtainActiveTransport
创建和 SocketAddress 的连接。
在 ManagedChannelImpl 内,定义了 NameResolverListener,实现了NameResolver.Listener 接口,在 NameResolverListener 内做了 LoadBalancer 和 NameResolver 的结合。
在 NameResolverListener 的 onResult 方法内,当服务器地址变更时会执行改方法。首先,会从参数中获取服务端列 表List<EquivalentAddressGroup>
,接下来调用 AutoConfiguredLoadBalancer 的 tryHandleResolvedAddresses
方法,再调用 LoadBalancer 的 handleResolvedAddresses
。整个调用实现比较绕,直接了解内置的 LB 即可。
File > Preferences > Editor > Dynamically Adjust Node Size to Label Size
新建节点时,node 大小根据 label 大小动态调整。
不启用效果
启用效果
Tools > Fit Node To Label
假设,有如下 node。
调整后效果如下。
1 | brew install rsync |
1 | P 等价于 --partial --progress |
1 | rsync -Pav <local> <user@ip:remote-dist> |
使用工具 fswatch。
1 | 安装依赖 |
dubbo-common
: Util 类和通用模型dubbo-remoting
: 远程通讯模块,提供通用的客户端和服务端的通讯能力;相当于 dubbo 协议的实现,如果 RPC 使用 RMI 协议则不需要此包;最小原则,可以只看 dubbo-remoting-api + dubbo-remoting-netty4
和 dubbo-remoting-zookeeper
dubbo-rpc
: 远程调用模块,抽象各种协议,以及动态代理,只包含一对一调用,不关心集群管理;从包图可以看出,dubbo-rpc
是 dubbo 的中心
dubbo-rpc-api
: 抽象各种协议以及动态代理,实现一对一调用dubbo-cluster
: 集群模块,提供多个服务方伪装为一个提供方能力,包括:负载均衡、容错、路由等,集群的地址列表可以静态配置,也可以由注册中心下发
Cluster
接口 + cluster.support
包;Cluster 将 Directory 中的多个 Invoker 伪装成一个 Invoker(Java 动态代理),对上层透明;伪装过程中包含了容错逻辑Directory
接口 + cluster.directory
包;Directory 代表了多个 Invoker,可以看做动态变化的 Invoker List,每个 Invoker 对应一个服务端节点Router
接口 + cluster.router
包;负责从多个 Invoker 中按路由规则选出子集,用于调用Configurator
接口 + cluster.configurator
包LoadBalance
接口和 cluster.loadbalance
包;负责从多个 Invoker 中根据负载均衡算法,选出具体的一个用于本次调用Merger
接口 + cluster.merger
包;合并返回结果,用于分组聚合dubbo-registry
: 注册中心模块,基于注册中心下发地址的集群方式,以及对各种注册中心的抽象dubbo-monitor
: 监控模块,统计服务调用次数,调用时间,调用链路跟踪的服务dubbo-config
: 配置模块,是 dubbo 对外的 API,用户通过 Config 使用 dubbo,隐藏 dubbo 的所有细节dubbo-container
: 容器模块,是一个 Standlone 容器,以简单的 Main 加载 Spring 启动(用于代替容器启动),因为服务通常不需要 Tomcat、JBoss 等Web 容器的特性,没必要用 Web 容器取加载服务dubbo-filter
:过滤器模块,提供了内置过滤器;
doubbo-filter-cache
:缓存过滤器dubbo-filter-validation
:参数验证过滤器dubbo-plugin
:插件模块,提供内置的插件
dubbo-qos
:提供在线运维命令BOM(Bill Of Materials),为了防止用 Maven 管理 Spring 项目时,不同的项目依赖了不同版本的 Spring,可以使用 Maven BOM 来解决这一问题。
dubbo-dependencies-bom/pom.xml
,统一定义了 Dubbo 依赖的三方库的版本号。
dubbo-parent
,会引入该 BOM。
dubbo-bom/pom.xml
,统一定义了 Dubbo 的版本号。
dubbo-demo
和 dubbo-test
会引入该 BOM 。以 dubbo-demo
为例。
Dubbo 的 Maven 模块,都会引入该 pom 文件。以 dubbo-cluster
为例。
dubbo/all/pom.xml
,定义了 dubbo 的打包脚本。我们在使用 dubbo 库时,引入该 pom 文件。
各层说明
ServiceConfig
, ReferenceConfig
为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类;由 dubbo-config
模块实现ServiceProxy
为中心,扩展接口为 ProxyFactory
RegistryFactory
, Registry
, RegistryService
Invoker
为中心,扩展接口为 Cluster
, Directory
, Router
, LoadBalance
Statistics
为中心,扩展接口为 MonitorFactory
, Monitor
, MonitorService
Invocation
, Result
为中心,扩展接口为 Protocol
, Invoker
, Exporter
Request
, Response
为中心,扩展接口为 Exchanger
, ExchangeChannel
, ExchangeClient
, ExchangeServer
Message
为中心,扩展接口为 Channel
, Transporter
, Client
, Server
, Codec
Serialization
, ObjectInput
, ObjectOutput
, ThreadPool
关系说明
展开总设计图左边服务提供方暴露服务的蓝色初始化链,时序图如下。
展开总设计图右边服务消费方引用服务的蓝色初始化链,时序图如下。
Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
类图。
Invocation 是会话域,它持有调用过程中的变量,比如方法名,参数等。
Result 是会话域,它持有调用过程中返回值,异常等。
过滤器接口,和我们平时理解的 javax.servlet.Filter
基本一致。
代理工厂接口。服务消费着引用服务的 主过程 如下图。
从图中我们可以看出,方法的 invoker
参数,通过 Protocol 将 Service接口 创建出 Invoker 。通过创建 Service 的 Proxy ,实现我们在业务代理调用 Service 的方法时,透明的内部转换成调用 Invoker 的 #invoke(Invocation)
方法。
服务提供者暴露服务的 主过程 如下图。
从图中我们可以看出,该方法创建的 Invoker ,下一步会提交给 Protocol ,从 Invoker 转换到 Exporter 。
类图如下。
从图中,我们可以看出 Dubbo 支持 Javassist 和 JDK Proxy 两种方式生成代理。
Protocol 是服务域,它是 Invoker 暴露和引用的主功能入口。它负责 Invoker 的生命周期管理。
Dubbo 处理服务暴露的关键就在 Invoker 转换到 Exporter 的过程。下面我们以 Dubbo 和 RMI 这两种典型协议的实现来进行说明。
Exporter ,Invoker 暴露服务在 Protocol 上的对象。
Invoker 监听器。
Exporter 监听器。
dubbo 支持多种配置方式:
所有配置项分为三大类。
所有配置最终都将转换为 Dubbo URL 表示,并由服务提供方生成,经注册中心传递给消费方,各属性对应 URL 的参数,参见配置项一览表中的对应URL参数列。
首先看下 dubbo-config-api
项目结构。
整理下配制间的关系。
配置可分为四部分。
application-shared
provider-side
consumer-side
sub-config
配置类关系如下。
做框架应该考虑的一些问题。
系统通过扩展机制,来横向扩展系统功能,只要符合对应扩展规范,扩展的过程无需修改系统代码。所谓扩展点,是系统定义的,可以支持扩展的点(流程的某个步骤、代码的某个点)。
dubbo 提供了大量扩展点。
dubbo 借鉴 JDK 标准的 SPI(Service Provider Interface)扩展点机制,重新实现了一套 SPI 机制,做了如下改进。
JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。
ubbo SPI 在 dubbo-common
的 extension
包实现,如下图所示。
扩展点接口的标识。
自适应拓展信息的标记。
可添加类或方法上,分别代表了两种不同的使用方式。标记在拓展接口的方法上,代表自动生成代码实现该接口的 Adaptive 拓展实现类;标记在类上,代表手动实现它是一个拓展接口的 Adaptive 拓展实现类。
一个拓展接口,有且仅有一个 Adaptive 拓展实现类。
自动激活条件的标记。
1 | bazel query //... --output label_kind |
1 | 仍会在项目创建 bazel-* 软链, 一般无需修改 |
在 WORKSPACE 中指定
1 | http_archive( |
这里的 8b976f7c6187f7f68956207b9a154bc278e11d7e 为 commit id,下载 url 为 https://github.com/{user}/{repo}/archive/{commit}.tar.gz
BUILD 文件中引用
1 | proto_library( |
proto 中引用
1 | import "google/api/annotations.proto"; |
WORKSPACE 中添加依赖
1 | RULES_JVM_EXTERNAL_TAG = "4.0" |
BUILD 文件添加 exporter
1 | java_gapic_library( |
执行 exporter
1 | 部署到本地 |
使用 gRPC + protocol buffers 时,会面临三种打包方式,proto library
、grpc library
、gapic library
。不同的打包方式需要使用不同的工具/插件,以如下定义为例。
1 | syntax = "proto3"; |
首先是打包 proto library,需要下载 protoc 程序(在 protobuf 的 release 页面下载)。
1 | protoc --java_out=jv greeter.proto |
这个命令只会生成数据结构,会忽略 service 定义。
然后,是打包 gRPC library,需要下载对应的 generator,工具下载 / 本地编译步骤参考这里,另外 gRPC 相关的库及代码在这里,命令参考如下。
1 | protoc --plugin=protoc-gen-grpc-java=/Users/wii/Downloads/protoc-gen-grpc-java-1.38.0-osx-x86_64.exe --grpc-java_out=jv --java_out=jv greeter.proto |
gRPC library 和 proto library 的区别是,会生成用于服务调用的 client 和 server 端代码。
最后,是打包 gapic library,同样需要下载对应的 generator,以 Java 为例,工具参考gapic-generator-java和gapic-generator。顺便,artman 作为一个可选的生成工具,已经停止维护。要打包 java 的 gapic library ,首先需要编译插件,从gapic-generator 下载 release 包,运行命令 ./gradlew fatjar
进行编译。
首先,生成 description 文件。
1 | protoc --include_imports --include_source_info -o greeter.description greeter.proto |
接着,生成 proto message classes。
1 | mkdir java-proto-lib-out |
然后,生成 gRPC stub 。
1 | mkdir java-grpc-lib-out |
进一步,新建服务描述文件。
1 | type: google.api.Service |
接着,生成客户端配置文件。
1 | java -XX:-OmitStackTraceInFastThrow -cp /Users/wii/Downloads/gapic-generator-2.11.1/build/libs/gapic-generator-2.11.1-fatjar.jar \ |
然后,创建 package 的 metadata 配置文件。
1 | artifact_type: GAPIC |
最后,生成代码。
java
1 | java -cp /Users/wii/Downloads/gapic-generator-2.11.1/build/libs/gapic-generator-2.11.1-fatjar.jar \ |
python
1 | java -cp /Users/wii/Downloads/gapic-generator-2.11.1/build/libs/gapic-generator-2.11.1-fatjar.jar \ |
顺便,这里提供了一个示例。
最后,在使用 gapic 时,可能需要引用一些 google 的 proto 文件,比如 google/api/annotation.proto
,这些文件定义在这里。
1 | COMMIT_ID=734b8d41d39a903c70132828616f26cb2c7f908c |
1 | install |
生成 gapic 库时,指定的配置文件。
1 | java_gapic_library( |
配置参考 auth.proto
内定义的 Authentication 结构。
1 | // `Authentication` defines the authentication configuration for API methods |
可以看到,有两个字段,rules & providers。对于 rules,数据结构为 AuthenticationRule。
1 | // Authentication rules for the service. |
示例如下。
1 | type: google.api.Service |
gRPC 是谷歌开源的一款 RPC 框架,protocal buffers 作为它的 IDL(Interface Definition Language,接口定义语言),也可以作为其底层数据交换格式。
在 gRPC,一个客户端应用可以直接调用不同机器上服务端的方法。在一些 RPC 系统中,gRPC 是基于以下思路,定义一个服务接口,指定可以远程调用的方法,机器参数和返回类型;在服务端,实现这个接口,并且运行一个 gRPC 服务,来处理客户端的请求;在客户端,保存一个存根(stub,在一些语言中简称为客户端)提供和服务端相同的方法。
默认情况下,gRPC 使用 Protocol Buffers (Protobuffer,PB,谷歌开源的且成熟的序列化结构化数据的机制/工具)。使用 PB ,首先需要在 proto 文件中定义一个需要序列化的结构。
1 | message Person { |
如果需要定义一个 Service。
1 | // file: greeter.proto |
如下命令。
1 | protoc --java_out=jv greeter.proto |
并不会生成用于 RPC 的 client 和 server,需要使用如下命令。
1 | protoc --java_out=jv --grpc-java_out=jv greeter.proto |
在使用之前,需要安装工具 protoc-gen-grpc-java
,工具下载 / 本地编译步骤参考这里。至于下载,可以移步 这里,选择一个版本并点击链接,在介绍表格部分有 Files 字段,点击 View all
,现在指定平台的可执行文件即可,然后运行如下命令。
1 | protoc --plugin=protoc-gen-grpc-java=/Users/wii/Downloads/protoc-gen-grpc-java-1.38.0-osx-x86_64.exe --grpc-java_out=jv --java_out=jv greeter.proto |
注意 指定 plugin 时,使用决定路径。
查看生成结果。
1 | ls jv |
gRPC 默认使用 Protocol Buffers 作为服务定义语言,如果需要也可以使用其他可选项。
1 | service HelloService { |
gRPC 允许定义多种类型的服务方法。
一元 RPCs(Unary RPCs),客户端发送一次请求,得到一次响应,和通常的方法调用类似
1 | rpc SayHello(HelloRequest) returns (HelloResponse); |
服务端流 RPCs(Server streaming RPCs),客户端向服务端发送一次请求,得到一个流来读取一系列的消息。客户端从返回的流中读取数据,直到没有更多信息。gRPC 保证一次独立调用中消息的顺序
1 | rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse); |
客户端流 RPCs(Client streaming RPCs ),客户端使用流,向服务端发送一系列的消息。客户端结束写入后,等待服务端读取并返回一个响应。同样,gRPC 保证一次独立调用中消息的顺序
1 | rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse); |
双向流 RPCs(Bidirectional streaming RPCs),客户端和服务端通过读写流发送消息序列,两个流操作独立,所以客户端和服务端可以以任意顺序读写。比如,服务端可以等待接受到所有消息后返回响应消息序列;也可以每接受一个消息,返回一个响应消息。
1 | rpc BidiHello(stream HelloRequest) returns (stream HelloResponse); |
首先,在 .proto
文件中定义服务(service),gRPC 提供 PB(protocol buffer)编译插件,来生成客户端和服务端代码。gRPC 用户通常在客户端调用这些API,在服务端实现这些 API。
gRPC 在大部分语言中,具备异步和同步风格的接口。
server streaming RPC 和 unary RPC 类似,不同的是服务端在响应中返回一个消息流(a stream of message)。在所有的消息发送完后,详细的状态码和可选的尾部 metadata 会被传送给客户端。
服务端返回单个消息的实际,通常是接受到所有消息后,但并非必须如此。
双向流RPC,服务端接受到的客户端的 metadata、请求方法、deadline。
gRPC 允许客户端指定超时时间。服务端可以知道一个指定 RPC 调用是否超时,或者剩余处理时间。
可能存在服务端成功返回,但是客户端请求超时的情况。也有可能,服务端在客户端还在发送请求时来结束本次RPC。
服务端和客户端都可以在任意时间取消一次 RPC 请求。
Metadata 包含一次特定 RPC 请求的信息,数据格式是 k-v 对的形式,key为string,value可以是string,也有可能是二进制数据。
一个 gRPC 通道提供一个和指定host和port的 gRPC 服务器的连接,当创建一个客户端 stub 时会用到。客户端可以指定通道参数,来修改 gPRC 的默认行为,比如控制消息压缩开关。一个 channel 包含状态,包括 connected
和 idle
。
gRPC 怎么处理关闭 channel 的流程,语言间有所不同。
关于 gRPC 的负载均衡,这里 列举了主要的几种方式,胖客户端、Lookaside LB、Proxy。