nginx上,http状态200响应,PHP空白返回的问题 最近的大半年中,编程语言从PHP换到了Golang后,就很少接触PHP,当然,更多的还是恋恋不舍。尽管如此,每当有人在群里聊起PHP的话题时,我总是想插几句,怀念怀念,同时也温故温故知识点,可不能把她给忘了。 昨天朋友tywei问我一个关于PHP奇怪问题,查到原因解决后,没有详细的解释。夜里睡觉时,老是回想这事,早上醒来,决定还是认真记录一下这些问题。也让自己回归正常状态,多写点博客,总结自己,记录自己。 问题描述 PHP+nginx的环境,任何PHP处理的结果,都是空白页面。OS是ubuntu 14.10 ,nginx 1.6.2 ,PHP5.5.12, 问任何PHP的页面,返回的HTTP状态200,但页面内容是空的,什么都没有,不管PHP页面里写的是什么,正常响应。初步怀疑这是拓展的问题,处理请求后,输出内容有冲突,输出为空之类。想查看拓展列表,看看加载了哪些拓展,但任何PHP代码都返回空,万不得已,在CLI模式下运行,确定ini是同一个。粗略的看了加载的模块,列表如下: 大约如上的模块加载,怀疑xdebug跟opcache冲突,尝试关闭后,仍未解决。 strace看系统调用信息 觉得好简短,好奇怪,访问的是SCRIPT_NAME index.php,怎么都没 lstat \open 这个PHP文件呢?直接返回了?起码要判断SCRIPT_FILENAME是否存在吧,要读取SCRIPT_FILENAME,解析里面的代码吧? 等下.....SCRIPT_FILENAME全部地址是啥?发来的CGI协议包中怎么没有SCRIPT_FILENAME? 仔细看下CGI包的内容 缺少: 那么问题来了 PHP-FPM接收CGI请求时,如果没有SCRIPT_FILENAME怎么处理的? 发来的CGI协议包中,为啥没有SCRIPT_FILENAME? (SCRIPT_FILENAME是什么,干啥用的,这个就不要问了吧。) 问题1:PHP-FPM接收CGI请求时,如果没有SCRIPT_FILENAME怎么处理的? fpm源码里(PHP5.5.x 为例) 同样是 fpm_main.c中init_request_info函数的代码如下: 从代码里看出,script_path_translated变量就是cgi协议包中SCRIPT_FILENAME的结果,其中1115行左右,判断如果script_path_translated为空,并且env_path_translated不为空,则用env_path_translated赋值到script_path_translated上。 之后,对request_info的几个属性给予了默认值,包括request_method为null,以及http_response_code默认200的http响应。 在后面的代码中if (script_path_translated) ,因为CGI包中没有SCRIPT_FILENAME,也没有PATH_TRANSLATED,即script_path_translated为空,故没有对request_method进行赋值,其默认值为NULL。 回到main函数中1839行附近if (!SG(request_info).request_method) ,则直接goto到了fastcgi_request_done,直接结束当前request请求,由于之前有设置过默认的http 响应状态为200 ,也就导致了每次返回http状态200成功响应的空白页面的问题。 同时也解释了strace系统调用中,没出现lstat、open 操作SCRIPT_FILENAME的记录了。 问题2,发来的CGI协议包中,为啥没有SCRIPT_FILENAME? PHP-FPM接收到的CGI协议包都是来自前面nginx的,cgi协议包中没有这个,肯定是nginx没发来,查看nginx配置看到fastcgi_params中没有这项。加上后就可以了。 解决办法: 在nginx配置的 fastcgt_params中加上SCRIPT_FILENAME的配置(在ubuntu的apt-get形式安装nginx配置中,默认是有这条的),比如 题外话: 在FPM的fpm_main.c文件的main函数中,1848行对SG(request_info).path_translated的判断,晚于1849行SG(request_info).request_method的判断。而在init_request_info函数中,对SG(request_info).path_translated的赋值却是比SG(request_info).request_method早的。 而且,明明找不到需要执行的脚本,却还返回200的http响应,很奇怪,也不方便排查。我觉得把对SG(request_info).request_method的判断放到SG(request_info).path_translated后面更合适一些。或者若找不到SCRIPT_FILENAME的话,http响应状态改为404,同时,写入LOG日志,便于排查。 后记: 从fpm代码里可以看到,其实作者是有考虑到没有SCRIPT_FILENAME的问题的,只是判断顺序搞错了。所以,我觉得这应该是个bug,就提了一下:BUG #69625:php-fpm return http 200 response on nginx without SCRIPT_FILENAME,不知道官方是否认为这是个BUG。 新的问题: 为什么如果SCRIPT_FILENAME不存在时,用PATH_TRANSLATED来代替它?PATH_TRANSLATED是每个CGI前端都要发送的吗? 这个问题,后来认真看了下,感觉还挺复杂,跟CGI客户端有关,PHPFPM针对IIS\APACHE\NGINX的处理都不一样。以后再写吧。 参考资料: RFC3875 - The Common Gateway Interface (CGI) Version 1.1 如果你在阅读PHP源码,或者阅读PHP SAPI、PHP拓展源码,可以关注一下PHP源码执行流程图 关于FPM的执行流程,可以看下下面这幅图: 2018年07月09日更新: Fix bugs #75120 and #69625 #3226里修复了我在2015年3月提的 bug report Fix bug #69625 php-fpm return http 200 response without SCRIPT_FILENAME #1270 2015-05-05 +3 7 0
Nginx模块fastcgi_cache的几个注意点 在web项目中,大家都已经非常熟悉其架构流程了。都说Cache是万金油,哪里不舒服抹哪里。这些流程中,几乎每个环节都会进行cache。从浏览器到webserver,到cgi程序,到DB数据库,会进行浏览器cache,数据cache,SQL查询的cache等等。对于fastcgi这里的cache,很少被使用。去年年底,我对nginx的fastcgi_cache进行摸索使用。在我的测试过程中,发现一些WIKI以及网络上没被提到的注意点,这里分享一下。 这里是我的NGinx配置信息 配置这些参数时,注意每个参数的作用域,像fastcgi_cache_path参数,只能在http配置项里配置,而fastcgi_cache_min_uses这个参数,可以在http、server、location三个配置项里配置。这样更灵活的会每个域名、每个匹配的location进行选择性cache了。具体的参数作用域,参考FASTCGI模块的官方WIKI。我为了调试方便,添加了一个『X-Cache-CFC』的http响应头,$upstream_cache_status 变量表示此请求响应来自cache的状态,分别为: MISS 未命中 EXPIRED - expired, request was passed to backend Cache已过期 UPDATING - expired, stale response was used due to proxy/fastcgi_cache_use_stale updating Cache已过期,(被其他nginx子进程)更新中 STALE - expired, stale response was used due to proxy/fastcgi_cache_use_stale Cache已过期,响应数据不合法,被污染 HIT 命中cache 程序代码是Discuz!论坛, 随便开启测试了几下,发现/dev/shm/nginx_cache/下没有任何目录建立,也没有文件创建。调试的http header响应头里的X-Cache-CFC 结果一直是MISS。从服务器进程上来看,Nginx cache manager process 跟Nginx cache loader process 进程也正常运行: 不知道为何会这样,为何没有cache成功,我以为我配置参数有问题,只好阅读WIKI。发现fastcgi_ignore_headers 参数下解释有这么一段 fastcgi_ignore_headers Syntax: fastcgi_ignore_headers field ... Default: Context: http server location Reference: fastcgi_ignore_headers This directive forbids processing of the named headers from the FastCGI-server reply. It is possible to specify headers like "X-Accel-Redirect", "X-Accel-Expires", "Expires" or "Cache-Control". 也就是说这个参数的值,将会被忽略掉,同样被忽略掉的响应头比如"X-Accel-Redirect", "X-Accel-Expires", "Expires" or "Cache-Control",而nginx配置中并没有fastcgi_ignore_headers参数的设定,那么问题会不会出现在FASTCGI响应结果里包含了类似"X-Accel-Redirect", "X-Accel-Expires", "Expires" or "Cache-Control"这几个响应头呢?用strace抓包,看了下nginx与fpm进程通讯的数据 从抓取的数据包里可以看到,fpm确实返回了包含“Expires”、“Cache-Control”头的http 响应头信息。那么疑问来了: nginx的fastcgi_cache没缓存这条http响应,是因为响应头里包含“Expires”、“Cache-Control”的原因吗? 程序里并没有输出“Expires”、“Cache-Control” http header的代码,这是谁输出的呢? 既然是fpm响应的时候,就已经有了,那么是php的core模块,还是其他拓展模块输出的? “Expires:”时间为何是“Thu, 19 Nov 1981 08:52:00 GMT”? 疑问比较多,一个一个查起,先从Nginx的fastcgi_cache没缓存这条http响应查起。我根据测试环境nginx版本1.1.9(ubuntu 12.04默认的),到nginx官方下了对应版本的源码,搜索了fastcgi参数使用的地方,在http\ngx_http_upstream.c找到了。虽然不能很流程的读懂nginx的代码,但粗略的了解,根据了解的情况加以猜测,再动手测试实验,也得出了结论,确定了nginx的fastcgi_cache的规则。 也就是说,fastcgi响应http请求的结果中,响应头包括Expires、Cache-Control、Set-Cookie三个,都会可能不被cache,但不只有这些,别忘了nginx配置中fastcgi_ignore_headers参数设定的部分。以及ngxin的X-ACCEL X-Accel-Redirect、X-Accel-Expires、X-Accel-Charset、X-Accel-Buffering等nginx自定义的响应头。由于这几个不常用,我也没深入研究。通过对nginx的ngx_http_upstream模块代码模糊理解,加猜测,以及写了脚本测试验证,可以得到结论是正确的。即Nginx fastcgi_cache在缓存后端fastcgi响应时,当响应里包含“set-cookie”时,不缓存;当响应头包含Expires时,如果过期时间大于当前服务器时间,则nginx_cache会缓存该响应,否则,则不缓存;当响应头包含Cache-Control时,如果Cache-Control参数值为no-cache、no-store、private中任意一个时,则不缓存,如果Cache-Control参数值为max-age时,会被缓存,且nginx设置的cache的过期时间,就是系统当前时间 + mag-age的值。 到了这里,疑问1解决了。那么疑问2、3呢?程序里并没有输出“Expires”、“Cache-Control” http header的代码,这是谁输出的呢?既然是fpm响应的时候,就已经有了,那么是php的core模块,还是其他拓展模块输出的?我精简了代码,只输出一个“hello world”,发现也确实被缓存了。显然,php脚本程序中并没输出http header 的“Expires”、“Cache-Control”,多次测试,最终定位到session_start函数,翻阅源码找到了这些代码: 到了这里,知道原因了,是程序调用session_start时,php的session拓展自己输出的。session.cache_limit参数来决定输出包含哪种Expires的header,默认是nocache,修改php.ini的session.cache_limit参数为“none”即可让session模块不再输出这些http 响应头。或在调用session_start之前,使用session_cache_limiter函数来指定下该参数值。那为什么要在使用session时,发Expires、Cache-Control的http response header呢?我猜测了下,需要session时,基本上是用户跟服务器有交互,那么,既然有交互,就意味着用户的每次交互结果也可能不一样,就不能cache这个请求的结果,给返回给这个用户。同时,每个用户的交互结果都是不一样的,nginx也就不能把包含特殊Cache-Control的个人响应cache给其他人提供了。 还有一个无聊的问题“Expires:时间为何是Thu, 19 Nov 1981 08:52:00 GMT”?我翻阅了session.c这段代码的添加时间,版本,作者信息,在php官方版本库中找到了这次提交的信息: Revision 17092 - (view) (download) (as text) (annotate) - [select for diffs] Modified Sun Dec 12 14:16:55 1999 UTC (13 years, 2 months ago) by sas File length: 28327 byte(s) Diff to previous 16964 Add cache_limiter and cache_expire options. Rename extern_referer_check to referer_check. 对比session.c两个版本的变更,果然是这块代码。作者是sas,也就是Sascha Schumann, http://php.net/credits.php里可以看到他的大名。关于这个expires过期时间的问题,有人在stackoverflow也提问过,Why is “Expires” 1981?,别人说那天是他生日。这是真的么?如果那天是他生日的话,而他增加session.cache_limiter时是1999年,他才17岁,17岁呀。我17岁时在干嘛?还不知道电脑长啥样,正在玩『超级玛丽』呢。 好奇的不是我一个人,还有个帖子是epoch date -- Expires: Thu, 19 Nov 1981 08:52:00也问了。另外两个地址虽然没问,也有人提到那天是他生日了。http://boinc.berkeley.edu/dev/forum_thread.php?id=2514、https://github.com/codeguy/Slim/issues/157,这些帖子都提到说原帖是http://www.phpbuilder.com/lists/php3-list/199911/3159.php ,我无法访问,被跳转到首页了。用http://web.archive.org找到了历史快照,发现上下文关系不大,也不能证明是他生日。 我更是好奇的发了两封邮件到他的不同邮箱里问他,不过,目前他还没回复。或许他没收到、没看到,或许懒得回了。N年后,“Expires:时间为何是Thu, 19 Nov 1981 08:52:00 GMT”这个日期,会不会又成了一段奇闻佳话了呢? 2013-02-28 +14 18 0