EasySwoole

性能优化

性能优化应从数据开始,而不是直接复制一组“高并发参数”。完整链路是:

客户端
  → 网络和 Linux 内核
  → Nginx / 负载均衡
  → Swoole Reactor 与 Worker
  → EasySwoole 控制器
  → MySQL、Redis、第三方接口

任何一层饱和,继续增大另一层并发都可能让排队时间、内存和超时更加严重。

优化前先确定指标

至少记录:

  • 吞吐量:RPS/QPS。
  • 延迟:P50、P95、P99,而不只是平均值。
  • 错误率:4xx、5xx、超时和连接失败。
  • 并发连接数、活跃协程数、请求排队情况。
  • Worker CPU、RSS 内存和进程重启次数。
  • MySQL/Redis 连接池等待时间与慢查询。
  • 外部 HTTP、DNS、磁盘和日志耗时。

优化目标应该类似“在 8 核 16GB 实例上,500 并发时 P99 小于 200ms、错误率低于 0.1%”,而不是笼统地追求“最大并发”。

Linux 文件描述符与 ulimit

每个客户端 Socket、Nginx 到 EasySwoole 的上游连接、MySQL/Redis 连接、日志文件和普通文件都会消耗文件描述符。文件描述符不足时,程序通常会出现 Too many open files、无法接受新连接或无法打开日志等问题。

先理解软限制和硬限制

查看当前 Shell 的限制:

ulimit -Sn  # soft nofile:当前实际采用的限制
ulimit -Hn  # hard nofile:普通用户可设置的上限

软限制可以在不超过硬限制的前提下调整;普通用户不能把硬限制提高到系统允许值以上。ulimit 不是全局配置,它只影响当前 Shell 以及从该 Shell 启动的子进程:

当前 Shell 的 ulimit
        ↓ 继承
php easyswoole.php server start
        ↓ 继承
EasySwoole Master → Manager → Worker/TaskWorker

因此,在终端 A 执行 ulimit -n 200000,不会改变终端 B、已经运行的 EasySwoole,或由 systemd 启动的服务。

临时调整并启动服务

适合开发环境或临时压测:

ulimit -n 200000
ulimit -Sn
php easyswoole.php server start

如果出现 ulimit: open files: cannot modify limit: Operation not permitted,通常表示目标值超过当前硬限制。此时不要反复执行命令,应配置 PAM limits、systemd 或容器运行参数。

也可以在启动脚本中设置,但必须确保该脚本就是 EasySwoole 进程的父进程:

#!/usr/bin/env bash
set -e

ulimit -n 200000
exec php /var/www/app/easyswoole server start

exec 会让 PHP 进程替换当前脚本进程,便于服务管理器正确跟踪 PID 和退出状态。

使用 systemd 永久配置

生产环境如果由 systemd 启动,Shell 的 ulimit 和 /etc/security/limits.conf 通常不会决定服务限制,应直接为 Unit 设置 LimitNOFILE。推荐用 override,避免升级服务文件时覆盖自定义配置:

sudo systemctl edit easyswoole

填写:

[Service]
LimitNOFILE=200000

应用并重启服务:

sudo systemctl daemon-reload
sudo systemctl restart easyswoole

查看 systemd 已加载的配置以及运行进程真正获得的限制:

systemctl show easyswoole -p LimitNOFILE -p MainPID

MAIN_PID=$(systemctl show easyswoole -p MainPID --value)
cat "/proc/${MAIN_PID}/limits" | grep 'Max open files'

不要只在 SSH 终端执行 ulimit -n 后就认为 systemd 服务也已生效。

配置登录用户的 PAM limits

如果通过 SSH 登录后手动启动 EasySwoole,可新建:

# /etc/security/limits.d/99-easyswoole.conf
www-data soft nofile 200000
www-data hard nofile 200000

将 www-data 替换为实际启动服务的用户。修改后需要退出并重新登录,再执行:

ulimit -Sn
ulimit -Hn

该方式依赖登录会话经过 PAM limits 模块,不会自动修改已经存在的会话,也不应拿来替代 systemd Unit 的 LimitNOFILE。

系统级上限

除了单个进程的 nofile,Linux 还有系统级约束:

sysctl fs.file-max   # 整个系统可分配的文件句柄数量
sysctl fs.nr_open    # 单进程 nofile 可设置的内核上限
cat /proc/sys/fs/file-nr

只有系统级文件句柄确实接近耗尽时,才需要提高 fs.file-max。如果 LimitNOFILE 超过 fs.nr_open,还需要评估并调整 fs.nr_open。示例配置:

# /etc/sysctl.d/99-easyswoole.conf
fs.file-max = 1000000
fs.nr_open = 1000000
sudo sysctl --system

