HttpClientFactory 与弹性策略
大约 11 分钟约 3199 字
HttpClientFactory 与弹性策略
简介
直接使用 HttpClient 可能导致端口耗尽(socket exhaustion)和 DNS 不刷新问题。IHttpClientFactory 通过池化 HttpMessageHandler 解决这些问题,并集成 Polly 提供重试、熔断等弹性策略。
特点
HttpClient 问题与解决
端口耗尽问题
// ❌ 问题 1:使用 using 创建 HttpClient(端口耗尽)
public async Task<string> BadExample()
{
using var client = new HttpClient(); // 每次 new 都创建新的 Handler
return await client.GetStringAsync("https://api.example.com/data");
// Handler 释放后,底层 Socket 进入 TIME_WAIT 状态(2分钟)
// 大量调用 → 端口耗尽 → SocketException
}
// ❌ 问题 2:单例 HttpClient(DNS 不刷新)
private static readonly HttpClient _staticClient = new()
{
BaseAddress = new Uri("https://api.example.com")
};
// 如果 api.example.com 的 IP 变更(如蓝绿部署)
// 单例 HttpClient 不会重新解析 DNS → 连接失败
// ✅ 解决:IHttpClientFactory
// 1. 池化 HttpMessageHandler(复用连接)
// 2. 定期回收 Handler(刷新 DNS)
// 3. Handler 默认 2 分钟生命周期
// 注册
builder.Services.AddHttpClient(); // 基本注册命名客户端
// 为不同服务配置不同的 HttpClient
builder.Services.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
client.Timeout = TimeSpan.FromSeconds(30);
});
builder.Services.AddHttpClient("InternalAPI", client =>
{
client.BaseAddress = new Uri("https://internal-api.example.com");
client.Timeout = TimeSpan.FromSeconds(10);
});
// 使用
public class MyService
{
private readonly IHttpClientFactory _factory;
public MyService(IHttpClientFactory factory) => _factory = factory;
public async Task<string> GetGitHubUserAsync(string username)
{
var client = _factory.CreateClient("GitHub");
return await client.GetStringAsync($"/users/{username}");
}
}类型化客户端(推荐)
// 强类型封装
public class GitHubService
{
private readonly HttpClient _client;
public GitHubService(HttpClient client)
{
_client = client;
}
public async Task<GitHubUser?> GetUserAsync(string username)
{
return await _client.GetFromJsonAsync<GitHubUser>($"/users/{username}");
}
public async Task<Repo[]> GetReposAsync(string username)
{
return await _client.GetFromJsonAsync<Repo[]>($"/users/{username}/repos")
?? Array.Empty<Repo>();
}
}
// 注册
builder.Services.AddHttpClient<GitHubService>(client =>
{
client.BaseAddress = new Uri("https://api.github.com");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
client.Timeout = TimeSpan.FromSeconds(30);
});
// 直接注入使用
public class UserController : ControllerBase
{
private readonly GitHubService _github;
public UserController(GitHubService github) => _github = github;
[HttpGet("{username}")]
public async Task<IActionResult> GetUser(string username)
{
var user = await _github.GetUserAsync(username);
return Ok(user);
}
}Polly 弹性策略
重试策略
// 安装:Microsoft.Extensions.Http.Polly
builder.Services.AddHttpClient<GitHubService>(client =>
{
client.BaseAddress = new Uri("https://api.github.com");
})
.AddTransientHttpErrorPolicy(builder =>
builder.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)))); // 指数退避:1s, 2s, 4s
// 自定义重试条件
builder.Services.AddHttpClient<PaymentService>(client =>
{
client.BaseAddress = new Uri("https://payment.example.com");
})
.AddRetryPolicyHandler(new HttpRetryPolicyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true // 添加随机抖动避免惊群
});
// 手动配置 Polly
using Polly;
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError() // 5xx 和 408
.OrResult(r => r.StatusCode == System.Net.HttpStatusCode.TooManyRequests) // 429
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(5)
}, onRetry: (outcome, timespan, retryCount, context) =>
{
Console.WriteLine($"重试 {retryCount},等待 {timespan.TotalSeconds}s");
});
builder.Services.AddHttpClient<GitHubService>()
.AddPolicyHandler(retryPolicy);熔断策略
// 熔断器:连续失败达到阈值后断开,一段时间后尝试半开
var circuitBreaker = HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5, // 连续5次失败触发熔断
durationOfBreak: TimeSpan.FromSeconds(30), // 熔断30秒
onBreak: (ex, duration) =>
{
Console.WriteLine($"熔断打开,持续 {duration.TotalSeconds}s");
},
onReset: () =>
{
Console.WriteLine("熔断关闭(恢复正常)");
});
builder.Services.AddHttpClient<GitHubService>()
.AddPolicyHandler(circuitBreaker);
// 组合策略:重试 + 熔断
builder.Services.AddHttpClient<GitHubService>()
.AddPolicyHandler(Policy.WrapAsync(retryPolicy, circuitBreaker));
// 执行顺序:先重试,重试都失败后触发熔断超时策略
// Polly 超时
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));
// 组合:重试 + 熔断 + 超时
var resilientPolicy = Policy.WrapAsync(retryPolicy, circuitBreaker, timeoutPolicy);
builder.Services.AddHttpClient<GitHubService>(client =>
{
client.Timeout = TimeSpan.FromSeconds(30); // 整体超时
})
.AddPolicyHandler(resilientPolicy);
// 限流(Bulkhead)
var bulkhead = Policy.BulkheadAsync<HttpResponseMessage>(
maxParallelization: 10, // 最大10个并发请求
maxQueuingActions: 20, // 队列中最多20个等待
onBulkheadRejectedAsync: context =>
{
Console.WriteLine("请求被限流");
return Task.CompletedTask;
});
builder.Services.AddHttpClient<GitHubService>()
.AddPolicyHandler(bulkhead);自定义 DelegatingHandler
请求/响应拦截
public class LoggingHandler : DelegatingHandler
{
private readonly ILogger _logger;
public LoggingHandler(ILogger<LoggingHandler> logger) => _logger = logger;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var sw = Stopwatch.StartNew();
_logger.LogInformation("→ {Method} {Uri}", request.Method, request.RequestUri);
// 可以修改请求(添加头、修改内容等)
request.Headers.Add("X-Request-Id", Guid.NewGuid().ToString());
try
{
var response = await base.SendAsync(request, cancellationToken);
sw.Stop();
_logger.LogInformation("← {StatusCode} ({Ms}ms)", (int)response.StatusCode, sw.ElapsedMilliseconds);
return response;
}
catch (Exception ex)
{
sw.Stop();
_logger.LogError(ex, "✗ HTTP 请求失败 ({Ms}ms)", sw.ElapsedMilliseconds);
throw;
}
}
}
// 注册 Handler
builder.Services.AddHttpClient<GitHubService>()
.AddHttpMessageHandler<LoggingHandler>();
// Handler 的生命周期是 Scoped(每次请求创建)
// HttpClient 的 Handler 是池化的Handler 生命周期管理
Handler 池化机制
// IHttpClientFactory 内部的 Handler 池化机制
// ┌─────────────────────────────────────────────────┐
// │ IHttpClientFactory │
// │ │
// │ CreateClient("GitHub") │
// │ → 从 HandlerPool 中获取或创建 HttpMessageHandler │
// │ → 包装为 HttpClient 返回 │
// │ │
// │ HandlerPool(DefaultHttpMessageHandlerBuilder) │
// │ ┌──────────────────────────┐ │
// │ │ HandlerA (active, 2min) │ ← 正在使用的 │
// │ │ HandlerB (active, 1min) │ │
// │ │ HandlerC (expired) │ ← 等待回收 │
// │ └──────────────────────────┘ │
// │ │
// │ HttpClient 是瞬态的,Handler 是池化的 │
// │ HttpClient 可以安全 Dispose(不影响池化 Handler)│
// └─────────────────────────────────────────────────┘
// Handler 生命周期默认 2 分钟,可配置
builder.Services.ConfigureAll<HttpClientFactoryOptions>(options =>
{
// 设置 Handler 生命周期(默认 2 分钟)
options.HttpMessageHandlerLifetime = TimeSpan.FromMinutes(5);
// Handler 过期后:
// 1. 不会立即销毁,而是标记为过期
// 2. 新请求会创建新的 Handler
// 3. 旧 Handler 在所有请求完成后被释放
// 4. 这确保了 DNS 刷新
});
// 验证 Handler 池化行为
public class HandlerLifecycleDemo
{
private readonly IHttpClientFactory _factory;
public HandlerLifecycleDemo(IHttpClientFactory factory) => _factory = factory;
public void Demonstrate()
{
var client1 = _factory.CreateClient("GitHub");
var client2 = _factory.CreateClient("GitHub");
// client1 和 client2 共享同一个 HttpMessageHandler
// 它们使用相同的 TCP 连接池
// 但 HttpClient 实例是不同的(瞬态)
Console.WriteLine(client1.GetHashCode() != client2.GetHashCode()); // true
}
}自定义 IHttpMessageHandlerBuilderFilter
// 全局为所有 HttpClient 添加 Handler(不修改每个客户端的注册)
public class GlobalLoggingHandlerFilter : IHttpMessageHandlerBuilderFilter
{
private readonly ILoggerFactory _loggerFactory;
public GlobalLoggingHandlerFilter(ILoggerFactory loggerFactory)
=> _loggerFactory = loggerFactory;
public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
{
return builder =>
{
next(builder);
// 在所有 Handler 链的最外层添加日志 Handler
builder.AdditionalHandlers.Insert(0,
new LoggingHandler(_loggerFactory.CreateLogger("GlobalHttp")));
};
}
}
// 注册
builder.Services.AddSingleton<IHttpMessageHandlerBuilderFilter, GlobalLoggingHandlerFilter>();Polly v8 弹性策略
迁移到 Polly v8(.NET 8+)
// Polly v8 新 API(推荐)
// 安装:Microsoft.Extensions.Http.Polly
// .NET 8 内置的重试策略
builder.Services.AddHttpClient<GitHubService>(client =>
{
client.BaseAddress = new Uri("https://api.github.com");
})
.AddStandardResilienceHandler(options =>
{
// 标准弹性策略(包含重试 + 熔断 + 超时 + 限流)
options.TotalRequestTimeout = TimeSpan.FromSeconds(30);
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromSeconds(1);
options.Retry.BackoffType = DelayBackoffType.Exponential;
options.Retry.UseJitter = true;
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
options.CircuitBreaker.FailureRatio = 0.3;
options.CircuitBreaker.MinimumThroughput = 5;
options.AttemptTimeout = TimeSpan.FromSeconds(5);
});
// 自定义组合策略
builder.Services.AddHttpClient<PaymentService>(client =>
{
client.BaseAddress = new Uri("https://payment.example.com");
})
.AddResilienceHandler("PaymentStrategy", builder =>
{
// 重试策略
builder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(500),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => (int)r.StatusCode >= 500 || r.StatusCode == System.Net.HttpStatusCode.RequestTimeout),
OnRetry = args =>
{
Console.WriteLine($"重试第 {args.AttemptNumber} 次");
return default;
}
});
// 熔断策略
builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
FailureRatio = 0.3,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromSeconds(15),
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => (int)r.StatusCode >= 500),
OnOpened = args =>
{
Console.WriteLine("熔断器打开");
return default;
},
OnClosed = args =>
{
Console.WriteLine("熔断器关闭(恢复正常)");
return default;
},
OnHalfOpened = args =>
{
Console.WriteLine("熔断器半开(试探)");
return default;
}
});
// 单次请求超时
builder.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(10),
OnTimeout = args =>
{
Console.WriteLine("请求超时");
return default;
}
});
});请求关联与可观测性
关联 ID 传播
// 在 DelegatingHandler 中实现跨服务追踪
public class CorrelationHandler : DelegatingHandler
{
private const string CorrelationIdHeader = "X-Correlation-Id";
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
// 如果请求中没有 CorrelationId,则生成一个
if (!request.Headers.Contains(CorrelationIdHeader))
{
var correlationId = Guid.NewGuid().ToString("N");
request.Headers.Add(CorrelationIdHeader, correlationId);
}
// 添加请求开始时间
request.Headers.Add("X-Request-Timestamp",
DateTimeOffset.UtcNow.ToString("O"));
var response = await base.SendAsync(request, cancellationToken);
// 确保响应中也包含 CorrelationId
if (!response.Headers.Contains(CorrelationIdHeader))
{
response.Headers.Add(CorrelationIdHeader,
request.Headers.GetValues(CorrelationIdHeader).First());
}
return response;
}
}
// 注册
builder.Services.AddHttpClient<OrderService>()
.AddHttpMessageHandler<CorrelationHandler>();
// 在日志中间件中读取 CorrelationId
app.Use(async (context, next) =>
{
if (context.Request.Headers.TryGetValue("X-Correlation-Id", out var correlationId))
{
using (LogContext.PushProperty("CorrelationId", correlationId))
{
await next(context);
}
}
else
{
await next(context);
}
});健康检查集成
// 使用 HttpClientFactory 进行下游服务健康检查
builder.Services.AddHttpClient("ServiceHealth", client =>
{
client.Timeout = TimeSpan.FromSeconds(5);
});
builder.Services.AddHealthChecks()
.AddCheck("external-api", async () =>
{
try
{
var factory = /* 获取 IHttpClientFactory */;
var client = factory.CreateClient("ServiceHealth");
var response = await client.GetAsync("https://api.example.com/health");
return response.IsSuccessStatusCode
? HealthCheckResult.Healthy()
: HealthCheckResult.Degraded($"状态码: {(int)response.StatusCode}");
}
catch (HttpRequestException ex)
{
return HealthCheckResult.Unhealthy("外部 API 不可达", ex);
}
});高级使用模式
动态 API 客户端(Refit 集成)
// Refit + IHttpClientFactory = 声明式 HTTP 客户端
// 安装:Refit.HttpClientFactory
// 定义接口
public interface IGitHubApi
{
[Get("/users/{username}")]
Task<GitHubUser> GetUserAsync(string username);
[Get("/users/{username}/repos")]
Task<List<Repo>> GetReposAsync(string username);
[Post("/repos/{owner}/{repo}/issues")]
Task<Issue> CreateIssueAsync(
string owner, string repo, [Body] CreateIssueRequest request);
}
// 注册 Refit 客户端
builder.Services.AddRefitClient<IGitHubApi>(new RefitSettings
{
ContentSerializer = new SystemTextJsonContentSerializer(
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
})
.ConfigureHttpClient(client =>
{
client.BaseAddress = new Uri("https://api.github.com");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddHttpMessageHandler<CorrelationHandler>();
// 使用
public class UserController : ControllerBase
{
private readonly IGitHubApi _github;
public UserController(IGitHubApi github) => _github = github;
[HttpGet("{username}/repos")]
public async Task<ActionResult<List<Repo>>> GetRepos(string username)
{
return Ok(await _github.GetReposAsync(username));
}
}基于 HttpContext 的动态配置
// 根据 HttpContext 动态配置 HttpClient(高级场景)
public class TenantAwareHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _accessor;
public TenantAwareHandler(IHttpContextAccessor accessor)
=> _accessor = accessor;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var tenant = _accessor.HttpContext?.Items["Tenant"] as string;
if (!string.IsNullOrEmpty(tenant))
{
// 根据租户动态修改请求
request.Headers.Add("X-Tenant-Id", tenant);
// 或者动态切换 BaseAddress
var tenantHost = GetTenantHost(tenant);
request.RequestUri = new Uri(
$"{tenantHost}{request.RequestUri?.PathAndQuery}");
}
return await base.SendAsync(request, cancellationToken);
}
private string GetTenantHost(string tenant) => tenant switch
{
"tenant-a" => "https://api-a.example.com",
"tenant-b" => "https://api-b.example.com",
_ => "https://api.example.com"
};
}请求/响应缓存 Handler
// 基于 Handler 的响应缓存
public class CacheHandler : DelegatingHandler
{
private readonly IMemoryCache _cache;
private readonly ILogger<CacheHandler> _logger;
public CacheHandler(IMemoryCache cache, ILogger<CacheHandler> logger)
{
_cache = cache;
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
// 只缓存 GET 请求
if (request.Method != HttpMethod.Get)
return await base.SendAsync(request, cancellationToken);
var cacheKey = $"http:{request.RequestUri}";
if (_cache.TryGetValue(cacheKey, out CachedResponse? cached))
{
_logger.LogDebug("缓存命中: {Uri}", request.RequestUri);
return cached!.ToResponseMessage();
}
var response = await base.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
_cache.Set(cacheKey, new CachedResponse(content, response.Headers),
TimeSpan.FromMinutes(5));
_logger.LogDebug("缓存写入: {Uri}", request.RequestUri);
}
return response;
}
private record CachedResponse(byte[] Content, HttpResponseHeaders Headers)
{
public HttpResponseMessage ToResponseMessage()
{
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ByteArrayContent(Content)
};
foreach (var header in Headers)
{
response.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
return response;
}
}
}优点
缺点
总结
IHttpClientFactory 通过池化 HttpMessageHandler 解决端口耗尽和 DNS 刷新问题。三种使用方式:基本 CreateClient()、命名客户端("name")、类型化客户端(推荐)。Polly 弹性策略:重试(指数退避)、熔断(连续失败断开)、超时、限流(Bulkhead),通过 WrapAsync 组合。DelegatingHandler 实现请求/响应拦截(日志、认证头注入)。Handler 默认 2 分钟回收,在此期间 DNS 不会刷新。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 框架与语言特性类主题要同时理解运行方式和工程组织方式。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 明确项目入口、配置管理、依赖管理、日志和测试策略。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 把 notebook 或脚本风格直接带入长期维护项目。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 继续补齐部署、打包、监控和性能调优能力。
适用场景
- 当你准备把《HttpClientFactory 与弹性策略》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《HttpClientFactory 与弹性策略》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《HttpClientFactory 与弹性策略》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《HttpClientFactory 与弹性策略》最大的收益和代价分别是什么?
