Skip to content

CSRedis RedisClientException : Connection was not opened

线上生产 .NET Core 项目偶尔会报如下错误:

txt
CSRedis.RedisClientException: Connection was not opened
   at CSRedis.CSRedisClient.GetAndExecute[T](RedisClientPool pool, Func\`2 handler, Int32 jump)
   at CSRedis.CSRedisClient.ExecuteScalar[T](String key, Func\`3 hander)
   at CSRedis.CSRedisClient.Set(String key, Object value, Int32 expireSeconds, Nullable\`1 exists)
   at Framework.Utils.RedisProvider.Set[T](String key, T value, Int32 minutes)

CSRedisCore 的版本号是 3.0.0

2019/02/01 追记

经过查看源码、线下模拟最终怀疑是创建 Socket 连接的时候发生了未知的问题导致创建连接失败,最终引发了这个异常。
这个未知的问题是什么?我也不知道。

先说下 CSRedis,它的连接字符串有一个属性叫 preheat 预热,该属性默认为 true
启用预热时,会自动创建连接至池中的最大值(poolsize)。poolsize 设置的是 50,我感觉对于 Redis 来说这个值其实并不高。
连接创建后会放回到连接池中待用,此时若超过一定时间未使用,则该连接会被关闭。这个时间由 Redis 的超时时间 (timeout) 设置决定。

问题就出在创建连接时,多个服务同时启动时,会触发 n * 50 次创建连接的请求。短时间创建的连接过多导致更容易引发上述异常。

然而,在线下的测试环境中模拟短时间创建大量连接时并没有出现该异常。只有在较长时间重复创建连接到一定数量后(至少 1w 以上)有可能发生该异常。
因为模拟了好多种情况,总共出现过如下几种异常:

  1. ERR max number of clients reached

    这个估计是达到了 Redis 服务器的连接数上限导致的。

    txt
    RedisSocket.Connect endpoint : 192.168.0.66:8000
    RedisSocket.Connect method cost 00:00:00.1314587
    【192.168.0.66:8000/0】仍然不可用,下一次恢复检查
    时间:01/29/2019 13:58:23,错误:(ERR max number of clients reached)
  2. 您的主机中的软件中止了一个已建立的连接

    这个应该是创建的连接过多导致本机的端口号耗尽导致的。这种情况应该不会发生在线上,不可能有那么多的请求量。

    txt
    ---> (Inner Exception #93) System.IO.IOException:
    Unable to write data to the transport connection:
    您的主机中的软件中止了一个已建立的连接。. ---> System.Net.Sockets.SocketException: 您的主机中的软件
    中止了一个已建立的连接。
      at System.Net.Sockets.NetworkStream.Write(Byte[] buffer, Int32 offset, Int32 size)
      --- End of inner exception stack trace ---
      at OctToolServices.Controllers.CSRedisController.<>c__DisplayClass8_0.<TestConnectionMultiThread>b__0() in D:\middleware\octmiddlewarenet\OctApiService\OctToolServices\Controllers\CSRedisController.cs:line 157
      at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
    --- End of stack trace from previous location where exception was thrown ---
      at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)<---
  3. Connection was not opened

    个人认为这个错误是最接近线上情况的。
    其中打印的时间就是创建 Socket 连接所消耗的时间。正常值都是在 1ms 以下,但这是显示超过了 20s。
    另外的两行的控制台消息不确定是哪边打印出来的。
    另外这个也是在大量创建连接时发生的异常,但大部分时候是报(您的主机中的软件中止了一个已建立的连接)的错误消息。

    txt
    RedisSocket.Connect method cost 00:00:21.0015986
    【192.168.0.76:8000/0】仍然不可用,下一次恢复检查时间:01/29/2019 13:27:21,错误:(Connection was not opened)
    RedisSocket.Connect method cost 00:00:21.1406340
    【192.168.0.76:8000/0】仍然不可用,下一次恢复检查时间:01/29/2019 13:27:23,错误:(Connection was not opened)
  4. 状态不可用,等待后台检查程序恢复方可使用。Connection was not opened

    这个应该是和上面的 3 是同一个错误。

    txt
    ---> (Inner Exception #49) System.Exception: 【192.168.0.66:8000/0】状态不可用,等待后台检查程序恢复方可使用。Connection was not opened
      at OctToolServices.Controllers.CSRedisController.<>c__DisplayClass9_1.<CreateTestData>b__0() in D:\middleware\octmiddlewarenet\OctApiService\OctToolServices\Controllers\CSRedisController.cs:line 218
      at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
    --- End of stack trace from previous location where exception was thrown ---
      at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)<---

根据上面的测试结果,比较大的可能性在于创建 Socket 连接的时候出了问题导致的。在 Java 建立 Socket 慢的问题 里说了一个建立 Socket 慢的原因。

使用 HostName 查找主机 IP 时可能会因为要查询 DNS 服务器,所以导致了连接缓慢。

由于这里使用的 IP 应该不会有这个问题才对。官方的一个 bug 修复 v3.0.33 解决域名访问,同时开启 ssl 无法连接的 bug 里正好有类似的改动。

修改前:

csharp
public RedisClient OnCreate() {
    var ips = Dns.GetHostAddresses(_ip);
    if (ips.Length == 0) throw new Exception($"无法解析“{_ip}”");
    var client = new RedisClient(new IPEndPoint(ips[0], _port), _ssl, 100, _writebuffer);
    // ...
}

修改后:

csharp
public RedisClient OnCreate() {
    RedisClient client = null;
    if (IPAddress.TryParse(_ip, out var tryip)) {
        client = new RedisClient(new IPEndPoint(tryip, _port), _ssl, 100, _writebuffer);
    } else {
        var ips = Dns.GetHostAddresses(_ip);
        if (ips.Length == 0) throw new Exception($"无法解析“{_ip}”");
        client = new RedisClient(_ip, _port, _ssl, 100, _writebuffer);
    }
    // ...
}

修改前使用 Dns.GetHostAddresses 函数来获取 Redis 服务器地址,其 MSDN Dns.GetHostAddresses(String) Method 上的说明如下:

GetHostAddresses 方法将查询与主机名关联的 IP 地址的 DNS 子系统。如果 hostNameOrAddress 是 IP 地址,而无需查询 DNS 服务器返回此地址。
如果为空字符串作为传递 hostNameOrAddress 参数,则此方法返回本地主机的 IPv4 和 IPv6 地址。
Pv6 地址进行筛选的结果中的 GetHostAddresses 方法如果本地计算机没有安装 IPv6。因此,很可能返回一个空 IPAddress 只要 IPv6 结果已可供 hostNameOrAddress 参数。

修改后明确的判断 IP 优先。

最终的解决方案

  1. 按照上面 Dns.GetHostAddresses 的说明上面的补丁效果应该是一样的。为了以防万一还是打上了这个补丁。
  2. 关闭了 CSRedis 的预热(preheat)功能。
  3. 调低了 CSRedis 的 poolsize (原为 50 降到 5)。

从更新补丁上线到现在 2 天了,暂时还没有再出现该异常。

2019/03/11 追记

╮(╯﹏╰)╭
非常残念的是从更新到现在 40 天的时间里还是出现了 2 次这个异常。

Page Layout Max Width

Adjust the exact value of the page width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the page layout
A ranged slider for user to choose and customize their desired width of the maximum width of the page layout can go.

Content Layout Max Width

Adjust the exact value of the document content width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the content layout
A ranged slider for user to choose and customize their desired width of the maximum width of the content layout can go.