本文目录导读:

熔断机制(Circuit Breaker)在PHP中的落地,通常是为了防止对下游服务(如API、数据库、缓存)的雪崩式级联失败,核心思想是:当检测到失败率超过阈值时,快速失败(直接返回兜底数据),避免资源浪费,并定期尝试恢复。
以下是几种典型的落地方式,从简单到复杂:
基于第三方库(推荐生产使用)
PHP生态有成熟的熔断器库,最好的是 ackintosh/ganesha(基于 Kong 的熔断器思想)和 friendsofphp/php-cache 中的CircuitBreaker。
示例:使用 ackintosh/ganesha
composer require ackintosh/ganesha
<?php
use Ackintosh\Ganesha;
use Ackintosh\Ganesha\Builder;
use Ackintosh\Ganesha\Storage\Adapter\Redis;
// 1. 构建熔断器实例(建议作为单例或依赖注入)
$adapter = new Redis(new \Redis(), [
'failureCount' => 5, // 5秒内失败5次触发熔断
'failureRate' => 50, // 或失败率达到50%
'intervalToHalfOpen' => 30, // 30秒后尝试半开状态
'timeWindow' => 5, // 统计的时间窗口(秒)
]);
$circuitBreaker = Builder::withRateThreshold()
->failureRateThreshold(50)
->intervalToHalfOpen(30)
->timeWindow(5)
->adapter($adapter)
->build();
// 2. 使用:包裹你的外部调用
$result = $circuitBreaker->call('order_service', function () {
// 这里是你的实际调用(HTTP请求、RPC、数据库查询)
return $httpClient->get('http://order-service/api/orders');
});
// 如果熔断器打开(拒绝请求),会抛出 Ackintosh\Ganesha\Exception\RejectedException
// 你可以 catch 这个异常,返回降级数据
关键点:
- 状态存储:适配器支持
Redis、Memcached、APCu(单机)。Redis最适合分布式PHP应用。 - 闭包隔离:实际的业务逻辑被封装在闭包中,熔断器在闭包执行前检查状态。
如果不想引入外部库:手动实现单机版
适用于简单场景或老旧项目,利用APCu(本地缓存)或Session,但不适合多进程/多服务器。
<?php
class SimpleCircuitBreaker
{
private string $service;
private array $config;
private string $prefix = 'cb_';
public function __construct(string $service, array $config = [])
{
$this->service = $service;
// 熔断器配置
$this->config = array_merge([
'failure_threshold' => 5, // 连续失败次数
'success_threshold' => 2, // 半开状态下连续成功次数(恢复到关闭)
'timeout' => 30, // 熔断持续时间(秒)
'time_window' => 60, // 重置失败计数的窗口(秒)
], $config);
}
public function call(callable $callback)
{
$state = $this->getState();
if ($state === 'open') {
// 检查熔断时间是否超时(进入半开状态)
if (time() - apcu_fetch($this->prefix . 'open_time_' . $this->service) >= $this->config['timeout']) {
$this->setState('half_open');
} else {
throw new \RuntimeException('Circuit breaker is open');
}
}
if ($state === 'half_open') {
// 半开状态,允许一次试探性请求
try {
$result = $callback();
$this->recordSuccess();
$this->setState('closed'); // 成功,恢复到关闭
return $result;
} catch (\Throwable $e) {
$this->recordFailure();
$this->setState('open'); // 失败,回到打开
$this->resetHalfOpenCounters();
throw $e;
}
}
// 关闭状态:正常调用
try {
$result = $callback();
$this->recordSuccess();
return $result;
} catch (\Throwable $e) {
$this->recordFailure();
if ($this->getFailureCount() >= $this->config['failure_threshold']) {
$this->setState('open');
apcu_store($this->prefix . 'open_time_' . $this->service, time());
}
throw $e;
}
}
// ... getState, setState, recordFailure, recordSuccess 等辅助方法
// 注意:apcu_inc/apcu_dec 需要原子操作
}
局限:
- 依赖
apcu,多服务器(负载均衡)下各自统计,无法统一熔断。 - 没有平滑的失败率(基于滑动窗口)统计,只基于连续失败次数。
在框架中集成(原生装饰器/Pipeline)
以 Laravel 为例,利用其 Pipeline 或 Decorator(装饰器)可以优雅地整合。
// 1. 定义一个中间件 / 装饰器类
class CircuitBreakerMiddleware
{
public function handle($request, \Closure $next)
{
try {
// 实际调用
return app('circuit_breaker')->call('httpbin', function () use ($next, $request) {
return $next($request);
});
} catch (\Ackintosh\Ganesha\Exception\RejectedException $e) {
// 降级逻辑:返回缓存数据或默认值
return response()->json(['message' => 'Service temporarily unavailable, using cached data'], 200);
}
}
}
// 2. 绑定到路由组
Route::middleware([CircuitBreakerMiddleware::class])->group(function () {
Route::get('/orders', [OrderController::class, 'index']);
});
对于框架无关的PHP,可以用对象代理:
class OrderServiceProxy
{
public function __construct(
private RealOrderService $service,
private CircuitBreakerInterface $breaker
) {}
public function getOrders($userId)
{
return $this->breaker->call('getOrders', fn() => $this->service->getOrders($userId));
}
}
关键设计决策
| 决策项 | 建议 |
|---|---|
| 状态存储 | Redis(分布式必备),支持lua原子操作;单机用APCu |
| 失败统计 | 滑动窗口(例如最后10秒的失败率)优于简单计数(突发流量下更准确) |
| 半开试探 | 不应该每次请求都进半开,而是定期单次试探(库 ackintosh/ganesha 已实现) |
| 降级策略 | 必须定义:返回缓存、返回默认值(如空数组)、调用备选源(如备份API) |
| 超时与重试 | 熔断器不应与超时/重试混杂,建议重试逻辑放在熔断器外面(比如重试1次后仍然失败才触发熔断) |
危险 / 反模式
- 熔断器的“太敏感”:如果失败阈值设的太低(如1次失败就熔断),一个小抖动就会把服务打死,建议阈值至少3-5次,或失败率>30%。
- 全局单一熔断器:不同的下游服务(订单、支付、库存)应该有不同的熔断器实例,避免一个崩溃拖垮所有。
- 长时间不恢复:
intervalToHalfOpen设置得太长(比如5分钟),会导致恢复慢;设置太短(2秒),可能刚恢复又立刻熔断,建议指数退避:第一次30秒,第二次60秒... - 只在业务逻辑内try...catch:熔断器应该包裹整个外部调用链,而不是在内部逐层catch,否则异常被吃掉,熔断器无法感知。
推荐路径:
- 小团队/单机环境:手写
APCu版熔断器(但注意重启后统计丢失)。 - 分布式生产环境:直接用
ackintosh/ganesha+ Redis。 - 框架集成:Laravel的中间件、Symfony的事件订阅器、或自定义的装饰器模式。
熔断器的核心不是代码复杂度,而是统计口径(滑动窗口 vs 固定计数)和 恢复策略(半开试探),PHP的共享无状态特性(每个请求独立进程)使得分布式状态(Redis)几乎成了刚需。