批量下载25k个6字节小文件速度过慢,求优化方案
问题描述
我使用HttpClient尝试下载约25000个6字节大小的文件到内存中,以此生成对应的SHA256密钥。尽管已经实现了并行处理,但速度毫无提升——目前下载总计约5.5MB的文件耗时约15分钟,处理器和网络资源几乎未被利用,程序处于空闲等待状态。
相关代码
并行下载任务包装函数
SemaphoreSlim downloadConcurrencySemaphore = new SemaphoreSlim(40); ConcurrentQueue<Task> Sha256Tasks = new ConcurrentQueue<Task>(); foreach (string url in urls) { var t = Task.Run(async () => { await downloadConcurrencySemaphore.WaitAsync(); try { await uploadedFile.CalculateChecksum(); } catch (Exception ex) { { } // breakpoint for debugging } finally { downloadConcurrencySemaphore.Release(); } }); Sha256Tasks.Enqueue(t); } Task.WaitAll(Sha256Tasks.ToArray());
CalculateChecksum方法(下载文件并生成SHA256摘要)
public async Task CalculateChecksum() { // is being called up to 40 times in parallel (connection limit) byte[] file = await API.DownloadClient.Download(URL); Sha256Sum = Sha256.GetSha256Sum(file); }
DownloadClient类(并行下载实现)
internal static class DownloadClient { static DownloadClient() { _Client = new HttpClient(); ServicePointManager.DefaultConnectionLimit = 40; var handler = new HttpClientHandler(); handler.ClientCertificateOptions = ClientCertificateOption.Manual; handler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => { return true; }; handler.MaxConnectionsPerServer = 40; _Client = new HttpClient(handler); } private static HttpClient _Client; internal static async Task<byte[]> Download(string url) { HttpResponseMessage response = await _Client.GetAsync(url); response.EnsureSuccessStatusCode(); System.Net.Http.HttpContent content = response.Content; byte[] file = await content.ReadAsByteArrayAsync(); return file; } }
优化方案
1. 修复HttpClient初始化冗余问题
在DownloadClient静态构造函数中,你先创建了一个默认HttpClient实例,随后又用handler重新创建了一个,前一个实例被无意义丢弃。同时ServicePointManager.DefaultConnectionLimit的设置应该放在HttpClient实例化之前,确保配置生效。
2. 移除不必要的Task.Run包装
CalculateChecksum本身就是异步方法,无需用Task.Run额外包裹(这会浪费线程池线程)。可以直接收集异步任务,用更简洁的方式等待全部完成:
var semaphore = new SemaphoreSlim(40); var tasks = new List<Task>(); foreach (var url in urls) { var targetFile = uploadedFile; // 对应你的uploadedFile实例 tasks.Add(ProcessFileAsync(targetFile, semaphore)); } await Task.WhenAll(tasks); async Task ProcessFileAsync(UploadedFile file, SemaphoreSlim semaphore) { await semaphore.WaitAsync(); try { await file.CalculateChecksum(); } catch (Exception ex) { // 替换空catch,记录异常便于排查问题 Console.WriteLine($"处理文件失败: {ex.Message}"); } finally { semaphore.Release(); } }
3. 边下载边计算哈希,减少内存开销
每个文件仅6字节,无需先把整个文件读到内存再计算哈希。可以直接通过流读取并实时计算SHA256,减少内存拷贝步骤:
// 修改CalculateChecksum方法 public async Task CalculateChecksum() { using var sha256 = SHA256.Create(); Sha256Sum = await API.DownloadClient.DownloadAndComputeSha256(URL, sha256); } // 在DownloadClient中添加新方法 internal static async Task<byte[]> DownloadAndComputeSha256(string url, SHA256 sha256) { // 使用ResponseHeadersRead,避免等待全部内容下载完成再处理 using var response = await _Client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); using var stream = await response.Content.ReadAsStreamAsync(); // 边读流边计算哈希 await sha256.ComputeHashAsync(stream); return sha256.Hash; }
4. 调整并发数并尝试HTTP/2
- 并发数优化:40的并发数未必是最优值,小文件请求的TCP握手、等待响应开销占比极高,可以尝试调高并发数(如100、200),但需注意目标服务器是否有限流策略。
- 启用HTTP/2:如果目标服务器支持HTTP/2,可配置HttpClientHandler启用该协议,多个请求可复用同一个TCP连接,大幅减少握手开销:
var handler = new HttpClientHandler(); handler.ClientCertificateOptions = ClientCertificateOption.Manual; handler.ServerCertificateCustomValidationCallback = (_, cert, _, _) => true; handler.MaxConnectionsPerServer = 100; // 启用HTTP/2 handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13; handler.HttpVersion = HttpVersion.Version20; handler.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
5. 排查网络与服务端限制
- 程序空闲等待可能源于DNS解析延迟,可提前批量解析所有URL的IP地址,避免重复解析。
- 确认目标服务器是否存在请求速率限制,如果有,可尝试联系服务端调整,或者改用批量请求接口(若API支持),减少总请求次数。
6. 移除空Catch块
空Catch会隐藏请求失败的异常,导致你无法排查潜在问题(比如大量请求失败拖慢整体速度),至少要添加日志记录逻辑。
内容的提问来源于stack exchange,提问作者julian bechtold
相关产品推荐
相关产品推荐

