Foreword
请先了解
本章内容针对DubboProtocol
.
连接数
默认情况下, 一个Consumer和一个指定的Provider之间只会建立一个连接(无论两者之间有多少个服务调用).
在Linux平台中, 可以通过netstat
命令查看两者之间的连接. 下方栗子为在同一机器上运行Provider, Consumer的结果:
1 | tcp6 0 0 :::48707 :::* LISTEN 32753/java -> Provider |
服务端的端口是配置中指定的, 而Consumer端开启的端口是一个随机的临时端口. 如果有同一机器上有多个Dubbo Consumer和Provider实例的情况下, 偶尔会遇到端口被占用的情况, 即为临时端口的缘故.
连接的建立
连接的建立和前文的ProtocolFilterWrapper.buildInvokerChain
在同一链路上. 起于DubboProtocol.refer
中, 其中调用了getClients()
, 获取到了ExchangeClient[]
, 用来实例化DubboInvoker
:
1 | public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException { |
getClients
方法注释中有说明:
If not configured, connection is shared, otherwise, one connection for one service
当然了, 即一个Provider的一个服务建立一个连接. DubboProtocol
中通过referenceClientMap
来持有共享的ReferenceCountExchangeClient
实例, key为URL
如10.1.1.1:20880
. 默认情况下一个URL只会调用getSharedClient
获取一个共享的实例, 如果用户有配置connections
, 则会调用多次initClient
, 建立多个连接.
更多: ReferenceCountExchangeClient, ghostClientMap
延迟连接
继续观察initClient
方法:
1 | if (url.getParameter(Constants.LAZY_CONNECT_KEY, false)) { |
可以看到如果配置了lazy = true
, 则实际上并没有直接建立连接, 而是返回LazyConnectExchangeClient
. 它也是ExchangeClient
的实现类. 实现延迟的方式实际上只是覆盖request
,send
方法, 在其中调用initClient
, 判断client
为空才建立连接. 建立连接也是调用上文代码else
分支的这一句: client = Exchangers.connect(url, requestHandler);
从Exchangers
到具体连接建立
1 | Exchangers.connect(URL, ExchangeHandler) |
服务端连接控制
上面说明的都是IO的客户端内容, 服务端的连接控制acceptes
是在客户端与其连接建立之后发生的:
1 | NettyHandler.channelConnnected |
其中直接判断, 如果连接数量大于指定数量, 即将连接关闭:
1 | if (accepts > 0 && channels.size() > accepts) { |
心跳
Dubbo中有实现应用层的心跳机制. 在Consumer(HeaderExchangeClient
)端和Provider(HeaderExchangeServer
)端都会开启一个HeartBeatTask
.
Consumer端如下:
1 | DubboProtocol.refer |
而在Provider端export
和Consumer端refer
之后也都会实例化一个HeartbeatHandler
.
1 | DubboProtocol.export |
整体流程
- 观察
HeartbeatHandler
, 除了received
方法之外的其他方法, 基本只是额外加了对于最后读写时间的记录. - 两端都会开启
HeartbeatTask
, 默认每隔一分钟(heartbeat
)执行一次. 运行时, 对所有活跃的渠道遍历- 如果最后读取写时间距今超过一分钟(
heartbeat
), 则向Channel
中发送一个event
为Request.HEARTBEAT_EVENT
的Request
. - 如果最后读取时间距今超过三分钟((
heartbeat timeout
), 则如果当前实例为Consumer, 则尝试重新连接, 如果为Provider端, 则关闭连接.
- 如果最后读取写时间距今超过一分钟(
- 继续看
HeartbeatHandler.recieved
- 如果收到心跳请求, 则回复一个心跳响应.
- 如果收到心跳响应, 则直接忽略.
值得注意的是, 由于Consumer和Provider端都开启了心跳调度线程, 所以在空闲时哪端先发起心跳是不确定的.
心跳的作用
文档中有对heartbeat
属性做简要说明:
心跳间隔,对于长连接,当物理层断开时,比如拔网线,TCP的FIN消息来不及发送,对方收不到断开事件,此时需要心跳来帮助检查连接是否已断开
其中已经说明了FIN消息的不可靠性, 不仅拔网线, 系统层次的崩溃也会导致FIN消息无法发送. 如果有尝试写过Netty的channelInactive
方法Demo, 可能会发现, 突然断网是无法触发的.
那么协议层的keepAlive
呢? 翻过的资料相比应用层的心跳缺点如下:
- 心跳的作用不止是了解两端是否连接活跃, 更重要的是判断对方功能是否正常.
- TCP协议的keepalive不能穿过代理服务器(TCP keepalive would only check the connection up to the proxy and not the end-to-end connection).
综合上面两点看来, 心跳的用处是判断对方是否能够提供正常功能. 而实际落实到Dubbo的语境下, 似乎不止于此.
// TODO 没有ZK的情况下, 实际上拔线或者kill程序, API端并没有立即反应, 等到调用时才报错.
Netty Configuration
针对老版本Netty, 并非Netty4, 但实现类似.
NettyServer
和NettyClient
的bossExecutor
和workerExecutor
都是Executors.newCachedThreadPool
.
然而对于Netty线程模型并不熟悉, 不能对这一点发表更多意见. - -||
NettyClient
此外NettyClient
还有一些额外配置 :
- `keepAlive`: 上面已经提到过
- `connectTimeoutMillis`
- `tcpNoDelay`: 防止消息没有及时发送. 详见下方参考资料.