微服务熔断与降级

大白话先说清楚

没有熔断器:
  支付服务卡死 → 订单服务一直等 → 线程全卡住 → 整个系统挂掉

有了熔断器:
  支付服务卡死 → 熔断器:"不对劲,直接返回失败,别等了"
  → 订单服务正常跑,返回"支付暂不可用"
  → 过一会儿熔断器自动试探支付服务是否恢复

三种状态:
  关闭(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降级 → 熔断时不返回错误,返回兜底数据,用户无感知

更多推荐