HttpClient 502 错误网关仅异步

HttpClient 502 Bad Gateway Async only

我在异步调用 Web 服务时遇到问题。我在一个循环中同步调用它数百次而没有错误,但是当我异步调用它时,我得到一个 502 Bad Gateway 错误,通常是在大约 250 次异步调用之后。如您所见,我已经尝试使用 Thread.Sleep() 等待 50 毫秒,然后每条记录最多重试 5 次。当我无法验证记录时,预计会出现 404 Not Found 错误。我的框架是netstandard2.0.

class Program
    {
        static void Main(string[] args)
        {
            List<OriginalRecord> originalRecords = new List<OriginalRecord>();
            originalRecords.Add(new OriginalRecord()
            {
                Id = "IdOne",
                Foo = "fooOne"
            });
            originalRecords.Add(new OriginalRecord()
            {
                Id = "IdTwo",
                Foo = "fooTwo"
            });

            List<RecordValidation> unvalidatedRecords = new List<RecordValidation>();

            try
            {
                RecordValidator validator = new RecordValidator("myAccessToken", "myBarId");
                var task = validator.ValidateRecordsAsync(originalRecords.Select(r => r.Id).ToList());
                task.Wait();
                unvalidatedRecords = task.Result;
            }
            catch
            {
                throw;
            }
        }
    }
    public class RecordValidator
    {
        public RecordValidator(string AccessToken, string BarId)
        {
            httpClient = new HttpClient();
            httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd("MessageService/3.1");

            httpClient.DefaultRequestHeaders.Authorization =
                new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", AccessToken);

            httpClient.DefaultRequestHeaders.Add("Accept", "application/hal+json");
            httpClient.DefaultRequestHeaders.ExpectContinue = false;

            barId = BarId;
        }

        private static HttpClient httpClient { get; set; }
        
        public string barId { get; set; }

        public async Task<List<RecordValidation>> ValidateRecordsAsync(List<string> Records)
        {
            List<Task<RecordValidation>> tasks = new List<Task<RecordValidation>>();

            foreach (var record in Records)
            {
                tasks.Add(Task.Run(() => ValidateRecordAsync(record)));
            }

            var results = await Task.WhenAll(tasks);
            return results.Where(r => !r.Validated).ToList();
        }

        private async Task<RecordValidation> ValidateRecordAsync(string Record)
        {
            string url = $"https://data.somewebapp.com/api/bar/{barId}/record/{Uri.EscapeDataString(Record)}";
            int retries = 0;
            bool success = false;
            HttpResponseMessage response = new HttpResponseMessage();
            try
            {

                do
                {
                    response = await httpClient.GetAsync(url);
                    if (response.IsSuccessStatusCode)
                    {
                        success = true;
                    }
                    else
                    {
                        Thread.Sleep(50);
                        retries++;
                    }
                } while (retries < 5 && !success);


                response.EnsureSuccessStatusCode();

                string responseString = await response.Content.ReadAsStringAsync();

                var output = JsonConvert.DeserializeObject<RecordValidation>(responseString);

                if (!string.IsNullOrEmpty(output.RecordId))
                    output.Validated = true;
                return output;
            }

            catch (HttpRequestException ex) when (ex.Message == "Response status code does not indicate success: 404 (Not Found).")
            {
                return new RecordValidation()
                {
                    RecordId = Record,
                    Validated = false
                };
            }

            catch
            {
                throw;
            }
        }
    }
    public class RecordValidation
    {
        public RecordValidation()
        {
            RecordId = string.Empty;
            Validated = false;
        }

        [JsonProperty("id")]
        public string RecordId { get; set; }

        public bool Validated { get; set; }
    }

    public class OriginalRecord
    {
        public string Id { get; set; }
        public string Foo { get; set; }
    }
}

502 响应是实际响应。您的 HTTP 请求成功,服务器返回 502 响应。这可能是因为您访问服务器的速度太快太用力,导致您无法访问。

当您同步执行时,它会发送请求并等待响应,然后再继续下一个请求。

但是在这个循环中:

foreach (var record in Records)
{
    tasks.Add(Task.Run(() => ValidateRecordAsync(record)));
}

您正在为每个请求创建一个新线程,其效果是几乎同时发送每个请求。显然服务器不喜欢那样。

您不需要为每个请求创建一个新线程。您可以在没有 Task.Run() 的情况下异步启动它们,例如:

foreach (var record in Records)
{
    tasks.Add(ValidateRecordAsync(record));
}

这只会减慢一点点,但不会太大。这将在同一个线程上 运行 ValidateRecordAsync() 直到发送 HTTP 请求,然后移动到循环的下一次迭代并开始下一次。但这对于服务器来说可能仍然太快,因为在上一个请求完成之前您仍在发送所有请求。

您可能不得不限制您的请求。如果你想让它尽可能快,你可以找出网络服务允许你发送多少请求,你可以为网络服务定制你的代码。否则,你只是在猜测。

您可以一次发送 10 个,然后在中间添加一个暂停。

foreach (var record in Records)
{
    tasks.Add(ValidateRecordAsync(record));
    if (tasks.Count % 10 == 0) await Task.Delay(100);
}

或者等到这 10 个完成后再继续:

foreach (var record in Records)
{
    tasks.Add(ValidateRecordAsync(record));
    if (tasks.Count % 10 == 0) await Task.WhenAll(tasks);
}

您可以对同一个任务列表多次调用 Task.WhenAll

250 个并行调用太多了。它应该是 10-30。 C# Http 客户端的默认最大并发请求数是 10,因此如果您不覆盖该值,尝试同时打开超过 10 个套接字连接是没有意义的。另外,请记住,502 状态很可能是由反向代理返回的,这意味着您并没有真正访问网络 API,代理告诉您“放慢速度,我无法再处理了”。所以在你的情况下,这不是真正的 HttpClient 问题,否则,你会得到一些异常,比如 SocketException 由于套接字耗尽,你可能不会因为 HttpClient 限制并发到 10。另外,你有很多线程阻塞代码,比如 Thread.Sleep()task.Wait(),将你的主要方法签名更改为 static async Task Main() 并在任何地方使用 await,否则,你会运行 进入其他类型的问题,例如线程耗尽