webman 高并发进阶:协程实战、连接池调优与压测方法论
webman 裸奔就能跑出很高的 QPS,但"框架快"不等于"业务快"——一个同步 HTTP 调用就能让 worker 的事件循环卡住,吞吐瞬间跌到个位数。这篇讲 webman 高并发的三板斧:协程化改造、连接池调优、压测方法论。这是 webman 中高级的分水岭。
一、先理解瓶颈:worker 事件循环是怎么被卡死的
webman 的 worker 是单进程单线程事件循环(Reactor 模型)。在同一个 worker 里,请求是串行处理的:
worker-1 事件循环:
事件:请求A到达 → 处理A(其中调用外部API耗时 200ms,同步阻塞!)
(这 200ms 内,分配给 worker-1 的所有其他请求全部排队)
事件:请求B到达 → 处理B推论:平均响应时间 = 排队时间 + 处理时间。 只要处理路径上有阻塞调用,worker 的吞吐上限就是 1 / 平均阻塞时长。200ms 的外部调用意味着单个 worker 每秒最多 5 个请求,16 个 worker 也就 80 QPS——和框架本身的几十万 QPS 上限毫无关系。
解法只有两条:消灭阻塞(异步/协程化),或者增加并行度(协程)。
二、协程:webman 的三种姿势
1. 内置 Swoole/Swow 协程驱动
webman 支持通过配置切换协程运行时:
// config/server.php
'settings' => [
'event_loop' => \Workerman\Events\Swoole::class, // 或 Swow
],开启后,worker 内自动协程化:遇到 IO(MySQL、Redis、HTTP)时挂起当前协程,让出事件循环处理其他请求——同一个 worker 从串行变成并发。
Swoole 与 Swow 的选择:Swoole 生态成熟但与 PHP 原生扩展偶有兼容性问题(hook 覆盖不全的扩展会退化为阻塞);Swow 是 PHP 官方系方案更干净但生态年轻。生产选型建议:现有业务全是标准 PDO/cURL 的选 Swoole 一把梭;有奇怪扩展依赖的先在预发验证 Swow。
2. 协程客户端
协程化后最大的红利是并发聚合调用:
use Swoole\Coroutine;
use function Swoole\Coroutine\go;
use function Swoole\Coroutine\waitGroup;
// 聚合页:并行拉取用户/商品/优惠券,总耗时 = max 而不是 sum
$wg = new waitGroup();
$results = [];
go(function () use ($wg, &$results) {
$wg->add();
$results['user'] = $this->userApi->get($uid);
$wg->done();
});
go(function () use ($wg, &$results) {
$wg->add();
$results['item'] = $this->itemApi->get($itemId);
$wg->done();
});
$wg->wait(3.0); // 总超时 3s
// 原来 200+150+100=450ms,现在 max(200,150,100)=200msBFF/聚合接口的标准优化手段,P99 延迟直接砍半。
3. 协程的三个纪律
纪律一:协程切换点后不信任局部状态。 yield 期间其他协程可能改了共享数据。请求级数据老老实实走 $request 属性,全局状态放 Redis。
纪律二:连接必须绑协程。 协程 A 半截的事务,切换后协程 B 接着用同一个连接 = 事务串台。Swoole 的 PDO hook + webman/database 的连接池已处理绑定关系,但你自己 new PDO 存静态属性再跨协程用,必炸。
纪律三:CPU 密集任务协程救不了。 协程只解决 IO 并发,CPU 密集计算照样卡死事件循环。重计算丢给自定义进程或队列,别在 worker 里跑。
三、连接池:协程时代的正确配置
协程化后并发量上来了,单连接成了新瓶颈,必须配连接池(webman 通过 webman/database/webman/redis 的连接池实现,Swoole 驱动下生效):
// config/database.php(连接池相关参数)
'pool' => [
'max_connections' => 32, // 每进程最大连接数
'min_connections' => 2, // 保底连接
'connect_timeout' => 3, // 取连接超时
'wait_timeout' => 3, // 等待池中连接的超时
'heartbeat' => -1, // 心跳检测间隔(配合 wait_timeout 用负数自动探测)
'max_idle_time' => 60, // 空闲连接回收
],容量估算公式:
每进程需要的连接数 ≈ 协程并发度(同时进行中的查询数)
总连接 = worker 数 × max_connections
例:16 worker × 32 连接 = 512 连接 < MySQL max_connections(1000) ✓常见翻车:池配了 32,MySQL max_connections 只有 100,另一台应用共用它——高峰期互相挤兑连接,报错 connection timeout 却以为是网络问题。跨应用共享数据库时,连接预算要全局规划。
池的监控指标
每个指标都应该有告警:
| 指标 | 健康值 | 异常含义 |
|---|---|---|
| 池利用率 | < 80% | 持续 100% = 池太小或查询太慢 |
| 等待取连接耗时 | < 1ms | 高 = 池不够用 |
| 活跃连接数 | 平稳 | 锯齿状抖动 = 连接泄漏 |
| 慢查询数 | 趋近 0 | 高 = 先治慢查询再扩池 |
四、压测方法论:别让数据骗了你
压测的正确姿势
- 预热:先压 1 分钟丢弃数据(连接懒加载、JIT、文件缓存都要热身)
- 阶梯加压:并发 10 → 50 → 100 → 200,每档 3-5 分钟,找到拐点
- 看 P99 而不是平均值:平均值 50ms、P99 3s 的服务,用户体感是灾难
- 压测端别成为瓶颈:用独立机器跑 wrk/vegeta,本机压本机数据不可信
# wrk 示例:16 线程 200 并发,持续 3 分钟
wrk -t16 -c200 -d180s -s post.lua http://10.0.0.5:8787/api/orders拐点诊断表
| 现象 | 第一嫌疑 | 验证手段 |
|---|---|---|
| CPU 打满 100% | 业务计算 / 序列化开销 | perf top 看热点函数 |
| CPU 很低但延迟飙升 | 阻塞 IO(同步调用没协程化) | strace 看 worker 阻塞在哪个 syscall |
| 连接池等待耗时高 | 池容量不足或慢查询 | 慢查询日志 + 池监控 |
| MySQL CPU 高 | 缺索引 / 慢 SQL | explain 逐条过 |
| 延迟毛刺 | reload / GC / 心跳任务 | 对齐时间线看毛刺时刻发生了什么 |
一个容易被忽略的真相:压测时开着 Xdebug、开着 debug 日志、没开 opcache/jit,得出的"webman 不行"结论全是冤案。压测环境配置必须对齐生产:php start.php start -d(daemonize)、opcache 开启、日志降级。
webman 侧的性能自检
php start.php status # 每个worker的连接数、请求统计关注 total_request(累计请求数)和 request_per_second。如果各 worker 请求分布严重不均,检查是否用了 sticky 逻辑(比如按 uid hash 到特定连接)。
五、一个真实调优案例
场景:订单列表接口,压测到 800 QPS 后延迟从 40ms 涨到 2s。
排查过程:
status显示 worker CPU 只有 30% → 不是 CPU 瓶颈- strace 发现 worker 大量时间阻塞在
recvfrom(MySQL 等待)→ IO 等待 - 慢查询日志:一条
ORDER BY created DESC LIMIT 20 OFFSET 5000没有覆盖索引,深分页扫全表 1.2s - 修复:游标分页(
WHERE id < last_id LIMIT 20)+ 联合索引 - 复测:5000 QPS 稳定,P99 = 65ms
教训:框架层的优化(协程/连接池)解决的是"传导"问题,SQL 层的慢查询是"源头"问题。源头不治,协程只是让请求更快地堵在数据库门口。
六、优化优先级清单
按投入产出比排序,从上往下做:
1. 慢 SQL 治理(索引、深分页、N+1) ← 90% 的性能问题在这
2. 聚合接口并行化(协程并发调用)
3. 热点数据缓存(Redis,注意缓存一致性)
4. 协程运行时 + 连接池配置
5. opcache / JIT / preload
6. worker 数与压测验证写在最后
高并发的本质是让每个 worker 的每一毫秒都在干活:协程消灭 IO 空转,连接池消灭建连开销,压测验证每一项优化真实生效。记住那条优先级清单——先治 SQL,再谈协程。最后一篇我们讲生产化:部署、进程守护、监控告警和微服务化实践。
评论 (0)