【问题标题】:Reasons for stuck AppDomain UnloadAppDomain Unload 卡住的原因
【发布时间】:2018-04-25 18:16:00
【问题描述】:

我仍在尝试了解持续存在的问题,但它几乎可以概括为无法卸载 AppDomain

它发生在将 ASP.NET WebAPI 部署到 Azure 应用服务的过程中,我们观察到以下情况:

  1. 进程 ID 不变,新部署托管在同一个进程中(AFAIU 是通过卸载旧 AppDomain 并使用更新的二进制文件启动新 AppDomain 来完成的)
  2. Azure PaaS 诊断在错误部分显示以下内容:

"在 w3wp_12396.dmp 中,应用程序的 HttpRuntime /LM/W3SVC/1523308129/ROOT 正在关闭。”

  1. 分析内存转储,我们看到设置了 IsAbortRequested 标志的线程,但它们似乎从未完成(WinDbg !threads 的输出:https://pastebin.com/7CXYcffy

  2. 在内存转储中,我们还看到许多带有“UNLOAD_REQUESTED”阶段的 AppDomain,它们似乎从未完成卸载(!DumpDomain 的完整输出在这里:https://pastebin.com/kahZQuWN

域 7:000001c67062c800 低频堆:000001c67062cff8 高频堆:000001c67062d088 存根堆:000001c67062d118 阶段:UNLOAD_REQUESTED 安全描述符:000001c6705c5680 名称:/LM/W3SVC/1523308129/ROOT-6-131687140950004974
  1. 未检测到死锁(至少通过 WinDbg SOSEX 插件的 !dlk 命令,通常涵盖大多数死锁情况)

  2. 没有代码取消线程中止(没有调用Thread.ResetAbort()

我们现在解决问题的唯一方法是终止进程(停止 Azure AppService)。

AppDomain 无法卸载的可能原因有哪些?

更新。在线程堆栈中,我们得到一个提示,它可能与我们的自定义 Azure Blob Log4net appender 有关,我发现当创建此类 appender(每个应用一次)时,它会产生具有以下结构的新线程。

while (true)
{
   try
   {
        Flush(); // pseudocode
        Thread.Sleep(10000);
   }
   catch(Exception)
   {
   }
}

我不确定为什么它会导致完全无法停止的线程(因为ThreadAbortException 不会被catch 停止),但看起来将while (true) 更改为while (!Environment.HasShutdownStarted && !_stopping) 解决了这个问题(_stopping 在 Appender 时设置OnClose 被调用,这是 log4net 的一种优雅关闭)...

【问题讨论】:

  • AppDomain.Unload 非常棘手,CLR 必须对仍在该域中执行代码的任何线程执行“粗鲁中止”。这实际上并没有那么粗鲁,线程必须在一个安全的地方,这样杀死它才能有一个好的结局。您遇到的第一个问题是该域中仍然有线程在执行代码,这根本不是一个好的情况。您遇到的下一个问题是您不知道这些线程在做什么,隐含不知道它们在做什么。只有非托管调试器才能向您显示麻烦的堆栈跟踪。
  • @HansPassant,感谢您,如此著名的 SO 世界人物,提供您的意见。请查看我刚刚添加的答案。该问题似乎是由讨厌的 JIT 错误引起的,可以通过下面答案中的小代码 sn-p 轻松重现。
  • 您只是看到了 CLR 必须执行哪些操作才能在一个不容易处理的线程上执行中止。它反复尝试,希望在执行托管代码或调用可警告的 OS 函数时最终命中它。可报警的 OS 函数是允许 QueueUserApc() 执行的函数。像 SleepEx()。 CLR 非常努力地提供这种保证。但这并不是说,反恶意软件是反的不能给它带来困难。您只能通过非托管调试器看到这样的操作系统功能。
  • @HansPassant,这是有道理的,感谢您提供深入的解释性参考!但是来吧,你有没有想过ThreadAbortException也就是claimed by MSDN会被自动重新抛出而不是被重新抛出? :)
  • 粗鲁的中止不使用 TAE。请一位不怕粗鲁和不受管理的调试的团队成员来帮助您。

标签: .net azure appdomain azure-web-app-service domainunload


【解决方案1】:

这似乎是一个 JIT 错误。是的,JIT 中的错误!我在那里发现了几乎相同的故事:http://labs.criteo.com/2017/04/ryujit-never-ending-threadabortexception/

为了演示该问题,您可以运行以下代码。仅适用于发布模式,仅适用于 x64 平台(我的目标是 .NET 4.5.2)。

除非您手动重新抛出异常,否则您将观察到无穷无尽的异常链记录。为什么它是 CLR/JIT 中的错误?因为 CLR/JIT 负责在设置线程的 AbortRequested 标志时将 throw ThreadAbortException 注入到“安全位置”。

引用 Jeffrey Richter 来自“CLR via C#”(违反以下代码):

即使代码捕获了ThreadAbortException,CLR 也不允许 被吞下的例外。换句话说,在捕获结束时 块,CLR 自动重新抛出 ThreadAbortException 例外。

还有 GitHub 中的 bug: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();
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-06
    • 2018-10-14
    • 1970-01-01
    • 1970-01-01
    • 2019-10-24
    相关资源
    最近更新 更多