Fix #2146 E112 EHOSTDOWN in short and pooled connection#2177
Fix #2146 E112 EHOSTDOWN in short and pooled connection#2177Huixxi wants to merge 5 commits intoapache:masterfrom
Conversation
test/brpc_channel_unittest.cpp
Outdated
| MyEchoService _svc; | ||
| }; | ||
|
|
||
| void TestBlockServer(bool single_server, bool short_connection, const char* lb) { |
There was a problem hiding this comment.
TestBlockServer 应该 是ChannelTest 的成员函数。
There was a problem hiding this comment.
TestBlockServer 应该 是ChannelTest 的成员函数。
哦哦 知道了
| } | ||
|
|
||
| inline void maybe_block_server(int error_code, Controller* cntl, SharedLoadBalancer* lb, SocketId sock) { | ||
| if (!does_error_affect_main_socket(error_code)) { |
|
CI单测失败了 |
嗯嗯。。 我先在本地调试一下。 |
| for (int i = 0; i < 4; ++i) { | ||
| // although ::connect would succeed, the stall in hc_service makes | ||
| // the health check rpc fail. | ||
| // the health check rpc overtime but not fail. |
| @@ -1070,8 +1074,9 @@ TEST_F(LoadBalancerTest, revived_from_all_failed_intergrated) { | |||
| // all servers are down, the very first call that trigger recovering would | |||
| does_error_affect_main_socket(error_code) && | ||
| (sending_sock == NULL || sending_sock->id() != peer_id)) { | ||
| Socket::SetFailed(peer_id); | ||
| maybe_block_server(error_code, c, c->_lb.get(), peer_id); |
There was a problem hiding this comment.
实现是"block socket"不是block server". 或许叫checkAndSetFailed更明确一点?
| // main socket should die as well | ||
| // NOTE: main socket may be wrongly set failed (provided that | ||
| // short/pooled socket does not hold a ref of the main socket). | ||
| // E.g. a in-parallel RPC sets the peer_id to be failed |
| } | ||
|
|
||
| inline void maybe_block_server(int error_code, Controller* cntl, SharedLoadBalancer* lb, SocketId sock) { | ||
| if (!does_error_affect_main_socket(error_code)) { |
There was a problem hiding this comment.
为什么要把does_error_affect_main_socket放到这个函数里?
There was a problem hiding this comment.
因为这个函数最终的动作是SetFailed main socket,所以要先检查一下错误号是否满足影响main socket 的条件
|
hello @Huixxi @wwbmmm @serverglen 这个 pr 是还存在什么问题吗? |
请教下,为什么健康检查后仍然无法解决 E112 问题? |
这个pr当时导致了一些关键单测绕不过去,阻塞无法合入,可以参考下这里面的逻辑实现。 |
好的,请教一下大佬,为什么健康检查无法解决 E112 问题? |
|
E112我们遇到两次了,都是大故障。有点 想换掉brpc了。这个问题不解决,后面不敢再用了 后端明明没压力,就是提示E112 或者 没有服务可以选 @Huixxi 请问下,这种解决方案,对POOL这种方式可行么 |
|
@Huixxi 能给解释一下,当前的代码是因为bug还是因为设计上导致的E112无法恢复吗?想知道根音,然后目前的方案是解决设计问题,还是做一个兜底。 |
当时的实现,对连接池这种方案是可行的,可以先参考这部分在内部代码做一个patch修复。 |
这个是当时把冰哥的内部patch搬运了出来,我理解还是设计上的问题,估计要彻底解决得大改,这个pr还只是个兜底在连接池和长连接的场景下。 |
收到 |
|
这个PR应该还是有作用的,至少百度内部在这样升级之后,E112相关的问题反馈少了很多。只是遗留了个工作,就是处理单连接的情况,因为改动太大当时没有做。 |
我计较好奇“E112无法恢复”的原因。我理解上“E112无法恢复”有两种场景:
|
|
那通过健康检查来兜底可行吗?或者使用熔断策略,或者tcp keepalive这些能避免这个问题吗? |
现在问题就是熔断导致没有可用的实例吧。所有实例不可用,都在做健康检查,所以导致E112失败。 在RPC的角度,在所有实例都不可用的时候,相比直接E112失败,还不如选一个实例进行通信,还有成功的机会。 但是我觉得该PR在集群模式下,只保留了一个实例来接受所有流量,在负载均衡方面不太友好。 |
我思考过这个方案,改动很大且复杂。 或许可以从LB的角度来实现,ServerId增加EndPoint字段,在E112期间构造出备用Socket(管理策略得再详细设计一下)来进行通信了,这样实现会简单很多。 LB选实例的时候,记录下第一次选到的实例EndPoint,用于E112的时候通信。另外,在E112期间,也能保持一定的负载均衡。更近一步,或许可以参考Envoy的恐慌策略,当不可用实例比例超过阈值,使能恐慌策略。但是Envoy的连接的实现跟bRPC Socket的实现不一样,所以恐慌策略不一定合适,得做一些权衡。 @wwbmmm 有空看看这个方案可行性? |
补充一下,还有一种情况是健康检查流程没有正常启动,即Socket在SetFail之后引用计数仍然一直>2,WaitAndReset陷入死循环。我记得之前有PR修复过Socket引用计数的问题,但不确认是否完全解决了。 |
嗯,其实引入恐慌策略是比较合理的,只是因为brpc把Socket和LB耦合在一起了,导致实现起来比较麻烦。
好像从已有的Failed的Socket上也能够获取到EndPoint字段? |
可以使用AddressFailedAsWell。
目前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 有什么建议吗? |
备用Socket这里还有个问题,采用什么连接策略?比如原来是单连接,那么备用Socket也是单连接,还是说退化成一个临时的短连接?如果备用Socket也是单连接,管理起来比较复杂,如果退化成短连接,可能会导致性能下降或给后端带来太大压力?
怎么判断实例是完全不可用呢?比如某一次连接ECONNREFUSED,后面再连有可能就成功了 |
|
我在想能不能把SocketPool的逻辑稍微改一下,变成SingleSocketPool: GetSocket的逻辑: RPC连接的时候,对于单连接的情况,不再使用main socket进行交互,而是从main socket的SingleSocketPool中获取Socket。 这样就是main socket的Failed状态表示屏蔽状态,和LB的现有逻辑兼容,然后socket pool中的socket的状态表示连接状态。 |
备用Socket还是通过SelectOut::ptr返回给调用方。调用方将其当做main socket用。连接策略是由调用方使用方式决定即可。在调用方看来,备用Socket应该跟naming service下发的Socket并无区别。应该可以加多一个备用Socket的SocketMap,相同Channel不用LB复用Socket,连接数不会增加很多,不会给后端带来太大压力。 相比之下,SingleSocketPool的方案更好,将Socket管理逻辑收敛到main socket中,LB只需要实现恐慌策略,简化LB的实现逻辑,这样对于用户定义的LB更友好。
Failed Socket在健康检查成功之前可以看作是不可用吧。如果是能连成功的话,健康检查成功,就Socket就会恢复了。 SingleSocketPool的方案将屏蔽状态和连接状态区分开来,更清晰了。
这个方案看起来可行,可以将SocketPool(或许得改了名字,不应该叫SocketPool了)抽象成基类,各个连接模式(single、pooled、short和待支持的multi #536)实现对应的SocketPool。或许single跟目前的short一样,单独实现一个函数即可。 以下是当前的方案:
@wwbmmm 还有什么补充的吗? |
看起来没问题 |
这个方案的pr啥时候提出了? |
目前写了个大概,还需要再完善一下和补充UT。尽量下个版本支持恐慌策略。 |
👍,这个就相当于把遗留的一个bug就完全解决了吧。 |
期待,补充个case,单连接的情况下服务端重启过程中被kill -9杀掉,socket被置为logoff状态,这时候健康检查不会启动。猜测由于网络波动,客户端没有收到RST包留下了一个僵尸连接,导致永远无法重连。暂时用tcp keepalive绕过去了 |
What problem does this PR solve?
Issue Number:
Problem Summary:
以下引起E112错误之后无法自行恢复的几种场景:
配置dns访问外网,dns只解析出一个ip,然后网络抖动过程中出现少量连接失败,返回错误码为ECONNREFUSED或ENETUNREACH或EHOSTUNREACH或EINVAL,满足does_error_affect_main_socket从而触发main socket SetFailed。
但是正常来说,SetFailed之后过100ms就开始健康检查,每3秒重试一次。所以按理当网格抖动恢复后,最长3秒后,就不应该再出现E112了。但是实际上仍有E112持续出现,如果重启进程,则不会再出现。
What is changed and the side effects?
Changed:
目前针对各种原因导致的E112错误,提供一套通用的解决方案:
一、防止实例封禁导致的E112:在对实例进行封禁的时候,加上是否为唯一可用实例判断
二、防止重试去重或LB调权导致的E112:在选择实例的时候,增加一次额外的尝试
注意:以上方案仅对连接池和短连接模式下生效,单连接的情况下,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: