openresty的unescape_uri函数处理百分号后面字符的小特性 我们的 WAF (Web Application Firewall)是搭建在 nginx 上,使用 lua modules 来实现的。也就是 openresty 的 luajit 功能来实现WEB 攻击判断。处理收到的请求时,一般会进行unescape_uri 处理后,再走规则匹配。 在离线分析的机器学习识别结果里,发现有这么一条漏报/aa%20a?openId=%%3Cscript%3E%,这个问题却出乎我们意料,为此,开始排查产生该现象的原因。 问题描述 源字符串: openresty的unescape_uri函数处理后结果 期望的结果: 问题现象: 转义不对;丢了百分号。 如何重现 通过浏览器,构造一个包含如上特征的 uri,按回车提交到 nginx?浏览器会自动把字符串转码吗?转码是按照什么规范来的?IE9、IE10、chrome、safari是分别如何处理的? 习惯用postman的人,知道postman是否会escape uri呢? 如果你忽略了这一点,那么会对你的测试造成很大的影响,耽误时间经历,让你在重试阶段都对问题产生了怀疑。 从HTTP协议角度考虑,只要确保通过协议发送到 http server 的请求中,uri 是符合我们需要的即可。至于浏览器的 escape做法,可以忽略,直接构造 http 协议来重现。 重现时,通过浏览器 提交「问题字符串」,在 nginx 层 输出 unescape 后的结果。 注意浏览器是否对 URI 进行了 URI_ENCODE,不同浏览器执行的 RFC 标准也不一样。 最根本的方式是,跳过浏览器,直接构造 http request 包,确保源字符串完整发送只 nginx。 重现该问题,开始排查 nginx 对 uri decode 的实现方式。 分析问题: waf的处理方式: 使用ngx.unescape_uri函数 确认处理 uri 的原始函数ngx.unescape_uri 该函数在 nginx lua modules 模块中,在src/ngx_http_lua_string.c 的64、65行: 如上代码,可知ngx.unescape_uri函数是ngx_http_lua_ngx_unescape_uri实现,对于真正 decode 字符串功能,是使用了src/ngx_http_lua_util.c文件内的ngx_http_lua_unescape_uri函数做 decode,该函数代码如下: 代码阅读: 对 uri 的字符串按照从左往右挨个处理 初始化 state 为 sw_usual,当state 为sw_usual时 若第一个字符是「?」,则把「?」赋值给 dst 字符串,然后直接返回 若第一个字符串是「+」,则替换成空格,赋值给 dst 字符串,然后跳出流程,走到下一个字符判断。(nginx 函数ngx_unescape_uri是没这个处理的) 若第一个字符是「%」,则跳过当前字符,对src 源字符串的下一个字符判断,走到sw_quoted流程处理下一个字符。 sw_quoted判断当前字符是符合[0-9a-f],则进入下一个字符的处理。 若不在[0-9a-f]范围内,则赋值给 dst 字符串,跳出当前流程处理下一个字符 sw_quoted_second 处理两个字符串,转换,赋值到 dst 中 结合案例分析: 结合案例中的源字符串%%3Cscript%3E%来看, 第一个字符% 走到上面第5条,不会被赋值到dst中,会继续判断下一个字符 第二个字符也是% ,走到上面第7条,直接被赋值 dst 中 第3、4个字符是普通字符,直接赋值到 dst 中。 第11个字符%走到上面第5条,跳过,继续判断下一个字符 第12、13个字符走到上面第8条,正确unescape转码。 最后一个字符% ,走到上面第5条,跳过,进行下一个字符判断 下一个字符没了,结束 所以,%%3Cscript%3E% 转换为 %3Cscript> ,这里的% 是源字符串的第二个百分号,第一个丢了(因为后面的字符也是%,不符合[0-9a-z]规则);最后一个% 也丢了,因为后面没字符了,while 跳出循环,结束了。 总结问题: 百分号后面还是百分号问题的修复 百分号是最后一个字符串的修复 修复问题: 测试代码: 备注: nginx 底层的 ngx_unescape_uri 函数(/src/core/ngx_string.c)实现部分,跟 lua modules 实现不太一样,openresty 里多了对「+」转空格的处理。() 疑问: nginx的做法是按照哪个 RFC 标准转换的? lua modules的做法符合 RFC 标准吗? uri_decode在其他语言里的是如何实现的? 解答: 翻阅 nginx 官方文档,并没找到 uri unescape的规范文档,只是代码注释里看到了是RFC3986 符合 RFC 规范,但RFC3986 规范并没提到百分号%后面不符合的字符该如何处理,也没提到单独的百分号该如何处理。 你自己去试试? 结论 nginx 的这个处理,并不是 bug,RFC3986 规范里只是规定了百分号%后面的字符哪些字符,转换成哪种格式,并没有规定不符合规范的字符改如何处理,没规定是丢弃还是保留。所以,这只是 nginx 的一个特性feature,特性feature,特性feature,并不是BUG,不是 BUG,不是 BUG。 参考: Uniform Resource Identifier (URI): Generic Syntax 2018-09-20 1 0
TCP SOCKET中backlog参数的用途是什么? 在前年时,业务中遇到好多次因为PHP-FPM的backlog参数引发的性能问题,一直想去详细研究一番,还特意在2013年总结里提到这事《为何PHP5.5.6中fpm backlog Changed default listen() backlog to 65535》,然而,我稍于懒惰,一拖再拖,直至今日,才动脑去想,动笔去写。 2013年12月14发布的PHP5.5.6中,changelog中有一条变更, FPM: Changed default listen() backlog to 65535. 这条改动,是在10月28日改的,见increase backlog to the highest value everywhere It makes no sense to use -1 for *BSD (which is the highest value there) and still use 128 for Linux. Lets raise it right to up the limit and let the people lower it if they think that 3.5Mb is too much for a process. IMO this is better than silently dropping connections. patch提交者认为,提高backlog数量,哪怕出现timeout之类错误,也比因为backlog满了之后,悄悄的忽略TCP SYN的请求要好。 当我最近开始想好好了解一下backlog的细节问题时,发现fpm的默认backlog已经不是65535了,现在是511了。(没写在changelog中,你没注意到,这不怪你。)我翻阅了github提交记录,找到这次改动,是在2014年7月22日,Set FPM_BACKLOG_DEFAULT to 511 It is too large for php-fpm to set the listen backlog to 65535. It is realy NOT a good idea to clog the accept queue especially when the client or nginx has a timeout for this connection. Assume that the php-fpm qps is 5000. It will take 13s to completely consume the 65535 backloged connections. The connection maybe already have been closed cause of timeout of nginx or clients. So when we accept the 65535th socket, we get a broken pipe. Even worse, if hundreds of php-fpm processes get a closed connection they are just wasting time and resouces to run a heavy task and finally get error when writing to the closed connection(error: Broken Pipe). The really max accept queue size will be backlog+1(ie, 512 here). We take 511 which is the same as nginx and redis. 其中理由是“backlog值为65535太大了。会导致前面的nginx(或者其他客户端)超时”,而且提交者举例计算了一下,假设FPM的QPS为5000,那么65535个请求全部处理完需要13s的样子。但前端的nginx(或其他客户端)已经等待超时,关闭了这个连接。当FPM处理完之后,再往这个SOCKET ID 写数据时,却发现连接已关闭,得到的是“error: Broken Pipe”,在nginx、redis、apache里,默认的backlog值兜是511。故这里也建议改为511。(后来发现,此patch提交者,是360团队「基础架构快报」中《TCP三次握手之backlog》的作者shafreeck) 对于backlog的参数的含义,在linux kernel手册中也给了注释说明listen - listen for connections on a socket #include /* See NOTES */ #include int listen(int sockfd, int backlog); Description listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2). The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET. The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow. If a connection request arrives when the queue is full, the client may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt at connection succeeds. Notes To accept connections, the following steps are performed: 1. A socket is created with socket(2). 2. The socket is bound to a local address using bind(2), so that other sockets may be connect(2)ed to it. 3. A willingness to accept incoming connections and a queue limit for incoming connections are specified with listen(). 4. Connections are accepted with accept(2). POSIX.1-2001 does not require the inclusion of , and this header file is not required on Linux. However, some historical (BSD) implementations required this header file, and portable applications are probably wise to include it. The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog. When syncookies are enabled there is no logical maximum length and this setting is ignored. See tcp(7) for more information. If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently truncated to that value; the default value in this file is 128. In kernels before 2.4.25, this limit was a hard coded value, SOMAXCONN, with the value 128. backlog的定义是已连接但未进行accept处理的SOCKET队列大小,已是(并非syn的SOCKET队列)。如果这个队列满了,将会发送一个ECONNREFUSED错误信息给到客户端,即 linux 头文件 /usr/include/asm-generic/errno.h中定义的“Connection refused”,(如果协议不支持重传,该请求会被忽略)如下: 在linux 2.2以前,backlog大小包括了半连接状态和全连接状态两种队列大小。linux 2.2以后,分离为两个backlog来分别限制半连接SYN_RCVD状态的未完成连接队列大小跟全连接ESTABLISHED状态的已完成连接队列大小。互联网上常见的TCP SYN FLOOD恶意DOS攻击方式就是用/proc/sys/net/ipv4/tcp_max_syn_backlog来控制的,可参见《TCP洪水攻击(SYN Flood)的诊断和处理》。 在使用listen函数时,内核会根据传入参数的backlog跟系统配置参数/proc/sys/net/core/somaxconn中,二者取最小值,作为“ESTABLISHED状态之后,完成TCP连接,等待服务程序ACCEPT”的队列大小。在kernel 2.4.25之前,是写死在代码常量SOMAXCONN,默认值是128。在kernel 2.4.25之后,在配置文件/proc/sys/net/core/somaxconn (即 /etc/sysctl.conf 之类 )中可以修改。我稍微整理了流程图,如下: 如图,服务端收到客户端的syn请求后,将这个请求放入syns queue中,然后服务器端回复syn+ack给客户端,等收到客户端的ack后,将此连接放入accept queue。 大约了解其参数代表意义之后,我稍微测试了一番,并抓去了部分数据,首先确认系统默认参数 在FPM的配置中,listen.backlog值默认为511,而如上结果中看到的Send-Q却是128,可见确实是以/proc/sys/net/core/somaxconn跟listen参数的最小值作为backlog的值。 apache ab这边的结果中,非2xx的http响应有271个,在NGINX日志数据如下: 从nginx结果中看出,本次压测总请求数为10000。http 200响应数量9729个;http 502 响应数量186个;http 504响应数量未85个;即非2xx响应总数为502+504总数,为271个。同时,也跟error.log中数据吻合。同时,也跟TCP数据包中的RST包数量吻合。 在nginx error中,错误号为111,错误信息为“Connection refused”的有186条,对应着所有http 502响应错误的请求;错误号为110,错误信息为“Connection timed out”的有85条,对应着所有http 504响应错误的请求。在linux errno.h头文件定义中,错误号111对应着ECONNREFUSED;错误号110对应着ETIMEDOUT。linux man手册里,对listen参数的说明中,也提到,若client连不上server时,会报告ECONNREFUSED的错。 Nginx error日志中的详细错误如下: 在压测的时候,我用tcpdump抓了通讯包,配合通信包的数据,也可以看出,当backlog为某128时,accept queue队列塞满后,TCP建立的三次握手完成,连接进入ESTABLISHED状态,客户端(nginx)发送给PHP-FPM的数据,FPM处理不过来,没有调用accept将其从accept quque队列取出时,那么就没有ACK包返回给客户端nginx,nginx那边根据TCP 重传机制会再次发从尝试...报了“111: Connection refused”错。当SYNS QUEUE满了时,TCPDUMP的结果如下,不停重传SYN包。 对于已经调用accept函数,从accept queue取出,读取其数据的TCP连接,由于FPM本身处理较慢,以至于NGINX等待时间过久,直接终止了该fastcgi请求,返回“110: Connection timed out”。当FPM处理完成后,往FD里写数据时,发现前端的nginx已经断开连接了,就报了“Write broken pipe”。当ACCEPT QUEUE满了时,TCPDUMP的结果如下,不停重传PSH SCK包。(别问我TCP RTO重传的机制,太复杂了,太深奥了 、TCP的定时器系列 — 超时重传定时器) 对于这些结论,我尝试搜了很多资料,后来在360公司的「基础架构快报」中也看到了他们的研究资料《TCP三次握手之backlog》,也验证了我的结论。 关于ACCEPT QUEUE满了之后的表现问题,早上IM鑫爷给我指出几个错误,感谢批评及指导,在这里,我把这个问题再详细描述一下。如上图所示 NO.515 client发SYN到server,我的seq是0,消息包内容长度为0. (这里的seq并非真正的0,而是wireshark为了显示更好阅读,使用了Relative SeqNum相对序号) NO.516 server回SYN ACK给client,我的seq是0,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。(请发后面的) NO.641 client发ACK给server,我是seq 1 ,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。 NO.992 client发PSH给server,我是seq 1 ,消息包内容长度是496,已经收到你发的seq 1 之前的TCP包。 ...........等了一段时间之后(这里约0.2s左右) NO.4796 client没等到对方的ACK包,开始TCP retransmission这个包,我是seq 1,消息包长度496,已经收到你发的seq 1 之前的TCP包。 ..........又...等了一段时间 NO.9669 client还是没等到对方的ACK包,又开始TCP retransmission这个包,我是seq 1,消息包长度496,已经收到你发的seq 1 之前的TCP包。 NO.13434 server发了SYN ACK给client,这里是tcp spurious retransmission 伪重传,我的seq是0,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。距离其上次发包给client是NO.516 已1秒左右了,因为没有收到NO.641 包ACK。这时,client收到过server的SYN,ACK包,将此TCP 连接状态改为ESTABLISHED,而server那边没有收到client的ACK包,则其TCP连接状态是SYN_RCVD状态。(感谢IM鑫爷指正)也可能是因为accept queue满了,暂时不能将此TCP连接从syns queue拉到accept queue,导致这情况,这需要翻阅内核源码才能确认。 NO.13467 client发TCP DUP ACK包给server,其实是重发了N0.641 ,只是seq变化了,因为要包括它之前发送过的seq的序列号总和。即..我的seq 497 ,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。 NO.16573 client继续重新发消息数据给server,包的内容还是NO.992的内容,因为之前发的几次,都没收到确认。 NO.25813 client继续重新发消息数据给server,包的内容还还是NO.992的内容,仍没收到确认。(参见下图中绿色框内标识) NO.29733 server又重复了NO.13434包的流程,原因也一样,参见NO.13434包注释 NO.29765 client只好跟NO.13467一样,重发ACK包给server。 NO.44507 重复NO.16573的步骤 NO.79195 继续重复NO.16573的步骤 NO.79195 server立刻直接回了RST包,结束会话 详细的包内容备注在后面,需要关注的不光是包发送顺序,包的seq重传之类,还有一个重要的,TCP retransmission timeout,即TCP超时重传。对于这里已经抓到的数据包,wireshark可以看下每次超时重传的时间间隔,如下图: RTO的重传次数是系统可配置的,见/proc/sys/net/ipv4/tcp_retries1 ,而重传时间间隔,间隔增长频率等,是比较复杂的方式计算出来的,见《TCP/IP重传超时--RTO》。 backlog大小设置为多少合适? 从上面的结论中可以看出,这跟FPM的处理能力有关,backlog太大了,导致FPM处理不过来,nginx那边等待超时,断开连接,报504 gateway timeout错。同时FPM处理完准备write 数据给nginx时,发现TCP连接断开了,报“Broken pipe”。backlog太小的话,NGINX之类client,根本进入不了FPM的accept queue,报“502 Bad Gateway”错。所以,这还得去根据FPM的QPS来决定backlog的大小。计算方式最好为QPS=backlog。对了这里的QPS是正常业务下的QPS,千万别用echo hello world这种结果的QPS去欺骗自己。当然,backlog的数值,如果指定在FPM中的话,记得把操作系统的net.core.somaxconn设置的起码比它大。另外,ubuntu server 1404上/proc/sys/net/core/somaxconn 跟/proc/sys/net/ipv4/tcp_max_syn_backlog 默认值都是128,这个问题,我为了抓数据,测了好几遍才发现。 对于测试时,TCP数据包已经drop掉的未进入syns queue,以及未进入accept queue的数据包,可以用netstat -s来查看: 经过相关资料查阅,技术点研究,再做一番测试之后,又加深了我对TCP通讯知识点的记忆,以及对sync queue、accept queue所处环节知识点薄弱的补充,也是蛮有收获,这些知识,在以后的纯TCP通讯程序研发过程中,包括高性能的互联网通讯中,想必有很大帮助,希望自己能继续找些案例来实践检验一下对这些知识的掌握。 参考资料 TCP三次握手之backlog linux里的backlog详解 tcp-queue-的一些问题 The meaning of listen(2)'s backlog parameter TCP 的那些事儿(上) TCP/IP重传超时--RTO TCP的定时器系列—超时重传定时器 Coping with the TCP TIME-WAIT state on busy Linux servers 2018年10月22日新增How TCP backlog works in Linux 2015-06-02 +8 12 0
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
nginx、php-fpm默认配置与性能–TCP socket还是unix domain socket 前几天看到一篇博客,提到php所在服务器在大并发情况下,频繁创建TCP短连接,而其所在服务器的2MSL时间过长,导致没有端口可用,系统无法创建TCP socket,而大量报错。博主在后面给的解决方案是减少2MSL的时间,尽快清除TIME_WAIT状态的TCP连接,回收端口。同时,文章结尾写了不用长连接的理由,但这真的是最好的解决办法吗?有其他办法可以更好的做法吗? 类似经历 之所以多这篇文章兴趣这么高,是因为在前段时间,末学也经历了一件类似的优化历程,先简短的描述下我们的服务器架构。如图 想必看到这幅架构图的同学,都比较熟悉吧,也比较简单。“最前面”的nginx 反代负责与玩家的http请求通讯,这里是长连接。在其与后端游戏大区通讯时,使用了短连接,也就是意味着,每处理用户的一个http请求,都要重新与后端的nginx建立一次TCP(http)请求,后端nginx处理完之后就关闭。后端的nginx与php在同一台服务器上,通讯配置如下: 在这个架构图中,反代承担这承上启下的作用,前面是用户,后面是web app服务器,长连接用户,短连接后端。若用户数在1W人,1W人与反代的连接,在反代服务器上都用80端口。但这1W人的每一个请求,反代都要重新使用服务器的端口与后端nginx的80创建tcp socket,来处理请求。若2MSL时间为1分钟以上,同时,玩家两次请求间隔很短,想我们游戏的服务器,两次请求间隔大约5秒(甚至2-3秒)。那么反代的端口,5秒用1W个,10秒2W,15秒3W,20秒4W,25秒5W,30秒6W!!!,前面已经用掉的端口还没被回收掉。那么将会有很多的用户请求被nginx反代接收之后,因没有端口资源而无法与后端nginx创建tcp连接。。。以游戏的其中一个大区为例: 关于这个问题,末学在新浪微博上请教过其他大牛,其中tengine负责人淘叔度前辈以及@120斤的大青蛙前辈告诉我,nginx将在1.1.4版本开始支持ngx_http_upstream_keepalive,3月1号左右,是1.1.16 dev版本,我们的运维团队稍微由于几天,之后在部分小区的服务器上适用了1.1.16,效果不错,Nginx反代上与目标服务器的80端口的TCP连接中处于ESTABLISHED状态的非常多,TIME_WAIT的几乎没有,很稳定。我们几乎计划为了这个性能的提升,决定用dev版了。接着nginx1.2Stable version也出来了,那么也就用这个版本了。至于反代上nginx与后端app server建立TCP连接个数,也就是端口数(满载情况下,维持ESTABLISHED状态)的为nginx worker_processes * ((upstream1 server * keepalive参数数量) + (upstream2 server * keepalive参数数量) + ...),并不多。 如上,端口占用数少了,那么还有其他有点吗?先看一副 socket创建,监听、数据接收发送、关闭与客户端连接的socket流程图: 如上图,nginx反代跟后端nginx的形式跟php-fpm与mysqld socket的模型一样的。 nginx反代跟php-fpm 相对来说,就是客户端,这种做法,他们的每个新socket 请求的发起,都会跟着左边的流程走一遍,一遍又一遍。。。。每次的socket创建,端口等资源申请,与服务端的三次握手,关闭时的四次握手,端口的回收。。。。 改用长连接之后,如下图: 只有php-fpm(或者nginx反代)的子进程分别与mysqld创建1次TCP连接,之后都是send、recv数据的事情了,端口占用也是为数不多的几个(每个fpm子进程将与mysqld维持一个TCP长连接)。 同样,后端服务器的nginx与php-fpm的通讯也是如此,只是请求两没有反代那么大。区别就是IP地址是回环地址127.0.0.1。 既然是回环地址,那么两个服务都是在同一台机器上跑的,既然是同一台机器,为何不用进程间通讯的socket--unix domain socket呢? socket是神马?摘抄一段描述: Socket 可以被定义描述为两个应用通信通道的端点。一个 Socket 端点可以用 Socket 地址来描述, Socket 地址结构由 IP 地址,端口和使用协议组成( TCP or UDP )。http协议可以通过socket实现,socket在传输层上实现。从这个角度来说,socket介于应用层和传输层之间。但是socket作为一种进程通信机制,操作系统分配唯一一个socket号,是依赖于通信协议的,但是这个通信协议不仅仅是 tcp或udp,也可以是其它协议。 在同一台服务器上,用tcp socket与unix domain socket有什么区别? 如图所示,对于进程间通讯的两个程序,unix domain socket的流程不会走到TCP 那层,直接以文件形式,以stream socket通讯。如果是TCP socket,则需要走到IP层。 对于非同一台服务器上,TCP socket走的就更多了。 至于localhost\127.0.0.1以及网络IP他们之间的区别,无意中找到一篇博客写的是以mysql作为验证,来说明localhost不走TCP/IP层,跟127.0.0.1不一样。末学认为他理解错了。他的理由如下 (以下截图均在linux上,windows的没有unix domain socket) mysql连接本机时,不加-h参数: mysql连接本机时,加-h参数且值是localhost: mysql连接本机时,加-h参数且值是127.0.0.1: 那位同学从mysql工具的使用方法、与结果的区别,来理解推导localhost与127.0.0.1的区别,这从方向上就存在问题,我更相信,这是mysql这个程序自己的行为,遇到-h参数没加,或者-h参数的值不是IP形式,且my.cnf里指定mysql的socket路径时,则直接使用unix domain socket来连接服务器,当然,这也是我的猜测,没有去验证,大家听听就好,别相信。 鉴于末学对以上的理解,将服务器的架构配置变更如下 至此,优化还为完毕,若php-fpm与mysql使用mysql_pconnect的话,那么php-fpm的子进程生成模式最好用static模式,若为dynamic模式,可能会出现mysql连接数被占满的情况,这也跟mysql服务的连接超时时间有关,适当调整也容易避免。 不过,我们目前还没用mysql_pconnect,主要原因是我们的代码中,有些事务处理开启之后,对于代码的失败处理,忘记写回滚语句,在短连接的情况下,这个连接的销毁,哪怕客户端没提交ROLLBACK或者COMMIT指令,mysql会自动回滚之前的事务。但使用长连接之后,不同请求会使用同一个MYSQL连接句柄,每个事务开启都会禁用MYSQL的自动提交,即SET AUTOCOMMIT=0语句,这语句会提交之前的事务。对于我们代码忘记写回滚,而直接返回结果的情况下,这是会出大问题的,也是我们目前唯一没有使用MYSQL_pconnect的原因。(计划近期找到没有写回滚语句的代码,修复,继续使用mysql_pconnect) 其实还有,我们php-fpm使用了APC来缓存php file,以及 变量数据等,这些也是有优化的地方(如果有时间的话,则待续)。 回过头来再理解下文章开头那位同学给的解决办法,我仍不能从他给的理由中,理解长连接的缺点,哪怕是解决了TIME_WAIT的问题,但每次创建TCP socket ,连接到服务器时三次握手,关闭TCP socket时的四次握手 这些也是开销。当然,缩短2MSL的时间,也是更好利用服务器资源的一个好方法。 最后,我们调整优化的服务器架构图如下: 好像有点偏离这篇文章的标题了,其实我更想说我不能理解为啥nginx跟php-fpm给的默认配置中,都是TCP socket通讯的,为啥不默认给unix domain socket的默认配置呢?如果说为了方便非同一台服务器时的情况,但给的默认IP也是回环地址呀。 而且,nginx给默认配置中,对于uri请求中的php文件的处理,匹配规则还是老的,之前发生因为NGINX与PHP的配置而导致的安全问题,虽然不是nginx的错,但nginx也可给出更严谨的范例,但仍没有。 值得欣慰的是,在UBUNTU 12.4中,nginx的默认配置有了很大的改进,不管是匹配uri的规则,还是nginx与php-fpm的交互方式: PS:末学只是个web程序员,这些只是末学学习研究如上知识的总结,理解上难免有错,各位请海涵。 2012-05-31 +18 22 0
WEB开发安全与运维安全浅见 前段时间,同事LeeQueen为公司同事做了个《安全意识防护》的PPT,效果很好,各位新同事也知道日常公工作中,不太注意到的隐私保护问题。PPT中也提到了一些社会工程学的案例,提到了某团购网站前台人员被声称是新开礼品公司的工作人员,以填写信息就送礼品为由,获取到此团购网站的所有员工联系方式。后此团购网站的高层打电话回访,得知对方是猎头公司[详情见乌云网的链接]。此案例非常典型,讨论也尤为激烈,可谓是非常成功的培训。 此次培训之后,职业欠钱[sina微博][腾讯微博]让我给我们开发部门的PHP程序员同事们做一次PHP开发、运维时候一些安全注意事项。由于我在安全这块资历较浅,没有经验,我了解的大家都懂,这个话题太高深,我的水平完全不够,没法写这个PPT。而且,同事里有好几位安全界的前辈,我怎可关公面前耍大刀呢。上次写过一次关于正则表达式PPT,也就是一个月之前,深刻的体会到写PPT的痛苦,尤其是排版。实在不想写了。 后来,经常看到网上的安全工程师提到程序员安全意识较差,以至于同样的危险代码,危险程序,却屡次不改。当然,不是程序员不想改,而是程序员不太理解代码形成安全漏洞的原因。为此,我也觉得有必要提高一下我们程序员的安全意识了。 PPT中写了网络上常见的漏洞,形成原因,漏洞原理,防护建议等;也列举了近年来跟WEB相关的安全漏洞,以程序员的视角理解漏洞原理,并给出修复建议。这些建议仅供参考,不是正确答案。 由于本人水平原因,PPT中难免有错误,请见谅。也请安全界工程师指正。 借用网友的一句话,作为程序员,尽量要做到“知其然,知其所以然”,多多关注web安全,不要给安全工程师添麻烦,不能丢了程序员的脸。 同时,公司招聘PHP程序员,地点上海,公司规模为150人+,做网络游戏研发、运营。招聘条件以及待遇之类的,可以通过以下方式联系我,我们私聊。 http://weibo.com/cfc4nx http://t.qq.com/cfc4nx 邮箱:cfc4n@博客域名点COM 最后,PPT是周末两天写的,这两天老婆挺着6个月的大肚子,为我做饭,老婆实在是太辛苦了,感谢我老婆。 PS:讨厌写PPT[流泪]。 在线阅读: Web开发与运维安全浅见View more presentations from cfc4n. 下载到本地: PPT下载:web开发与运维安全浅见PPT下载,猛点这里 更多亮点在PPT的备注里,各位一定要看备注。转载的朋友麻烦保留PPT完整,包括招人信息。尤其是备注要保留。 ------- PPT在2011/09/19 更新了,更换匹配NGINX 对URI中PHP脚本的正则,考虑到了拓展名为 .phpa .phpb .phpc之类的情况。主要是更换了正则匹配的图,更改了正则内容。 2015/06/09 备注, nginx匹配URI时,对于PHP的解析,更改为 location ~ ^(?:(? 2011-09-19 +13 17 0
wordpress博客永久地址的二次重写 在很久很久以前,从前有座山,山上有个庙,庙里有个和尚在讲故事,讲什么呢? 从前有座山,山上有个庙,庙里有个和尚在讲故事,讲什么呢…… ...... 言归正传,很久以前,博客的URL是这种形式http://www.cnxct.com/cnxct/612/ 这种形式,后来呢,听网上SEOer说,为了更好的SEO,URL中包含关键词,可提高权重等等等等。遂打算把老的URL形式重写成hhttp://www.cnxct.com/%e6%88%91%e6%9c%80%e8%bf%91%e5%be%88%e6%b5%ae%e8%ba%81/这种形式。 服务器web services是nginx,需要更改如下规则 把/cnxct/数字/这种形式的老URL重写到/?p=数字这种形式,然后,Wordpress里在启用新的URL形式,让wordpress再把/?p=数字形式的URL再重写到目标URL。 效果如下图 一网友问到,遂共享出来 2010-09-25 +2 6 0