这些值不是固定推荐值,应根据实例内存、服务数量和连接预算决定。

如何估算和验证

nofile 不能只等于 Swoole 的 max_conn,因为程序还需要访问上游和文件。可以先按下面的思路做容量预算:

nofile 下限
≈ 客户端最大连接数
 + MySQL/Redis/HTTP 客户端连接数
 + 日志、静态文件和监听 Socket
 + 20%~30% 安全余量

例如计划承载 50,000 个长连接,并预留 10,000 个上游连接和其他句柄,可把 80,000 作为压测起点,而不是不加分析地设置为 1,000,000。

服务启动后,找到实际 Master/Worker PID 并验证:

pgrep -af 'easyswoole|EasySwoole'
cat /proc/<PID>/limits | grep 'Max open files'
ls "/proc/<PID>/fd" | wc -l
lsof -p <PID> | wc -l

/proc/<PID>/fd 反映该进程当前打开的描述符数量。Swoole 使用多进程模型,应同时观察 Master、Manager、Worker 和 TaskWorker,而不是随意取第一个 PHP 进程。最终还要确认:

  • max_conn 小于进程实际的软限制,并留出上游连接和文件余量。
  • Nginx 自身的 worker_rlimit_nofile、systemd 限制和 worker_connections 相互匹配。
  • 容器部署时同时检查宿主机和容器的 nofile;例如 Docker/Compose 需要单独传入 --ulimit 或 ulimits。
  • 调高限制只增加容量上限,不会直接提高 RPS;内存、带宽和下游服务仍可能先达到瓶颈。

Linux TCP 队列

两个容易混淆的队列:

tcp_max_syn_backlog
    半连接队列:收到 SYN、尚未完成三次握手

somaxconn
    listen() 已完成握手、等待应用 accept 的队列上限

查看当前值:

sysctl net.core.somaxconn
sysctl net.ipv4.tcp_max_syn_backlog
ss -s
netstat -s | grep -i listen

只有监控显示突发连接导致队列溢出时才提高。示例起点:

# /etc/sysctl.d/99-easyswoole.conf
net.core.somaxconn = 8192
net.ipv4.tcp_max_syn_backlog = 8192

应用:

sudo sysctl --system

Swoole 的 backlog 最终还会受到 net.core.somaxconn 截断,所以两边需要配套。Linux 新旧版本的默认值不同,不能假设所有服务器都是 128 或 4096。

临时端口与 TIME_WAIT

EasySwoole 大量主动连接 MySQL、Redis 或 HTTP 上游时,会消耗本机临时端口:

sysctl net.ipv4.ip_local_port_range
ss -ant state time-wait | wc -l

可以在确认端口确实不足后扩大范围:

net.ipv4.ip_local_port_range = 10240 65535

更重要的是复用连接池和 HTTP Keep-Alive,避免每个请求重新建立上游连接。不要为了减少 TIME_WAIT 盲目修改 tcp_tw_reuse、tcp_fin_timeout 或关闭协议保护;TIME_WAIT 是 TCP 正确性的一部分,异常数量通常意味着连接没有复用或上游设计存在问题。

TCP Keepalive

长连接或 WebSocket 可结合 Swoole 应用心跳与 TCP Keepalive。Linux 参数包括:

sysctl net.ipv4.tcp_keepalive_time
sysctl net.ipv4.tcp_keepalive_intvl
sysctl net.ipv4.tcp_keepalive_probes

TCP Keepalive 用于发现死连接,不等同于业务心跳。参数过小会制造大量探测包;还要与 Nginx、云负载均衡和防火墙的空闲超时共同设计。

一个保守的内核配置示例

下面只是中等规模服务的起点,不是通用最优值:

# /etc/sysctl.d/99-easyswoole.conf
fs.file-max = 1000000
net.core.somaxconn = 8192
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.ip_local_port_range = 10240 65535
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5

变更前保存旧值,先在一台测试或灰度实例验证,并观察连接失败、内存和网络指标。容器环境还要确认参数属于宿主机、Pod 还是 Network Namespace,以及平台是否允许修改。

Swoole 配置放在哪里

EasySwoole 把 MAIN_SERVER.SETTING 原样传给 Swoole Server。开发环境通常修改根目录 dev.php,生产修改 produce.php:

'MAIN_SERVER' => [
    'LISTEN_ADDRESS' => '127.0.0.1',
    'PORT' => 9501,
    'SERVER_TYPE' => EASYSWOOLE_WEB_SERVER,
    'SETTING' => [
        'reactor_num' => 4,
        'worker_num' => 8,
        'backlog' => 8192,
        'max_conn' => 50000,
        'max_request' => 100000,
        'max_request_grace' => 10000,
        'max_request_execution_time' => 30,
        'reload_async' => true,
        'max_wait_time' => 10,
        'package_max_length' => 2 * 1024 * 1024,
        'buffer_output_size' => 2 * 1024 * 1024,
    ],
],

