IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    nginx上,http状态200响应,PHP空白返回的问题

    CFC4N发表于 2015-05-05 10:38:49
    love 0

    最近的大半年中,编程语言从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是同一个之后,粗略的看了加载的模块

    Configuration File (php.ini) Path => /etc/php5/cli
    Loaded Configuration File => /etc/php5/cli/php.ini
    Scan this dir for additional .ini files => /etc/php5/cli/conf.d
    Additional .ini files parsed => /etc/php5/cli/conf.d/05-opcache.ini,
    /etc/php5/cli/conf.d/10-mysqlnd.ini,
    /etc/php5/cli/conf.d/10-pdo.ini,
    /etc/php5/cli/conf.d/20-curl.ini,
    /etc/php5/cli/conf.d/20-gd.ini,
    /etc/php5/cli/conf.d/20-imagick.ini,
    /etc/php5/cli/conf.d/20-json.ini,
    /etc/php5/cli/conf.d/20-memcache.ini,
    /etc/php5/cli/conf.d/20-memcached.ini,
    /etc/php5/cli/conf.d/20-mysql.ini,
    /etc/php5/cli/conf.d/20-mysqli.ini,
    /etc/php5/cli/conf.d/20-pdo_mysql.ini,
    /etc/php5/cli/conf.d/20-readline.ini,
    /etc/php5/cli/conf.d/20-redis.ini,
    /etc/php5/cli/conf.d/20-xdebug.ini
    

    大约如上的模块加载,怀疑xdebug跟opcache冲突,尝试关闭后,仍未解决。
    strace看系统调用信息

    10:02:03.432445 accept(0, {sa_family=AF_INET, sin_port=htons(49617), sin_addr=inet_addr("127.0.0.1")}, [16]) = 4
    10:02:06.125142 times({tms_utime=0, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 1718358917
    10:02:06.125177 poll([{fd=4, events=POLLIN}], 1, 5000) = 1 ([{fd=4, revents=POLLIN}])
    10:02:06.125332 read(4, "\1\1\0\1\0\10\0\0", 8) = 8
    10:02:06.125363 read(4, "\0\1\0\0\0\0\0\0", 8) = 8
    10:02:06.125381 read(4, "\1\4\0\1\3)\7\0", 8) = 8
    10:02:06.125394 read(4, "\f\0QUERY_STRING\16\3REQUEST_METHODGET\f\0CONTENT_TYPE\16\0CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\v\1REQUEST_URI/\f\nDOCUMENT_URI/index.php\r\22DOCUMENT_ROOT/data/web/test.com\17\10SERVER_PROTOCOLHTTP/1.1\21\7GATEWAY_INTERFACECGI/1.1\17\vSERVER_SOFTWAREnginx/1.6.2\v\10REMOTE_ADDR10.0.2.2\v\5REMOTE_PORT50057\v\tSERVER_ADDR10.0.2.15\v\2SERVER_PORT80\v\10SERVER_NAMEtest.com\17\3REDIRECT_STATUS200\t\10HTTP_HOSTtest.com\17\nHTTP_CONNECTIONkeep-alive\22\tHTTP_CACHE_CONTROLmax-age=0\vJHTTP_ACCEPTtext/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\17yHTTP_USER_AGENTMozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36\24\23HTTP_ACCEPT_ENCODINGgzip, deflate, sdch\24\27HTTP_ACCEPT_LANGUAGEzh-CN,zh;q=0.8,en;q=0.6\v\6HTTP_RA_VER2.10.1\v&HTTP;_RA_SIDB4A714D2-20150327-061728-7c932d-97f1f0\0\0\0\0\0\0\0", 816) = 816
    10:02:06.125416 read(4, "\1\4\0\1\0\0\0\0", 8) = 8
    10:02:06.125439 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={60, 0}}, NULL) = 0
    10:02:06.125468 rt_sigaction(SIGPROF, {0x6ec3d0, [PROF], SA_RESTORER|SA_RESTART, 0x7f3c26740da0}, {0x6ec3d0, [PROF], SA_RESTORER|SA_RESTART, 0x7f3c26740da0}, 8) = 0
    10:02:06.125508 rt_sigprocmask(SIG_UNBLOCK, [PROF], NULL, 8) = 0
    10:02:06.125570 times({tms_utime=0, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 1718358917
    10:02:06.125601 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={0, 0}}, NULL) = 0
    10:02:06.125637 fcntl(3, F_SETLK, {type=F_UNLCK, whence=SEEK_SET, start=0, len=0}) = 0
    10:02:06.125668 write(4, "\1\6\0\1\0@\0\0X-Powered-By: PHP/5.5.12-2ubuntu4.4\r\nContent-type: text/html\r\n\r\n\1\3\0\1\0\10\0\0\0\0\0\0\0\0\0\0", 88) = 88
    10:02:06.125697 shutdown(4, SHUT_WR)    = 0
    10:02:06.125713 recvfrom(4, "\1\5\0\1\0\0\0\0", 8, 0, NULL, NULL) = 8
    10:02:06.125728 recvfrom(4, "", 8, 0, NULL, NULL) = 0
    10:02:06.125797 close(4)                = 0
    

    觉得好简短,好奇怪,访问的是SCRIPT_NAME index.php,怎么都没 lstat \open 这个PHP文件呢?直接返回了?起码要判断SCRIPT_FILENAME是否存在吧,要读取SCRIPT_FILENAME,解析里面的代码吧? 等下…..SCRIPT_FILENAME全部地址是啥?发来的CGI协议包中怎么没有SCRIPT_FILENAME?
    仔细看下CGI包的内容

    QUERY_STRING
    REQUEST_METHODGET
    CONTENT_TYPE
    CONTENT_LENGTH
    
    SCRIPT_NAME /index.php
    REQUEST_URI /
    DOCUMENT_URI /index.php
    DOCUMENT_ROOT /data/web/test.com
    SERVER_PROTOCOL HTTP/1.1
    GATEWAY_INTERFACE CGI/1.1
    SERVER_SOFTWARE nginx/1.6.2
    REMOTE_ADDR 10.0.2.2
    REMOTE_PORT 50057
    SERVER_ADDR 10.0.2.15
    SERVER_PORT 80
    SERVER_NAME test.com
    REDIRECT_STATUS 200
    
    HTTP_HOST test.com
    HTTP_CONNECTION keep-alive
    

    缺少:

    SCRIPT_FILENAME
    //PATH_TRANSLATED //这个暂时无视
    

    那么问题来了

    • PHP-FPM接收CGI请求时,如果没有SCRIPT_FILENAME怎么处理的?
    • 发来的CGI协议包中,为啥没有SCRIPT_FILENAME? (SCRIPT_FILENAME是什么,干啥用的,这个就不要问了吧。)

    问题1:PHP-FPM接收CGI请求时,如果没有SCRIPT_FILENAME怎么处理的?
    fpm源码里(PHP5.5.x 为例)

    //fpm_main.c
    /* {{{ main
     */
    int main(int argc, char *argv[])
    {
    	//...
    
    	//fpm_main.c 1820行
    	while (fcgi_accept_request(&request;) >= 0) {
    		request_body_fd = -1;
    		SG(server_context) = (void *) &request;
    		init_request_info(); 	//这里对应986行左右的的init_request_info 函数中代码
    		char *primary_script = NULL;
    
    		fpm_request_info();
    
    		/* request startup only after we've done all we can to
    		 *            get path_translated */
    		if (php_request_startup() == FAILURE) {
    			fcgi_finish_request(&request;, 1);
    			SG(server_context) = NULL;
    			php_module_shutdown();
    			return FPM_EXIT_SOFTWARE;
    		}
    
    		/* check if request_method has been sent.
    		 * if not, it's certainly not an HTTP over fcgi request */
    		if (!SG(request_info).request_method) {//这里判断request.method是否存在,在init_request_info方法里,最上面设置了默认的NULL
    			goto fastcgi_request_done;
    		}
    
    		//...
    		fastcgi_request_done:
    			//结束当前request请求,给及响应
    
    	}
    }
    

    同样是 fpm_main.c中init_request_info函数的代码如下:

    //fpm_main.c 986行
    static void init_request_info(void)
    {
    	char *env_script_filename = sapi_cgibin_getenv("SCRIPT_FILENAME", sizeof("SCRIPT_FILENAME") - 1);
    	char *env_path_translated = sapi_cgibin_getenv("PATH_TRANSLATED", sizeof("PATH_TRANSLATED") - 1);
    	char *script_path_translated = env_script_filename;
    	char *ini;
    	int apache_was_here = 0;
    
    	/* some broken servers do not have script_filename or argv0
    	 * an example, IIS configured in some ways.  then they do more
    	 * broken stuff and set path_translated to the cgi script location */
    	if (!script_path_translated && env_path_translated) {
    		script_path_translated = env_path_translated;
    	}
    
    	/* initialize the defaults */
    	SG(request_info).path_translated = NULL;
    	SG(request_info).request_method = NULL;
    	SG(request_info).proto_num = 1000;
    	SG(request_info).query_string = NULL;
    	SG(request_info).request_uri = NULL;
    	SG(request_info).content_type = NULL;
    	SG(request_info).content_length = 0;
    	SG(sapi_headers).http_response_code = 200;	//	这里默认给了200的响应
    
    	/* script_path_translated being set is a good indication that
    	 * we are running in a cgi environment, since it is always
    	 * null otherwise.  otherwise, the filename
    	 * of the script will be retreived later via argc/argv */
    	if (script_path_translated) {
    		if (CGIG(fix_pathinfo)) {
    			//对pathinfo做处理,剥离出SCRIPT_FILENAME,并重置SCRIPT_FILENAME
    		} else {
    		}
    
    		if (is_valid_path(script_path_translated)) {
    			//这里如果script_path_translated是合法路径,就给转化一下,赋值给SG(request_info).path_translated
    			SG(request_info).path_translated = estrdup(script_path_translated);
    		}
    
    		SG(request_info).request_method = sapi_cgibin_getenv("REQUEST_METHOD", sizeof("REQUEST_METHOD") - 1);//这里从CGI包李获取method,赋值给request_method
    		// ...
    	}
    }
    

    从代码里看出,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配置中,默认是有这条的),比如

      fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    

    题外话:
    在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的执行流程,可以看下下面这幅图:
    fpm__main_8c_a0ddf1224851353fc92bfbff6f499fa97_cgraph



沪ICP备19023445号-2号
友情链接