2023年双11,我们有个订单批量查询接口,需要同时请求3个外部API(物流、库存、价格),然后聚合数据返回。传统同步写法:
// 同步版本 - 总耗时 = 3个API耗时之和
$logistics = $this->callLogisticsApi($orderId); // 平均800ms
$inventory = $this->callInventoryApi($orderId); // 平均600ms
$price = $this->callPriceApi($orderId); // 平均400ms
// 总耗时:1800ms+,用户直接超时
压测数据:100并发下,P99延迟达到2.3秒,直接触发网关超时告警。当时用了Swoole协程临时救火,但项目是纯FPM架构,迁移成本太高。直到PHP8.1原生Fiber出现,才彻底解决这个问题。
| 方案 | 实现复杂度 | 内存占用(1000并发) | QPS(单机) | 代码侵入性 |
|---|---|---|---|---|
| 同步阻塞 | 低 | 2.3MB | 120 | 无 |
| Generator协程 | 中 | 3.1MB | 450 | 需改造函数为yield |
| Swoole协程 | 高 | 5.8MB | 1200 | 需替换框架 |
| PHP8.1 Fiber | 中 | 2.7MB | 980 | 低,仅改造IO部分 |
测试环境:PHP 8.1.20, Laravel 10, MySQL 8.0.35, 8核16G云服务器。压测工具:wrk -t4 -c100 -d30s
传统PHP进程模型:每个请求独占一个C栈(约2MB),IO阻塞时整个进程挂起。Fiber在用户态维护独立的栈空间(默认8KB),通过Fiber::suspend()和Fiber::resume()实现协作式调度。
// Fiber底层原理示意(简化版)
class Fiber {
private $stack; // 用户态栈
private $callable;
public function start() {
// 保存当前执行上下文
$this->stack = new SplStack();
// 执行协程体
$this->callable->call($this);
}
public function suspend($value = null) {
// 保存当前栈帧
$this->stack->push(debug_backtrace());
// 返回控制权给调度器
return $value;
}
public function resume($value = null) {
// 恢复栈帧
$frame = $this->stack->pop();
// 继续执行
$frame->resume($value);
}
}
class CoroutineScheduler {
private SplQueue $readyQueue;
private array $waiting = [];
private int $maxCoroutines = 1000;
public function __construct() {
$this->readyQueue = new SplQueue();
}
public function create(callable $fn): Fiber {
$fiber = new Fiber(function () use ($fn) {
try {
$fn();
} catch (Throwable $e) {
// 协程异常处理
echo "Coroutine error: " . $e->getMessage() . PHP_EOL;
}
});
$this->readyQueue->enqueue($fiber);
return $fiber;
}
public function run() {
while (!$this->readyQueue->isEmpty() || !empty($this->waiting)) {
// 处理就绪队列
while (!$this->readyQueue->isEmpty()) {
$fiber = $this->readyQueue->dequeue();
if (!$fiber->isStarted()) {
$fiber->start();
} elseif ($fiber->isSuspended()) {
$fiber->resume();
}
}
// 检查等待队列(简化版,实际需用select/epoll)
foreach ($this->waiting as $id => $item) {
if ($this->isReady($item['resource'])) {
$this->readyQueue->enqueue($item['fiber']);
unset($this->waiting[$id]);
}
}
// 避免CPU空转
if ($this->readyQueue->isEmpty()) {
usleep(1000); // 1ms
}
}
}
private function isReady($resource): bool {
// 实际项目中用stream_select或event扩展
return true;
}
}
class CoroutineHttpClient {
private CoroutineScheduler $scheduler;
public function __construct(CoroutineScheduler $scheduler) {
$this->scheduler = $scheduler;
}
public function getAsync(string $url): Fiber {
return $this->scheduler->create(function () use ($url) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_CONNECTTIMEOUT => 2,
]);
// 关键:将curl转换为非阻塞
curl_setopt($ch, CURLOPT_NOSIGNAL, 1);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
Fiber::suspend(['code' => $httpCode, 'body' => $result]);
});
}
}
class OrderController {
public function batchQuery(int $orderId): array {
$scheduler = new CoroutineScheduler();
$client = new CoroutineHttpClient($scheduler);
// 并发发起3个请求
$fiber1 = $client->getAsync("https://logistics.api/order/{$orderId}");
$fiber2 = $client->getAsync("https://inventory.api/order/{$orderId}");
$fiber3 = $client->getAsync("https://price.api/order/{$orderId}");
// 运行调度器
$scheduler->run();
// 获取结果
return [
'logistics' => $fiber1->getReturn(),
'inventory' => $fiber2->getReturn(),
'price' => $fiber3->getReturn(),
];
}
}
| 指标 | 同步版本 | Fiber版本 | 提升 |
|---|---|---|---|
| 平均响应时间 | 1850ms | 820ms | 55.7% |
| P99延迟 | 2300ms | 950ms | 58.7% |
| QPS | 120 | 980 | 716% |
| 内存峰值(100并发) | 2.3MB | 2.7MB | +17% |
| CPU使用率 | 45% | 78% | +73% |
测试条件:wrk -t4 -c100 -d60s http://localhost/batch/12345,每个请求内部调用3个外部API(模拟延迟800ms/600ms/400ms)
// 错误示范
$counter = 0;
$fiber = new Fiber(function () use (&$counter) {
$counter++; // 多个Fiber同时操作,数据错乱
});
// 正确做法:使用Thread-safe容器
$counter = new AtomicInteger();
$fiber = new Fiber(function () use ($counter) {
$counter->increment();
});
// Fiber内未捕获异常会直接终止进程
$fiber = new Fiber(function () {
throw new RuntimeException('test');
});
$fiber->start(); // 这里会抛出Fatal Error
// 正确做法:在Fiber内try-catch
$fiber = new Fiber(function () {
try {
// 业务逻辑
} catch (Throwable $e) {
Fiber::suspend(['error' => $e->getMessage()]);
}
});
默认Fiber栈大小8KB,递归调用超过128层会栈溢出。解决方案:
// 创建大栈Fiber
$fiber = new Fiber(function () {
// 深度递归
}, 65536); // 64KB栈空间
PDO默认是阻塞IO,在Fiber内使用会导致整个进程阻塞。必须使用异步MySQL驱动:
// 使用react/promise + amp/mysql
$connection = new Amp\Mysql\Connection('host=127.0.0.1 user=root password=123 db=test');
$result = yield $connection->query('SELECT * FROM users');
// sleep()会阻塞整个进程,不是协程
$fiber = new Fiber(function () {
sleep(1); // 错误!整个进程挂起1秒
});
// 正确做法:使用协程版sleep
$fiber = new Fiber(function () {
$deadline = microtime(true) + 1;
while (microtime(true) < $deadline) {
Fiber::suspend(); // 让出CPU
}
});
PHP Session默认使用文件锁,多个Fiber同时操作Session会导致死锁。解决方案:
// 在Fiber开始前关闭Session
session_write_close();
// 使用自定义Session驱动(如Redis)
Fiber执行完毕后不会自动释放,需要显式调用Fiber::__destruct()或使用unset():
$fiber = new Fiber(function () {
// 业务逻辑
});
$fiber->start();
// 使用完毕后
unset($fiber); // 手动释放
OPcache会缓存Fiber的闭包,导致旧代码被执行。解决方案:
// 在开发环境禁用OPcache
// php.ini: opcache.enable=0
// 生产环境使用opcache.revalidate_freq=0
class CoroutineConnectionPool {
private array $connections = [];
private int $maxConnections = 10;
private int $currentConnections = 0;
public function getConnection(): Fiber {
return new Fiber(function () {
// 尝试从池中获取空闲连接
foreach ($this->connections as $key => $conn) {
if ($conn['in_use'] === false) {
$this->connections[$key]['in_use'] = true;
return $conn['resource'];
}
}
// 创建新连接
if ($this->currentConnections < $this->maxConnections) {
$conn = $this->createNewConnection();
$this->connections[] = [
'resource' => $conn,
'in_use' => true,
];
$this->currentConnections++;
return $conn;
}
// 等待空闲连接(简化版)
Fiber::suspend();
return $this->getConnection();
});
}
private function createNewConnection() {
return new PDO('mysql:host=127.0.0.1;dbname=test', 'root', '123', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
}
Fiber不是银弹,它解决的是IO密集型场景的并发问题。对于CPU密集型计算,JIT+多进程仍然是更好的选择。我的建议:
最后,记住一句话:Fiber让PHP有了真正的协程能力,但用好它需要理解底层原理和踩坑经验。
专注技术分享与实战