233 lines
6.0 KiB
Plaintext
233 lines
6.0 KiB
Plaintext
---
|
||
description: "Swoole 协程与高并发编程规范 — 协程安全/连接池/Worker配置/内存管理"
|
||
globs:
|
||
- "Case-Database-Backend/app/**/*.php"
|
||
- "Case-Database-Backend/config/**/*.php"
|
||
- "Case-Database-Backend/bin/hyperf.php"
|
||
alwaysApply: false
|
||
---
|
||
|
||
# ⚡ Swoole Coroutine & High-Concurrency Standards
|
||
|
||
## 核心原则
|
||
|
||
Swoole 是常驻内存的协程服务器,与传统 PHP-FPM 有本质区别。
|
||
**每个请求共享 Worker 进程内存**,必须避免全局状态污染。
|
||
|
||
## 协程安全规则
|
||
|
||
### 绝对禁止
|
||
|
||
```php
|
||
// ❌ 全局变量存储请求数据(会被其他协程覆盖)
|
||
global $currentUser;
|
||
|
||
// ❌ 静态属性存储请求级数据
|
||
class UserContext {
|
||
public static ?User $user = null; // WRONG: shared across coroutines
|
||
}
|
||
|
||
// ❌ 阻塞 I/O 函数(会阻塞整个 Worker)
|
||
file_get_contents('https://api.example.com'); // use Guzzle coroutine client
|
||
sleep(5); // use Coroutine::sleep()
|
||
flock($fp, LOCK_EX); // use Redis distributed lock
|
||
|
||
// ❌ 非协程安全的全局单例
|
||
$pdo = new PDO(...); // use connection pool instead
|
||
```
|
||
|
||
### 正确做法
|
||
|
||
```php
|
||
// ✅ 使用 Hyperf Context 传递请求数据
|
||
use Hyperf\Context\Context;
|
||
|
||
// 在中间件中设置
|
||
Context::set('current_user', $user);
|
||
|
||
// 在任意位置读取(协程安全)
|
||
$user = Context::get('current_user');
|
||
|
||
// ✅ 协程安全的延时
|
||
use Swoole\Coroutine;
|
||
Coroutine::sleep(0.1);
|
||
|
||
// ✅ 使用协程 HTTP 客户端
|
||
use Hyperf\Guzzle\ClientFactory;
|
||
$client = $this->clientFactory->create();
|
||
$response = $client->get('https://api.example.com');
|
||
```
|
||
|
||
## 连接池配置
|
||
|
||
### MySQL 连接池
|
||
|
||
```php
|
||
// config/autoload/databases.php
|
||
'pool' => [
|
||
// 公式: min = Worker数, max = Worker数 * 2 ~ 4
|
||
'min_connections' => (int) env('DB_POOL_MIN', 5),
|
||
'max_connections' => (int) env('DB_POOL_MAX', 50),
|
||
'connect_timeout' => 10.0, // seconds
|
||
'wait_timeout' => 3.0, // wait for available connection
|
||
'heartbeat' => -1, // disable heartbeat
|
||
'max_idle_time' => 60, // recycle idle connections
|
||
],
|
||
```
|
||
|
||
### Redis 连接池
|
||
|
||
```php
|
||
// config/autoload/redis.php
|
||
return [
|
||
'default' => [
|
||
'host' => env('REDIS_HOST', 'localhost'),
|
||
'port' => (int) env('REDIS_PORT', 6379),
|
||
'auth' => env('REDIS_AUTH', null),
|
||
'db' => (int) env('REDIS_DB', 0),
|
||
'pool' => [
|
||
'min_connections' => 5,
|
||
'max_connections' => 30,
|
||
'connect_timeout' => 10.0,
|
||
'wait_timeout' => 3.0,
|
||
'heartbeat' => -1,
|
||
'max_idle_time' => 60,
|
||
],
|
||
],
|
||
];
|
||
```
|
||
|
||
### 连接池容量计算
|
||
|
||
```
|
||
单 Worker 最大协程数: max_coroutine (default: 100000)
|
||
实际并发协程数 ≈ QPS * avg_response_time
|
||
|
||
DB Pool Max = ceil(concurrent_coroutines * db_query_ratio)
|
||
Redis Pool Max = ceil(concurrent_coroutines * redis_query_ratio)
|
||
|
||
示例 (百万级):
|
||
- 4 Worker, 每 Worker 1000 并发协程
|
||
- DB 查询占比 60% → DB Pool Max = 1000 * 0.6 = 600 (per worker)
|
||
- 实际受 MySQL max_connections 限制,需配合读写分离
|
||
```
|
||
|
||
## Worker 进程配置
|
||
|
||
```php
|
||
// config/autoload/server.php
|
||
'settings' => [
|
||
'worker_num' => (int) env('SWOOLE_WORKER_NUM', swoole_cpu_num() * 2),
|
||
'task_worker_num' => (int) env('SWOOLE_TASK_WORKER_NUM', 4),
|
||
'max_request' => 10000, // restart worker after N requests (prevent memory leak)
|
||
'max_coroutine' => 100000, // max coroutines per worker
|
||
'enable_coroutine' => true,
|
||
'open_tcp_nodelay' => true,
|
||
'socket_buffer_size' => 3 * 1024 * 1024,
|
||
'buffer_output_size' => 3 * 1024 * 1024,
|
||
'package_max_length' => 5 * 1024 * 1024,
|
||
'heartbeat_check_interval' => 60,
|
||
'heartbeat_idle_time' => 120,
|
||
],
|
||
```
|
||
|
||
## 协程并发控制
|
||
|
||
```php
|
||
use Hyperf\Coroutine\Parallel;
|
||
|
||
// ✅ 并行执行多个无依赖的 I/O 操作
|
||
$parallel = new Parallel(10); // max concurrency = 10
|
||
|
||
$parallel->add(function () {
|
||
return $this->orderService->getStatistics($orderId);
|
||
});
|
||
|
||
$parallel->add(function () {
|
||
return $this->paymentService->getPayments($orderId);
|
||
});
|
||
|
||
$parallel->add(function () {
|
||
return $this->deliveryService->getDeliveries($orderId);
|
||
});
|
||
|
||
[$stats, $payments, $deliveries] = $parallel->wait();
|
||
```
|
||
|
||
```php
|
||
use Hyperf\Coroutine\WaitGroup;
|
||
|
||
// ✅ WaitGroup: wait for all coroutines to complete
|
||
$wg = new WaitGroup();
|
||
$results = [];
|
||
|
||
$wg->add(1);
|
||
go(function () use ($wg, &$results) {
|
||
$results['users'] = $this->userService->getOnlineCount();
|
||
$wg->done();
|
||
});
|
||
|
||
$wg->add(1);
|
||
go(function () use ($wg, &$results) {
|
||
$results['orders'] = $this->orderService->getTodayCount();
|
||
$wg->done();
|
||
});
|
||
|
||
$wg->wait(5.0); // timeout 5s
|
||
```
|
||
|
||
## 内存管理
|
||
|
||
- `max_request = 10000`: Worker 处理 N 个请求后自动重启,防止内存泄漏
|
||
- 避免在全局/静态变量中累积数据
|
||
- 大数组处理完后手动 `unset()`
|
||
- 使用 `memory_get_usage()` 监控内存
|
||
- 对象生命周期与请求绑定,不跨请求持有引用
|
||
|
||
```php
|
||
// ✅ 大数据分批处理
|
||
ProductionOrder::query()
|
||
->where('status', 'pending')
|
||
->chunk(500, function ($orders) {
|
||
foreach ($orders as $order) {
|
||
$this->processOrder($order);
|
||
}
|
||
// chunk 结束后自动释放内存
|
||
});
|
||
```
|
||
|
||
## 定时任务
|
||
|
||
```php
|
||
use Hyperf\Crontab\Annotation\Crontab;
|
||
|
||
#[Crontab(
|
||
name: 'CheckPaymentTimeout',
|
||
rule: '*/5 * * * *',
|
||
memo: 'Check payment timeout orders',
|
||
singleton: true, // prevent duplicate execution
|
||
onOneServer: true, // only run on one server in cluster
|
||
mutexPool: 'default',
|
||
)]
|
||
class CheckPaymentTimeoutTask
|
||
{
|
||
public function execute(): void
|
||
{
|
||
// batch processing with chunk
|
||
}
|
||
}
|
||
```
|
||
|
||
## 性能监控
|
||
|
||
```php
|
||
// Swoole Server status
|
||
$server = $this->container->get(ServerInterface::class);
|
||
$stats = $server->stats();
|
||
// Returns: start_time, connection_num, request_count, worker_request_count, coroutine_num
|
||
|
||
// Redis monitor
|
||
$redis = $this->container->get(RedisFactory::class)->get('default');
|
||
$info = $redis->info();
|
||
```
|