我有一个 ASP.NET MVC (.NET 4.8) 应用程序,它调用 ASP.NET Core 8 Web API 作为生成 PDF 文档的服务。我们称它们为“主要”和“服务”。
PDF 发布针对一个或多个国家循环完成,数据获取和制图完全同步。数据准备好后,我将针对每个国家/地区并行发送一个请求到 service 应用程序。
服务使用 Razor 生成 HTML,并使用 HTML 到 PDF 库 (IronPDF) 将其转换为 PDF。该文件作为响应发送回main应用程序。
重命名并省略不必要部分后的代码如下。
主要:
public async Task PublishDocuments(List<string> countries)
{
var requests = new Dictionary<string, PublishRequest>();
foreach (var country in countries)
{
// prepare data synchronously
// add data to requests Dictionary
}
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);
// more DB work
}
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) =>
{
// We are using HTML to PDF library, and rely on Razor for strict typing and intellisense in HTML
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:“发送请求时发生错误。”
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() > 在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(任务任务)
在 System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
在 PublishService.d__22.MoveNext() 中...
有一个内部例外:
底层连接已关闭:连接意外关闭。
在 System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
在 System.Net.Http.HttpClientHandler.GetResponseCallback(IAsyncResult ar)
在
GetDocument
:
将内容复制到流时出错。
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(任务任务)
在 System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
在 PublishService.d__22.MoveNext() 在 ...
有一个内在的例外:
无法从传输连接读取数据:连接已关闭。
在 System.Net.ConnectStream.IOError(异常异常,布尔值 willThrow)
在 System.Net.ConnectStream.InternalWrite(布尔异步,Byte[] 缓冲区,Int32 偏移量,Int32 大小,AsyncCallback 回调,对象状态)
在 System.Net.ConnectStream.BeginWrite(Byte[] 缓冲区,Int32 偏移量,Int32 大小,AsyncCallback 回调,对象状态)
在 System.Threading.Tasks.TaskFactory6 beginMethod、Func1.FromAsyncImpl[TArg1,TArg2,TArg3](Func
1 endAction、TArg1 arg1、TArg2 arg2、TArg3 arg3、对象状态、TaskCreationOptions 创建选项)2 endFunction, Action
在 System.Net.Http.ByteArrayContent.SerializeToStreamAsync(流 流,TransportContext 上下文)
在 System.Net.Http.HttpContent.CopyToAsync(流流,TransportContext 上下文)
我尝试了这些建议,其中大部分仍然存在于代码中:
增加
HttpClient
实例的超时:
httpClient.Timeout = TimeSpan.FromMinutes(15);
将
Expect-100
设置为 false(当它没有帮助时我将其删除):
System.Net.ServicePointManager.Expect100Continue = false;
将
HttpClient
传递到 GetDocument
,而不是每个请求创建一个 HttpClient
增加 service API 方法的请求超时:
.WithRequestTimeout(TimeSpan.FromMinutes(10));
我仍然有同样的行为。这全部在我的本地机器上。
有人知道我还能尝试什么吗?
当您用多个并发请求淹没它时,我怀疑您向其发送 HTTP 请求的第 3 方服务器出现故障。
您应该限制并发请求。
SemaphoreSlim
实现锁定机制的示例:
private const int MaxNumberOfConcurrentCalls = 3; // choose the appropriate number
private readonly SemaphoreSlim semaphore = new SemaphoreSlim(0, MaxNumberOfConcurrentCalls);
private async Task GetAndSaveFile(PublishRequest request, string fullFilePath, HttpClient httpClient)
{
await semaphore.WaitAsync();
try
{
var res = await GetDocument(request, httpClient);
File.WriteAllBytes(fullFilePath, res);
}
finally
{
semaphore.Release();
}
}