线上的一个项目,过去一周数据展示异常。排查的过程中发现PHP7和PHP5的一个有趣的差异,特记录下来。
为了尽量把问题描述清楚,单独抽象了一个极简模型:电视节目排行榜,每个电视节目有4项数据(阅读数、互动数、搜索数、播放数),假如数据已经生成好,要求根据这4项数据制作一个Top100榜单,显示节目排名和分值(0-100之间)
模拟数据如下(真实数据可能来自数据部门,通过HTTP接口获取JSON数据,或者MQ获取)
$data = [
[
'id' => 1,
'name' => '如懿传',
'read_count' => 11714096,
'interactive_count' => 230,
'search_count' => 1078,
'play_count' => 20,
],
[
'id' => 2,
'name' => '甄嬛传',
'read_count' => 21714096,
'interactive_count' => 410,
'search_count' => 900,
'play_count' => 9,
],
[
'id' => 3,
'name' => '芈月传',
'read_count' => 8018618,
'interactive_count' => 333,
'search_count' => 1700,
'play_count' => 2,
],
];
算法:
0、对每一个节目每个维度的数据进行归一化处理A1, B1, C1, D1。
1、定义每个维度的权重w1, w2, w3, w4(w1 + w2 + w3 + w4 = 1)。
2、计算每个节目的分值score。每个维度归一化后的数据 * 相应权重求和(score = A1 * w1 + B1 * w2 + C1 * w3 + D1 * w4)。
3、按分值排序。
4、计算排名。
计算归一值的方法
/**
* 计算归一值
* @param $ini int 当前值
* @param $max int 当前维度最大值
* @param $min int 当前维度最小值
*
* @return float 分值
*/
public static function normal($ini, $max, $min) {
$max = log(1 + $max);
$min = log(1 + $min);
return (log(1 + $ini) - $min) / ($max- $min);
}
计算分值
public static function get_score($data, $weight) {
$data['read'] = self::normal($data['read_count'], $data['read_max'], $data['read_min']);
$data['interactive'] = self::normal($data['interactive_count'], $data['interactive_max'], $data['interactive_min']);
$data['search'] = self::normal($data['search_count'], $data['search_max'], $data['search_min']);
$data['play'] = self::normal($data['play_count'], $data['play_max'], $data['play_min']);
$add = $data['read'] * $weight['w1'] + $data['interactive'] * $weight['w2'] +
$data['search'] * $weight['w3'] + $data['play'] * $weight['w4'];
$score = log($add + 1, 2);
return $score;
}
以上是部分实现,在归一化的时候,max和min一般不会都为0, 确实在线上跑了2年也没出问题,但是前不久,数据依赖方做了调整play这一项数据全部为0,导致方法normal()中除数为0,返回NAN,get_score()也返回NAN。根本原因还是没有检查除数为0的情况。
事后验证对比发现,
PHP5下除数为0返回false, PHP7下除数为0返回NAN。
PHP中1 + false = 1,1 + NAN = NAN;
PHP中json_encode($data),$data中值包含NAN将返回false。
PHP中的NAN入到MySQL中显示为0。
echo date('Y-m-d H:i:s') . PHP_EOL;
echo "PHP版本: " . phpversion() . PHP_EOL;
$data = 0 / 0;
$res = 100 + $data;
$count = [
'id' => 1,
'read_count' => 100,
'score' => NAN,
];
var_dump($data, $res, $count, json_encode($count), 2333);die;
// PHP5返回结果begin
2018-09-24 09:34:18
PHP版本: 5.6.9-0+deb8u1
bool(false)
int(100)
array(3) {
["id"]=>
int(1)
["read_count"]=>
int(100)
["score"]=>
float(NAN)
}
bool(false)
int(2333)
PHP Warning: Division by zero in /usercode/file.php on line 5
// PHP7下返回结果
2018-09-24 09:33:32
PHP版本: 7.0.0-dev
Warning: Division by zero in /usercode/file.php on line 5
float(NAN)
float(NAN)
array(3) {
["id"]=>
int(1)
["read_count"]=>
int(100)
["score"]=>
float(NAN)
}
bool(false)
int(2333)
总结:
0、浏览代码的时候一定要看完整,不要漏了某一行,导致逻辑看不懂。
1、解决问题的正确方式,复现现象,在有问题的代码中加日志。加日志的时候,缩小日志的记录,方便查看定位问题。切记不要猜测,有些问题的原因根本是猜不到的。【谁会想到PHP7下0 /0会返回NAN,谁能想到NAN + 1 = NAN,谁能想到json_encode()的数组中带NAN返回异常,谁能想到NAN入到MySQL中为0 】
2、时刻提醒自己线上环境和验证环境是否完全一直。最好开发环境和生产环境一直,起码PHP版本要一致。
3、长期运行稳定的代码突然出问题,一定要了解下其他方近期做了什么调整,依赖方上下线了什么,自己上线了什么,运营调整了什么运维调整了什么。