正常:请求 → 本机房节点(dc1)
  dc1全挂:请求 → 自动切到备用机房(dc2)
  dc1恢复:流量自动切回来

  三个状态机(熔断器):
  CLOSED   → 正常转发
  OPEN     → 直接跳过,不试了(熔断中)
  HALF_OPEN → 试一个请求,成功就恢复,失败继续熔断

  ---
  代码

  <?php
  define('LOCAL_DC',       'dc1'); // 本节点所在机房
  define('FAIL_THRESHOLD', 3);     // 连续失败3次开熔断
  define('CIRCUIT_TTL',    30);    // 熔断30秒后半开尝试

  // 节点表:所有机房的节点状态,共享内存跨worker可见
  $nodeTable = new Swoole\Table(1024);
  $nodeTable->column('dc',         Swoole\Table::TYPE_STRING, 16);
  $nodeTable->column('host',       Swoole\Table::TYPE_STRING, 64);
  $nodeTable->column('port',       Swoole\Table::TYPE_INT);
  $nodeTable->column('fails',      Swoole\Table::TYPE_INT);
  $nodeTable->column('circuit',    Swoole\Table::TYPE_STRING, 16); // closed/open/half_open
  $nodeTable->column('open_until', Swoole\Table::TYPE_INT);
  $nodeTable->create();

  foreach ([
      'dc1-1' => ['dc' => 'dc1', 'host' => '10.1.0.1', 'port' => 8080],
      'dc1-2' => ['dc' => 'dc1', 'host' => '10.1.0.2', 'port' => 8080],
      'dc2-1' => ['dc' => 'dc2', 'host' => '10.2.0.1', 'port' => 8080],
      'dc2-2' => ['dc' => 'dc2', 'host' => '10.2.0.2', 'port' => 8080],
  ] as $id => $n) {
      $nodeTable->set($id, [...$n, 'fails' => 0, 'circuit' => 'closed', 'open_until' => 0]);
  }

  // ─── 熔断器:选节点,本机房优先,全挂切备用 ─────────────────
  function pickNode(Swoole\Table $t, string $localDc): ?array {
      $local = $remote = [];
      $now   = time();

      foreach ($t as $id => $node) {
          if ($node['circuit'] === 'open') {
              if ($now < $node['open_until']) continue;       // 熔断期内跳过
              $t->set($id, [...$node, 'circuit' => 'half_open']); // 到期转半开
              $node['circuit'] = 'half_open';
          }
          ($node['dc'] === $localDc ? $local : $remote)[$id] = $node;
      }

      // 本机房有节点就用本机房,否则降级到备用机房
      $pool = $local ?: $remote;
      if (!$pool) return null;

      $id = array_rand($pool);
      return ['id' => $id, ...$pool[$id]];
  }

  function markFail(Swoole\Table $t, string $id): void {
      $n     = $t->get($id);
      $fails = $n['fails'] + 1;
      if ($fails >= FAIL_THRESHOLD) {
          $t->set($id, [...$n, 'fails' => $fails, 'circuit' => 'open', 'open_until' => time() + CIRCUIT_TTL]);
          echo "[熔断开启] $id{$n['dc']} 机房失败节点+1\n";
      } else {
          $t->set($id, [...$n, 'fails' => $fails]);
      }
  }

  function markOk(Swoole\Table $t, string $id): void {
      $n = $t->get($id);
      $t->set($id, [...$n, 'fails' => 0, 'circuit' => 'closed', 'open_until' => 0]);
  }

  // ─── 转发请求到目标节点 ──────────────────────────────────────
  function forward(array $node, \Swoole\Http\Request $req): array {
      $c = new Swoole\Coroutine\Http\Client($node['host'], $node['port']);
      $c->set(['timeout' => 3]);
      $c->setMethod($req->server['request_method']);
      $c->setHeaders(array_merge($req->header ?? [], ['X-Forwarded-DC' => LOCAL_DC]));
      $c->setData($req->rawContent() ?? '');
      $ok = $c->execute($req->server['request_uri']);
      $result = ['ok' => $ok && $c->statusCode < 500, 'status' => $c->statusCode, 'body' => $c->body];
      $c->close();
      return $result;
  }

  // ─── Swoole 服务 ─────────────────────────────────────────────
  $server = new Swoole\Http\Server('0.0.0.0', 9502);
  $server->set(['worker_num' => swoole_cpu_num()]);

  // worker 0 跑主动健康检查:每5秒探活所有节点
  $server->on('workerStart', function ($server, $workerId) use ($nodeTable) {
      if ($workerId !== 0) return;

      Swoole\Timer::tick(5000, function () use ($nodeTable) {
          foreach ($nodeTable as $id => $node) {
              if ($node['circuit'] === 'open' && time() < $node['open_until']) continue;

              Swoole\Coroutine::create(function () use ($id, $node, $nodeTable) {
                  $c = new Swoole\Coroutine\Http\Client($node['host'], $node['port']);
                  $c->set(['timeout' => 2]);
                  $ok = $c->execute('/health') && $c->statusCode === 200;
                  $c->close();

                  $ok ? markOk($nodeTable, $id) : markFail($nodeTable, $id);
                  echo ($ok ? '[恢复]' : '[故障]') . " $id ({$node['dc']})\n";
              });
          }
      });
  });

  $server->on('request', function ($req, $res) use ($nodeTable) {
      $node = pickNode($nodeTable, LOCAL_DC);

      if (!$node) {
          $res->status(503);
          $res->end(json_encode(['error' => '全部机房不可用']));
          return;
      }

      $result = forward($node, $req);

      if (!$result['ok']) {
          markFail($nodeTable, $node['id']);

          // 立刻重试:换一个节点(可能已切到备用机房)
          $retry = pickNode($nodeTable, LOCAL_DC);
          if ($retry && $retry['id'] !== $node['id']) {
              $result = forward($retry, $req);
              $node   = $retry;
              markOk($nodeTable, $retry['id']);
          }
      } else {
          markOk($nodeTable, $node['id']);
      }

      $degraded = $node['dc'] !== LOCAL_DC;
      if ($degraded) echo "[降级] 流量已切到备用机房 {$node['dc']}\n";

      $res->header('X-Node',    $node['id']);
      $res->header('X-DC',      $node['dc']);
      $res->header('X-Degraded', $degraded ? 'true' : 'false');
      $res->status($result['status'] ?: 502);
      $res->end($result['body'] ?: json_encode(['error' => '上游异常']));
  });

  $server->start();

  ---
  解释

  为什么要两层检测:主动健康检查(定时探活)发现节点慢慢挂掉;被动熔断(请求失败计数)发现节点突然挂掉。两层结合,发现故
  障更快

  熔断器三个状态
  CLOSED ──(连续失败≥3)──> OPEN ──(30秒后)──> HALF_OPEN
    ↑                                               │
    └──────────────(请求成功)──────────────────────┘
  OPEN 期间直接跳过这个节点,不发请求,避免大量请求堆积在死节点上

  pickNode 本机房优先:先把所有节点分成 $local$remote
  两桶,本机房桶不为空就从本机房选,本机房全挂了(桶空了)才从备用机房选。切换是自动的,不需要人工干预

  X-Degraded: true:响应头告诉调用方"这次请求走的是备用机房",方便监控告警。看到这个头大量出现,说明本机房出问题了

  半开探活:熔断30秒后不是直接恢复,而是放一个请求进去试试。成功了才关闭熔断,失败了重新计30秒。防止机房刚恢复就被大量流
  量打垮

  Swoole\Table 跨 worker 共享:4个 worker 进程共用同一份节点状态表。worker 0 的健康检查更新了节点状态,其他 worker
  的请求路由立刻感知到,不用 Redis

  ---
  测试

  php proxy.php

  # 正常请求,走本机房
  curl -v http://localhost:9502/api/users
  # X-DC: dc1
  # X-Degraded: false

  # 模拟 dc1 全挂(停掉 dc1 的节点)
  # 3次失败后熔断开启,自动切流

  # 再请求,走备用机房
  curl -v http://localhost:9502/api/users
  # X-DC: dc2
  # X-Degraded: true   ← 已降级

  # dc1 恢复后,30秒内自动切回
  curl -v http://localhost:9502/api/users
  # X-DC: dc1
  # X-Degraded: false  ← 恢复正常

更多推荐