Fix #2146 E112 EHOSTDOWN in short and pooled connection
What problem does this PR solve?
Issue Number:
Problem Summary: 以下引起E112错误之后无法自行恢复的几种场景:
- 网络抖动后遗症,一直出现E112无法自行恢复 配置dns访问外网,dns只解析出一个ip,然后网络抖动过程中出现少量连接失败,返回错误码为ECONNREFUSED或ENETUNREACH或EHOSTUNREACH或EINVAL,满足does_error_affect_main_socket从而触发main socket SetFailed。 但是正常来说,SetFailed之后过100ms就开始健康检查,每3秒重试一次。所以按理当网格抖动恢复后,最长3秒后,就不应该再出现E112了。但是实际上仍有E112持续出现,如果重启进程,则不会再出现。
- 以single server模式初始化channel,第一次连接失败之后socket被SetFailed,后面重试就都选不出服务了。
- 出现一个ETIMEOUT之后,就一直出现EHOSTDOWN,没有恢复。
What is changed and the side effects?
Changed: 目前针对各种原因导致的E112错误,提供一套通用的解决方案: 一、防止实例封禁导致的E112:在对实例进行封禁的时候,加上是否为唯一可用实例判断
- 如果是SingleServer模式(即直接以ip+port方式初始化Channel,不通过NamingService+LB),则只有唯一一个实例,所以不做封禁
- 如果是集群模式(NamingService+LB方式初始化Channel),则尝试以排除当前实例的条件下SelectServer,如果这种条件下选不出实例,或者选出实例仍为当前实例,则说明当前实例为LB内部唯一可用的实例,在这种情况下不做封禁 二、防止重试去重或LB调权导致的E112:在选择实例的时候,增加一次额外的尝试
- 即使LB内部存在可用的实例,也有可能因为重试去重或LB调权的原因无法选中。解决方法为在选择实例的时候,如果返回E112,则改变选择条件:excluded_server=NULL, changable_weights=false,以这种条件重新选择,如果LB内部存在可用实例,则一定能够选择出实例。
注意:以上方案仅对连接池和短连接模式下生效,单连接的情况下,main socket和rpc实际通信的socket是同一个,通信过程可能有各种原因导致socket被SetFail,从而导致main socket失效,无法被LB选择。解决该问题的根本办法是将单连接的main socket和rpc通信的socket进行拆分(有点类似于只有一个连接的连接池),当rpc通信socket SetFailed之后,可以从main socket新建新的socket进行连接,但这个方案对brpc的改动较大。
Side effects:
-
Performance effects(性能影响):
-
Breaking backward compatibility(向后兼容性):
Check List:
- Please make sure your changes are compilable(请确保你的更改可以通过编译).
- When providing us with a new feature, it is best to add related tests(如果你向我们增加一个新的功能, 请添加相关测试).
- Please follow Contributor Covenant Code of Conduct.(请遵循贡献者准则).
CI单测失败了
CI单测失败了
嗯嗯。。 我先在本地调试一下。
@Huixxi @wwbmmm 是不是有关于lb E61的优化,可以提个PR吗?
@Huixxi @wwbmmm 是不是有关于lb E61的优化,可以提个PR吗?
有,那个是另一个问题,有空提下
hello @Huixxi @wwbmmm @serverglen 这个 pr 是还存在什么问题吗?
SetFailed之后过100ms就开始健康检查,每3秒重试一次。所以按理当网格抖动恢复后,最长3秒后,就不应该再出现E112了。但是实际上仍有E112持续出现
请教下,为什么健康检查后仍然无法解决 E112 问题?
hello @Huixxi @wwbmmm @serverglen 这个 pr 是还存在什么问题吗?
这个pr当时导致了一些关键单测绕不过去,阻塞无法合入,可以参考下这里面的逻辑实现。
hello @Huixxi @wwbmmm @serverglen 这个 pr 是还存在什么问题吗?
这个pr当时导致了一些关键单测绕不过去,阻塞无法合入,可以参考下这里面的逻辑实现。
好的,请教一下大佬,为什么健康检查无法解决 E112 问题?
E112我们遇到两次了,都是大故障。有点 想换掉brpc了。这个问题不解决,后面不敢再用了
后端明明没压力,就是提示E112 或者 没有服务可以选
@Huixxi 请问下,这种解决方案,对POOL这种方式可行么 从代码看,就是当没有可用的时候,就从没有通过健康检测的中间取一个节点来使用
@Huixxi 能给解释一下,当前的代码是因为bug还是因为设计上导致的E112无法恢复吗?想知道根音,然后目前的方案是解决设计问题,还是做一个兜底。
E112我们遇到两次了,都是大故障。有点 想换掉brpc了。这个问题不解决,后面不敢再用了
后端明明没压力,就是提示E112 或者 没有服务可以选
@Huixxi 请问下,这种解决方案,对POOL这种方式可行么 从代码看,就是当没有可用的时候,就从没有通过健康检测的中间取一个节点来使用
当时的实现,对连接池这种方案是可行的,可以先参考这部分在内部代码做一个patch修复。
@Huixxi 能给解释一下,当前的代码是因为bug还是因为设计上导致的E112无法恢复吗?想知道根音,然后目前的方案是解决设计问题,还是做一个兜底。
这个是当时把冰哥的内部patch搬运了出来,我理解还是设计上的问题,估计要彻底解决得大改,这个pr还只是个兜底在连接池和长连接的场景下。
@Huixxi 能给解释一下,当前的代码是因为bug还是因为设计上导致的E112无法恢复吗?想知道根音,然后目前的方案是解决设计问题,还是做一个兜底。
这个是当时把冰哥的内部patch搬运了出来,我理解还是设计上的问题,估计要彻底解决得大改,这个pr还只是个兜底在连接池和长连接的场景下。
收到
这个PR应该还是有作用的,至少百度内部在这样升级之后,E112相关的问题反馈少了很多。只是遗留了个工作,就是处理单连接的情况,因为改动太大当时没有做。
@Huixxi 能给解释一下,当前的代码是因为bug还是因为设计上导致的E112无法恢复吗?想知道根音,然后目前的方案是解决设计问题,还是做一个兜底。
我计较好奇“E112无法恢复”的原因。我理解上“E112无法恢复”有两种场景:
- 健康检查一直失败;
- E112后经健康检查恢复后,又出现E112,一直反复。
那通过健康检查来兜底可行吗?或者使用熔断策略,或者tcp keepalive这些能避免这个问题吗?
那通过健康检查来兜底可行吗?或者使用熔断策略,或者tcp keepalive这些能避免这个问题吗?
现在问题就是熔断导致没有可用的实例吧。所有实例不可用,都在做健康检查,所以导致E112失败。
在RPC的角度,在所有实例都不可用的时候,相比直接E112失败,还不如选一个实例进行通信,还有成功的机会。
但是我觉得该PR在集群模式下,只保留了一个实例来接受所有流量,在负载均衡方面不太友好。
解决该问题的根本办法是将单连接的main socket和rpc通信的socket进行拆分(有点类似于只有一个连接的连接池),当rpc通信socket SetFailed之后,可以从main socket新建新的socket进行连接,但这个方案对brpc的改动较大。
我思考过这个方案,改动很大且复杂。
或许可以从LB的角度来实现,ServerId增加EndPoint字段,在E112期间构造出备用Socket(管理策略得再详细设计一下)来进行通信了,这样实现会简单很多。
LB选实例的时候,记录下第一次选到的实例EndPoint,用于E112的时候通信。另外,在E112期间,也能保持一定的负载均衡。更近一步,或许可以参考Envoy的恐慌策略,当不可用实例比例超过阈值,使能恐慌策略。但是Envoy的连接的实现跟bRPC Socket的实现不一样,所以恐慌策略不一定合适,得做一些权衡。
@wwbmmm 有空看看这个方案可行性?
@Huixxi 能给解释一下,当前的代码是因为bug还是因为设计上导致的E112无法恢复吗?想知道根音,然后目前的方案是解决设计问题,还是做一个兜底。
我计较好奇“E112无法恢复”的原因。我理解上“E112无法恢复”有两种场景:
- 健康检查一直失败;
- E112后经健康检查恢复后,又出现E112,一直反复。
补充一下,还有一种情况是健康检查流程没有正常启动,即Socket在SetFail之后引用计数仍然一直>2,WaitAndReset陷入死循环。我记得之前有PR修复过Socket引用计数的问题,但不确认是否完全解决了。 还有一种情况是连接时如果连续3次Connect timeout,brpc会把错误码标记成E112,但其实不一定是Server端的问题,比如Client端自身问题也有可能导致Connect timeout,这种情况下就导致Server被误屏蔽了。
解决该问题的根本办法是将单连接的main socket和rpc通信的socket进行拆分(有点类似于只有一个连接的连接池),当rpc通信socket SetFailed之后,可以从main socket新建新的socket进行连接,但这个方案对brpc的改动较大。
我思考过这个方案,改动很大且复杂。
或许可以从LB的角度来实现,ServerId增加EndPoint字段,在E112期间构造出备用Socket(管理策略得再详细设计一下)来进行通信了,这样实现会简单很多。
LB选实例的时候,记录下第一次选到的实例EndPoint,用于E112的时候通信。另外,在E112期间,也能保持一定的负载均衡。更近一步,或许可以参考Envoy的恐慌策略,当不可用实例比例超过阈值,使能恐慌策略。但是Envoy的连接的实现跟bRPC Socket的实现不一样,所以恐慌策略不一定合适,得做一些权衡。
@wwbmmm 有空看看这个方案可行性?
嗯,其实引入恐慌策略是比较合理的,只是因为brpc把Socket和LB耦合在一起了,导致实现起来比较麻烦。
或许可以从LB的角度来实现,ServerId增加EndPoint字段,在E112期间构造出备用Socket(管理策略得再详细设计一下)来进行通信了,这样实现会简单很多。
好像从已有的Failed的Socket上也能够获取到EndPoint字段? 我觉得关键问题在于,需要区分Socket的连接状态和屏蔽状态,LB看的是屏蔽状态,实际连接的时候,再看连接状态,如果连接状态是Failed时再启动备用连接之类的。
好像从已有的Failed的Socket上也能够获取到EndPoint字段?
可以使用AddressFailedAsWell。
需要区分Socket的连接状态和屏蔽状态,LB看的是屏蔽状态,实际连接的时候,再看连接状态,如果连接状态是Failed时再启动备用连接之类的。
目前LB将会屏蔽Failed连接状态的Socket,除此之外,还会屏蔽ExcludedServers和Socket::IsAvailable(目前只有logff是unavailable)的Socket。
bRPC的恐慌策略可以这样设计:保持LB屏蔽策略不变,当LB选不出实例的时候,不返回E112,而是使用第一次选到实例的信息构造出一个备用Socket用于通信。在E112期间,负载均衡也是生效的,开销也比较小,只是多一次创建Socket的开销,这个Socket在恐慌周期内是可以复用的,完全可接受。
跟Envoy的可调整的恐慌阈值不同,bRPC的恐慌阈值为100%,是这样考虑的:虽然Envoy区分连接状态和屏蔽状态,但是在LB处只看到屏蔽状态。触发恐慌的时候(阈值小于100%),会跟一些完全不可用的实例通信(连接错误码是ECONNREFUSED、ENETUNREACH、EHOSTUNREACH),这是没有意义,会浪费一次通信机会。
@wwbmmm 有什么建议吗?
bRPC的恐慌策略可以这样设计:保持LB屏蔽策略不变,当LB选不出实例的时候,不返回E112,而是使用第一次选到实例的信息构造出一个备用Socket用于通信。在E112期间,负载均衡也是生效的,开销也比较小,只是多一次创建Socket的开销,这个Socket在恐慌周期内是可以复用的,完全可接受。
备用Socket这里还有个问题,采用什么连接策略?比如原来是单连接,那么备用Socket也是单连接,还是说退化成一个临时的短连接?如果备用Socket也是单连接,管理起来比较复杂,如果退化成短连接,可能会导致性能下降或给后端带来太大压力?
跟Envoy的可调整的恐慌阈值不同,bRPC的恐慌阈值为100%,是这样考虑的:虽然Envoy区分连接状态和屏蔽状态,但是在LB处只看到屏蔽状态。触发恐慌的时候(阈值小于100%),会跟一些完全不可用的实例通信(连接错误码是ECONNREFUSED、ENETUNREACH、EHOSTUNREACH),这是没有意义,会浪费一次通信机会。
怎么判断实例是完全不可用呢?比如某一次连接ECONNREFUSED,后面再连有可能就成功了
我在想能不能把SocketPool的逻辑稍微改一下,变成SingleSocketPool:
GetSocket的逻辑: 如果pool里能找到不是Failed状态的Socket,就返回该Socket,且不从pool中删除 如果找到Failed状态的Socket,就从pool中删除 如果找不到可用的Socket,就新建一个Socket,并加入到Pool中,然后返回该Socket。
RPC连接的时候,对于单连接的情况,不再使用main socket进行交互,而是从main socket的SingleSocketPool中获取Socket。
这样就是main socket的Failed状态表示屏蔽状态,和LB的现有逻辑兼容,然后socket pool中的socket的状态表示连接状态。
备用Socket这里还有个问题,采用什么连接策略?比如原来是单连接,那么备用Socket也是单连接,还是说退化成一个临时的短连接?如果备用Socket也是单连接,管理起来比较复杂,如果退化成短连接,可能会导致性能下降或给后端带来太大压力?
备用Socket还是通过SelectOut::ptr返回给调用方。调用方将其当做main socket用。连接策略是由调用方使用方式决定即可。在调用方看来,备用Socket应该跟naming service下发的Socket并无区别。应该可以加多一个备用Socket的SocketMap,相同Channel不用LB复用Socket,连接数不会增加很多,不会给后端带来太大压力。
相比之下,SingleSocketPool的方案更好,将Socket管理逻辑收敛到main socket中,LB只需要实现恐慌策略,简化LB的实现逻辑,这样对于用户定义的LB更友好。
怎么判断实例是完全不可用呢?比如某一次连接ECONNREFUSED,后面再连有可能就成功了
Failed Socket在健康检查成功之前可以看作是不可用吧。如果是能连成功的话,健康检查成功,就Socket就会恢复了。
SingleSocketPool的方案将屏蔽状态和连接状态区分开来,更清晰了。
我在想能不能把SocketPool的逻辑稍微改一下,变成SingleSocketPool:
GetSocket的逻辑: 如果pool里能找到不是Failed状态的Socket,就返回该Socket,且不从pool中删除 如果找到Failed状态的Socket,就从pool中删除 如果找不到可用的Socket,就新建一个Socket,并加入到Pool中,然后返回该Socket。
RPC连接的时候,对于单连接的情况,不再使用main socket进行交互,而是从main socket的SingleSocketPool中获取Socket。
这样就是main socket的Failed状态表示屏蔽状态,和LB的现有逻辑兼容,然后socket pool中的socket的状态表示连接状态。
这个方案看起来可行,可以将SocketPool(或许得改了名字,不应该叫SocketPool了)抽象成基类,各个连接模式(single、pooled、short和待支持的multi #536)实现对应的SocketPool。或许single跟目前的short一样,单独实现一个函数即可。
以下是当前的方案:
- 抽象SocketPool为基类,各个连接模式(single、pooled、short和待支持的multi)实现对应的SocketPool。main socket的状态是屏蔽状态,SocketPool的sub socket是连接状态。sub socket的一些Failed连接状态会使得main socket屏蔽状态变成Failed。
- 正常模式下,LB屏蔽Failed main socket。当全部main socket都是Failed,返回第一次选到实例的main socket。
- RPC AddressFailedAsWell(main socket)后,根据连接模式,获取sub socket进行通信。
@wwbmmm 还有什么补充的吗?
这个方案看起来可行,可以将SocketPool(或许得改了名字,不应该叫SocketPool了)抽象成基类,各个连接模式(single、pooled、short和待支持的multi #536)实现对应的SocketPool。或许single跟目前的short一样,单独实现一个函数即可。
以下是当前的方案:
- 抽象SocketPool为基类,各个连接模式(single、pooled、short和待支持的multi)实现对应的SocketPool。main socket的状态是屏蔽状态,SocketPool的sub socket是连接状态。sub socket的一些Failed连接状态会使得main socket屏蔽状态变成Failed。
- 正常模式下,LB屏蔽Failed main socket。当全部main socket都是Failed,返回第一次选到实例的main socket。
- RPC AddressFailedAsWell(main socket)后,根据连接模式,获取sub socket进行通信。
看起来没问题
备用Socket这里还有个问题,采用什么连接策略?比如原来是单连接,那么备用Socket也是单连接,还是说退化成一个临时的短连接?如果备用Socket也是单连接,管理起来比较复杂,如果退化成短连接,可能会导致性能下降或给后端带来太大压力?
备用Socket还是通过SelectOut::ptr返回给调用方。调用方将其当做main socket用。连接策略是由调用方使用方式决定即可。在调用方看来,备用Socket应该跟naming service下发的Socket并无区别。应该可以加多一个备用Socket的SocketMap,相同Channel不用LB复用Socket,连接数不会增加很多,不会给后端带来太大压力。
相比之下,SingleSocketPool的方案更好,将Socket管理逻辑收敛到main socket中,LB只需要实现恐慌策略,简化LB的实现逻辑,这样对于用户定义的LB更友好。
怎么判断实例是完全不可用呢?比如某一次连接ECONNREFUSED,后面再连有可能就成功了
Failed Socket在健康检查成功之前可以看作是不可用吧。如果是能连成功的话,健康检查成功,就Socket就会恢复了。
SingleSocketPool的方案将屏蔽状态和连接状态区分开来,更清晰了。
我在想能不能把SocketPool的逻辑稍微改一下,变成SingleSocketPool: GetSocket的逻辑: 如果pool里能找到不是Failed状态的Socket,就返回该Socket,且不从pool中删除 如果找到Failed状态的Socket,就从pool中删除 如果找不到可用的Socket,就新建一个Socket,并加入到Pool中,然后返回该Socket。 RPC连接的时候,对于单连接的情况,不再使用main socket进行交互,而是从main socket的SingleSocketPool中获取Socket。 这样就是main socket的Failed状态表示屏蔽状态,和LB的现有逻辑兼容,然后socket pool中的socket的状态表示连接状态。
这个方案看起来可行,可以将SocketPool(或许得改了名字,不应该叫SocketPool了)抽象成基类,各个连接模式(single、pooled、short和待支持的multi #536)实现对应的SocketPool。或许single跟目前的short一样,单独实现一个函数即可。
以下是当前的方案:
- 抽象SocketPool为基类,各个连接模式(single、pooled、short和待支持的multi)实现对应的SocketPool。main socket的状态是屏蔽状态,SocketPool的sub socket是连接状态。sub socket的一些Failed连接状态会使得main socket屏蔽状态变成Failed。
- 正常模式下,LB屏蔽Failed main socket。当全部main socket都是Failed,返回第一次选到实例的main socket。
- RPC AddressFailedAsWell(main socket)后,根据连接模式,获取sub socket进行通信。
@wwbmmm 还有什么补充的吗?
这个方案的pr啥时候提出了?
这个方案的pr啥时候提出了?
目前写了个大概,还需要再完善一下和补充UT。尽量下个版本支持恐慌策略。
这个方案的pr啥时候提出了?
目前写了个大概,还需要再完善一下和补充UT。尽量下个版本支持恐慌策略。
👍,这个就相当于把遗留的一个bug就完全解决了吧。
这个方案的pr啥时候提出了?
目前写了个大概,还需要再完善一下和补充UT。尽量下个版本支持恐慌策略。
期待,补充个case,单连接的情况下服务端重启过程中被kill -9杀掉,socket被置为logoff状态,这时候健康检查不会启动。猜测由于网络波动,客户端没有收到RST包留下了一个僵尸连接,导致永远无法重连。暂时用tcp keepalive绕过去了