Skip to content
On this page

Redis Bug浅析

1、Redis 线程劫持

发现

发现Bug的过程是因为在一段时间收到很多超时的错误警告,虽然对业务的影响不大,但是不能理解为什么会出现超时的情况。

img

原因

表面的结果如下,是StatckExchange.Redis官方给出了说明,把这种情况叫做“线程劫持”导致的发往Redis的请求无法找到对应的线程进行读取。解释链接如下:

Thread Theft | StackExchange.Redis

在后台运行中,对于每个到redis的连接,StackExchange.Redis保留了一个队列(管道),其中包括我们已经发送给redis的、正在等待回复的命令。当每个回复进来的时候,我们会查看下一个等待的命令(顺序被保留,这让事情变得简单),然后我们为该回复触发 "这是你的结果 "API。对于async/await代码来说,这将导致你的 "continuation"被重新激活,也就是当一个await任务完成时,你的代码如何重新恢复活力。这是一个简单的版本,但现实是有点细微差别的。

默认情况下,当你在一个任务上触发TrySetResult时,"continuation"会被同步调用,也就是说,正在设置结果的线程现在会立即继续运行你的"continuation"。在我们的例子中,这将是非常糟糕的,因为这将意味着专门的读取器循环(本来是要处理来自redis的结果)现在正在运行你的应用逻辑;这是线程盗窃,并且会表现为rs.CompletePendingMessage的大量超时。CompletePendingMessage的信息(rs是阅读器的状态;你不应该经常在CompletePendingMessage*步骤中观察到它,因为它应该是非常快的;如果你经常看到它,可能意味着阅读器在试图设置结果时被劫持了)。

解决方案

如下是StackExchange.Redis官方给出的解决方案:

ConnectionMultiplexer.SetFeatureFlag("preventthreadtheft", true);

但是问题解决不是最后的目的

解决原因

原因深究如下:

简单来说,StackExchange.Redis默认的线程模式为RunContinuationsAsynchronously,按照asp.net core的本意,该模式下的线程会强制以异步方式运行,但是在遇到Task.WhenAll、 Task.WhenAny或TaskExtensions.Unwrap创建线程的时候,该模式下的线程会以同步的方式运行,在设置上述的解决方案之后,线程会以None模式下运行,运行策略为:

如果未指定延续选项,应在执行延续任务时使用指定的默认行为。 延续任务在前面的任务完成后以异步方式运行,与前面任务最终的 Status 属性值无关。 如果延续是子任务,则会将其创建为分离的嵌套任务。

避免了线程劫持的发生

TaskContinuationOptions 枚举 (System.Threading.Tasks) | Microsoft Learn

RunContinuationsAsynchronously 不会以异步方式运行延续 - Microsoft 支持

2、超时

发现

往往收到的错误中会有不包含线程劫持信息的错误信息。

img

复现

img

原因

因为管道的应用,所以导致在高并发的情况下,会出现线程池枯竭的情况。默认的最小线程数为8,暂时的解决办法是扩充最小线程数。当然,不是最优解。

管道和链接复用

解决方案

ThreadPool.SetMinThreads(200, 200);

Date: 2023/3/3

Authors: 刘军衔

Tags: Redis