【问题标题】:C# - HTTP Get run in parallel tasks randomly fails with: Error while copying content to streamC# - HTTP Get 在并行任务中随机运行失败:将内容复制到流时出错
【发布时间】:2021-03-24 21:25:09
【问题描述】:

在我的 C# 应用程序 (.NET Core 3.1) 中,有一个自动任务,每隔 X 小时启动另一个任务,该任务使用不同的参数并行运行多次。 在这个自动任务结束时,会调用 await Task.WhenAll(tasksList). 以等待并行任务完成。

每个任务都会发出一个 HTTPClient(使用 IHttpClientFactory 工厂方法)并发出一个 GET 请求,语法如下:

var res = await client.GetAsync(url);
if (res.IsSuccessStatusCode)
{
  var exit = await res.Content.ReadAsStringAsync();
  [...omitted]
}

当共享相同 GET URL 的两个任务在最长 60-70 毫秒的距离上运行时,问题随机发生。有时两个任务一个接一个地失败,每个任务都有同样的例外:

System.Net.Http.HttpRequestException: Error while copying content to a stream.
 ---> System.IO.IOException: The response ended prematurely.
   at System.Net.Http.HttpConnection.FillAsync()
   at System.Net.Http.HttpConnection.ChunkedEncodingReadStream.CopyToAsyncCore(Stream destination, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionResponseContent.SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken)
   at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)

从日志中,我可以看到服务器如何正确启动和接收两个不同的 HTTP 请求。

如果我删除 ReadAsStringAsync 部分,问题永远不会发生,所以我认为它与 内容读取(在状态码检查之后)有关,几乎就像两者任务结束共享 Get 结果(同时发出两个不同的活动连接)。我尝试使用 ReadAsStreamAsync,但问题仍然存在(不过,这有助于减少发生率)。

另一件可能相关的事情是检索到的结果非常重(我上次下载它时,它最终是一个 4.5MB 的 .json 文件,或多或少)。

我应该按顺序运行每个任务吗?还是我发出的 HTTP 请求有误?

如果您想测试此问题,您可以在此处找到我用来重现该问题的控制台应用程序的源代码(如果前 20 次调用未发生,请重新启动应用程序,直到它发生):

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static async Task<int> Main(string[] args)
        {
            var builder = new HostBuilder()
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHttpClient();
                    services.AddTransient<TaskRunner>();
                }).UseConsoleLifetime();

            var host = builder.Build();
            using (var serviceScope = host.Services.CreateScope())
            {
                var services = serviceScope.ServiceProvider;
                try
                {
                    var x = services.GetRequiredService<TaskRunner>();
                    var result = await x.Run();

                    Console.WriteLine(result);
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error Occured");
                }
            }

            return 0;
        }

        public class TaskRunner
        {
            private static IHttpClientFactory _httpFactory { get; set; }
            public TaskRunner(IHttpClientFactory httpFactory)
            {
                _httpFactory = httpFactory;
            }
            public async Task<string> Run()
            {
                Console.WriteLine("Starting loop...");
                do
                {
                    await Task.Delay(2500); // wait app loading
                    await SendRequest();
                } while (true);
            }
            private static async Task SendRequest()
            {
                await Task.WhenAll(new Task[] { ExecuteCall(), ExecuteCall()};
            }
            private async static Task<bool> ExecuteCall()
            {
                try
                {
                    var client = _httpFactory.CreateClient();
                    // fake heavy API call (> 5MB data)
                    var api = "https://api.npoint.io/5896085b486eed6483ce";
                    Console.WriteLine("Starting call at " + DateTime.Now.ToUniversalTime().ToString("o"));
                    var res = await client.GetAsync(api);
                    if (res.IsSuccessStatusCode)
                    {
                        var exit = await res.Content.ReadAsStringAsync();
                        /* STREAM read alternative
                        var ed = await res.Content.ReadAsStreamAsync();
                        StringBuilder result = new StringBuilder();
                        using var sr = new StreamReader(ed);
                        while (!sr.EndOfStream)
                        {
                            result.Append(await sr.ReadLineAsync());
                        }
                        var exit = result.ToString();
                        */
                        Console.WriteLine(exit.Substring(0, 10));
                        //Console.WriteLine(exit);
                        
                        Console.WriteLine("Ending call at " + DateTime.Now.ToUniversalTime().ToString("o"));
                        return true;
                    }
                    Console.WriteLine(res.StatusCode);
                        Console.WriteLine("Ending call at " + DateTime.Now.ToUniversalTime().ToString("o"));
                    return false;
                }
                catch (Exception ex)
                {
                    // put breakpoint here
                    // Exception => called on line:78 but if content isn't read it never occurs
                    Console.WriteLine(ex.ToString());
                    return false;
                }
            }
        }
    }
}

感谢您给我的任何帮助/建议!

【问题讨论】:

  • 服务器可能不允许同时来自同一客户端的两个连接,这会导致错误。不确定。或者您正在使用具有相同源和目标的相同端口号。也许在 80 和 8080 等并行连接上尝试不同的端口号。
  • 嗨@jdweng,奇怪的是这个问题很少随机发生 - 我解释自己:当我运行测试应用程序时,如果发生异常,下一个调用仍然通过(它不会再次发生)。同时,呼叫和内容似乎已完成并在失败时收到......我尝试了不同的端口技巧但没有运气。
  • 当两个连接发生的时间非常接近时,您认为会发生故障吗?这可能是一个已知的核心问题。如果您使用网络,也会发生同样的情况吗?请参阅:docs.microsoft.com/en-us/dotnet/core/compatibility/3.1
  • 听起来@jdweng 是对的,你可以通过每次创建一个新的cookie容器来避免它(这很糟糕,因为你可能需要重新授权)var cookieContainer = new CookieContainer(); using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer }) using (var client = new HttpClient(handler) { BaseAddress = baseAddress }) {}
  • 你应该建立你自己的服务器来测试这个代码,不要依赖一些在线测试服务器,因为这可能是由那个服务器(不是你的代码)引起的。这个错误看起来确实像这样。

标签: c# asp.net-core task-parallel-library asp.net-core-webapi dotnet-httpclient


【解决方案1】:

我正在回答我的问题,将我申请的解决方案留给可能遇到相同问题的任何人:)

我在 Api Call 之前添加了以下行:

var client = _httpFactory.CreateClient();
var api = "https://api.npoint.io/5896085b486eed6483ce";

>>> client.DefaultRequestVersion = HttpVersion.Version10; // new line

var res = await client.GetAsync(api);

这个问题似乎与端点服务器有关,当 HttpVersion 为 11 时,它会丢弃并发连接。它可能与 Keep-Alive Connection 标头有关,因为在 10 v 上,标头设置为 Close。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-04-10
    • 1970-01-01
    • 2016-01-18
    • 1970-01-01
    • 2018-11-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多