【问题标题】:Prevent IIS from killing a Task before it ends防止 IIS 在任务结束之前杀死它
【发布时间】:2015-06-19 00:05:01
【问题描述】:

我正在构建一个将所有内容存储在 Azure 表中的日志库。写入该表显然需要很多时间(从不超过 1 秒,但仍然太多让用户等待),所以 Log 方法返回一个 LogResult 实例,这是类

public class LogResult
{
    public string Id { get; set; }
    public Task LoggingTask { get; set; }

    public LogResult(string id, Task task)
    {
        Id = id;
        LoggingTask = task;
    }
}

下面是 Log 方法的完成方式

return new LogResult(id, Task.Factory.StartNew(() => 
    DoLogInAzure(account, id, exception, request))
);

为调用者提供等待它完成的选项(例如,如果它是控制台应用程序)。我面临的问题是 IIS 不应该在返回用户响应之前等待它......如果我不等待它,IIS 不会 总是 执行任务。这个想法是向用户显示一条消息“......如果您与我们联系,请务必提及您的问题编号,XXX”,并且不要让他等到写入日志条目。

有没有办法强制 IIS 等到任务完成,即使它返回了响应?我想我可能需要编写一个异步接收请求的 Windows 服务,但看起来只是添加一个日志条目需要做很多工作......特别是如果我可以强制 IIS 等待它的话。

感谢您的任何想法!

【问题讨论】:

  • 我听说 IIS 会重新启动占用太多内存的“网站”...
  • 我明白 :) 但是,我不认为这是我的情况,好像我等待任务结束,IIS 完成执行并且日志条目被写入 100% 次跨度>
  • 作为替代方案,您能否仅使用 azure 队列登录。然后一个单独的消息处理程序可以异步写入 azure 表。更好的是,如果您在处理过程中遇到错误,消息仍保留在队列中,稍后将再次处理。
  • @usr:Id 可以是 Guid 或增量数字。如果是Guid,则生成不花时间,logger返回Id,然后将数据存储在后台。我确实需要向用户显示 ID。
  • @ChrisChilvers 是的... Windows 服务或辅助角色可以解决问题,但实际上... 应用程序拥有的组件越多,您需要在新版本中更新的内容就越多,更多的事情会出错......如果没有其他办法,我会选择这条路......但我想把它作为最后的手段

标签: .net iis task-parallel-library


【解决方案1】:

来自 Phil Hack 的This post 谈到在 ASP.NET 应用程序中运行后台任务。

【讨论】:

  • 我刚刚读完那篇文章...谢谢,这确实是我需要的东西。但是,它并不能解释为什么我的任务并不总是被执行......我在我的机器上运行它,如果我不等待任务完成,在一分钟内生成 100 个异常存储大约 92 个。应用程序池也没有重新启动...这有意义吗?
  • 你是如何产生异常的?当您等待时,您只需冻结正在运行的线程,直到所有任务完成,但如果进程仍在运行,则不需要这样做。进程是否在任务实际运行之前停止?在每个任务中使用模拟逻辑(例如:打印到调试窗口)以确保所有任务都有时间开始和完成。
  • 我现在正在处理的异常是 b/c 我在实体上的不可为空的字符串上分配 null ...然后,context.SaveChanges(); 失败。但是,如果我生成一个异常并且不做任何其他事情,它总是被存储。使其失败的方法是在短时间内产生大量异常。我猜想 IIS 可能会重用线程来处理新请求,这就是我的任务被中止的时候。我在不到 10 秒的时间内完成了 100 个请求(会引发异常),这就是它失败的时候。
  • 谢谢达米安!!!我想我的猜测是正确的......看起来 IIS 正在重用线程来处理传入的请求(我在短期内做了很多)然后,处理我的任务的线程被终止了。使用HostingEnvironment.RegisterObject() 我已经能够通知 IIS 我正在该线程上做一些工作,现在我什至没有丢失一个日志条目。谢谢,您解决了一个对我从事的许多项目有重大影响的问题。谢谢
【解决方案2】:

感谢 Damian Schenkelman 和 Phil Haack 的博文,我找到了问题和解决方案。问题是 IIS 在需要处理新请求时会重用线程。而且由于它不知道我的任务正在做一些工作,所以它重用了那个线程(这是有道理的)。然后,我只需要通知 IIS 我正在使用该线程并且它不能被重用(因此,它必须重用另一个线程,创建一个新线程,或者让它等待)。我最终使用了我自己的 TaskFactory 来处理任务创建,并在 IIS 中自动注册一个通知程序。为了完整起见,为了帮助与我有同样问题的其他人,并阅读其他建议,这就是我所做的

public class IISNotifier : IRegisteredObject
{
    public void Stop(bool immediate)
    {
        // do nothing, I only run tasks if I know that they won't
        // take more than a few seconds.
    }

    public void Started()
    {
        HostingEnvironment.RegisterObject(this);
    }

    public void Finished()
    {
        HostingEnvironment.UnregisterObject(this);
    }
}

然后

public class IISTaskFactory
{
    public static Task StartNew(Action act)
    {
        IISNotifier notif = new IISNotifier();
        notif.Started();
        return Task.Factory.StartNew(() => {
            act.Invoke();
            notif.Finished();
        });
    }
}

现在,当我想启动一个日志任务时,我只是这样做

return new LogResult(id, IISTaskFactory.StartNew(() => 
    DoLogInAzure(account, id, exception, request))
);

您可以在https://github.com/gmc-dev/IISTask查看(并下载代码)

【讨论】:

    【解决方案3】:

    信息还不够,但我怀疑它可能与 GC 和引用有关,如果它在您等待任务时有效。出于您的目的,更好的方法是使用 ETW (EventProvider) 并为每个请求设置 ActivityId。只需配置一个 ETW 会话就可以将所有消息重定向到一个文件。您可以向最终用户显示 ActivityId(一个 Guid)。

    【讨论】:

    • Task 即使没有引用它也会运行良好,这不是问题。
    • 为了检查这是否是问题所在,我将所有对 Task 的引用存储在一个单例实例中......但是,这并没有改变它的行为......到目前为止,唯一有效的是在检索响应之前等待任务
    【解决方案4】:

    很抱歉没有将此作为评论添加,我没有足够的代表。

    https://msdn.microsoft.com/en-us/library/system.web.hosting.iregisteredobject(v=vs.110).aspx

    应用程序只能有一个注册类型的实例。

    这似乎表明 Gervasio Marchand 接受的答案有些不正确,因为每次调用他的静态帮助方法都会创建一个新的 IISNotifier,即 IRegisteredObject。

    【讨论】:

    • 你能找到这个解决方案不起作用的例子吗?我很想看到这样的东西,因为我在很多项目中都使用过这种方法,从未见过任何奇怪的东西
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-06-15
    相关资源
    最近更新 更多