ASP.NET Identity 中的零星应用程序死锁
Sporadic application deadlock in ASP.NET Identity
我有一个 ASP.NET 4.7.2 MVC 5 应用程序,它使用 ASP.NET Identity 和 OWIN OAuthAuthorizationServerMiddleware
。在 RefreshTokenProvider.OnReceive
方法中,我正在访问 SignInManager.CreateUserIdentity
方法,它是一种 ASP.NET 身份方法,在内部使用 AsyncHelper
(见下文)来调用异步方法。每隔一段时间(在繁忙的系统上通常相隔数月),这会崩溃并锁定整个应用程序。从我收集的内存转储中,有 565 个线程在里面等待 GetResult
// Copyright (c) Microsoft Corporation, Inc. All rights reserved.
// Licensed under the MIT License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Identity
{
internal static class AsyncHelper
{
private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None,
TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
var cultureUi = CultureInfo.CurrentUICulture;
var culture = CultureInfo.CurrentCulture;
return _myTaskFactory.StartNew(() =>
{
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = cultureUi;
return func();
}).Unwrap().GetAwaiter().GetResult();
}
public static void RunSync(Func<Task> func)
{
var cultureUi = CultureInfo.CurrentUICulture;
var culture = CultureInfo.CurrentCulture;
_myTaskFactory.StartNew(() =>
{
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = cultureUi;
return func();
}).Unwrap().GetAwaiter().GetResult();
}
}
}
内存转储显示 580 多个任务,全部 处于 RanToCompletion
状态。鉴于任务已完成,我无法诊断为什么 GetResult
没有成功,也无法诊断为什么在几个月前工作时有那么多线程迅速堆积在那里,以及为什么整个应用程序变得无响应,即使它们不响应不要行使这条道路。这导致生产中断多次,除了重启之外我不知道如何解决这个问题。
我试过使用 OnReceiveAsync
方法,但这些方法似乎毫无意义,因为在调用它们之前,有这个小片段:
if (OnReceiveAsync != null && OnReceive == null)
{
throw new InvalidOperationException(Resources.Exception_AuthenticationTokenDoesNotProvideSyncMethods);
}
编辑:问题的再现和解释:
此 WebAPI 2 控制器可以重现该问题
using System.Threading.Tasks;
using System.Web.Http;
namespace WebApplication65.Controllers
{
public class ValuesController : ApiController
{
public string Post([FromBody]string value)
{
return AsyncHelper.RunSync(PostAsync);
}
private async Task<string> PostAsync()
{
await Task.Delay(10);
return "Hello World";
}
}
}
和这个程序生成负载
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace ConsoleApp36
{
class Program
{
static void Main(string[] args)
{
HttpClient client = new HttpClient();
MediaTypeHeaderValue mediaTypeHeaderValue = new MediaTypeHeaderValue("application/json");
var tasks = new List<Task>();
for (int i = 0; i < 100; i++)
{
int x = i;
var t = Task.Run(async () =>
{
var content = new StringContent("\"" + Guid.NewGuid().ToString() + "\"");
content.Headers.ContentType = mediaTypeHeaderValue;
await client.PostAsync("https://localhost:44371/api/values", content);
Console.WriteLine(x);
});
tasks.Add(t);
}
Task.WhenAll(tasks).Wait();
}
}
}
程序将在几毫秒内向应用程序发送 100 个请求。这将导致所有以前空闲的线程都停留在 AsyncHelper.RunSync
,并且有更多的请求排队并且根本没有发送任何响应。 ThreadPool 注意到它需要更多线程,但每秒只会添加大约一个线程,这会立即卡在 AsyncHelper.RunSync
处,试图为其中一个排队的请求提供服务。大约一分钟后,当 ThreadPool 扩展了大约 100 个额外的线程来服务 100 个请求时,所有待处理的请求将在眨眼间响应,应用程序再次响应。
我的应用程序的不同之处在于,请求不断传入,这与一次仅发送 100 个请求的示例不同。这意味着我的应用程序无法从这种情况中恢复,因为线程池创建新线程的速度不足以跟上传入的请求。
创建我的 ReceiveRefreshToken
方法的异步副本并在 OnReceive
之外填充 OnReceiveAsync
似乎足以避免问题,不会在 ThreadPool 上耗尽。
我认为应用程序耗尽了 ThreadPool。
AsyncHelper 使用 TaskScheduler.Default 这意味着在 ThreadPool 上执行。它应该可以正常工作,直到所有线程都被阻塞(例如,许多用户 and/or 来自令牌端点的响应缓慢)并且没有更多的空闲线程可以继续。这会导致死锁(您可以阅读更多相关内容 here)。由于应用程序只有一个线程池,如果用完就全部停止。
无论如何我都会尝试使用 OnReceiveAsync 并从这里以异步方式创建用户。您可能需要一个虚拟的 OnReceive 以避免异常。
或者,您可以尝试扩展 ThreadPool 的大小,但这只是一个短期解决方案,不能保证始终有效。
我有一个 ASP.NET 4.7.2 MVC 5 应用程序,它使用 ASP.NET Identity 和 OWIN OAuthAuthorizationServerMiddleware
。在 RefreshTokenProvider.OnReceive
方法中,我正在访问 SignInManager.CreateUserIdentity
方法,它是一种 ASP.NET 身份方法,在内部使用 AsyncHelper
(见下文)来调用异步方法。每隔一段时间(在繁忙的系统上通常相隔数月),这会崩溃并锁定整个应用程序。从我收集的内存转储中,有 565 个线程在里面等待 GetResult
// Copyright (c) Microsoft Corporation, Inc. All rights reserved.
// Licensed under the MIT License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Identity
{
internal static class AsyncHelper
{
private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None,
TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
var cultureUi = CultureInfo.CurrentUICulture;
var culture = CultureInfo.CurrentCulture;
return _myTaskFactory.StartNew(() =>
{
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = cultureUi;
return func();
}).Unwrap().GetAwaiter().GetResult();
}
public static void RunSync(Func<Task> func)
{
var cultureUi = CultureInfo.CurrentUICulture;
var culture = CultureInfo.CurrentCulture;
_myTaskFactory.StartNew(() =>
{
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = cultureUi;
return func();
}).Unwrap().GetAwaiter().GetResult();
}
}
}
内存转储显示 580 多个任务,全部 处于 RanToCompletion
状态。鉴于任务已完成,我无法诊断为什么 GetResult
没有成功,也无法诊断为什么在几个月前工作时有那么多线程迅速堆积在那里,以及为什么整个应用程序变得无响应,即使它们不响应不要行使这条道路。这导致生产中断多次,除了重启之外我不知道如何解决这个问题。
我试过使用 OnReceiveAsync
方法,但这些方法似乎毫无意义,因为在调用它们之前,有这个小片段:
if (OnReceiveAsync != null && OnReceive == null)
{
throw new InvalidOperationException(Resources.Exception_AuthenticationTokenDoesNotProvideSyncMethods);
}
编辑:问题的再现和解释:
此 WebAPI 2 控制器可以重现该问题
using System.Threading.Tasks;
using System.Web.Http;
namespace WebApplication65.Controllers
{
public class ValuesController : ApiController
{
public string Post([FromBody]string value)
{
return AsyncHelper.RunSync(PostAsync);
}
private async Task<string> PostAsync()
{
await Task.Delay(10);
return "Hello World";
}
}
}
和这个程序生成负载
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace ConsoleApp36
{
class Program
{
static void Main(string[] args)
{
HttpClient client = new HttpClient();
MediaTypeHeaderValue mediaTypeHeaderValue = new MediaTypeHeaderValue("application/json");
var tasks = new List<Task>();
for (int i = 0; i < 100; i++)
{
int x = i;
var t = Task.Run(async () =>
{
var content = new StringContent("\"" + Guid.NewGuid().ToString() + "\"");
content.Headers.ContentType = mediaTypeHeaderValue;
await client.PostAsync("https://localhost:44371/api/values", content);
Console.WriteLine(x);
});
tasks.Add(t);
}
Task.WhenAll(tasks).Wait();
}
}
}
程序将在几毫秒内向应用程序发送 100 个请求。这将导致所有以前空闲的线程都停留在 AsyncHelper.RunSync
,并且有更多的请求排队并且根本没有发送任何响应。 ThreadPool 注意到它需要更多线程,但每秒只会添加大约一个线程,这会立即卡在 AsyncHelper.RunSync
处,试图为其中一个排队的请求提供服务。大约一分钟后,当 ThreadPool 扩展了大约 100 个额外的线程来服务 100 个请求时,所有待处理的请求将在眨眼间响应,应用程序再次响应。
我的应用程序的不同之处在于,请求不断传入,这与一次仅发送 100 个请求的示例不同。这意味着我的应用程序无法从这种情况中恢复,因为线程池创建新线程的速度不足以跟上传入的请求。
创建我的 ReceiveRefreshToken
方法的异步副本并在 OnReceive
之外填充 OnReceiveAsync
似乎足以避免问题,不会在 ThreadPool 上耗尽。
我认为应用程序耗尽了 ThreadPool。
AsyncHelper 使用 TaskScheduler.Default 这意味着在 ThreadPool 上执行。它应该可以正常工作,直到所有线程都被阻塞(例如,许多用户 and/or 来自令牌端点的响应缓慢)并且没有更多的空闲线程可以继续。这会导致死锁(您可以阅读更多相关内容 here)。由于应用程序只有一个线程池,如果用完就全部停止。
无论如何我都会尝试使用 OnReceiveAsync 并从这里以异步方式创建用户。您可能需要一个虚拟的 OnReceive 以避免异常。
或者,您可以尝试扩展 ThreadPool 的大小,但这只是一个短期解决方案,不能保证始终有效。