apache / dubbo-getty

a netty like asynchronous network I/O library based on tcp/udp/websocket; a bidirectional RPC framework based on JSON/Protobuf; a microservice framework based on zookeeper/etcd
Apache License 2.0
218 stars 69 forks source link

Is it correct to continue to maintain the connection pool when a connection encounters an error? #116

Closed No-SilverBullet closed 2 months ago

No-SilverBullet commented 3 months ago

What happened: When the dubbo provider reports a connection error, such as exceeding the upper limit of provider accepts, reConnect() will cause the client to continuously reconnect, causing the client's CPU and memory to continue to increase.

Here is the call chain :

image image image

What you expected to happen:

I think that when there is a problem with the TCP connection, the practice of maintaining the connection pool should be temporarily stopped

How to reproduce it (as minimally and precisely as possible):

In dubbo settings, set the getty-session-param's connection-number less than the provider's accepts will cause this problem.

Anything else we need to know?:

No-SilverBullet commented 3 months ago

similar with this open issue https://github.com/apache/dubbo-getty/issues/105,i think should just close the client or distinguish errors encountered in handlePackage() in a more granular manner to choose whether to reconnect.

AlexStocks commented 3 months ago

Getty 中 session 代表一个网络连接,client 其实是一个网络连接池,维护一定数量的连接 session,这个数量当然是用户设定的。Getty client 初始版本【2018 年以前的版本】中,每个 client 单独启动一个 goroutine 轮询检测其连接池中 session 数量,如果没有达到用户设定的连接数量就向 server 发起新连接。

当 client 与 server 连接断开时,server 可能是被下线了,可能是意外退出,也有可能是假死。如果上层用户判定对端 server 确实不存在【如收到注册中心发来的 server 下线通知】后,调用 client.Close() 接口把连接池关闭掉。如果上层用户没有调用这个接口把连接池关闭掉,client 就认为对端地址还有效,就会不断尝试发起重连,维护连接池。

综上,从一个旧 session 关闭到创建一个新 session,getty client 初始版本的重连处理流程是:

1 旧 session 关闭网络接收 goroutine;

2 旧 session 网络发送 goroutine 探测到 网络接收 goroutine 退出后终止网络发送,进行资源回收后设定当前 session 无效;

3 client 的轮询 goroutine 检测到无效 session 后把它从 session 连接池删除;

4 client 的轮询 goroutine 检测到有效 session 数目少于 getty 上层使用者设定的数目 且 getty 上层使用者没有通过 client.Close() 接口关闭连接池时,就调用连接接口发起新连接。

上面这种通过定时轮询方式不断查验 client 中 session pool 中每个 session 有效性的方式,可称之为主动连接。主动连接的缺点显然是每个 client 都需要单独启用一个 goroutine。当然,其进一步优化手段之一是可以启动一个全局的 goroutine,定时轮询检测所有 client 的 session pool,不必每个 client 单独启动一个 goroutine。但是个人从 2016 年开始一直在思考一个问题:能否换一种 session pool 维护方式,去掉定时轮询机制,完全不使用任何的 goroutine 维护每个 client 的 session pool?

2018 年 5 月个人在一次午饭后遛弯时,把 getty client 的重连逻辑又重新梳理了一遍,突然想到了另一种方法,在步骤 2 中完全可以对 网络发送 goroutine 进行 “废物利用”,在这个 goroutine 标记当前 session 无效的逻辑步骤之后再加上一个逻辑:

1 如果当前 session 的维护者是一个 client【因为 session 的使用者也可能是 server】;

2 且如果其当前 session pool 的 session 数量少于上层使用者设定的 session number;

3 且如果上层使用者还没有通过 client.Close() 设定当前 session pool 无效【即当前 session pool 有效,或者说是对端 server 有效】

4 满足上面三个条件,网络发送 goroutine 执行连接重连即可;

5 新网络连接 session 建立成功且被加入 client 的 session pool 后,网络发送 goroutine 使命完成直接退出。

