Error[8]: Undefined offset: 19, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]p[+++][+++]", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 20, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]p[+++][+++]", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 21, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++]p[+++][+++]", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 22, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++]p[+++][+++]", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 23, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++]p[+++][+++]", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 24, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++]p[+++][+++]", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 25, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++]p[+++][+++]", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 26, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++]p[+++][+++]", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 27, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++]p[+++][+++]", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 28, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++]p[+++][+++]", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 29, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++]p[+++][+++]", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 30, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p[+++][+++]", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 31, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p[+++]", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 32, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 33, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 34, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 35, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++][+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 36, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "[+++]", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 37, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 38, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 39, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 40, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 41, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 42, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 43, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 44, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 45, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++][+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 46, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
[+++]![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 47, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
![+++])SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 48, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f[+++]QUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 49, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\f[+++]CONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 50, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPE[+++]CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 51, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 52, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 53, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 54, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 55, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 56, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 57, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 58, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 59, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 60, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 61, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 62, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 63, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 64, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 65, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 66, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++][+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 67, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*[+++]", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 68, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++][+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 69, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "[+++]7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 70, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7[+++]X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 71, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 72, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 73, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 74, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 75, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 76, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 77, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 78, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 79, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++][+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 80, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}[+++]\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 81, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}\"}[+++]", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 82, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}\"}", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

[+++]

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 83, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}\"}", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

抓取了 php-fpm work 进程

[+++]

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 84, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}\"}", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

抓取了 php-fpm work 进程

[+++]

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

Error[8]: Undefined offset: 85, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}\"}", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

抓取了 php-fpm work 进程

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

[+++]

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)

流量一来,时间变慢,怪PHP-FPM进程数不够?_PHP_内存溢出

流量一来,时间变慢,怪PHP-FPM进程数不够?

流量一来,时间变慢,怪PHP-FPM进程数不够?,第1张

流量一来,时间变慢,怪PHP-FPM进程数不够? 感觉PHP-FPM进程数不够?

作为一个 phper,用的最多的架构就是 LNMP。每次一到流量来了,我们的服务就从原来的 几百毫秒到几秒的时间。这个时候我们各种猜测,mysql 有慢 sql,redis 有大 key,php-fpm 进程数不够等等情况。其中可以通过业务的一些日志来排查如上情况。我们这次主要证明的却是 php-fpm 进程数不够情况的实践。

重现现场

1.将我本地的的 PHP-FPM 进程数调整为 2

#vim /etc/php-fpm.d/www.conf
pm = static
pm.max_children = 2

2.使用 ab 来压测接口

