关于PHP-FPM的reload

由于最近在看接口监控的时候发现有一些502的情况,所以打算排查下原因。看了下nginx及php-fpm日志后偶然发现报502的时间节点跟fpm重启正好吻合。确认了下这个重启只是reload的操作而非restart的操作。什么?reload不是平滑启动吗?怎么会导致这样的问题出现呢?上网查了下资料后发现也有很多人遇到过这种情况。

接下来重现下这种情况。写几行代码来实际模拟下:

sleep(10);
echo "test";

然后在浏览器中访问一下,同时执行fpm reload的操作。可以看到执行完reload后页面立马就502了。
实际上通过process_control_timeout参数可以实现我们的目标。可惜这个参数缺省是 0,也就是不生效,这里把它设置成为2s。然后再重复一次上面的操作后发现页面可以正常输出test。同时会发现页面并不会等待10s后打印test。当我们 reload 的时候,sleep 立刻就结束了,这是因为 sleep 收到 reload 发出的信号后直接返回了。

process_control_timeout参数参考官网文档的解释:设置子进程接受主进程复用信号的超时时间。可用单位:s(秒),m(分),h(小时)或者 d(天)。默认单位:s(秒)。默认值:0(关闭)。
即reload的时候,如果有正在执行的请求进程便会等待该进程设置的时长。而其他进程直接就结束掉。等待正在执行的进程执行完或者是超过了设置的时间后fpm的master进程才开始生成新的fpm worker进程。

结论
默认情况下,PHP-FPM 无法保证平滑的执行 reload 操作,必须设置一个合理的 process_control_timeout 才行,同时需要注意的是其值不能设置的过大,否则系统可能出现严重的请求堵塞问题。

PHP中的null字符问题

由于 PHP 的文件系统操作是基于 C 语言的函数的,所以它可能会以您意想不到的方式处理 Null 字符。 Null字符在 C 语言中用于标识字符串结束,一个完整的字符串是从其开头到遇见 Null 字符为止。
借用官方手册中的一个例子:

$file = $_GET['file']; // "../../etc/passwd\0"
if (file_exists('/home/wwwrun/'.$file.'.php')) {
    // file_exists will return true as the file /home/wwwrun/../../etc/passwd exists
    include '/home/wwwrun/'.$file.'.php';
    // the file /etc/passwd will be included
}

以上代码在PHP5.3以前版本中,文件/etc/passwd将会被加载。对以后的版本没有影响。
虽然这个遇到\0会把字符截断的问题已经被修复了,但是在PHP中文件系统相关函数中还是有一点影响的,比如file_exists(),is_file()等。
如下代码:

$filename = "/etc/passwd\0"; 
$res = file_exists($filename); 
var_dump($res);

以上代码的执行结果如下:
Warning: file_exists() expects parameter 1 to be a valid path, string given in /usr/local/var/www/a.php on line 3
/usr/local/var/www/a.php:4:null

解决办法

对null字符进行替换

$input = str_replace(chr(0), '', $input);

重新认识PHP中的set_time_limit

想必大家都已经知道set_time_limit函数的作用,即设置脚本最大执行时间。但是这个函数还是有一些细节有些人可能没太注意。

首先先看一下set_time_limit函数的解释。设置允许脚本运行的时间,单位为秒。如果超过了此设置,脚本返回一个致命的错误。默认值为30秒,或者是在php.ini的max_execution_time被定义的值,如果此值存在。当此函数被调用时,set_time_limit()会从零开始重新启动超时计数器。换句话说,如果超时默认是30秒,在脚本运行了了25秒时调用 set_time_limit(20),那么,脚本在超时之前可运行总时间为45秒。

还有一个注意的地方是set_time_limit()函数和配置指令max_execution_time只影响脚本本身执行的时间。任何发生在诸如使用system()的系统调用,流操作,数据库操作等的脚本执行的最大时间不包括其中。同时sleep函数的时间也不包括在其中。

也就是说set_time_limit()函数只针对的是PHP代码本身的执行时间。

例子1:

set_time_limit(5);
sleep(10);
echo "test";

以上代码在10秒后正常的输出test,并且不会报错。
换成下面的代码试试

set_time_limit(5);
for(;;){
   //do something  
}

以上代码执行5秒后会报Fatal error: Maximum execution time of 5 seconds exceeded的错误。

例子2:

set_time_limit(5);
//假设curl需要10秒才能返回数据
$ch = curl_init(); 
curl_setopt($ch, CURLOPT_URL, "http://example.com"); 
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
$output = curl_exec($ch);  
curl_close($ch);  
var_dump($output);

以上代码在10秒后正常的打印出curl请求返回的结果,并不会在5秒的时候报错。
同理,数据库的相关操作也是如此。

所以,看似一个简单的函数调用,有些细节上的东西还是很有必要了解的。不然坑的可就是自己了?。

关于nginx http code 499的错误

最近在查看nginx日志的的时候,发现了一些状态码为499的记录。所以Google查了下大致内容如下:

499 CLIENT CLOSED REQUEST A non-standard status code introduced by nginx for the case when a client closes the connection while nginx is processing the request. 链接:https://httpstatuses.com/499

大概意思就是nginx还在处理请求的时候客户端已经关闭了连接。

下面来简单模拟下这种情况

  • 新建test.php放在web根目录下,代码如下
<?php
sleep(20);//sleep20秒
echo 'ok';
  • 进行curl请求,超时时间和数据传输时间都设置为10秒
curl --connect-timeout 10 -m 10 http://localhost/test.php
  • nginx日志如下
127.0.0.1 - - [24/Jun/2017:23:11:23 +0800] "GET /test.php HTTP/1.1" 499 0 -  "-" "curl/7.51.0" "-"

以上可以看见http code 为499,并且客户端已经连接关闭了,没有接收到任何数据。

如果curl不设置超时时间进行请求的话就正常了。

curl http://localhost/test.php
会返回ok
同时nginx日志为
127.0.0.1 - - [24/Jun/2017:23:13:01 +0800] "GET /test.php HTTP/1.1" 200 12 -  "-" "curl/7.51.0" "-"

解决方案

  • 客户端超时时间设置大一点,不要等服务端还没处理完就关闭连接。
  • 服务端代码及逻辑优化,减少执行时间。如果是比较耗时的操作业务容许的情况下可以改为异步去处理(具体如何调整还需看业务流程及具体场景)

mysql中存储emoji表情

在做移动应用时,会遇到ios或者android用户会在文本的区域输入emoji表情(如:??),如果不做一定处理,就会导致插入数据库异常。如下:

Incorrect string value: '\xF0\x9F\x99\x82" ...' for column 'Source'

这是因为emoji表情符号为4个字节的字符,而 utf8 字符集只支持1-3个字节的字符,导致无法写入数据库。

解决方案:

1.修改数据库的字符集为utf8mb4。(注意:mysql版本需要5.5.3以上)具体是需要修改整个mysql库、表或者具体字段字符集自己决定。当然,修改完数据库字符集后还没有大功告成。需要看下程序里连接数据库的时候有没有指定字符集为utf8mb4。php中PDO连接方式如下:

$db = new pdo('mysql:host=127.0.0.1;port=3306;dbname=mysql;charset=utf8mb4','user','password');

2.如果不想修改数据库字符集或者版本比较低不支持,那就只能存的时候转义一下了。可以转为base64格式或者其他支持的转义方式。