【发布时间】:2020-06-27 16:13:27
【问题描述】:
我正在使用 MS Graph API 将数百万用户从本地 AD 迁移到 Azure AD B2C,以在 B2C 中创建用户。我编写了一个 .Net Core 3.1 控制台应用程序来执行此迁移。为了加快速度,我正在对 Graph API 进行并发调用。这工作得很好 - 有点。
在开发过程中,我从 Visual Studio 2019 运行时体验到了可接受的性能,但对于测试,我是从 Powershell 7 中的命令行运行的。从 Powershell 并发调用 HttpClient 的性能非常糟糕。从 Powershell 运行时,HttpClient 允许的并发调用数量似乎存在限制,因此大于 40 到 50 个请求的并发批次中的调用开始堆积。它似乎正在运行 40 到 50 个并发请求,同时阻止其余请求。
我不是在寻求异步编程方面的帮助。我正在寻找一种方法来解决 Visual Studio 运行时行为和 Powershell 命令行运行时行为之间的差异。从 Visual Studio 的绿色箭头按钮在发布模式下运行的行为符合预期。从命令行运行不会。
我用异步调用填充任务列表,然后等待 Task.WhenAll(tasks)。每次调用需要 300 到 400 毫秒。从 Visual Studio 运行时,它按预期工作。我同时进行了 1000 个呼叫,每个呼叫都在预期时间内单独完成。整个任务块只比最长的单个调用长几毫秒。
当我从 Powershell 命令行运行相同的构建时,行为会发生变化。前 40 到 50 次调用预计需要 300 到 400 毫秒,但随后各个调用时间会增加到 20 秒。我认为这些调用正在序列化,因此一次只执行 40 到 50 个调用,而其他调用则在等待。
经过数小时的反复试验,我能够将其范围缩小到 HttpClient。为了隔离问题,我使用一个执行 Task.Delay(300) 并返回模拟结果的方法模拟了对 HttpClient.SendAsync 的调用。在这种情况下,从控制台运行与从 Visual Studio 运行的行为相同。
我正在使用 IHttpClientFactory,我什至尝试调整 ServicePointManager 上的连接限制。
这是我的注册码。
public static IServiceCollection RegisterHttpClient(this IServiceCollection services, int batchSize)
{
ServicePointManager.DefaultConnectionLimit = batchSize;
ServicePointManager.MaxServicePoints = batchSize;
ServicePointManager.SetTcpKeepAlive(true, 1000, 5000);
services.AddHttpClient(MSGraphRequestManager.HttpClientName, c =>
{
c.Timeout = TimeSpan.FromSeconds(360);
c.DefaultRequestHeaders.Add("User-Agent", "xxxxxxxxxxxx");
})
.ConfigurePrimaryHttpMessageHandler(() => new DefaultHttpClientHandler(batchSize));
return services;
}
这是 DefaultHttpClientHandler。
internal class DefaultHttpClientHandler : HttpClientHandler
{
public DefaultHttpClientHandler(int maxConnections)
{
this.MaxConnectionsPerServer = maxConnections;
this.UseProxy = false;
this.AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate;
}
}
这是设置任务的代码。
var timer = Stopwatch.StartNew();
var tasks = new Task<(UpsertUserResult, TimeSpan)>[users.Length];
for (var i = 0; i < users.Length; ++i)
{
tasks[i] = this.CreateUserAsync(users[i]);
}
var results = await Task.WhenAll(tasks);
timer.Stop();
这是我模拟 HttpClient 的方式。
var httpClient = this.httpClientFactory.CreateClient(HttpClientName);
#if use_http
using var response = await httpClient.SendAsync(request);
#else
await Task.Delay(300);
var graphUser = new User { Id = "mockid" };
using var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(JsonConvert.SerializeObject(graphUser)) };
#endif
var responseContent = await response.Content.ReadAsStringAsync();
以下是通过 GraphAPI 使用 500 个并发请求创建的 10k B2C 用户的指标。前 500 个请求比正常时间长,因为正在创建 TCP 连接。
这是console run metrics的链接。
这是Visual Studio run metrics的链接。
VS 运行指标中的阻塞时间与我在这篇文章中所说的不同,因为我将所有同步文件访问移至进程末尾,以尽可能隔离有问题的代码以进行测试运行.
项目使用 .Net Core 3.1 编译。我正在使用 Visual Studio 2019 16.4.5。
【问题讨论】:
-
您是否查看了第一批后与 netstat 实用程序的连接状态?它可能会提供一些关于前几项任务完成后发生的情况的见解。
-
如果您最终没有以这种方式解决它(异步 HTTP 请求),您始终可以在 ConcurrentQueue[object] 消费者/生产者并行机制中为每个用户使用同步 HTTP 调用。我最近在 PowerShell 中处理了大约 2 亿个文件。
-
@thepip3r 我刚刚重新阅读了您的推荐并理解了它。我会记住这一点的。
-
不,如果你想使用 PowerShell 而不是 c#:leeholmes.com/blog/2018/09/05/…。
-
@thepip3r 只需阅读 Stephen Cleary 的博客条目。我应该很好。
标签: c# visual-studio powershell .net-core httpclient