感觉PHP-FPM进程数不够?

[TOC]

感觉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
  1. 使用 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 了。详细步骤参考:www.cnblogs.com/tinywan/p/6848269....

$ 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 大概工作的过程,这个时候我们就需要通过一次请求大概知道 nginxphp-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="\1\1\0\1\0\10\0\0\0\1\0\0\0\0\0\0\1\4\0\1\2!\7\0\17)SCRIPT_FILENAME/data/miner_platform/src/public/index.php\f\0QUERY_STRING\16\3REQUEST_METHODGET\f\0CONTENT_TYPE\16\0CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\v\23REQUEST_URI/group/check_groups\f\nDOCUMENT_URI/index.php\r\37DOCUMENT_ROOT/data/miner_platform/src/public\17\10SERVER_PROTOCOLHTTP/1.1\16\4REQUEST_SCHEMEhttp\21\7GATEWAY_INTERFACECGI/1.1\17\fSERVER_SOFTWAREnginx/1.16.0\v\tREMOTE_ADDR127.0.0.1\v\5REMOTE_PORT46616\v\tSERVER_ADDR127.0.0.1\v\2SERVER_PORT80\v\21SERVER_NAMEminer_platform.cn\17\3REDIRECT_STATUS200\17\vHTTP_USER_AGENTcurl/7.29.0\t\21HTTP_HOSTminer_platform.cn\v\3HTTP_ACCEPT*/*\0\0\0\0\0\0\0\1\4\0\1\0\0\0\0\1\5\0\1\0\0\0\0", iov_len=592}], 1) = 592
     epoll_wait(8, [{EPOLLIN|EPOLLOUT, {u32=1226163953, u64=94773974517489}}], 512, 60000) = 1
     // 4. 接受 PHP-FPM响应结果   
     recvfrom(11, "\1\6\0\1\0\257\1\0X-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.\"}\0\1\3\0\1\0\10\0\0\0\0\0\0\0\"}\0", 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, 
  2. php-fpm 系统调用

    抓取了php-fpm work 进程

    // 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, "\1\6\0\1\0\257\1\0X-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.\"}\0\1\3\0\1\0\10\0\0\0\0\0\0\0p\0\0", 200) = 200
    // 3. 不给给这个socket 写数据了
    965   shutdown(4, SHUT_WR)              = 0
    // 4. 接受nginx(127.0.0.1:45512 )客户端数据 
    965   recvfrom(4, "\1\5\0\1\0\0\0\0", 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, 

TCP三次握手

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

  1. 我们调用 listen (上面是 php-fpm master 进程执行的),于此同时内核创建了两个队列 sync queueaccept queue
  2. 三次握手第二步当 Server (指的是 php-fpm master 进程)发送了 SYN+ACK报文后,此时会将这个信息放入到 sync queue
  3. 当三次握手完成时,未被应用(指的是 php-fpm work 进程)调用 accept 取走的连接队列。此时的 socket处于 ESTABLISHED 状态。每次应用调用 accept() 函数会移除队列头的连接。如果队列为空,accept() 通常会阻塞。全连接队列也被称为 accept queue

结论

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

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 2年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 25

好东西

2年前 评论
悲剧不上演 (楼主) 2年前

写的很好收藏了

2年前 评论
悲剧不上演 (楼主) 2年前

给大佬端茶

2年前 评论
悲剧不上演 (楼主) 2年前
playmaker

咋看出有北风弟子的影子

2年前 评论
一个猫 2年前
悲剧不上演 (楼主) 2年前

给大佬揉脚

2年前 评论
悲剧不上演 (楼主) 2年前

相类似的这种干货,就应该多多益善 :yum:

2年前 评论
悲剧不上演 (楼主) 2年前

给大佬按摩 :stuck_out_tongue_winking_eye:

2年前 评论
悲剧不上演 (楼主) 2年前
windawake

我感觉进程数跟cpu核心数量一致就可以,多余php-fpm的进程只会排队,最终发现处理效率都一样的结果。最好的解决方案是把mysql性能和php性能都提升5400倍哈。

2年前 评论
JerryBool 2年前
悲剧不上演 (楼主) 2年前
mowangjuanzi 2年前

迷迷糊糊看完了。

2年前 评论

PHP多进程编程

2年前 评论

php-fpm 系统调用 怎么 strace,启动很多个php-fpm只能那个✅ ?预先 只启动一个?

2年前 评论
laisxn

大佬,普洱,铁观音,大红袍自选

2年前 评论

curl也应该在考虑范围

8个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!