从“马蜂窝数据事件”谈软件开发
我是在世界杯期间才知道有个旅游网站叫“马蜂窝”的,后来一直也没关注。没想到最近几天,马蜂窝重新回到了大众的视野,只不过,这次亮相好像是从广告的另一面出现的,因为这几篇文章对蚂蜂窝进行了数据分析,得出了若干结论。
目前来看,马蜂窝网站对这几篇文章提出了异议,但是似乎还没有给出合理可信的解释。事实的真相如何,或许还要等等才有答案。不过我还是推荐大家阅读这几篇文章,相信大多数人会从中获得不少启发。
说到数据分析,似乎许多人都会一点,无非是算算总数啦,算术平均啦,看看极值啦,好一点的还知道方差、标准差。但是光这样做数据分析,能得出的 结论相当有限。怎么办呢?更复杂的数据分析要怎么做?许多人一看到“更复杂”,就想起复杂的数学公式、建模、曲线拟合等等,让人不胜头痛。
其实数据分析也可以不用那么复杂,搞清楚数据之间的关联就可以有不少发现了。比如这几篇文章就提供了非常有趣的视角,比如:
•所有创造内容的账号中,最活跃的1万5千个账号,他们的行为似乎有共同的节奏,同时活跃,同时沉寂;
•所有餐饮点评的发布日期分布,周一到周五保持了相对平缓的变化,周六周日则猛跌,大众点评则以周末两天为高峰;
•所有酒店点评的发布日期分布,周四到周六开始骤降,而携程艺龙则以周末为高峰;
•所有餐饮点评的发布时间分布,中午12点-13点,下午18点-20点为低谷,这两个时间段对应大众点评为高峰;
看到这些分析的时候,我忽然想起许多年前看过的一部破案电视剧。犯罪分子为了打劫资金,苦心孤诣提前半年做准备,有人提前“出国打工”,有人男 扮女装,时间选择大年三十以鞭炮声为掩护……劫案完全按照预先的设计进行,可谓天衣无缝。警方翻来覆去侦查,始终找不到任何破绽和线索。最终,案件的突破 来自一个小细节:犯罪嫌疑人说自己某年某月某日乘火车回来,所有的行程都能对上,都有证明。然而警方检查当天车站记录的时候,发现当天那次火车晚点了。沿 着这条线索,之前那些精心设计、严丝合缝的环节就像多米诺骨牌一样悉数倒下了……
这几天的蚂蜂窝事件,让我想起了之前的电视剧,它们说明的都是同样的道理:制造几份漂亮的数据(证据)是很容易的,但是制造内在逻辑统一的数据(证据)是很难的。
那么,上面说的这些事情,和软件开发有什么关系呢?
要知道,我们开发的软件大多是要运行在现实世界里的,是要与现实世界打交道,与现实世界保持一致的。只不过真实世界的内在逻辑——火车晚点了就 不能直接坐上当班汽车,饭点才有事件和兴致发餐馆点评——往往不能原样进入软件世界,所以软件系统里只剩下数据作为真实世界的载体,用数据来反映世界。虽 然大家常说“要用数据说话”,但许多时候数据也是会愚弄人甚至骗人的。那么怎样避免被数据愚弄,怎样识破数据的骗局呢?简单的经验是,不能仅仅就数据来看 数据,而要看到许多数据之外的东西。
数据之外有什么?最明显的是现实世界的约束。比如汽车上坡当然要比下坡慢,口袋里没有钞票了就买不了东西等等。在生活中,这些东西都很好理解, 似乎是“不言自明”的,甚至你没觉察到也摆脱不了这些约束。但是进入到软件的世界里,程序员开发的时候往往就把这些约束忘在脑后了,或者完全依赖于产品经 理给出的规则。所以有的游戏里汽车上坡和下坡的速度竟然是一样的,有的账户系统可以可以“逆向”充值给银行卡,储值余额却变成负数……
我刚刚工作的时候,在书上看到的一个例子让我印象深刻。天上的飞机需要知道航向,所以Plane对象有个成员变量是heading。通常我们会 用一个整型数值表示它,但是那本书上提到这是不对的!因为航向只能在0到360度之间,所以你用整型数值时一定需要给setter加上一段逻辑,确保这个 值只能设置在0到360之间。否则,难保其它系统读到heading时不会出错。“只有加上了对应的约束,变量才不再是原始普通的数字,而变成了能承载业 务价值的数据”。
许多年过去了,我仍然记得当时的惊讶:原来竟然可以这样!原来竟然应该这样!
后来我自己在开发中一直保持这样的习惯,虽然麻烦,也因此避免了许多故障。再往后看到《领域驱动设计》才明白:在设计软件系统时,领域知识往往 是最重要的,而领域知识的一大部分就在于识别领域中的各种约束。这些约束很难由产品经理巨细靡遗地穷举出来,而必须由参与开发的所有人达成共识,在系统里 实现它。但是无论如何,这些约束都是必不可少的,没有它们,系统就很可能出现各种稀奇古怪的现象。
我曾经见过奇特的仓储费账单,数额大到让所有人吃了一惊,大家伙查来查去才发现入仓时间竟然在公元229年。如果约束落在系统里,第一时间发现 异常并且报出来,就可以省去后面的许多折腾。当时我说“公元229年这还是三国时期呢”,所有人都笑了。其实这个例子一点也不好笑,看看我们常见的系统, 身高、年龄、体重、车速、生日等等数据,许多时候就是想当然直接用原生数据类型来表示的,所以取值为负数、甚至几千万上亿也“无可厚非”,当然后果也非常 严重。
数据之外的天地不只有约束,数据的内在逻辑和关联也是相当重要的。
在分析马蜂窝的文章里提到,马蜂窝做了些修改,比如清空了某些账号,所以看起来数据正确了。但是稍作一点拓展就会发现,数据完全对不上,比如之 前这些账号留下的那些牛头不对马嘴,“身份今天是男明天是女”的点评仍然存在着。如果数据真实,并不会发生性别剧烈变化的情况;如果数据关联可信,也不会 出现“删了账号帖子还在”的情况。
不过,这种“按下葫芦浮起瓢”,“顾头不固腚”的现象并不是个例,我就见过许多系统有这种问题,稍微改动某个细节,就会全盘大乱。这还不是最要命的,最要命的是发现异常的时候根本不知道因果链条是怎样的,从哪里萌发,中间经历了哪些环节,下面还会影响什么……
之所以会出现这种问题,不少时候是系统开发过于想当然,过于图省事,假设其它一切都是“按部就班”正常发生的,所以只记录了“最核心”的数据, 相关的数据和状态则完全没有保存。于是,系统的状态就只能靠一堆零散孤立的点来承载,点与点之间没有明确关联,不能彼此印证,也无法回溯历史上的各个片 段。
这方面最明显的例子就是记账系统。许多人都见过“想当然”的记账系统,每一笔账目发生的时间、金额都要记录下来,但也仅此而已了,绝没有记录所 涉及账户的即时余额,涉及的账户里也没有对应的记录。结果某天发现账户出错,只能手工一笔笔地追查,效率极低,而且容易额外引入错误。
出现这种情况当然很糟糕,但也有一定的道理。记账这回事已经有几千年的历史,但“在会计活动中对每一项经济业务按相等金额在两个或两个以上有关 账户相互对应地同时进行登记”的复式簿记法,直到12世纪才在意大利诞生,引入我国更是要等到晚清时期。如今许多做软件开发的人未必学过会计知识,做出的 记账系统没有复式簿记的思想,也情有可原。
其实按我的经验,往往越是复杂的系统,越是复杂的功能,越是应该学“复式簿记”,也就是不怕麻烦,清楚记录因果链条,提供详细回溯能力。以前我 们曾做过一套计费系统,其实就是把一大堆阶梯价目表做到系统里,上线后经常有客户来吵架,说计费出错了,每次都要劳心劳力来核对。即便系统没有出错,也没 有办法第一时间否认系统出错,第一时间证明给用户看。
后来大家痛定思痛,花大力气把计算过程做成了“自解释”的:每一步都有详细的记录,按照哪一份标价表格的第几条规则,具体是怎么计算的……总 之,每一笔费用都有详细的计算过程可查,而且各子系统之间需要维持状态关联,保证彼此的数据和状态实时一致,如果出现问题及时报警。 这样做繁琐是繁琐了点,但上线之后不久就解决了争议,程序员有了更多不被打扰的时间,用户也有了信心。
最后,在我们分析问题时也应当注意数据的关联。
我经常说,正确合理的分析,一定有多个独立数据来源可以彼此验证。之所以这么说,是因为看到了太多的分析,给出的原因、理由看似合理,却根本无 法得到多重验证,所以其实根本没有找对原因。比如某个程序异常的原因是“网络瞬断”,那么它用的是什么样的网络?哪一段在当时发生了抖动?共用此网络的其 它应用有没有受影响?如果我们再模拟一次网络瞬断,程序的表现是否相同?…… 只有得到这一系列答案,掌握多种数据,我们才能最终下结论说是“网络瞬断”。否则一口咬定“网络瞬断”,往往要么是能力不及,要么是偷懒甩锅。
总而言之,蚂蜂窝的事情或许还要“让子弹飞一会儿”,但检查自己手头的代码有没有落实约束,有没有实践“复式簿记”的思想,定位问题时多找一些数据彼此验证,却是马上就可以做的事情。