【问题标题】:Improve performance of Async Post using C# HttpClient使用 C# HttpClient 提高 Async Post 的性能
【发布时间】:2020-04-21 05:27:21
【问题描述】:

我正在努力寻找方法来进一步提高我的控制台应用程序的性能(已经完全正常工作)。

我有一个 CSV 文件,其中包含地址列表(大约 100k)。 我需要查询一个 Web API,其 POST 响应将是这些地址的地理坐标。然后我将用地理坐标(纬度和经度)丰富的地址数据将 GeoJSON 文件写入文件系统。

我当前的解决方案将数据拆分为 1000 条记录的批次,并使用 HttpClient(带有控制台应用程序的 .NET core 3.1 和使用 .NET Standard 2.0 的类库)向 Web API 发送异步 POST 请求。 GeoJSON 是我的 DTO 类。

public class GeoJSON
    {
        public string Locality { get; set; }
        public string Street { get; set; }
        public string StreetNumber { get; set; }
        public string ZIP { get; set; }
        public string Latitude { get; set; }
        public string Longitude { get; set; }
    }


public static async Task<List<GeoJSON>> GetAddressesInParallel(List<GeoJSON> geos)
        {
            //calculating number of batches based on my batchsize (1000)
            int numberOfBatches = (int)Math.Ceiling((double)geos.Count() / batchSize);

            for (int i = 0; i < numberOfBatches; i++)
            {
                var currentIds = geos.Skip(i * batchSize).Take(batchSize);
                var tasks = currentIds.Select(id => SendPOSTAsync(id));
                geoJSONs.AddRange(await Task.WhenAll(tasks));
            }

            return geoJSONs;
        }

我的异步 POST 方法如下所示:

 public static async Task<GeoJSON> SendPOSTAsync(GeoJSON geo)
        {
            string payload = JsonConvert.SerializeObject(geo);
            HttpContent c = new StringContent(payload, Encoding.UTF8, "application/json");
            using HttpResponseMessage response = await client.PostAsync(URL, c).ConfigureAwait(false);

            if (response.IsSuccessStatusCode)
            {
                var address = JsonConvert.DeserializeObject<GeoJSON>(await response.Content.ReadAsStringAsync());
                geo.Latitude = address.Latitude;
                geo.Longitude = address.Longitude;
            }
            return geo;
        }

Web API 作为自托管 x86 应用程序在我的本地计算机上运行。 整个应用程序在不到 30 秒内结束。 最耗时的部分是 Async POST 部分(大约 25 秒)。 Web API 每篇文章只需要一个地址,否则我会在一个请求中发送多个地址。

关于如何提高针对 Web API 的请求性能的任何想法?

【问题讨论】:

标签: c# performance asynchronous post request


【解决方案1】:

您的批处理方法的一个潜在问题是单个延迟响应可能会延迟整个批处理的完成。这可能不是一个实际问题,因为您正在调用的 Web 服务可能具有非常一致的响应时间,但无论如何您都可以尝试一种替代方法,该方法允许在不使用批处理的情况下控制并发。下面的示例使用了 TPL Dataflow 库,它内置在 .NET Core 平台中,可作为 .NET Framework 的 package

public static async Task<List<GeoJSON>> GetAddressesInParallel(List<GeoJSON> geos)
{
    var block = new ActionBlock<GeoJSON>(async item =>
    {
        await SendPOSTAsync(item);
    }, new ExecutionDataflowBlockOptions()
    {
        MaxDegreeOfParallelism = 1000
    });

    foreach (var item in geos)
    {
        await block.SendAsync(item);
    }
    block.Complete();

    await block.Completion;
    return geos;
}

您的SendPOSTAsync 方法只返回作为参数接收的相同GeoJSON,因此GetAddressesInParallel 也可以返回作为参数接收的相同List&lt;GeoJSON&gt;

ActionBlock 是库中可用的最简单的块。它只是为每个项目执行同步或异步操作,允许配置 MaxDegreeOfParallelism 以及其他选项。您还可以尝试将工作流程拆分为多个块,然后将它们链接在一起以形成管道。例如:

  1. TransformBlock&lt;GeoJSON, (GeoJSON, string)&gt;GeoJSON 对象序列化为 JSON。
  2. TransformBlock&lt;(GeoJSON, string), (GeoJSON, string)&gt; 发出 HTTP 请求。
  3. ActionBlock&lt;(GeoJSON, string)&gt; 反序列化 HTTP 响应并使用接收到的值更新 GeoJSON 对象。

这样的安排可以让你微调每个块的MaxDegreeOfParallelism,并有望达到最佳性能。

【讨论】:

  • 当我尝试您的建议时,应用程序运行了大约 1 分钟。使用我原来的解决方案大约需要 30 秒。我尝试了 MaxDegreeOfParallelism 的不同值,但无法真正改善它。
  • @fpsanti 感谢您的反馈。请记住,此解决方案的MaxDegreeOfParallelism 配置是不变的,即从开始到结束(除了从一开始到结束),您不断有 1000 个活动任务。批处理解决方案的并行度是可变的,因为它以 1000 开始每个批处理,然后逐渐减少到零。很难知道平均 DOP,但我们可以假设它约为最大值的一半。因此,由于批处理解决方案的最佳值是 300,因此该解决方案的最佳值应该在 150 左右。
  • 我尝试了 150 和 100 但与我原来的解决方案相比它仍然相当慢。我错过了什么吗?
  • 这很奇怪。使用该库的开销很小,而且不应该引起注意。您可以尝试添加选项BoundedCapacity = 1000 吗?保持块内部队列的大小可能会有所帮助。
  • 它仍然比原始代码慢 3 倍左右。
【解决方案2】:

上面的答案可能是正确的,但是这种依赖不是必须的。您可以使用 Task.WhenAll 。此代码来自不同的 Rest 库,但概念相同:

var tasks = new List<Task<Response<Person>>>();
const int maxCalls = 100;

Parallel.For(0, maxCalls, (i) =>
{
    var client = clientFactory.CreateClient();
    tasks.Add(client.GetAsync<Person>(new Uri("JsonPerson", UriKind.Relative)));
});

var results = await Task.WhenAll(tasks);

客户端以 100 倍并行创建和请求。然后并行等待所有任务。这意味着所有可用的资源都被利用了。

Full code

【讨论】:

  • 这可能会给网络服务器带来太大的压力。它还可能触发一些反 DOS 攻击机制。 throttle I/O 操作更友好、更安全。
  • 你能详细说明一下吗?
  • 正如其他人在您提到的帖子的相同答案中指出的那样 - 最大并行度可能是一个不错的方法:docs.microsoft.com/en-us/dotnet/api/…。那里的 SemaphorSlim 似乎有点矫枉过正。
  • 您的回答会立即启动所有任务。这将导致 100,000 个几乎同时的 Web 请求。一个请求和下一个请求之间的唯一延迟是序列化有效负载的时间,这应该是微不足道的。
  • 关于Parallel+async结合出现的问题,这里是本站众多相关问题之一:Parallel foreach with asynchronous lambda
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-01-04
  • 1970-01-01
  • 1970-01-01
  • 2021-08-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多