我把这种重连方式称之为 lazy reconnect,网络发送 goroutine 在其生命周期的最后阶段应该被称之为 网络重连 goroutine。通过 lazy reconnect这种方式,上述重连步骤 3 和 步骤 4 的逻辑被合入了步骤 2,client 当然也就没必要再启动一个额外的 goroutine 通过定时轮询的方式维护其 session pool 了。

image

lazy reconnect 整体流程图如上。如果对相关代码流程感兴趣,请移步 "参考 13" 给出的链接,很容易自行分析出来。

以上内容来自 Go 语言网络库 getty 的那些事 第三章。

你先理解下,如果觉得机制不合理,我们可以在这个 issue 里面继续聊。

AlexStocks commented 3 months ago

Getty's session in the initial version (prior to 2018) represents a network connection, and the client is actually a network connection pool that maintains a certain number of session connections, which is determined by the user. In the initial version of Getty client, each client starts a separate goroutine to periodically check the session pool's connection count. If the count does not reach the user-defined connection quantity, a new connection request is made to the server.

When the client's connection to the server is disconnected, it could be due to the server being offline, unexpectedly terminated, or stuck. If the upper-layer user confirms that the server does not exist (e.g., receiving a server offline notification from the registry), they can call the client.Close() interface to close the connection pool. If the upper-layer user does not call this interface to close the connection pool, the client assumes that the remote address is still valid and continues to attempt reconnection to maintain the connection pool.

In summary, the process of closing an old session and creating a new session in the initial version of Getty client's reconnection handling is as follows:

The network receiving goroutine of the old session is closed. The network sending goroutine of the old session detects the exit of the network receiving goroutine, terminates the network sending, performs resource cleanup, and marks the current session as invalid. The client's polling goroutine detects the invalid session and removes it from the session connection pool. The client's polling goroutine detects that the number of valid sessions is less than the user-defined quantity and the connection pool has not been closed through the client.Close() interface, and then initiates a new connection. The above approach, which continuously checks the validity of each session in the session pool through periodic polling, is called active reconnection. The disadvantage of active reconnection is that each client needs to start a separate goroutine. One possible optimization is to start a global goroutine that periodically polls all client's session pools, instead of starting a goroutine for each client. However, since 2016, the author has been contemplating an alternative session pool maintenance method that eliminates the need for periodic polling and does not use any goroutine to maintain each client's session pool.

In May 2018, the author came up with another method during a walk after lunch. In step 2, the network sending goroutine can be "reused" by adding an additional logic after marking the current session as invalid:

If the current session is maintained by a client (as the session user could also be a server). If the current session pool of the client has fewer sessions than the user-defined session number. If the upper-layer user has not set the current session pool as invalid through client.Close() (meaning the current session pool is valid or, in other words, the remote server is valid). If the above three conditions are met, the network sending goroutine initiates a reconnection. After successfully establishing a new network connection session and adding it to the client's session pool, the network sending goroutine completes its mission and exits. This reconnection method is referred to as "lazy reconnect," and the network sending goroutine in its final phase can be called the "network reconnection goroutine." With lazy reconnect, the logic of step 3 and step 4 is merged into step 2, eliminating the need for the client to start an additional goroutine for periodic polling to maintain its session pool.

image

The overall flowchart of lazy reconnect is as described above. If you are interested in the related code flow, please refer to the link provided in "Reference 13," where you can easily analyze it yourself.

The above content is from Chapter 3 of "Getty: The Story of Go Network Library." Please take some time to understand it, and if you find any flaws in the mechanism, we can continue discussing them in this issue.

No-SilverBullet commented 3 months ago

Getty 中 session 代表一个网络连接,client 其实是一个网络连接池,维护一定数量的连接 session,这个数量当然是用户设定的。Getty client 初始版本【2018 年以前的版本】中,每个 client 单独启动一个 goroutine 轮询检测其连接池中 session 数量,如果没有达到用户设定的连接数量就向 server 发起新连接。

