sofa-rpc
sofa-rpc copied to clipboard
AllConnectConnectionHolder心跳重试线程优化讨论
Your question
Consumer中,ConnectConnectionHolder负责管理所有provider的长连接。 其中,com.alipay.sofa.rpc.client.AllConnectConnectionHolder#startReconnectThread 函数,会启动一个异步线程,每隔一段时间去检查的长连接是否健康。
如果AllConnectConnectionHolder中的aliveConnections(存活的客户端列表)无法连接,就会将该长连接从aliveConnections中移除,加入到retryConnections(失败待重试的客户端列表)中,假如retryConnections中的连接恢复正常,会把该长连接从retryConnections中移除,加回至 aliveConnections中。 这段逻辑见:com.alipay.sofa.rpc.client.AllConnectConnectionHolder#doReconnect 函数。
private void doReconnect() {
// 省略代码
// 检查可用连接
for (Map.Entry<ProviderInfo, ClientTransport> alive : aliveConnections.entrySet()) {
ClientTransport connection = alive.getValue();
if (connection != null && !connection.isAvailable()) {
// 加入至retryConnections
aliveToRetry(alive.getKey(), connection);
}
}
// 检查失败待重试的客户端列表
for (Map.Entry<ProviderInfo, ClientTransport> entry : getRetryConnections()
.entrySet()) {
// 省略代码。。
transport.connect();
}
}
如果retryConnection一直没连上, transport.connect();函数底层会调用至com/alipay/sofa/rpc/transport/bolt/ReuseBoltClientConnectionManager#getConnection方法,该方法会打印warn日志: get connection failed in url
正常情况,很难去执行到AllConnectConnectionHolder的重试线程,因为provider从注册中心下线,注册中心会向consumer主动推送,consumer会去摘除该长连接。
但我们在实战中,有两个场景碰到了此类问题,会导致以上get connection failed in url的warn日志疯狂打印,造成困扰。
Your scenes
目前,我们服务都是以容器化部署,若宿主机的资源不足,在重启provider服务的时候,原provider的容器ip可能会发生变化。
情况1:
假如consumer没收到注册中心老provider ip下线节点的通知,就会复现get connection failed in url 疯狂打印的场景。 补充说明:因为宿主机ip分配问题,老的provider ip已经永远消失,provider是以一个新的ip注册到注册中心,但老的provider ip仍一直保留在AllConnectConnectionHolder中的retryConnections中,所以会导致warn日志疯狂打印。
情况2:
注册中心摘除provider节点代码设计不合理。
背景:灰度环境和测试环境集群只有一台provider
通过分析源码,定位到consumer摘除provider下线节点的代码在:
com.alipay.sofa.rpc.client.AbstractCluster#updateProviders 函数。
但是该函数做了一个判断:
public void updateProviders(ProviderGroup providerGroup) {
// 代码省略。。。
if (ProviderHelper.isEmpty(providerGroup)) {
addressHolder.updateProviders(providerGroup);
if (!ProviderHelper.isEmpty(oldProviderGroup)) {
if (LOGGER.isWarnEnabled(consumerConfig.getAppName())) {
LOGGER.warnWithApp(consumerConfig.getAppName(), "Provider list is emptied, may be all " +
"providers has been closed, or this consumer has been add to blacklist");
closeTransports();
}
}
} else {
addressHolder.updateProviders(providerGroup);
connectionHolder.updateProviders(providerGroup);
}
// 代码省略。。。
}
}
如果provider中一个节点都没有,只做了 addressHolder.updateProviders(providerGroup);并没有去做 connectionHolder.updateProviders(providerGroup); 而connectionHolder.updateProviders(providerGroup); 底层是会把下线节点从aliveConnections或者retryConnections中删除。 删除的代码在:AllConnectConnectionHolder#remove函数中。
这样会导致,即使consumer收到了provider下线的通知,因为provider在灰度和测试环境只有一台机器,provider重启过程中,就命中了ProviderHelper.isEmpty(providerGroup) 分支,并不会去执行connectionHolder.updateProviders(providerGroup);函数。 该下线的IP一直还在 AllConnectConnectionHolder中的retryConnections属性中。
假如灰度环境或者测试环境的provider ip发生了变化,但老的provider ip仍一直保留在AllConnectConnectionHolder中的retryConnections中,也会导致warn日志疯狂打印。
Your advice
1、建议com.alipay.sofa.rpc.client.AllConnectConnectionHolder#startReconnectThread 机制加上最大重试次数,若失败次数超过x.则将该连接彻底移除。
2、没懂为何provider一台机器没有的情况下,不去执行connectionHolder.updateProviders(providerGroup)函数,希望作者可以解释下如此设计的原因。
Environment
- SOFARPC version:5.6
- jdk 8 -windows/mac
- Maven version:3.9
- IDE version:2024/01