bulkhead策略是资源耗尽前的最后一道隔离墙,它不阻止请求进入,只拒绝超出容量的执行请求,必须显式处理bulkheadrejectedexception,否则调用链会崩溃;policy.bulkheadasync(5,10)无效的主因是未正确套用到执行路径——需用await executeasync(async () => {...})调用,而非同步执行或错误嵌套;与circuitbreaker组合时须外层bulkhead限流、内层circuitbreaker判断稳定性,否则熔断失效;相比semaphoreslim,bulkheadasync面向服务调用容错,天然支持策略组合与http集成,语义更高级。

Bulkhead 策略不是“并发控制开关”,而是资源耗尽前的最后一道隔离墙——它不阻止请求进入,只拒绝超出容量的执行请求,且必须配合 BulkheadRejectedException 显式处理,否则会直接崩掉调用链。
为什么 Policy.BulkheadAsync(5, 10) 有时完全没效果?
常见现象:配置了最大并发 5、队列 10,但实际压测时仍看到 20+ 并发请求发出,甚至 BulkheadRejectedException 一个都没抛。
根本原因在于策略未真正“套用”到执行路径上。Bulkhead 是被动拦截器,不是全局钩子:
- 同步策略
Policy.Bulkhead(...)只能包裹Action或Func<tresult></tresult>,不能用于async方法体内部 - 异步策略
Policy.BulkheadAsync(...)必须用await ExecuteAsync(...)调用,不能写成.ExecuteAsync(() => DoSomething())—— 这样DoSomething()会立即同步执行,绕过舱壁 - 若用在
IHttpClientFactory的AddPolicyHandler中,必须搭配Polly.Extensions.Http提供的HttpPolicyExtensions,原生BulkheadAsync不支持HttpRequestMessage自动注入
正确写法示例(独立使用):
var bulkhead = Policy.BulkheadAsync(5, 10);
await bulkhead.ExecuteAsync(async () => {
// ✅ 所有 await 操作都在舱壁保护下执行
return await httpClient.GetAsync("https://api.example.com/data");
});
BulkheadRejectedException 为什么总被吞掉或变成 AggregateException?
当你看到“未处理的异常”或日志里只有 System.AggregateException 而看不到 BulkheadRejectedException,基本是以下两种情况之一:
- 在
async void方法中调用ExecuteAsync,导致异常逃逸出上下文,无法被捕获 - 外层用了
Task.WhenAll或Task.WhenAny包裹多个ExecuteAsync调用,此时拒绝异常会被打包进AggregateException.InnerExceptions,需手动展开检查 - 在 ASP.NET Core 中注册为全局策略(如通过
AddPolicyHandler),但没在Startup.ConfigureServices里启用AddControllers().AddMvcOptions(...)或对应异常过滤器,导致异常未路由到OnBulkheadRejectedAsync回调
安全捕获方式:
try {
await bulkhead.ExecuteAsync(...);
} catch (BulkheadRejectedException) {
// ✅ 直接捕获,可降级返回缓存/空数据
return GetFallbackData();
} catch (AggregateException ex) {
// ✅ 展开检查是否含 BulkheadRejectedException
if (ex.InnerExceptions.OfType<bulkheadrejectedexception>().Any()) {
return GetFallbackData();
}
throw;
}
</bulkheadrejectedexception>
Bulkhead 和 CircuitBreaker 组合时,为什么顺序反了就等于白配?
错误做法:CircuitBreaker 包外层,Bulkhead 包内层。结果是:熔断器一打开,所有新请求仍涌入 Bulkhead 队列,直到队列满才开始拒绝,线程池/连接池早已被占满。
正确嵌套逻辑(外层限流、内层稳态判断):
var circuitBreaker = Policy.Handle<httprequestexception>()
.CircuitBreakerAsync(3, TimeSpan.FromMinutes(1));
var bulkhead = Policy.BulkheadAsync(5, 10);
// ✅ Bulkhead 在外,先控总量;CircuitBreaker 在内,只对已准入的请求做稳定性判断
var policyWrap = Policy.WrapAsync(bulkhead, circuitBreaker);
await policyWrap.ExecuteAsync(async () => {
return await httpClient.GetAsync("https://api.example.com/data");
});
</httprequestexception>
关键点:
-
Bulkhead控制的是“谁可以排队/执行”,不关心下游是否健康 -
CircuitBreaker控制的是“已获得执行权的请求是否值得继续发”,它需要真实调用才能积累失败统计 - 两者异常类型不同:
BulkheadRejectedException表示资源不足,BrokenCircuitException表示下游已不可用,降级逻辑必须分开写
和 SemaphoreSlim 对比:什么场景该选 Bulkhead?
两者都能限并发,但语义和适用层级完全不同:
-
SemaphoreSlim是底层同步原语,适合控制“某段代码块”的并发数(比如同时最多 3 个文件下载任务),但它不提供拒绝回调、不集成重试/熔断、也不感知 HTTP 上下文 -
Policy.BulkheadAsync是声明式弹性策略,天然适配HttpClient、支持策略组合、自带队列缓冲与拒绝事件,且与 DI 容器、IHttpClientFactory无缝集成 - 真实项目中混用风险高:如果在
ExecuteAsync内部又手动await semaphore.WaitAsync(),会导致双重排队、死锁风险上升,且监控指标难以统一
简单判断标准:
—— 要做服务间调用容错(尤其 HTTP)、要和重试/熔断联动、要统一管理策略生命周期 → 选 BulkheadAsync;
—— 只是限制本地 CPU 密集型任务并行度、或需精确控制线程调度时机 → 用 SemaphoreSlim 更轻量直接。











