ASP.NET并行调用API遇连接意外关闭问题求助
场景说明
主应用为ASP.NET MVC(.NET 4.8),调用ASP.NET Core 8 Web API生成PDF文档。批量生成时先同步完成所有国家的数据准备与映射,再按国家并行发送请求到服务端;服务端通过Razor生成强类型HTML,借助IronPDF库转换为PDF后返回主应用。
核心代码
主应用代码
public async Task PublishDocuments(List<string> countries) { var requests = new Dictionary<string, PublishRequest>(); foreach (var country in countries) { // 同步准备数据并添加到requests字典 } using var httpClientHandler = new HttpClientHandler(); httpClientHandler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true; using var httpClient = new HttpClient(httpClientHandler); httpClient.Timeout = TimeSpan.FromMinutes(15); var publishTasks = requests.Select(x => GetAndSaveFile(x.Value, x.Key, httpClient)); await Task.WhenAll(publishTasks); // 后续数据库操作 } private async Task GetAndSaveFile(PublishRequest request, string fullFilePath, HttpClient httpClient) { var res = await GetDocument(request, httpClient); File.WriteAllBytes(fullFilePath, res); } private async Task<byte[]> GetDocument(PublishRequest request, HttpClient httpClient) { var serializedRequest = JsonConvert.SerializeObject(request); var content = new StringContent(serializedRequest, System.Text.Encoding.UTF8, "application/json"); var response = await httpClient.PostAsync("https://localhost:7000/publish", content); return await response.Content.ReadAsByteArrayAsync(); }
服务端代码
app.MapPost("/publish", async (PublishRequest request, HttpContext context) => { // 使用Razor生成强类型HTML,依赖IronPDF转PDF var htmlContent = await RenderRazorViewToStringAsync(context, "Document", request); var renderer = new ChromePdfRenderer(); renderer.RenderingOptions.EnableJavaScript = true; renderer.RenderingOptions.WaitFor.JavaScript(500); var pdf = renderer.RenderHtmlAsPdf(htmlContent); context.Response.ContentType = "application/pdf"; context.Response.StatusCode = 200; context.Response.Headers.Add("Content-Disposition", "attachment; filename=generated.pdf"); await context.Response.Body.WriteAsync(pdf.BinaryData); }).WithRequestTimeout(TimeSpan.FromMinutes(10));
异常信息
请求数量超过阈值时出现两类错误:
- PublishDocuments方法中抛出:
System.Net.Http.HttpRequestException: 'An error occurred while sending the request.'
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at PublishService.d__22.MoveNext() in...
内部异常:
The underlying connection was closed: The connection was closed unexpectedly.
at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
at System.Net.Http.HttpClientHandler.GetResponseCallback(IAsyncResult ar)
- GetDocument方法中抛出:
Error while copying content to a stream.
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at PublishService.d__22.MoveNext() in ...
内部异常:
Unable to read data from the transport connection: The connection was closed.
at System.Net.ConnectStream.IOError(Exception exception, Boolean willThrow)
at System.Net.ConnectStream.InternalWrite(Boolean async, Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state)
at System.Net.ConnectStream.BeginWrite(Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state)
at System.Threading.Tasks.TaskFactory1.FromAsyncImpl[TArg1,TArg2,TArg3](Func6 beginMethod, Func2 endFunction, Action1 endAction, TArg1 arg1, TArg2 arg2, TArg3 arg3, Object state, TaskCreationOptions creationOptions)
at System.Net.Http.ByteArrayContent.SerializeToStreamAsync(Stream stream, TransportContext context)
at System.Net.Http.HttpContent.CopyToAsync(Stream stream, TransportContext context)
已尝试无效方案
- 将HttpClient实例超时设置为15分钟;
- 曾设置
System.Net.ServicePointManager.Expect100Continue = false(无效后移除); - 复用HttpClient实例而非每个请求创建新实例;
- 服务端API方法设置10分钟请求超时。
可行解决办法
1. 限制并行请求数量
.NET 4.8中ServicePointManager.DefaultConnectionLimit默认值为2,批量并行请求会耗尽连接池,引发连接异常。可以:
- 全局调整连接限制(比如在Global.asax的
Application_Start中):System.Net.ServicePointManager.DefaultConnectionLimit = 20; // 根据机器性能调整 - 使用
SemaphoreSlim控制并发请求数,避免瞬间压垮服务端:// 在PublishDocuments方法中修改任务创建逻辑 var semaphore = new SemaphoreSlim(10); // 限制同时10个请求 var publishTasks = requests.Select(async x => { await semaphore.WaitAsync(); try { await GetAndSaveFile(x.Value, x.Key, httpClient); } finally { semaphore.Release(); } }); await Task.WhenAll(publishTasks);
2. 优化服务端资源占用
IronPDF的ChromePdfRenderer每次实例化会启动Chrome进程,批量请求会消耗大量CPU、内存:
- 复用全局单例的
ChromePdfRenderer实例,减少资源开销:// 在服务端启动类中定义全局实例 private static readonly ChromePdfRenderer _renderer = new ChromePdfRenderer(); static YourStartupClass() { _renderer.RenderingOptions.EnableJavaScript = true; _renderer.RenderingOptions.WaitFor.JavaScript(500); } // 在接口中复用该实例 app.MapPost("/publish", async (PublishRequest request, HttpContext context) => { var htmlContent = await RenderRazorViewToStringAsync(context, "Document", request); var pdf = _renderer.RenderHtmlAsPdf(htmlContent); // 响应写入逻辑 }).WithRequestTimeout(TimeSpan.FromMinutes(10));
3. 规范服务端响应处理
替换直接写入Response.Body的方式,使用ASP.NET Core内置的Results.File返回,确保响应完整发送:
app.MapPost("/publish", async (PublishRequest request, HttpContext context) => { var htmlContent = await RenderRazorViewToStringAsync(context, "Document", request); var renderer = new ChromePdfRenderer(); renderer.RenderingOptions.EnableJavaScript = true; renderer.RenderingOptions.WaitFor.JavaScript(500); var pdf = renderer.RenderHtmlAsPdf(htmlContent); return Results.File(pdf.BinaryData, "application/pdf", "generated.pdf"); }).WithRequestTimeout(TimeSpan.FromMinutes(10));
4. 启用HTTP/2优化连接
.NET 4.8的HttpClient支持HTTP/2,启用后可提升并发请求处理效率:
// 在主应用创建HttpClientHandler时配置 var httpClientHandler = new HttpClientHandler(); httpClientHandler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true; httpClientHandler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13; httpClientHandler.UseHttp2 = true;
5. 添加错误重试机制
针对偶发的连接异常,使用Polly库实现重试逻辑:
// 安装Polly NuGet包后配置重试策略 var retryPolicy = Policy .Handle<HttpRequestException>() .Or<IOException>() .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); // 在GetDocument方法中应用策略 private async Task<byte[]> GetDocument(PublishRequest request, HttpClient httpClient) { return await retryPolicy.ExecuteAsync(async () => { var serializedRequest = JsonConvert.SerializeObject(request); var content = new StringContent(serializedRequest, System.Text.Encoding.UTF8, "application/json"); var response = await httpClient.PostAsync("https://localhost:7000/publish", content); response.EnsureSuccessStatusCode(); // 触发重试条件 return await response.Content.ReadAsByteArrayAsync(); }); }
内容的提问来源于stack exchange,提问作者dzenesiz

