性能之巅 · 第6章 CPU 标题①:上下文切换开销 —— 协程切换有多便宜 大白话:线程/进程切换要陷入内核、保存一大堆寄存器(微秒级);协程切换在用户态完成,只存几个寄存器(纳秒级)。这就是 Swoole 能开几十万并发的根因。?php// ctx_switch.phpuse Swoole\Coroutine;use Swoole\Coroutine\Channel;use Swoole\Coroutine\WaitGroup;use function Swoole\Coroutine\run;run(function(){$n500000;$anewChannel(1);$bnewChannel(1);// 两个协程用它来回传球$wgnewWaitGroup();$wg-add(2);$smicrotime(true);Coroutine::create(function()use($a,$b,$n,$wg){for($i0;$i$n;$i){$a-push(1);$b-pop();}// 推一下就切走$wg-done();});Coroutine::create(function()use($a,$b,$n,$wg){for($i0;$i$n;$i){$a-pop();$b-push(1);}$wg-done();});$wg-wait();$cmicrotime(true)-$s;$switches$n*2;printf(%d 次协程切换,耗时 %.3fs,每次切换 ≈%.0f 纳秒\n,$switches,$c,$c/$switches*1e9);});看什么:每次协程切换大概几百纳秒。对比:线程/进程切换约1~5微秒(慢10倍以上),可用 perf stat-e context-switches 验证。 标题②:协程创建成本——10万个协程秒级、内存极小 大白话:开10万个线程会把机器搞死,开10万个协程轻轻松松。?php// create_cost.phpuse Swoole\Coroutine;use Swoole\Coroutine\WaitGroup;use function Swoole\Coroutine\run;run(function(){$n100000;$smicrotime(true);$wgnewWaitGroup();for($i0;$i$n;$i){$wg-add();Coroutine::create(function()use($wg){$wg-done();});}$wg-wait();$cmicrotime(true)-$s;printf(创建 %d 个协程,耗时 %.3fs,平均 %.2f μs/个,峰值内存%.1fMB\n,$n,$c,$c/$n*1e6,memory_get_peak_usage()/1048576);});看什么:10万协程通常1秒内、内存几十 MB。换成10万进程/线程根本起不来——这就是量级差距。 标题③:CPU亲和性绑定 ——把进程钉在指定核上 大白话:默认进程在各个核之间跳来跳去,每跳一次缓存就失效。绑定到固定核能减少缓存抖动,提升性能。?php// cpu_affinity.phpuse Swoole\Process;echo本机 CPU 核数 .swoole_cpu_num().\n;$pnewProcess(function(Process $worker){$worker-setAffinity([0]);// 把本进程钉死在 0 号核echo进程 .getmypid(). 已绑定到 CPU#0,开始吃 CPU(用 top 看)\n;$x0;for($i0;$i2_000_000_000;$i)$x$i;// 持续吃CPU便于观察});$p-start();Process::wait();看什么:运行后 top 按1看每核占用,会发现只有 CPU#0被打满,其它核闲着。生产中可把每个 Worker 绑不同核,避免互相争抢。 标题④:运行队列/负载 ——Worker 数为什么对齐核数 大白话:核就那么几个,进程比核多时就得排队轮流上CPU(运行队列变长负载升高)。所以 CPU 密集型 worker_num 别超过核数。?php// run_queue.php$coresswoole_cpu_num();echo物理核数 $cores\n;echo建议 worker_num:\n;echo CPU 密集型 →$cores(再多只会排队,见第2章 USL 曲线)\n;echo I/O 密集型 →.($cores*2). ~ .($cores*4).(等待时让出CPU,可适当超配)\n;// 验证:用 uptime 看 load average,若 load 远大于核数,说明运行队列在堆积看什么:跑业务时 uptime 看 load average,load 长期核数进程在排队抢 CPU,该减 Worker 或加机器。---性能之巅 · 第10章 网络 ▎ 下面的网络示例都先启动这个 TCP 服务,再用客户端去打它:?php// tcp_server.phpuse Swoole\Server;$servernewServer(0.0.0.0,9501,SWOOLE_PROCESS,SWOOLE_SOCK_TCP);$server-set([worker_num2,backlog128,// 握手完成、等 accept 的队列长度(饱和度)open_tcp_nodelaytrue,// 关 Nagle:小包立即发,降延迟heartbeat_check_interval5,// 每 5 秒巡检一次连接heartbeat_idle_time10,// 10 秒没数据就踢掉(防死连接堆积)]);$server-on(connect,fn($s,$fd)print(连接进来 fd$fd\n));$server-on(receive,fn($s,$fd,$rid,$data)$s-send($fd,echo:$data));$server-on(close,fn($s,$fd)print(连接关闭 fd$fd(可能被心跳踢了)\n));$server-start();标题⑤:连接延迟——三次握手要花时间 大白话:每建一个新连接,都要 TCP 三次握手,这部分时间是白交的过路费。?php// connect_latency.phpuse Swoole\Coroutine\Client;use function Swoole\Coroutine\run;run(function(){$smicrotime(true);$clinewClient(SWOOLE_SOCK_TCP);$cli-connect(127.0.0.1,9501,1.0);// 三次握手在这一步printf(建立连接(三次握手)耗时 %.3f ms\n,(microtime(true)-$s)*1000);$cli-close();});看什么:本机几毫秒,跨机房可能几十毫秒。连接越远,这笔过路费越贵 →引出下一个:别频繁建连。 标题⑥:长连接vs 短连接 ——复用连接省掉重复握手 大白话:短连接每次都交过路费(握手TIME_WAIT),长连接交一次费用后一直用。?php// reuse.phpuse Swoole\Coroutine\Client;use function Swoole\Coroutine\run;// A:长连接 ——握手 1 次,复用 1000 次run(function(){$cnewClient(SWOOLE_SOCK_TCP);$c-connect(127.0.0.1,9501,1.0);$smicrotime(true);for($i0;$i1000;$i){$c-send(hi);$c-recv();}printf(【长连接】1000 次请求 %.3fs\n,microtime(true)-$s);$c-close();});// B:短连接 ——每次都重新握手关闭run(function(){$smicrotime(true);for($i0;$i1000;$i){$cnewClient(SWOOLE_SOCK_TCP);$c-connect(127.0.0.1,9501,1.0);$c-send(hi);$c-recv();$c-close();}printf(【短连接】1000 次请求 %.3fs(每次握手,明显慢)\n,microtime(true)-$s);});看什么:长连接明显快。这就是为什么数据库/Redis 一定要用连接池(连接复用)。 标题⑦:Nagle算法/open_tcp_nodelay ——小包要不要立即发 大白话:Nagle 会把小包攒一攒再发(省带宽但增延迟)。交互式/小包场景(游戏、RPC)要关掉它,让小包立即发。?php// nodelay.php ——客户端也可单独设use Swoole\Coroutine\Client;use function Swoole\Coroutine\run;run(function(){$cnewClient(SWOOLE_SOCK_TCP);$c-set([open_tcp_nodelaytrue]);// 关 Nagle$c-connect(127.0.0.1,9501,1.0);$smicrotime(true);for($i0;$i500;$i){$c-send(x);$c-recv();}// 大量小包printf(nodelayon 时 500 个小包来回 %.3fs(关Nagle后延迟更低)\n,microtime(true)-$s);$c-close();});看什么:服务端客户端都 open_tcp_nodelaytrue时,小包交互延迟更低。记住:吞吐优先开 Nagle,延迟优先关 Nagle。 标题⑧:backlog——握手完成等待 accept 的队列 大白话:握手好了但 Worker 还没来得及 accept 的连接,会先放进 backlog 队列;队列满了,新连接直接被拒。这就是网络层的饱和度。?php// 在 tcp_server.php 里设置(已包含):// backlog 128, // 这个队列能缓冲多少个已握手未处理的连接验证(瞬间发起远超 backlog 的连接):# 用 ss 看 listen 队列的当前长度/最大值(Recv-Q/Send-Q)ss-lntsport :9501#State Recv-Q Send-Q...#LISTEN0128←Send-Q 就是 backlog 上限,Recv-Q 是当前积压看什么:高并发瞬时建连时 Recv-Q 逼近 Send-Q →该调大 backlog,否则报connection refused。 标题⑨:TIME_WAIT——短连接的后遗症大白话:主动关闭连接的一方,socket 会停在 TIME_WAIT 状态~60秒占着端口。短连接太多会把本地端口耗尽,报 Cannot assign requested address。?php// time_wait.php ——制造大量短连接use Swoole\Coroutine\Client;use function Swoole\Coroutine\run;run(function(){for($i0;$i1000;$i){$cnewClient(SWOOLE_SOCK_TCP);$c-connect(127.0.0.1,9501,1.0);$c-send(hi);$c-recv();$c-close();// 客户端主动关 →本端进入 TIME_WAIT}echo建了 1000 个短连接,马上用 ss -s 看 TIME_WAIT 数量\n;});验证:ss-s # 看 timewait 计数 # 或 netstat-an|grep TIME_WAIT|wc-l 看什么:TIME_WAIT 数量飙升。解法:用长连接/连接池(治本);或调内核tcp_tw_reuse(治标)。 标题⑩:心跳/keepalive ——踢掉死连接,防资源泄漏 大白话:有些连接对端断网了但服务端不知道,会一直占着内存。心跳定期巡检,长时间没动静的连接就主动踢掉。?php// 在 tcp_server.php 里已配置:// heartbeat_check_interval 5, // 每 5 秒巡检// heartbeat_idle_time 10, // 10 秒没收发数据就关掉它验证:# 用 nc 连上去,然后啥也不发,等10秒 nc127.0.0.19501#10秒后服务端会打印连接关闭 fdx(可能被心跳踢了),nc 被断开 看什么:空闲连接10秒后被自动踢掉。生产必开,否则死连接越积越多,最终连接数耗尽。---这两章一句话总览 第6章 CPU-协程切换纳秒级、创建轻量,所以能撑超高并发(①②);-绑核减少缓存抖动,Worker 数对齐核数防排队(③④)。 第10章 网络-握手是过路费 →用长连接/连接池复用,别频繁建连(⑤⑥);-小包延迟敏感就关Nagle(open_tcp_nodelay)(⑦);-backlog 是连接队列的饱和度阀门(⑧);-短连接留 TIME_WAIT,会耗尽端口(⑨);-心跳踢死连接,防资源泄漏(⑩)。
性能之巅=上下文切换开销 网络
发布时间:2026/6/7 1:27:35
性能之巅 · 第6章 CPU 标题①:上下文切换开销 —— 协程切换有多便宜 大白话:线程/进程切换要陷入内核、保存一大堆寄存器(微秒级);协程切换在用户态完成,只存几个寄存器(纳秒级)。这就是 Swoole 能开几十万并发的根因。?php// ctx_switch.phpuse Swoole\Coroutine;use Swoole\Coroutine\Channel;use Swoole\Coroutine\WaitGroup;use function Swoole\Coroutine\run;run(function(){$n500000;$anewChannel(1);$bnewChannel(1);// 两个协程用它来回传球$wgnewWaitGroup();$wg-add(2);$smicrotime(true);Coroutine::create(function()use($a,$b,$n,$wg){for($i0;$i$n;$i){$a-push(1);$b-pop();}// 推一下就切走$wg-done();});Coroutine::create(function()use($a,$b,$n,$wg){for($i0;$i$n;$i){$a-pop();$b-push(1);}$wg-done();});$wg-wait();$cmicrotime(true)-$s;$switches$n*2;printf(%d 次协程切换,耗时 %.3fs,每次切换 ≈%.0f 纳秒\n,$switches,$c,$c/$switches*1e9);});看什么:每次协程切换大概几百纳秒。对比:线程/进程切换约1~5微秒(慢10倍以上),可用 perf stat-e context-switches 验证。 标题②:协程创建成本——10万个协程秒级、内存极小 大白话:开10万个线程会把机器搞死,开10万个协程轻轻松松。?php// create_cost.phpuse Swoole\Coroutine;use Swoole\Coroutine\WaitGroup;use function Swoole\Coroutine\run;run(function(){$n100000;$smicrotime(true);$wgnewWaitGroup();for($i0;$i$n;$i){$wg-add();Coroutine::create(function()use($wg){$wg-done();});}$wg-wait();$cmicrotime(true)-$s;printf(创建 %d 个协程,耗时 %.3fs,平均 %.2f μs/个,峰值内存%.1fMB\n,$n,$c,$c/$n*1e6,memory_get_peak_usage()/1048576);});看什么:10万协程通常1秒内、内存几十 MB。换成10万进程/线程根本起不来——这就是量级差距。 标题③:CPU亲和性绑定 ——把进程钉在指定核上 大白话:默认进程在各个核之间跳来跳去,每跳一次缓存就失效。绑定到固定核能减少缓存抖动,提升性能。?php// cpu_affinity.phpuse Swoole\Process;echo本机 CPU 核数 .swoole_cpu_num().\n;$pnewProcess(function(Process $worker){$worker-setAffinity([0]);// 把本进程钉死在 0 号核echo进程 .getmypid(). 已绑定到 CPU#0,开始吃 CPU(用 top 看)\n;$x0;for($i0;$i2_000_000_000;$i)$x$i;// 持续吃CPU便于观察});$p-start();Process::wait();看什么:运行后 top 按1看每核占用,会发现只有 CPU#0被打满,其它核闲着。生产中可把每个 Worker 绑不同核,避免互相争抢。 标题④:运行队列/负载 ——Worker 数为什么对齐核数 大白话:核就那么几个,进程比核多时就得排队轮流上CPU(运行队列变长负载升高)。所以 CPU 密集型 worker_num 别超过核数。?php// run_queue.php$coresswoole_cpu_num();echo物理核数 $cores\n;echo建议 worker_num:\n;echo CPU 密集型 →$cores(再多只会排队,见第2章 USL 曲线)\n;echo I/O 密集型 →.($cores*2). ~ .($cores*4).(等待时让出CPU,可适当超配)\n;// 验证:用 uptime 看 load average,若 load 远大于核数,说明运行队列在堆积看什么:跑业务时 uptime 看 load average,load 长期核数进程在排队抢 CPU,该减 Worker 或加机器。---性能之巅 · 第10章 网络 ▎ 下面的网络示例都先启动这个 TCP 服务,再用客户端去打它:?php// tcp_server.phpuse Swoole\Server;$servernewServer(0.0.0.0,9501,SWOOLE_PROCESS,SWOOLE_SOCK_TCP);$server-set([worker_num2,backlog128,// 握手完成、等 accept 的队列长度(饱和度)open_tcp_nodelaytrue,// 关 Nagle:小包立即发,降延迟heartbeat_check_interval5,// 每 5 秒巡检一次连接heartbeat_idle_time10,// 10 秒没数据就踢掉(防死连接堆积)]);$server-on(connect,fn($s,$fd)print(连接进来 fd$fd\n));$server-on(receive,fn($s,$fd,$rid,$data)$s-send($fd,echo:$data));$server-on(close,fn($s,$fd)print(连接关闭 fd$fd(可能被心跳踢了)\n));$server-start();标题⑤:连接延迟——三次握手要花时间 大白话:每建一个新连接,都要 TCP 三次握手,这部分时间是白交的过路费。?php// connect_latency.phpuse Swoole\Coroutine\Client;use function Swoole\Coroutine\run;run(function(){$smicrotime(true);$clinewClient(SWOOLE_SOCK_TCP);$cli-connect(127.0.0.1,9501,1.0);// 三次握手在这一步printf(建立连接(三次握手)耗时 %.3f ms\n,(microtime(true)-$s)*1000);$cli-close();});看什么:本机几毫秒,跨机房可能几十毫秒。连接越远,这笔过路费越贵 →引出下一个:别频繁建连。 标题⑥:长连接vs 短连接 ——复用连接省掉重复握手 大白话:短连接每次都交过路费(握手TIME_WAIT),长连接交一次费用后一直用。?php// reuse.phpuse Swoole\Coroutine\Client;use function Swoole\Coroutine\run;// A:长连接 ——握手 1 次,复用 1000 次run(function(){$cnewClient(SWOOLE_SOCK_TCP);$c-connect(127.0.0.1,9501,1.0);$smicrotime(true);for($i0;$i1000;$i){$c-send(hi);$c-recv();}printf(【长连接】1000 次请求 %.3fs\n,microtime(true)-$s);$c-close();});// B:短连接 ——每次都重新握手关闭run(function(){$smicrotime(true);for($i0;$i1000;$i){$cnewClient(SWOOLE_SOCK_TCP);$c-connect(127.0.0.1,9501,1.0);$c-send(hi);$c-recv();$c-close();}printf(【短连接】1000 次请求 %.3fs(每次握手,明显慢)\n,microtime(true)-$s);});看什么:长连接明显快。这就是为什么数据库/Redis 一定要用连接池(连接复用)。 标题⑦:Nagle算法/open_tcp_nodelay ——小包要不要立即发 大白话:Nagle 会把小包攒一攒再发(省带宽但增延迟)。交互式/小包场景(游戏、RPC)要关掉它,让小包立即发。?php// nodelay.php ——客户端也可单独设use Swoole\Coroutine\Client;use function Swoole\Coroutine\run;run(function(){$cnewClient(SWOOLE_SOCK_TCP);$c-set([open_tcp_nodelaytrue]);// 关 Nagle$c-connect(127.0.0.1,9501,1.0);$smicrotime(true);for($i0;$i500;$i){$c-send(x);$c-recv();}// 大量小包printf(nodelayon 时 500 个小包来回 %.3fs(关Nagle后延迟更低)\n,microtime(true)-$s);$c-close();});看什么:服务端客户端都 open_tcp_nodelaytrue时,小包交互延迟更低。记住:吞吐优先开 Nagle,延迟优先关 Nagle。 标题⑧:backlog——握手完成等待 accept 的队列 大白话:握手好了但 Worker 还没来得及 accept 的连接,会先放进 backlog 队列;队列满了,新连接直接被拒。这就是网络层的饱和度。?php// 在 tcp_server.php 里设置(已包含):// backlog 128, // 这个队列能缓冲多少个已握手未处理的连接验证(瞬间发起远超 backlog 的连接):# 用 ss 看 listen 队列的当前长度/最大值(Recv-Q/Send-Q)ss-lntsport :9501#State Recv-Q Send-Q...#LISTEN0128←Send-Q 就是 backlog 上限,Recv-Q 是当前积压看什么:高并发瞬时建连时 Recv-Q 逼近 Send-Q →该调大 backlog,否则报connection refused。 标题⑨:TIME_WAIT——短连接的后遗症大白话:主动关闭连接的一方,socket 会停在 TIME_WAIT 状态~60秒占着端口。短连接太多会把本地端口耗尽,报 Cannot assign requested address。?php// time_wait.php ——制造大量短连接use Swoole\Coroutine\Client;use function Swoole\Coroutine\run;run(function(){for($i0;$i1000;$i){$cnewClient(SWOOLE_SOCK_TCP);$c-connect(127.0.0.1,9501,1.0);$c-send(hi);$c-recv();$c-close();// 客户端主动关 →本端进入 TIME_WAIT}echo建了 1000 个短连接,马上用 ss -s 看 TIME_WAIT 数量\n;});验证:ss-s # 看 timewait 计数 # 或 netstat-an|grep TIME_WAIT|wc-l 看什么:TIME_WAIT 数量飙升。解法:用长连接/连接池(治本);或调内核tcp_tw_reuse(治标)。 标题⑩:心跳/keepalive ——踢掉死连接,防资源泄漏 大白话:有些连接对端断网了但服务端不知道,会一直占着内存。心跳定期巡检,长时间没动静的连接就主动踢掉。?php// 在 tcp_server.php 里已配置:// heartbeat_check_interval 5, // 每 5 秒巡检// heartbeat_idle_time 10, // 10 秒没收发数据就关掉它验证:# 用 nc 连上去,然后啥也不发,等10秒 nc127.0.0.19501#10秒后服务端会打印连接关闭 fdx(可能被心跳踢了),nc 被断开 看什么:空闲连接10秒后被自动踢掉。生产必开,否则死连接越积越多,最终连接数耗尽。---这两章一句话总览 第6章 CPU-协程切换纳秒级、创建轻量,所以能撑超高并发(①②);-绑核减少缓存抖动,Worker 数对齐核数防排队(③④)。 第10章 网络-握手是过路费 →用长连接/连接池复用,别频繁建连(⑤⑥);-小包延迟敏感就关Nagle(open_tcp_nodelay)(⑦);-backlog 是连接队列的饱和度阀门(⑧);-短连接留 TIME_WAIT,会耗尽端口(⑨);-心跳踢死连接,防资源泄漏(⑩)。