当 client 与 server 连接断开时,server 可能是被下线了,可能是意外退出,也有可能是假死。如果上层用户判定对端 server 确实不存在【如收到注册中心发来的 server 下线通知】后,调用 client.Close() 接口把连接池关闭掉。如果上层用户没有调用这个接口把连接池关闭掉,client 就认为对端地址还有效,就会不断尝试发起重连,维护连接池。

综上,从一个旧 session 关闭到创建一个新 session,getty client 初始版本的重连处理流程是:

1 旧 session 关闭网络接收 goroutine;

2 旧 session 网络发送 goroutine 探测到 网络接收 goroutine 退出后终止网络发送,进行资源回收后设定当前 session 无效;

3 client 的轮询 goroutine 检测到无效 session 后把它从 session 连接池删除;

4 client 的轮询 goroutine 检测到有效 session 数目少于 getty 上层使用者设定的数目 且 getty 上层使用者没有通过 client.Close() 接口关闭连接池时,就调用连接接口发起新连接。

上面这种通过定时轮询方式不断查验 client 中 session pool 中每个 session 有效性的方式,可称之为主动连接。主动连接的缺点显然是每个 client 都需要单独启用一个 goroutine。当然,其进一步优化手段之一是可以启动一个全局的 goroutine,定时轮询检测所有 client 的 session pool,不必每个 client 单独启动一个 goroutine。但是个人从 2016 年开始一直在思考一个问题:能否换一种 session pool 维护方式,去掉定时轮询机制,完全不使用任何的 goroutine 维护每个 client 的 session pool?

2018 年 5 月个人在一次午饭后遛弯时,把 getty client 的重连逻辑又重新梳理了一遍,突然想到了另一种方法,在步骤 2 中完全可以对 网络发送 goroutine 进行 “废物利用”,在这个 goroutine 标记当前 session 无效的逻辑步骤之后再加上一个逻辑:

1 如果当前 session 的维护者是一个 client【因为 session 的使用者也可能是 server】;

2 且如果其当前 session pool 的 session 数量少于上层使用者设定的 session number;

3 且如果上层使用者还没有通过 client.Close() 设定当前 session pool 无效【即当前 session pool 有效,或者说是对端 server 有效】

4 满足上面三个条件,网络发送 goroutine 执行连接重连即可;

5 新网络连接 session 建立成功且被加入 client 的 session pool 后,网络发送 goroutine 使命完成直接退出。

我把这种重连方式称之为 lazy reconnect,网络发送 goroutine 在其生命周期的最后阶段应该被称之为 网络重连 goroutine。通过 lazy reconnect这种方式,上述重连步骤 3 和 步骤 4 的逻辑被合入了步骤 2,client 当然也就没必要再启动一个额外的 goroutine 通过定时轮询的方式维护其 session pool 了。

image

lazy reconnect 整体流程图如上。如果对相关代码流程感兴趣,请移步 "参考 13" 给出的链接,很容易自行分析出来。

以上内容来自 Go 语言网络库 getty 的那些事 第三章。

你先理解下,如果觉得机制不合理,我们可以在这个 issue 里面继续聊。

你好,感谢回复。在一个session失效之前去触发重连的逻辑,这种设计没有任何问题,可以节约goroutine。但是,如果连接存在问题,会持续的去重连,我的理解是这种行为是不合理的,会给客户端以及服务端都造成压力。这里对于无效session的区分是不是可以更细一点呢,当连接出现问题的时候就不再去重连。

AlexStocks commented 3 months ago

