Skip to content

fix: avoid closing an idle RpcClient that is in use or still has in-flight requests - #146

Open
eric-zc1 wants to merge 2 commits into
trpc-group:masterfrom
eric-zc1:fix-skip-cleaning-inflight-client
Open

eric-zc1 wants to merge 2 commits into
trpc-group:masterfrom
eric-zc1:fix-skip-cleaning-inflight-client

Conversation

@eric-zc1

Copy link
Copy Markdown
Contributor

背景

RpcClusterClientManager#scanUnusedClient 每 15 分钟扫描一次空闲客户端:先从 CLUSTER_MAP 摘除,再批量 close()。而 DefClusterInvoker#getInvoker 的快速路径只判断缓存里有没有 invoker,不判断它是否还可用,因此存在一个窗口 —— 客户端已被判定空闲、正在被拆除,业务线程却拿到了它并发出了请求,最终在 DefResponseFutureManager#closeClient 里被判死刑:Client(...) stop(error code 999)。

999 不在熔断列表内,失败还会被上报成成功,所以这个异常会直接透传到业务侧。

改动

  1. scanUnusedClient 在真正 close() 之前再做一次空闲判定:若期间 lastUsedNanos 已被业务线程刷新,说明该客户端正在被使用,把它放回 CLUSTER_MAP 并跳过关闭;同时把原来 finally 里无条件打印的 WARN 改为正常关闭时才打印,并补上关闭失败的 catch,避免一个客户端关闭失败影响后续客户端;
  2. isIdleTimeout 增加「在途请求」闸门:客户端虽已空闲超时,但仍有未完成的请求时跳过本轮清理,留到下一轮再判定,避免 closeClient() 把这些请求全部打成 Client(...) stop
  3. 为支撑第 2 点,新增 RpcClient#getPendingRequestCount():接口提供 default 实现返回 0,不破坏已有第三方实现的二进制兼容;DefRpcClient 转发到 DefResponseFutureManager#getPendingCount()(即 futureMap.size()),RpcClusterClientManager.RpcClientProxy 做委托;
  4. DefClusterInvoker#getInvoker 快速路径上的 isAvailable() 检查补充注释(纯注释,无逻辑变化)。

边界

getPendingRequestCount() 是瞬时快照,判定与真正 close() 之间仍有极小窗口可能进来新请求;第 1 点的二次判定与快速路径的 isAvailable() 检查一起把该窗口收敛到最小。

有持续流量时客户端本来也不该被回收,属于预期行为;idle_timeout 配 0 仍可从源头关闭清理。

测试

trpc-core 新增 3 个用例、trpc-rpc-support 新增 1 个用例:

  • RpcClusterClientManagerTest#testScanSkipsClientWithInFlightRequest —— 空闲且有在途请求时不清理,请求结束后下一轮正常清理;
  • RpcClusterClientManagerTest#testScanRescuesClientUsedAgainBeforeClosing —— 清理过程中被再次使用时放回 CLUSTER_MAP、不被关闭;
  • RpcClusterClientManagerTest#testScanContinuesWhenClosingFails —— 某个客户端关闭失败不影响其余客户端的清理;
  • DefResponseFutureTest#testPendingRequestCount —— 在途请求数的透出。

前 3 个用例在未合入本次修复时会失败(已实测),属于回归测试。

mvn -pl trpc-core clean test -Dtest='com.tencent.trpc.core.cluster..*Test,com.tencent.trpc.core.rpc..*Test' -Djacoco.skip=true
→ Tests run: 105, Failures: 0, Errors: 0, Skipped: 0

mvn -pl trpc-proto/trpc-rpc-support test
→ Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

https://trpc-scan-unused-client.pages.woa.com/

RpcClusterClientManager#scanUnusedClient removes a client from CLUSTER_MAP and
then closes it, while DefClusterInvoker#getInvoker only checks whether the
invoker is already cached. A request issued in that window is therefore sent
through a client which is being torn down, and DefResponseFutureManager
#closeClient fails all its in-flight requests with "Client(...) stop"
(error code 999). That code is not in the circuit breaker list and the failure
is even reported as a success, so the exception is simply propagated to the
caller.

- RpcClient#getPendingRequestCount(): new method, with a default implementation
  returning 0 to keep binary compatibility for existing implementations. It is
  implemented by DefRpcClient on top of DefResponseFutureManager#getPendingCount()
  (i.e. the size of the in-flight future map) and delegated by RpcClientProxy;
- RpcClusterClientManager#isIdleTimeout: although the client is idle for too
  long, skip cleaning it when there are still requests in flight, so that it is
  re-checked in the next round instead of being closed;
- DefClusterInvoker#getInvoker: document why the availability check is required
  on the fast path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant