线上的一个项目,过去一周数据展示异常。排查的过程中发现PHP7和PHP5的一个有趣的差异,特记录下来。
为了尽量把问题描述清楚,单独抽象了一个极简模型:电视节目排行榜,每个电视节目有4项数据(阅读数、互动数、搜索数、播放数),假如数据已经生成好,要求根据这4项数据制作一个Top100榜单,显示节目排名和分值(0-100之间)
模拟数据如下(真实数据可能来自数据部门,通过HTTP接口获取JSON数据,或者MQ获取)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | $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、计算排名。
计算归一值的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 | /** * 计算归一值 * @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 ); } |
计算分值
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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。
测试代码:线上查看点击这里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 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、长期运行稳定的代码突然出问题,一定要了解下其他方近期做了什么调整,依赖方上下线了什么,自己上线了什么,运营调整了什么运维调整了什么。