Getty 中 session 代表一个网络连接,client 其实是一个网络连接池,维护一定数量的连接 session,这个数量当然是用户设定的。Getty client 初始版本【2018 年以前的版本】中,每个 client 单独启动一个 goroutine 轮询检测其连接池中 session 数量,如果没有达到用户设定的连接数量就向 server 发起新连接。 当 client 与 server 连接断开时,server 可能是被下线了,可能是意外退出,也有可能是假死。如果上层用户判定对端 server 确实不存在【如收到注册中心发来的 server 下线通知】后,调用 client.Close () 接口把连接池关闭掉。如果上层用户没有调用这个接口把连接池关闭掉,client 就认为对端地址还有效,就会不断尝试发起重连,维护连接池。 综上,从一个旧 session 关闭到创建一个新 session,getty client 初始版本的重连处理流程是: 1 旧 session 关闭网络接收 goroutine; 2 旧 session 网络发送 goroutine 探测到 网络接收 goroutine 退出后终止网络发送,进行资源回收后设定当前 session 无效; 3 client 的轮询 goroutine 检测到无效 session 后把它从 session 连接池删除; 4 client 的轮询 goroutine 检测到有效 session 数目少于 getty 上层使用者设定的数目 且 getty 上层使用者没有通过 client.Close () 接口关闭连接池时,就调用连接接口发起新连接。 上面这种通过定时轮询方式不断查验 client 中 session pool 中每个 session 有效性的方式,可称之为主动连接。主动连接的缺点显然是每个 client 都需要单独启用一个 goroutine。当然,其进一步优化手段之一是可以启动一个全局的 goroutine,定时轮询检测所有 client 的 session pool,不必每个 client 单独启动一个 goroutine。但是个人从 2016 年开始一直在思考一个问题:能否换一种 session pool 维护方式,去掉定时轮询机制,完全不使用任何的 goroutine 维护每个 client 的 session pool? 2018 年 5 月个人在一次午饭后遛弯时,把 getty client 的重连逻辑又重新梳理了一遍,突然想到了另一种方法,在步骤 2 中完全可以对 网络发送 goroutine 进行 “废物利用”,在这个 goroutine 标记当前 session 无效的逻辑步骤之后再加上一个逻辑: 1 如果当前 session 的维护者是一个 client【因为 session 的使用者也可能是 server】; 2 且如果其当前 session pool 的 session 数量少于上层使用者设定的 session number; 3 且如果上层使用者还没有通过 client.Close () 设定当前 session pool 无效【即当前 session pool 有效,或者说是对端 server 有效】 4 满足上面三个条件,网络发送 goroutine 执行连接重连即可; 5 新网络连接 session 建立成功且被加入 client 的 session pool 后,网络发送 goroutine 使命完成直接退出。 我把这种重连方式称之为 lazy reconnect,网络发送 goroutine 在其生命周期的最后阶段应该被称之为 网络重连 goroutine。通过 lazy reconnect 这种方式,上述重连步骤 3 和 步骤 4 的逻辑被合入了步骤 2,client 当然也就没必要再启动一个额外的 goroutine 通过定时轮询的方式维护其 session pool 了。 image lazy reconnect 整体流程图如上。如果对相关代码流程感兴趣,请移步 "参考 13" 给出的链接,很容易自行分析出来。 以上内容来自 Go 语言网络库 getty 的那些事 第三章。 你先理解下,如果觉得机制不合理,我们可以在这个 issue 里面继续聊。

你好,感谢回复。在一个 session 失效之前去触发重连的逻辑,这种设计没有任何问题,可以节约 goroutine。但是,如果连接存在问题,会持续的去重连,我的理解是这种行为是不合理的,会给客户端以及服务端都造成压力。这里对于无效 session 的区分是不是可以更细一点呢,当连接出现问题的时候就不再去重连。

当然可以,你能否构思下,哪些 error 可以让我们明确不再执行 reConnect?

No-SilverBullet commented 3 months ago

