从php-fpm解析FastCGI协议

这是一篇类似于开发笔记的文章,从php-fpm与nginx的tcp请求中,去理解FastCGI协议,因此不会详细的阐述FastCGI协议到底是什么样的。

从一段抓包说起

"No.","Time","Source","Destination","Protocol","Length","Info"
"431","22.975896289","127.0.0.1","127.0.0.1","TCP","76","55928  >  9000 [SYN] Seq=0 Win=43690 Len=0 MSS=65495 SACK_PERM=1 TSval=1732184618 TSecr=0 WS=128"
"432","22.975910047","127.0.0.1","127.0.0.1","TCP","76","9000  >  55928 [SYN, ACK] Seq=0 Ack=1 Win=43690 Len=0 MSS=65495 SACK_PERM=1 TSval=1732184618 TSecr=1732184618 WS=128"
"433","22.975920352","127.0.0.1","127.0.0.1","TCP","68","55928  >  9000 [ACK] Seq=1 Ack=1 Win=43776 Len=0 TSval=1732184618 TSecr=1732184618"
"434","22.975948796","127.0.0.1","127.0.0.1","TCP","1356","55928  >  9000 [PSH, ACK] Seq=1 Ack=1 Win=43776 Len=1288 TSval=1732184618 TSecr=1732184618"
"435","22.975953739","127.0.0.1","127.0.0.1","TCP","68","9000  >  55928 [ACK] Seq=1 Ack=1289 Win=174720 Len=0 TSval=1732184618 TSecr=1732184618"
"452","23.068068706","127.0.0.1","127.0.0.1","TCP","660","9000  >  55928 [PSH, ACK] Seq=1 Ack=1289 Win=174720 Len=592 TSval=1732184710 TSecr=1732184618"
"453","23.068076923","127.0.0.1","127.0.0.1","TCP","68","55928  >  9000 [ACK] Seq=1289 Ack=593 Win=44928 Len=0 TSval=1732184710 TSecr=1732184710"
"454","23.068097717","127.0.0.1","127.0.0.1","TCP","68","9000  >  55928 [FIN, ACK] Seq=593 Ack=1289 Win=174720 Len=0 TSval=1732184710 TSecr=1732184710"
"455","23.068153021","127.0.0.1","127.0.0.1","TCP","68","55928  >  9000 [FIN, ACK] Seq=1289 Ack=594 Win=44928 Len=0 TSval=1732184710 TSecr=1732184710"
"456","23.068163150","127.0.0.1","127.0.0.1","TCP","68","9000  >  55928 [ACK] Seq=594 Ack=1290 Win=174720 Len=0 TSval=1732184710 TSecr=1732184710"

为了抓这段包,需要将php-fpm中的监听地址改成tcp socket。注意:tcp socket的性能远远低于unix socket。

可以看到,这里面除了tcp的握手和断开以及应答部分,有PSH标志的是FastCGI的具体协议内容。可以看到nginx给php-fpm发送了一段数据,之后php-fpm进行响应。FastCGI协议就是这样简单的使用tcp协议,使得Web Server可以转发HTTP请求到FastCGI应用程序上,具体的协议内容可以参考FastCGI规范中文翻译

php-fpm在单次请求结束后,会主动断开连接,而在FastCGI协议中,明确说明单次连接是可以复用的。

https://stackoverflow.com/questions/43280573/whether-the-connection-between-php-fpm-and-nginx-by-fast-cgi-are-persistent-kee

这个链接中有关于nginx和php-fpm连接释放的相关说明。

web server 可以将关闭权限委托给php-fpm,这样php-fpm在每次请求结束后就会关闭。

将关闭权限委托给php-fpm的好处就是不会因为连接的占用导致子进程不释放。但是不断的建立和断开连接也会影响性能。

当前php-fpm和nginx的主动断开连接是否会影响性能

会影响性能,但是并不推荐保持连接。

如果希望php-fpm不主动关闭连接,可以使用以下设置:

Syntax: fastcgi_keep_conn on | off;
Default:    
fastcgi_keep_conn off;
Context:    http, server, location
This directive appeared in version 1.1.4.

记得在upstream中使用keepalive选项

upstream backend {
    server 127.0.0.1:9000
    keepalive 20
}

但是缺点也很明显,如果用户请求一直和nginx保持连接,那么nginx也不会释放该与php-fpm的连接。这样会一直占用php-fpm的子进程不释放。当达到php-fpm的最大子进程时,就会拒绝其他的请求。

同时需要注意的是,如果nginx和php-fpm都在本地,不断的重新建立连接的影响是很小的。因此并不推荐将fastcgi_keep_conn选项打开。

这里是一些压测数据(pm.max_children 设置为20,这里只使用20个并发):

测试命令

ab -k -n 100000 -c 20 http://localhost/php/index.php

在主动断开FastCGI连接的情况下:

Server Software:        nginx/1.13.3
Server Hostname:        localhost
Server Port:            80

Document Path:          /php/index.php
Document Length:        60 bytes

Concurrency Level:      20
Time taken for tests:   28.334 seconds
Complete requests:      100000
Failed requests:        0
Keep-Alive requests:    0
Total transferred:      22900000 bytes
HTML transferred:       6000000 bytes
Requests per second:    3529.39 [#/sec] (mean)
Time per request:       5.667 [ms] (mean)
Time per request:       0.283 [ms] (mean, across all concurrent requests)
Transfer rate:          789.29 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.4      0      14
Processing:     1    5   2.4      5     212
Waiting:        0    5   2.4      5     212
Total:          1    6   2.5      5     212

在不断开连接的情况下:

测试一直没法正常完成,部分请求会超时。

因此FastCGI是没有必要保持连接的,这会大大降低并发度。

benchmark 压测,请求直接超时退出

ab -k -c 100 -n 10000 "http://localhost/php/index.php"

php-fpm有一个子进程数量的限制,在并发过高时,没有办法为每一个请求分配一个子进程,导致请求一直在等待,直至超时退出。

unix socket和tcp socket的区别

unix socket相对于tcp socket来说,性能会提升很多。

unix socket虽然也有个socket,但是和网络一点关系都没有。unix socket是进程间的通信。

unix socket不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。

实现FastCGI协议时,tcp连接中读到EOF代表了什么

在写代码过程中,tcp连接读到了EOF。从表面上来看,是读到了流的结束。但也意味着对端至少关闭了写通道。这是因为php-fpm读到了它无法理解的请求,因此直接关闭了连接。

在开发过程中,遇到了php-fpm进程不释放的问题

在BeginRequestRecord中,将flags置为1,这样与fastcgi的连接会一直保持。但是我在tcp连接中读到EOF时,却没有释放这个连接。因此这个连接会占用一个php-fpm子进程不会释放。只要手动释放这个连接即可,或者将flags设为0。

参考

FastCGI协议分析

Nginx支持PHP的PATHINFO模式配置分析

Linux下的IPC-UNIX Domain Socket