线上的一个项目,过去一周数据展示异常。排查的过程中发现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、长期运行稳定的代码突然出问题,一定要了解下其他方近期做了什么调整,依赖方上下线了什么,自己上线了什么,运营调整了什么运维调整了什么。