Getty 中 session 代表一个网络连接,client 其实是一个网络连接池,维护一定数量的连接 session,这个数量当然是用户设定的。Getty client 初始版本【2018 年以前的版本】中,每个 client 单独启动一个 goroutine 轮询检测其连接池中 session 数量,如果没有达到用户设定的连接数量就向 server 发起新连接。 当 client 与 server 连接断开时,server 可能是被下线了,可能是意外退出,也有可能是假死。如果上层用户判定对端 server 确实不存在【如收到注册中心发来的 server 下线通知】后,调用 client.Close () 接口把连接池关闭掉。如果上层用户没有调用这个接口把连接池关闭掉,client 就认为对端地址还有效,就会不断尝试发起重连,维护连接池。 综上,从一个旧 session 关闭到创建一个新 session,getty client 初始版本的重连处理流程是: 1 旧 session 关闭网络接收 goroutine; 2 旧 session 网络发送 goroutine 探测到 网络接收 goroutine 退出后终止网络发送,进行资源回收后设定当前 session 无效; 3 client 的轮询 goroutine 检测到无效 session 后把它从 session 连接池删除; 4 client 的轮询 goroutine 检测到有效 session 数目少于 getty 上层使用者设定的数目 且 getty 上层使用者没有通过 client.Close () 接口关闭连接池时,就调用连接接口发起新连接。 上面这种通过定时轮询方式不断查验 client 中 session pool 中每个 session 有效性的方式,可称之为主动连接。主动连接的缺点显然是每个 client 都需要单独启用一个 goroutine。当然,其进一步优化手段之一是可以启动一个全局的 goroutine,定时轮询检测所有 client 的 session pool,不必每个 client 单独启动一个 goroutine。但是个人从 2016 年开始一直在思考一个问题:能否换一种 session pool 维护方式,去掉定时轮询机制,完全不使用任何的 goroutine 维护每个 client 的 session pool? 2018 年 5 月个人在一次午饭后遛弯时,把 getty client 的重连逻辑又重新梳理了一遍,突然想到了另一种方法,在步骤 2 中完全可以对 网络发送 goroutine 进行 “废物利用”,在这个 goroutine 标记当前 session 无效的逻辑步骤之后再加上一个逻辑: 1 如果当前 session 的维护者是一个 client【因为 session 的使用者也可能是 server】; 2 且如果其当前 session pool 的 session 数量少于上层使用者设定的 session number; 3 且如果上层使用者还没有通过 client.Close () 设定当前 session pool 无效【即当前 session pool 有效,或者说是对端 server 有效】 4 满足上面三个条件,网络发送 goroutine 执行连接重连即可; 5 新网络连接 session 建立成功且被加入 client 的 session pool 后,网络发送 goroutine 使命完成直接退出。 我把这种重连方式称之为 lazy reconnect,网络发送 goroutine 在其生命周期的最后阶段应该被称之为 网络重连 goroutine。通过 lazy reconnect 这种方式,上述重连步骤 3 和 步骤 4 的逻辑被合入了步骤 2,client 当然也就没必要再启动一个额外的 goroutine 通过定时轮询的方式维护其 session pool 了。 image lazy reconnect 整体流程图如上。如果对相关代码流程感兴趣,请移步 "参考 13" 给出的链接,很容易自行分析出来。 以上内容来自 Go 语言网络库 getty 的那些事 第三章。 你先理解下,如果觉得机制不合理,我们可以在这个 issue 里面继续聊。

你好,感谢回复。在一个 session 失效之前去触发重连的逻辑,这种设计没有任何问题,可以节约 goroutine。但是,如果连接存在问题,会持续的去重连,我的理解是这种行为是不合理的,会给客户端以及服务端都造成压力。这里对于无效 session 的区分是不是可以更细一点呢,当连接出现问题的时候就不再去重连。

当然可以,你能否构思下,哪些 error 可以让我们明确不再执行 reConnect?

上面回复中我表达的连接出现“错误“不太准确,这里对于传输层来说,很难去区分上层应用的错误。我想表达的是,当服务端主动断开连接这种情况(服务端发起FIN),是不是就代表着客户端不要再去重连维护这个连接池了呢

No-SilverBullet commented 3 months ago

再补充一下这个issue。当连接数超过provider上限之后,TCP连接能够完成,但是服务端会马上主动断开连接。针对这种情况我认为是需要避免重新连接的。期待回复,谢谢。

image
AlexStocks commented 3 months ago

