消失的 EF 电话之谜

The Mystery of the Vanishing EF Call

今天我接到 ASP.NET 生产系统用户的紧急电话。一些用户(不是全部)无法输入某些数据。用户发布数据,然后系统冻结;电话再也没有回。

我们试图在 QA 系统(具有生产数据的全新恢复)上重现该问题,但未能成功。然后我从我的开发环境 运行 直接连接到生产数据库,伪装成受影响的用户之一。再次,没问题。结论:生产环境中肯定存在某种问题,可能是托管网站的 IIS 进程中的某个地方。

所以我在生产服务器上启动Visual Studio,并附加到IIS进程(孩子们,不要在家里这样做!),设置一个有问题的代码中的断点,以用户身份登录,并试图保存数据。打断点并逐行执行,直到我打了这样一行代码:

try
{
  ...
  using (var db = new MyDataContext())
  {
    ...
    var fooToUpdate = db.Foos.Single(f => f.ID == fooId); // <-- THIS LINE
    ...
  }
}
catch (Exception ex)
{
  // some error logging
}

在那条线上点击 "step" 后,线程就消失了。消失得无影无踪。我在数据库上放了一个嗅探器,没有触发任何查询;不用说,没有涉及数据库锁定。没有抛出异常。代码进入Entity Framework,从未离开

数据的方式是每个用户每天都有一个不同且唯一的 fooId,因此其他用户不会有相同的 fooId。大多数用户能够加载他们的 Foo,但是 select 少数用户始终无法加载他们的个人 Foo。我尝试 运行 在 SSMS window 中加载 Foo 的查询;一点也不麻烦。唯一一次失败是在生产服务器上的这个特定 IIS 进程中。

现在,我可以回收应用程序池或重新启动 IIS,这可能会掩盖问题。但是一周前发生了类似的事情,当时我们也无法追踪。所以我们随后重置了 IIS,希望问题会消失。确实如此,持续了一个星期。现在又回来了。

有没有人知道线程怎么可能像这样简单地蒸发?诺曼贝茨躲在 EF 门后面吗?

鉴于线并没有神奇地蒸发这一事实,我们可以推测一些更可能的选择:

  1. 调试器很难跟踪在发布模式下编译的生产代码。仅仅因为调试发布代码在 90% 的时间都有效,不要误以为它是可靠的。优化的代码可以很快使调试器脱离实际执行的轨道。发生这种情况时,线程看起来就像消失了一样。
  2. 假设线程确实合法地进入调用而不是return(这似乎得到了应用程序最初投诉的支持"freezing"),那么最有可能的情况是某些线程的死锁类型。 EntityFramework 死锁并不常见,但也不是闻所未闻。我知道的最常见问题通常涉及 TransactionScopeCommitableTransaction。您是否在省略的代码部分中使用了任何交易?

事实证明,EF 部分毕竟是一条红鲱鱼。我去下载了 Telerik 的 JustDecompile 和 JustCode,希望进入 EF 代码,但是当我进入那一行时,我发现自己不在 Single() 扩展方法中,而是在我自己的方法调用之一中- 我 认为 我已经在上一行执行了。显然代码与生产版本不完全同步。

LESSON 1: If you attach to a process, your execution point may not be where you think it is, if your code is not identical to the code that was compiled into that process.

所以无论如何,现在我可以在不反编译任何东西的情况下进入代码,我注意到的第一件事是:

lock (_lockObj)
{
  ...
}

当我试图踏入它时,它僵在那里。确凿的证据。

所以在某个地方,某个其他线程正在锁定该对象。查看其他调用锁的地方,导致依赖项的意大利面条,以及另一个代码锁定段,具有多个 DB 调用甚至事务边界。它可能是代码锁/数据库事务死锁,尽管对数据库事务中代码的简短扫描未能在事务生命周期内找到任何阻塞其他任何东西的竞争者。另外,有证据表明数据库没有显示任何阻塞或打开的交易。相反,它可能只是几百个排队的 long-运行 进程的事实,所有内部代码锁内部代码锁,最后它看起来就像 [=24= 的 West Side Highway ] 在一个星期五,一辆折叠式拖车横跨 3 条车道接近 GW 桥。

LESSON 2: Code locks are dangerous, not only - but especially - when used in conjunction with DB transactions. Try to find ways to make your code thread safe without using code locks. And if you really must use code locks, make sure you get in and out as quickly as possible. Don't give your thread a magazine to read while it's occupying the only stall, so to speak.