AppDomain Unload 卡住的原因
Reasons for stuck AppDomain Unload
我仍在尝试了解持续存在的问题,但它几乎可以概括为无法卸载 AppDomain.
它发生在将 ASP.NET WebAPI 部署到 Azure 应用服务的过程中,我们观察到以下内容:
- 进程 ID 未更改,新部署托管在完全相同的进程中(AFAIU,这是通过卸载旧 AppDomain 并使用更新的二进制文件启动新 AppDomain 完成的)
- Azure PaaS 诊断在错误部分显示以下内容:
"In w3wp_12396.dmp, the HttpRuntime for the application
/LM/W3SVC/1523308129/ROOT is in the middle of a shutdown."
分析内存转储,我们看到设置了 IsAbortRequested 标志的线程,但它们似乎永远不会完成(此处 WinDbg !threads
的输出:https://pastebin.com/7CXYcffy)
在内存转储中,我们还看到很多 AppDomains 处于“UNLOAD_REQUESTED”阶段,它们似乎从未完成卸载(完整输出!DumpDomain
在这里:https://pastebin.com/kahZQuWN)
Domain 7: 000001c67062c800
LowFrequencyHeap: 000001c67062cff8
HighFrequencyHeap: 000001c67062d088
StubHeap: 000001c67062d118
Stage: UNLOAD_REQUESTED
SecurityDescriptor: 000001c6705c5680
Name: /LM/W3SVC/1523308129/ROOT-6-131687140950004974
未检测到死锁(至少通过 WinDbg SOSEX 插件的 !dlk
命令,通常涵盖大多数死锁情况)
没有代码取消线程中止(没有调用 Thread.ResetAbort()
)
我们现在解决问题的唯一方法是终止进程(停止 Azure AppService)。
AppDomain 无法卸载的可能原因是什么?
更新。在线程堆栈中,我们得到一个提示,它可能与我们自定义的 Azure Blob Log4net 附加程序有关,我发现当创建此类附加程序时(每个应用程序一次),它会生成具有以下结构的新线程。
while (true)
{
try
{
Flush(); // pseudocode
Thread.Sleep(10000);
}
catch(Exception)
{
}
}
不确定我是否理解为什么它会导致完全无法停止的线程(因为 ThreadAbortException
不会被 catch 停止),但看起来将 while (true)
更改为 while (!Environment.HasShutdownStarted && !_stopping)
可以解决问题(_stopping
在调用 Appender OnClose
时设置,这是 log4net 的一种正常关闭)...
这似乎是一个 JIT 错误。是的,JIT 中的 BUG!我发现那里记录了几乎相同的故事:http://labs.criteo.com/2017/04/ryujit-never-ending-threadabortexception/.
要演示该问题,您可以 运行 以下代码。仅适用于发布模式,仅适用于 x64 平台(我的目标是 .NET 4.5.2)。
除非您手动重新抛出异常,否则您将观察到记录的异常链是无穷无尽的。为什么它是 CLR/JIT 中的错误?因为 CLR/JIT 负责在设置线程的 AbortRequested 标志时在 "safe places" 中注入抛出 ThreadAbortException
。
Jeffrey Richter 引自 "CLR via C#"(以下代码违反):
Even when code catches the ThreadAbortException
, the CLR doesn’t allow
the exception to be swallowed. In other words, at the end of the catch
block, the CLR automatically rethrows the ThreadAbortException
exception.
还有 GitHub 中的错误:https://github.com/dotnet/coreclr/issues/16122。
static void Main(string[] args)
{
var mutex = new ManualResetEventSlim();
var t = new Thread(() =>
{
while (true)
{
try
{
if (!mutex.IsSet)
{
mutex.Set();
}
// Do some stuff
Thread.Sleep(100);
}
catch (Exception ex)
{
Console.WriteLine("Exception: " + ex.Message);
// the lines below FIX the issue
//if (ex is ThreadAbortException)
// throw;
}
// FIXES the issue as well
//Thread.Sleep(0);
}
});
t.Start();
// Wait for the thread to start
mutex.Wait();
t.Abort();
Console.ReadLine();
}
我仍在尝试了解持续存在的问题,但它几乎可以概括为无法卸载 AppDomain.
它发生在将 ASP.NET WebAPI 部署到 Azure 应用服务的过程中,我们观察到以下内容:
- 进程 ID 未更改,新部署托管在完全相同的进程中(AFAIU,这是通过卸载旧 AppDomain 并使用更新的二进制文件启动新 AppDomain 完成的)
- Azure PaaS 诊断在错误部分显示以下内容:
"In w3wp_12396.dmp, the HttpRuntime for the application /LM/W3SVC/1523308129/ROOT is in the middle of a shutdown."
分析内存转储,我们看到设置了 IsAbortRequested 标志的线程,但它们似乎永远不会完成(此处 WinDbg
!threads
的输出:https://pastebin.com/7CXYcffy)在内存转储中,我们还看到很多 AppDomains 处于“UNLOAD_REQUESTED”阶段,它们似乎从未完成卸载(完整输出
!DumpDomain
在这里:https://pastebin.com/kahZQuWN)
Domain 7: 000001c67062c800 LowFrequencyHeap: 000001c67062cff8 HighFrequencyHeap: 000001c67062d088 StubHeap: 000001c67062d118 Stage: UNLOAD_REQUESTED SecurityDescriptor: 000001c6705c5680 Name: /LM/W3SVC/1523308129/ROOT-6-131687140950004974
未检测到死锁(至少通过 WinDbg SOSEX 插件的
!dlk
命令,通常涵盖大多数死锁情况)没有代码取消线程中止(没有调用
Thread.ResetAbort()
)
我们现在解决问题的唯一方法是终止进程(停止 Azure AppService)。
AppDomain 无法卸载的可能原因是什么?
更新。在线程堆栈中,我们得到一个提示,它可能与我们自定义的 Azure Blob Log4net 附加程序有关,我发现当创建此类附加程序时(每个应用程序一次),它会生成具有以下结构的新线程。
while (true)
{
try
{
Flush(); // pseudocode
Thread.Sleep(10000);
}
catch(Exception)
{
}
}
不确定我是否理解为什么它会导致完全无法停止的线程(因为 ThreadAbortException
不会被 catch 停止),但看起来将 while (true)
更改为 while (!Environment.HasShutdownStarted && !_stopping)
可以解决问题(_stopping
在调用 Appender OnClose
时设置,这是 log4net 的一种正常关闭)...
这似乎是一个 JIT 错误。是的,JIT 中的 BUG!我发现那里记录了几乎相同的故事:http://labs.criteo.com/2017/04/ryujit-never-ending-threadabortexception/.
要演示该问题,您可以 运行 以下代码。仅适用于发布模式,仅适用于 x64 平台(我的目标是 .NET 4.5.2)。
除非您手动重新抛出异常,否则您将观察到记录的异常链是无穷无尽的。为什么它是 CLR/JIT 中的错误?因为 CLR/JIT 负责在设置线程的 AbortRequested 标志时在 "safe places" 中注入抛出 ThreadAbortException
。
Jeffrey Richter 引自 "CLR via C#"(以下代码违反):
Even when code catches the
ThreadAbortException
, the CLR doesn’t allow the exception to be swallowed. In other words, at the end of the catch block, the CLR automatically rethrows theThreadAbortException
exception.
还有 GitHub 中的错误:https://github.com/dotnet/coreclr/issues/16122。
static void Main(string[] args)
{
var mutex = new ManualResetEventSlim();
var t = new Thread(() =>
{
while (true)
{
try
{
if (!mutex.IsSet)
{
mutex.Set();
}
// Do some stuff
Thread.Sleep(100);
}
catch (Exception ex)
{
Console.WriteLine("Exception: " + ex.Message);
// the lines below FIX the issue
//if (ex is ThreadAbortException)
// throw;
}
// FIXES the issue as well
//Thread.Sleep(0);
}
});
t.Start();
// Wait for the thread to start
mutex.Wait();
t.Abort();
Console.ReadLine();
}