这些数值仅用于展示字段位置,不能原样用于所有服务器。修改底层 Setting 后应完整重启,并检查启动日志中是否有不支持或非法配置警告。

reactor_num

Reactor 线程负责监听 Socket 和投递网络事件,不执行 PHP 业务。通常从 CPU 核数附近开始测试:

'reactor_num' => swoole_cpu_num(),

Reactor 并非越多越快,数量过大会增加线程调度开销。官方文档建议不要超过 CPU 核数的约两倍,并指出它受到 Worker 数量关系的约束。HTTP 业务慢时,瓶颈更常在 Worker 或下游,而不是 Reactor。

worker_num

Worker 执行 PHP Controller 和业务代码。

  • CPU 密集:通常从 CPU 核数开始,过多会产生上下文切换。
  • 协程化 I/O:可从 CPU 核数的 1–4 倍压测,但仍受内存和下游容量限制。
  • 同步阻塞 I/O:增加 Worker 能暂时提高并发,但优先修复阻塞调用或迁移到合适的 Task/独立服务。

查看 CPU:

nproc
lscpu

调优方法是逐级测试 1×、2× 等配置,比较吞吐、P99、CPU 和上下文切换,而不是直接设置几十倍 CPU。

max_conn

max_conn 是 Swoole 同时接受的最大连接数,应同时满足:

max_conn < Worker 进程实际 LimitNOFILE
max_conn × 每连接内存 < 可用内存预算

HTTP Keep-Alive 和 WebSocket 都占连接。把它设置成几十万并不会创造内存、带宽或下游容量;超出业务承载能力时,较早拒绝通常比服务器 OOM 更安全。

backlog

backlog 控制应用 listen 队列请求值,实际值受 Linux somaxconn 上限约束:

'backlog' => 8192,

它主要吸收短时连接突发,不能解决 Worker 长时间处理不过来的问题。如果队列持续增长,应降低上游流量、扩容或优化业务。

max_request 与内存增长

max_request 让 Worker 处理一定请求数后重启,可释放扩展或业务代码缓慢泄漏的内存:

'max_request' => 100000,
'max_request_grace' => 10000,

Grace 随机区间避免全部 Worker 同时重启。它是兜底手段,不是修复内存泄漏的方法。先用监控确认 RSS 随请求数持续增长,再设置合理值并继续定位泄漏;持有长连接或复杂异步状态的服务更要谨慎轮换 Worker。

请求执行时间

支持该 Setting 的 Swoole 版本可以设置:

'max_request_execution_time' => 30,

它防止单个 HTTP 请求无限占用执行资源,但不能代替 MySQL、Redis、HTTP Client 自身的连接与读写超时。每个外部依赖都应设置更短、分层的超时。

优雅 reload

'reload_async' => true,
'max_wait_time' => 10,

异步 reload 允许 Worker 完成正在处理的事件,max_wait_time 限制最长等待。时间太短会中断慢请求,太长会拖慢发布。Server 类型、端口及部分底层配置变化仍需要完整重启。

请求与响应大小

'package_max_length' => 2 * 1024 * 1024,
'buffer_output_size' => 2 * 1024 * 1024,

限制必须根据真实 API 设置,并与 Nginx client_max_body_size 配套。不要为了“大文件”把所有接口上限放到数百 MB;上传应独立限流,超大文件使用分片或对象存储直传。

过大的输出缓存会增加每连接内存风险。列表接口要分页,文件使用 sendFile() 或代理/对象存储,不要把整文件读进 PHP 字符串。

协程与阻塞 I/O

协程只有在等待可协程化的 I/O 时才能让出执行权。以下调用需要重点检查:

  • 未启用 Hook 的阻塞网络库。
  • 本地大文件同步读写。
  • CPU 密集的压缩、图像和加密计算。
  • sleep()、外部命令和未知扩展调用。

项目通常在 EasySwooleEvent::initialize() 配置协程 Hook:

use Swoole\Coroutine;

Coroutine::set([
    'hook_flags' => SWOOLE_HOOK_ALL,
    'max_concurrency' => 1000,
]);

Hook 范围应按依赖兼容性测试,不要未经验证直接在生产从少量 Hook 切换到 ALL。max_concurrency 是保护上限,不代表应用和数据库真的能承载同等并发。

Task Worker