再补充一下这个 issue。当连接数超过 provider 上限之后,TCP 连接能够完成,但是服务端会马上主动断开连接。针对这种情况我认为是需要避免重新连接的。期待回复,谢谢。 image

这是调参问题,不宜让 getty 承担太多多余的逻辑。

第一,你把 provider 连接上线提升下; OR 第二,你降低下客户端连接池上限数目。

No-SilverBullet commented 3 months ago

再补充一下这个 issue。当连接数超过 provider 上限之后,TCP 连接能够完成,但是服务端会马上主动断开连接。针对这种情况我认为是需要避免重新连接的。期待回复,谢谢。 image

这是调参问题,不宜让 getty 承担太多多余的逻辑。

第一,你把 provider 连接上线提升下; OR 第二,你降低下客户端连接池上限数目。

在上面我提到的情况下,客户端CPU和内存会持续增长,服务端也会有大量的TIME-WAIT。调参当然可以解决问题,但是服务端的连接数是动态的,所以我觉得getty还是应该有一个兜底的逻辑,而不是“无脑的”一直去重连。如果短时间内没有这方面计划或者与设计理念不符,那么可以关闭issue了。

AlexStocks commented 3 months ago

再补充一下这个 issue。当连接数超过 provider 上限之后,TCP 连接能够完成,但是服务端会马上主动断开连接。针对这种情况我认为是需要避免重新连接的。期待回复,谢谢。 image

这是调参问题,不宜让 getty 承担太多多余的逻辑。 第一,你把 provider 连接上线提升下; OR 第二,你降低下客户端连接池上限数目。

在上面我提到的情况下,客户端 CPU 和内存会持续增长,服务端也会有大量的 TIME-WAIT。调参当然可以解决问题,但是服务端的连接数是动态的,所以我觉得 getty 还是应该有一个兜底的逻辑,而不是 “无脑的” 一直去重连。如果短时间内没有这方面计划或者与设计理念不符,那么可以关闭 issue 了。

当然可以添加一个最多重来次数或者重连时间限制,你愿意贡献一个 PR 吗?

No-SilverBullet commented 3 months ago

再补充一下这个 issue。当连接数超过 provider 上限之后,TCP 连接能够完成,但是服务端会马上主动断开连接。针对这种情况我认为是需要避免重新连接的。期待回复,谢谢。 image

这是调参问题,不宜让 getty 承担太多多余的逻辑。 第一,你把 provider 连接上线提升下; OR 第二,你降低下客户端连接池上限数目。

在上面我提到的情况下,客户端 CPU 和内存会持续增长,服务端也会有大量的 TIME-WAIT。调参当然可以解决问题,但是服务端的连接数是动态的,所以我觉得 getty 还是应该有一个兜底的逻辑,而不是 “无脑的” 一直去重连。如果短时间内没有这方面计划或者与设计理念不符,那么可以关闭 issue 了。

当然可以添加一个最多重来次数或者重连时间限制,你愿意贡献一个 PR 吗? 好的 我先构思一下

AlexStocks commented 3 months ago

再补充一下这个 issue。当连接数超过 provider 上限之后,TCP 连接能够完成,但是服务端会马上主动断开连接。针对这种情况我认为是需要避免重新连接的。期待回复,谢谢。 image

这是调参问题,不宜让 getty 承担太多多余的逻辑。 第一,你把 provider 连接上线提升下; OR 第二,你降低下客户端连接池上限数目。

在上面我提到的情况下,客户端 CPU 和内存会持续增长,服务端也会有大量的 TIME-WAIT。调参当然可以解决问题,但是服务端的连接数是动态的,所以我觉得 getty 还是应该有一个兜底的逻辑,而不是 “无脑的” 一直去重连。如果短时间内没有这方面计划或者与设计理念不符,那么可以关闭 issue 了。

当然可以添加一个最多重来次数或者重连时间限制,你愿意贡献一个 PR 吗? 好的 我先构思一下

感谢哈