$ ab -c 40  -n 3000 http://127.0.0.1/group/check_groups
Server Software:        nginx/1.16.0
Server Hostname:        miner_platform.cn
Server Port:            80
Document Path:          /group/check_groups
Document Length:        44 bytes
Concurrency Level:      40
Time taken for tests:   29.384 seconds
Complete requests:      3000
Failed requests:        0
Write errors:           0
Total transferred:      699000 bytes
HTML transferred:       132000 bytes
Requests per second:    102.10 [#/sec] (mean)
Time per request:       391.788 [ms] (mean)
Time per request:       9.795 [ms] (mean, across all concurrent requests)
Transfer rate:          23.23 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   306  344  80.6    318    3558
Waiting:      306  343  80.5    318    3555
Total:        307  344  80.6    318    3558
Percentage of the requests served within a certain time (ms)
  50%    318
  66%    322
  75%    333
  80%    369
  90%    428
  95%    461
  98%    508
  99%    553
 100%   3558 (longest request)

尝试解决问题

1. PHP-FPM STATUS

我们发现接口 318ms 到 3.558s 的都有,那我们如何知道 php-fpm 进程少不够导致这个问题呢?换一种说话有什么办法能让我们知道 php-fpm 内部是处理不过来吗? 这个时候我们就需要打开 php-fpm 内置 status 了。

详细步骤参考:https://www.php.cn/php-weizijiaocheng-485633.html

$ curl http://127.0.0.1/status.php
pool:                 www
process manager:      static
start time:           29/Nov/2021:18:27:38 +0800
start since:          6493
accepted conn:        3136
listen queue:         38
max listen queue:     39
listen queue len:     128
idle processes:       0
active processes:     2
total processes:      2
max active processes: 2
max children reached: 0
slow requests:        0

具体详细的字段可以参见上面的链接,有详细说明,我们主要说下几个参数

  • listen queue:这个就是此时此刻我们的 php-fpm 作为服务端,处于 accept 队列 的数量。

  • max listen queue: 从 php-fpm 进程启动到现在处于等待连接的最大数量(说白了,就是我们上面说的 listen queue 的最大值持久化)

  • listen queue len : 有过 socket 网络编程经验的同学都知道。int listen(int sockfd, int backlog); 是可以设置该参数,但是他和系统设置有关系。

2. netstat 查看链接状态

我们得到的结论是:当 php-fpm 进程处理不过来的时候,请求就会放在 accept 队列,知道了这个情况以后,我们甚至不需要通过 status。

第一行表示的监听 socket, Recv-Q 表示 accept queue 长度。

$netstat -antp | grep php-fpm
tcp       38      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      97/php-fpm: master  
tcp        8      0 127.0.0.1:9000          127.0.0.1:55540         ESTABLISHED 964/php-fpm: pool w 
tcp        8      0 127.0.0.1:9000          127.0.0.1:55536         ESTABLISHED 965/php-fpm: pool w

综上我们知道了,当 PHP-FPM 进程数不够的时候,nginx 客户端请求的连接的 accept 队列 长度就会变大。这样就完了吗?不,我们还需要去分析为什么能得到这个现象。

原理分析

简述 PHP-FPM 工作过程

首先我们需要简单里说一说 php-fpm 的工作过程。我们就简单模型一下它的伪代码(这里只为了表述整个 socket 的过程)

// 1. 创建 socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
// 2. 绑定socket
socket_bind($socket, "0.0.0.0", 9000);
// 3. 监听 socket
socket_listen($socket, 5);
for($i=0;$i<2;$i++) {
    $pid = pcntl_fork()
    // 4. 创建2个进程
    if ($pid == 0) {
        // 5. 子进程接受socket
        while($fd = socket_accept($socket)) {
            echo "客户端${fd}连接" . PHP_EOL;
            $tmp = socket_read($fd, 1024);
            echo "client data:" . $tmp . PHP_EOL;
            $data = "HTTP/1.1 200 ok\r\nContent-Length:2\r\n\r\nhi";
            socket_write($fd, $data, strlen($data));
        }    
        exit;
    }
}
// 5. 监听子进程退出
// 其他 TODO

1.master 进程创建了监听 socket,但是不处理业务正在

2.work 进程接受同步堵塞接受请求(堵塞在 accept),然后处理业务。

抓取 nginx->php-fpm socket

我们知道了 php-fpm 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginx 与 php-fpm 交互的过程。

$curl http://miner_platform.cn/group/check_groups
{"code":10006,"message":"sign\u65e0\u6548."}

1.nginx 系统调用

需要关注的点都在这个里面注释了。抓取的是 nginx work 进程

$ strace -f -s 64400 -p 958
 strace: Process 958 attached
 epoll_wait(8, [{EPOLLIN, {u32=1226150064, u64=94773974503600}}], 512, -1) = 1
 accept4(6, {sa_family=AF_INET, sin_port=htons(46616), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
 epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 epoll_wait(8, [{EPOLLIN, {u32=1226159737, u64=94773974513273}}], 512, 60000) = 1
 recvfrom(3, "GET /group/check_groups HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: miner_platform.cn\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 99
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 stat("/data/miner_platform/src/public/group/check_groups", 0x7ffcb593d1b0) = -1 ENOENT (No such file or directory)
 epoll_ctl(8, EPOLL_CTL_MOD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226159737, u64=94773974513273}}) = 0
 lstat("/data", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 lstat("/data/miner_platform/src/public", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
 getsockname(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
 // 1. 创建 socket    
 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 11
 ioctl(11, FIONBIO, [1])                 = 0
 epoll_ctl(8, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1226163953, u64=94773974517489}}) = 0
 // 2. 连接 127.0.0.1:9000    
 connect(11, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)    
 epoll_wait(8, [{EPOLLOUT, {u32=1226159737, u64=94773974513273}}, {EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 2
 getsockopt(11, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
 // 3. 按照FASTCGI协议写入这次请求     
 writev(11, [{iov_base="2.php-fpm 系统调用
// 1. accept 接收到了 nginx(127.0.0.1:45512 ) 客户端发送的数据
965   accept(9, {sa_family=AF_INET, sin_port=htons(45512), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 4
中间省略了许多
// 2. 响应给客户端
965   write(4, "TCP 三次握手结论7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:37:18 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}p", 200) = 200
// 3. 不给给这个socket 写数据了
965   shutdown(4, SHUT_WR)              = 0
// 4. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 8
// 5. 接受nginx(127.0.0.1:45512 )客户端数据 
965   recvfrom(4, "", 8, 0, NULL, NULL) = 0
// 6. 关闭这个连接
965   close(4)                          = 0
965   lstat("/data/miner_platform/src/vendor/composer/../../app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   stat("/data/miner_platform/src/app/Http/Middleware/BusinessHeaderCheck.php", {st_mode=S_IFREG|0777, st_size=989, ...}) = 0
965   chdir("/")                        = 0
965   times({tms_utime=3583, tms_stime=1977, tms_cutime=0, tms_cstime=0}) = 4315309933
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   fcntl(3, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
965   setitimer(ITIMER_PROF, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}, NULL) = 0
965   accept(9,
!)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\fQUERY_STRINGREQUEST_METHODGET\fCONTENT_TYPECONTENT_LENGTH\v\nSCRIPT_NAME/index.php\vREQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\rDOCUMENT_ROOT/data/miner_platform/src/publicSERVER_PROTOCOLHTTP/1.1REQUEST_SCHEMEhttpGATEWAY_INTERFACECGI/1.1\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\vREMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\vSERVER_PORT80\vSERVER_NAMEminer_platform.cnREDIRECT_STATUS200\vHTTP_USER_AGENTcurl/7.29.0\tHTTP_HOSTminer_platform.cn\vHTTP_ACCEPT*/*", iov_len=592}], 1) = 592 epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 // 4. 接受 PHP-FPM响应结果 recvfrom(11, "7X-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\nContent-Type: application/json\r\n\r\n{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}\"}", 4096, 0, NULL, NULL) = 200 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1 readv(11, [{iov_base="", iov_len=3896}], 1) = 0 // 5. 关闭这次socket连接 close(11) = 0 // 6. 响应给浏览器 writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.16.0\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Powered-By: PHP/7.2.16\r\nCache-Control: no-cache, private\r\nDate: Wed, 01 Dec 2021 12:24:52 GMT\r\n\r\n", iov_len=222}, {iov_base="2c\r\n", iov_len=4}, {iov_base="{\"code\":10006,\"message\":\"sign\u65e0\u6548.\"}", iov_len=44}, {iov_base="\r\n", iov_len=2}, {iov_base="0\r\n\r\n", iov_len=5}], 5) = 277 write(5, "127.0.0.1 - - [01/Dec/2021:20:24:52 +0800] \"GET /group/check_groups HTTP/1.1\" 200 55 \"-\" \"curl/7.29.0\" \"-\" 1.029 127.0.0.1:9000 200 1.030\n", 138) = 138 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0 epoll_wait(8, [{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=1226159737, u64=94773974513273}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(8,

抓取了 php-fpm work 进程

上面我们已经清楚了一次请求,请求并发高的时候流程也是如此,这个时候我们就引出了下面这个图与我们上面描述的过程是一样的,只是细化了三次握手的过程。这个时候我们引出了 sync queue 和 accept queue。

  • 我们调用 listen (上面是 php-fpm master 进程执行的),于此同时内核创建了两个队列 sync queue 和 accept queue

  • 三次握手第二步当 Server (指的是 php-fpm master 进程) 发送了 SYN+ACK 报文后,此时会将这个信息放入到 sync queue

  • 当三次握手完成时,未被应用 (指的是 php-fpm work 进程) 调用 accept 取走的连接队列。此时的 socket 处于 ESTABLISHED 状态。每次应用调用 accept() 函数会移除队列头的连接。如果队列为空,accept () 通常会阻塞。全连接队列也被称为 accept queue。

经过上面的分析,我们知道了什么是 sync queue 和 accept queue。应用程序 与 accept queue 与 内核 就是一个生产消费模型。内核为生产者,accept queue 存储队列信息,应用程序为消费者。使用过队列的同学都知道,当并发高的时候,队列里的数据就多,或者生产者消费的慢就会导致后面的连接处理的越来越慢,因此通常的做法就是增加消费者,提高消费速度这两个方案。这也与我们上面的现象不谋而合。

以上就是流量一来,时间变慢,怪PHP-FPM进程数不够?的详细内容,

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/679468.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-20
下一篇 2022-04-20

发表评论

登录后才能评论

评论列表(0条)

保存