这是一篇类似于开发笔记的文章,从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。