swoole方案 微服务熔断与降级 (Circuit Breaker) 架构
·
微服务熔断与降级
大白话先说清楚
没有熔断器:
支付服务卡死 → 订单服务一直等 → 线程全卡住 → 整个系统挂掉
有了熔断器:
支付服务卡死 → 熔断器:"不对劲,直接返回失败,别等了"
→ 订单服务正常跑,返回"支付暂不可用"
→ 过一会儿熔断器自动试探支付服务是否恢复
三种状态:
关闭(Closed) → 正常调用
打开(Open) → 直接失败,不调下游
半开(HalfOpen) → 放一个请求试探,成功就关闭,失败继续打开
比喻:
家里电闸(保险丝)
正常用电 → 闸合着(Closed)
电流过大 → 闸跳了(Open),保护整个电路
过一会儿 → 手动试一下(HalfOpen),没问题就合上
代码
<?php
// CircuitBreaker.php
class CircuitBreaker
{
const CLOSED = 'closed'; // 正常
const OPEN = 'open'; // 熔断中
const HALF = 'half'; // 试探中
private string $state = self::CLOSED;
private int $failCount = 0; // 连续失败次数
private int $successCount = 0; // 半开状态成功次数
private float $openAt = 0; // 熔断打开时间
public function __construct(
private string $name,
private int $failThreshold = 5, // 失败5次就熔断
private int $successNeeded = 2, // 半开成功2次就关闭
private int $resetTimeout = 10, // 熔断10秒后进入半开
private float $callTimeout = 3.0, // 单次调用超时秒数
) {}
// 执行调用,传入协程函数
public function call(callable $fn, callable $fallback = null): mixed
{
// 打开状态:检查是否到了试探时间
if ($this->state === self::OPEN) {
if ((microtime(true) - $this->openAt) >= $this->resetTimeout) {
$this->state = self::HALF;
$this->successCount = 0;
echo "[{$this->name}] 进入半开,开始试探\n";
} else {
// 还没到时间,直接降级
echo "[{$this->name}] 熔断中,直接降级\n";
return $fallback ? $fallback() : null;
}
}
try {
// 带超时的协程调用
$result = $this->callWithTimeout($fn);
$this->onSuccess();
return $result;
} catch (\Throwable $e) {
$this->onFail();
echo "[{$this->name}] 调用失败:{$e->getMessage()}\n";
return $fallback ? $fallback() : null;
}
}
// 带超时执行
private function callWithTimeout(callable $fn): mixed
{
$result = null;
$error = null;
$timeouted = false;
$cid = go(function () use ($fn, &$result, &$error) {
try {
$result = $fn();
} catch (\Throwable $e) {
$error = $e;
}
});
// 等待协程完成,超时就杀掉
$deadline = microtime(true) + $this->callTimeout;
while (Swoole\Coroutine::exists($cid)) {
if (microtime(true) > $deadline) {
Swoole\Coroutine::cancel($cid);
$timeouted = true;
break;
}
Swoole\Coroutine::sleep(0.01);
}
if ($timeouted) throw new \RuntimeException("调用超时 {$this->callTimeout}s");
if ($error !== null) throw $error;
return $result;
}
private function onSuccess(): void
{
if ($this->state === self::HALF) {
$this->successCount++;
echo "[{$this->name}] 半开试探成功 {$this->successCount}/{$this->successNeeded}\n";
if ($this->successCount >= $this->successNeeded) {
$this->state = self::CLOSED;
$this->failCount = 0;
echo "[{$this->name}] 恢复正常,熔断关闭\n";
}
return;
}
$this->failCount = 0; // 成功就清零失败计数
}
private function onFail(): void
{
if ($this->state === self::HALF) {
// 半开试探失败,继续打开
$this->trip();
return;
}
$this->failCount++;
echo "[{$this->name}] 失败计数 {$this->failCount}/{$this->failThreshold}\n";
if ($this->failCount >= $this->failThreshold) {
$this->trip();
}
}
private function trip(): void
{
$this->state = self::OPEN;
$this->openAt = microtime(true);
echo "[{$this->name}] 熔断打开!{$this->resetTimeout}秒后试探\n";
}
public function getState(): string { return $this->state; }
}
<?php
// server.php
require __DIR__ . '/CircuitBreaker.php';
$server = new Swoole\Http\Server('0.0.0.0', 9511);
$server->set(['worker_num' => 4]);
// 每个下游服务一个熔断器
$breakers = [];
$server->on('workerStart', function () use (&$breakers) {
$breakers['payment'] = new CircuitBreaker(
name: 'payment-service',
failThreshold: 5, // 连续失败5次熔断
successNeeded: 2, // 半开成功2次恢复
resetTimeout: 10, // 熔断10秒后试探
callTimeout: 3.0, // 超过3秒算失败
);
$breakers['inventory'] = new CircuitBreaker(
name: 'inventory-service',
failThreshold: 3,
successNeeded: 1,
resetTimeout: 5,
callTimeout: 2.0,
);
});
$server->on('request', function ($req, $resp) use (&$breakers) {
$path = $req->server['request_uri'];
if ($path === '/order') {
// 调库存服务(有熔断保护)
$inventoryResult = $breakers['inventory']->call(
fn: function () {
$client = new Swoole\Coroutine\Http\Client('库存服务IP', 9512);
$client->get('/inventory/check');
$result = json_decode($client->body, true);
$client->close();
if (empty($result)) throw new \RuntimeException('库存服务返回空');
return $result;
},
fallback: function () {
// 降级:返回默认库存(不影响主流程)
return ['stock' => 0, 'from' => 'fallback'];
}
);
// 调支付服务(有熔断保护)
$paymentResult = $breakers['payment']->call(
fn: function () {
$client = new Swoole\Coroutine\Http\Client('支付服务IP', 9513);
$client->post('/payment/pay', json_encode(['amount' => 100]));
$result = json_decode($client->body, true);
$client->close();
if (empty($result)) throw new \RuntimeException('支付服务返回空');
return $result;
},
fallback: function () {
// 降级:提示用户稍后重试
return ['success' => false, 'msg' => '支付服务暂不可用,请稍后重试'];
}
);
$resp->end(json_encode([
'inventory' => $inventoryResult,
'payment' => $paymentResult,
'breakers' => [
'payment' => $breakers['payment']->getState(),
'inventory' => $breakers['inventory']->getState(),
],
]));
return;
}
$resp->end('ok');
});
$server->start();
状态流转图
失败次数 >= 阈值
Closed ──────────────────→ Open
↑ │
│ │ 等待resetTimeout秒
│ ↓
│ 成功次数 >= 需要数 Half
└──────────────────────── │
│ 失败
└──→ Open(重新计时)
三种场景演示
场景1:正常
调支付 → 成功 → failCount清零
场景2:支付服务挂了
调支付 → 超时 → failCount=1
调支付 → 超时 → failCount=2
...
调支付 → 超时 → failCount=5 → 熔断打开
下一个请求 → 直接返回fallback,不调支付服务
场景3:支付服务恢复
10秒后 → 进入半开
放一个请求 → 成功 → successCount=1
再放一个 → 成功 → successCount=2 → 熔断关闭,恢复正常
核心三句话
1. 失败计数 → 连续失败超过阈值,打开熔断,保护主服务
2. 自动试探 → 打开N秒后进入半开,放一个请求探下游是否恢复
3. fallback降级 → 熔断时不返回错误,返回兜底数据,用户无感知
更多推荐
所有评论(0)