发送邮件、生成报表等不要求在 HTTP 响应内完成的工作可以投递 Task 或外部消息队列。CPU 密集任务放入 Task 只能隔离 HTTP Worker,并不会降低总 CPU 消耗。

Task Worker 是独立进程,也要设置并发、超时、重试、幂等和积压监控。需要可靠投递的业务不能只依赖内存 Task 队列,应使用持久化消息系统。

连接池容量

连接池总量按实例和 Worker 放大:

总 MySQL 连接
= 应用实例数 × worker_num × 每 Worker 池上限

例如 4 个实例、8 个 Worker、每 Worker 10 条连接,理论最大为 320。必须确认 MySQL max_connections、Redis、代理和其他服务都能承受,并预留管理连接。

连接池太小会排队,太大会压垮数据库。应同时观察池等待时间、连接使用率、SQL P99 和数据库 CPU。连接必须在 finally 中归还,并设置获取超时,不能无限等待。

应用层收益通常更大

在调整内核之前,优先检查:

  • 是否存在 N+1 查询。
  • 列表是否分页,字段是否只查询所需列。
  • 索引是否匹配真实过滤和排序条件。
  • Redis 缓存是否有命中率、TTL、防击穿和防雪崩设计。
  • 外部调用能否并发,但是否有限制最大并发。
  • JSON 和响应体是否过大。
  • 日志是否同步写大量内容或记录完整 Body。
  • 热路径是否重复读取配置、解析注解或创建连接。

缓存不是越多越好。缓存键数量、序列化成本、一致性和穿透同样需要监控。

Nginx 配合

Nginx 侧重点包括:

worker_processes auto;

events {
    worker_connections 16384;
    multi_accept on;
}

http {
    keepalive_timeout 30s;
    keepalive_requests 1000;

    upstream easyswoole_backend {
        server 127.0.0.1:9501;
        keepalive 64;
    }
}

理论连接容量还受 Nginx Worker 的文件描述符限制。上游 Keepalive 减少 Nginx 与 EasySwoole 反复握手;客户端 Keepalive 太长则会占用更多空闲连接。具体代理 Header、上传和 WebSocket 配置参见“反向代理部署”。

如何压测

先建立可重复基线:固定机器、数据集、PHP/Swoole 配置和压测机。压测机不能与服务共用 CPU。

简单示例:

wrk -t4 -c100 -d60s --latency http://127.0.0.1:9501/health/live
wrk -t8 -c500 -d120s --latency http://127.0.0.1:9501/api/users

健康接口只能测框架和网络上限,不能代表真实业务。还要测试带数据库、JSON Body、鉴权、缓存未命中和典型响应大小的接口。

逐次只改变一个变量,记录:

配置版本、RPS、P50/P95/P99、错误率、CPU、内存、连接数、下游指标

压测结束后继续低流量运行一段时间,观察常驻进程内存是否持续增长。只看 30 秒峰值会漏掉泄漏、连接池耗尽和缓存过期抖动。

运行时观察

常用系统命令:

pidstat -p ALL 1
vmstat 1
ss -s
sar -n TCP,ETCP 1
top -H -p <pid>

Swoole Server 的 stats() 可以提供连接、请求、Worker/Task 等运行指标。可以把必要字段接入内部监控,但不要把完整调试端点公开到互联网。

设置告警时关注趋势和饱和度:P99 上升、连接池等待、队列积压、Worker RSS 单调增长、连接拒绝和下游超时,比单独的 CPU 百分比更有价值。

推荐调优顺序

1. 建立真实业务压测与监控基线
2. 修复慢 SQL、阻塞 I/O、N+1 和超大响应
3. 设置依赖超时、连接池和并发上限
4. 调整 worker_num / reactor_num 并重复压测
5. 根据连接量设置 nofile、max_conn、backlog
6. 只有观察到队列或端口瓶颈时再调整 sysctl
7. 灰度发布并持续观察 P99、错误率和内存

常见误区

  • worker_num 设置越大性能越好。
  • 把 max_request 当作内存泄漏的永久解决方案。
  • 盲目复制 sysctl.conf,不知道每个值解决什么指标。
  • 只提高连接上限,不核算每连接内存和数据库容量。
  • 压测只访问空的 HelloWorld,然后推断业务接口性能。
  • 只看平均延迟,忽略 P99 和错误率。
  • 所有超时都设得很长,导致故障时请求堆积。
  • 在 Worker 内运行阻塞命令或 CPU 重任务。
  • 用本机静态数组保存跨 Worker 状态。

参考依据

项目使用的是 Swoole 扩展时,应以实际安装版本的配置文档和启动警告为准;OpenSwoole 文档用于解释同源配置概念,不代表每个版本都支持完全相同的 Setting。