60天,48次jira更新,1个bug,6个tip

达芬奇密码2018-08-23 09:33

不久之前,对蜂巢计费进行测试的时候发现了一个bug,定位解决的过程费了不少周折,所以记录一下,和大家分享。


背景

首先说一下这个 bug 的背景,我们的蜂巢(现已更名网易云计算基础服务计费系统。

蜂巢(现已更名网易云计算基础服务)是网易新出的一个基于 ssd 的 docker 服务。其基本实现是在网易自己的云计算平台上部署 docker 容器,然后对外卖这个服务。在这里,iaas 层对用户来说是透明的,用户不需要关心 docker 到底跑在哪个底层云主机上。

有买卖,就有计费。计费系统很重要的,IT 公司,每一 bit 都是钱,算多了算少了都不行,这是大背景。

我们的计费系统构成主要分为三部分:底层输出各自用量日志;计费系统抓取日志,根据消息服务中间件进行日志汇总;计费模块根据租户实例用量信息,进行费用结算。最后以 restful api 的形式提供给前端显示。

大概类似下图:

为了说的清楚一点,那就举个栗子。小王向房东老李租房子,每个月给老李交一次房租。这个房租由以下几个部分组成,每个月固定的房钱,网费,水电费,煤气费,物业费等等。其中房钱,网费,物业费都是固定的,都是单位为 1 进行计算;煤气费、水电费则是用多少算多少,因此每个月先查水电表,用了多少度,再乘以单价,加起来即可。最后总的相加,得到房租。

好了,这个基本上就和我们的计费模块干的事情差不多了,元数据的收集(相当于只收集用了多少度水电),处理,聚合,计算等等。(但是计费模块其实还要复杂点,比如高可用,但是这也正常,好比老李出差去了,房租不能不收啊,老李的老婆就会出现了)。


bug 的发现

这个 bug 是这么发现的。

蜂巢(现已更名网易云计算基础服务)在创建容器的时候可以选择套餐或者非套餐。套餐的话最小容量一个月 29 块钱,包含 25G 流量,再赠送 25G 的流量。我要测试一下,套餐超出 50G 流量之后,是否按照 0.79 元 1G 进行扣费。

这个说来简单,找一台云主机,启动一个iperf作服务端,然后在套餐的容器里开一个iperf客户端,跑上一段时间就能把流量耗完了。为了测试快一点,我把带宽设置的大一点,400Mb 每秒。

这时候问题来了,首先是我按照设置的速度消耗流量,到了预期时间流量没消耗完。继续跑iperf,流量消耗完了之后开始扣钱,发现 30G 左右的流量只扣大约 20G 左右的钱,相当于流量单价打了个 6.7 折。

作为用户肯定开心呀。但是作为公司这么赔钱做买卖可不行。那我们就要找问题。

很显然,curl一下接口,和前端显示的一致,说明不是前端的问题。我们首先当然认为是计费模块的问题。好比小王发现这个月房租一下高了很多,肯定认为老李把这个月房租算错了!

我也是这么想的,找了计费的同学核对了好几次。直接在数据库里查这个租户,当前容器的流量统计日志,根据流量算的就是这么点钱,没错呀。既然结算这里没有问题,那么看数据对不对,这时候发现了流量的日志,数据加起来就消耗了 20G。

推送过来的计费元数据就是错的。那么是ceilometer的问题咯?ceilometer从 cns 那里拿流量的信息,推送给计费平台作为计费的依据,开发同学每次和我说,他是大自然数据的搬运工,不产生数据,只搬运数据,不可能错的。

这到也是,其实偷偷说一句,这个模块当初我测试过,的确是没有发现 bug。怎么会每次推送都少了点呢?难不成ceilometer偷懒?

这个时候其实还考虑过其他可能,例如丢包问题,比如你以为iperf的速率为 400Mb,但是实际只有 200Mb,丢了 50% 的包,但是这个也被排除了,因为最后看跑了多少流量同时看 server 端和 clinet 端两边的结果,是一致的。还有曾经怀疑过iperf的流量统计到底是否准确,如果它给出的统计结果本身就是不准确的呢?后来这个也被排除了,通过登录到云主机和容器,看两边ifconfig在测试前后做差的结果,是和iperf的统计值一致的。

这就怪了,偏偏计费系统这边得到的流量不正确。

另外好要说一下,之前一直怀疑到ceilometer推送的流量有错误,那有没有怀疑过 cns 推送给ceilometer的数据是否正确呢?怀疑过的。在一开始的试验中,iperf一边是一台云主机,一边是一台容器,但是巧就巧在云主机这边的流量统计是正确的,而 只是 容器这边使用的流量统计错误。云主机ceilometer推送的流量正确,那么 cns 推送给ceilometer的流量也肯定正确,既然如此,正常人肯定都会认为是 nce 那边使用姿势错误,不知怎么把网络流量用错了。此处埋下了一个大坑。后续会讲。

前面说到因为怀疑是 nce 那边使用姿势导致 bug 问题,我还多次骚扰那边的同学帮我登到容器所在的云主机上去,看网卡的流量统计。有陆陆续续做了几组实验,问题依旧,而iperf的统计结果,容器ifconfig差值,以及容器所在云主机的ifconfig差值都是一致的结果,唯独计费那边推送过来的少 1/3 左右。

这下就真不知道问题在哪里了。


bug 的解决

直接剧透说结果吧。

这是底层openvswitch 2.30 版本的一个 bug,某一个统计接口用了一个 32 位的变量。而 cns 推送给ceilometer的数据就是通过ovs-dpctl -t 10 -s show获取的。至于为啥云主机那边数据正确,而容器不正确呢?因为蜂巢用的云主机和其他公有云云主机不在一个域里面,openvswitch 版本不一致,老的版本没这个问题。

就好像小王发现房租收多了,但其实不是老李算错了,而是他的那块电表统计就错了。那为啥住隔壁的小张电费没算错,那是因为虽然他和小王用同一个牌子的电表,但是她的版本比较早,统计正确,而小王的那块升级过了,每次统计耗电量都多算。

既然是这个 32 位的问题,那么要解决还是比较容易的,换成 64 位的就可以了。但是直接要对openvswitch动刀还是有点风险,毕竟涉及到底层那么多云主机的网络连通性。监控计费模块本来就是辅助行模块,如果他们不能正常工作,那么至少不要影响到业务。为了计费的一个 bug 而直接去升openvswitch,可能存在风险。所以就换一条路,从/proc/networks/dev 这个地方去读取数值,也就是 ifconfig 命令出来的网卡流量统计值。

经过验证,流量统计准确了。


bonus tips

发现bug是一回事,定位bug的原因是另一回事。定位到原因才能进一步修bug。于是我再送上一些tip。

感谢 知乎的这个问答,我从这里学到不少思想。

放大现象

如果有些 bug 现象不太明显,那么就想办法增大它的破坏性,把现象放大。

在美剧《豪斯医生》中有一集,医生怀疑病人心肺有问题,就让病人去跑步机上跑步,加重心肺负担,从而放大症状。

比如这这个 bug 当中,我们发现在带宽较大的情况下,流量丢失的就越明显。而在带宽较小的情况下,则不是那么明显。事后知道原因是因为 32 位溢出了之后很好解释,但是在不知道原因的时候,的确是很奇怪的。而且因为线上网络和 port qos 限速的原因,带宽的最大值有一个限制(400-500Mb),再往上就大不上去了。后来可以看到开发是直接在测试环境物理机上进行试验,才往这个方面去考虑。

通过这个案例,也提醒我们,以后测试的时候可以考虑 32 位这个限制,和 4GB 相关的数值要敏感。说不定就会发现一个溢出的 bug。


注意 diff 和 grep

这个技巧学来的时候,我都还不会用命令行下的工具:)不过就是初中那会儿的控制变量法嘛。不过后来在读一个物理学家写的blog里,有了更深的体会,虽然他在讲政治经济学,但我看到的是怎么debug。

就是在这篇 《六中全会公报的红楼梦研究》 文章中,作者详细说明了如何使用 grep 和 diff 进行研究公报。他的结论能够让我们提早知道为啥下一个五年计划工作重心放在了反腐倡廉上。我强烈建议大家读一读,搞不好用在投资领域,可能就要少调 10 年 bug 了。:)

回到 bug 上来,在调试和定位问题的时候,我们也能够利用 diff 的思想,分别实验。这个 bug 在公有云环境上有,在公共测试环境没有,那么就找找两个环境有什么区别。如果唯一区别是openvswitch版本不一致,那么很有可能是因为openvswitch的引起的 bug。那么就可以缩小范围,从openvswitch上找原因。

当变得东西多了,该法演变为二分定位法

关键就是就是不断迭代缩小范围,最终定位问题的症结所在 .

具体手段包括但不限于:注释掉部分代码、在不同位置插入试探性代码、对输入数据二分、对代码版本二分、对运行环境二分。

只要是有重现方式的 BUG,二分肯定能找到问题所在,要是有啥妙招那也可以用用,不过貌似没有妙招的时候多一点。 如果重现的方式比较复杂,耗时很长,那可能要专门写些程序或脚本来自动做二分。

git甚至特地加上了一个git bisect的子命令,专门用来在各个 commit 之间进行二分搜索,其主要目的,也就是在找不同。

比如 intel 开源中心做内核开发的就 这么 用程序来找 bug。

这套测试工具特别擅长抓 Regression bug。基于 Git 里面的 bisect 命令,测试系统通过二分法定位各种 Regression bug。比如 3.13 内核没问题,到 3.14 出问题了,这两个版本之间有上万个 commit,如果不知道是哪个 commit 造成这个问题,连该找谁修复都是问题。这种漫无目的的 debug 要求开发者非常专业,必须对内核各个方面都非常了解,才能感知和分析哪里出了问题。但用 bisect,相当于把计算机变成一个内核专家。bisect 可以迅速定位问题,找出有问题的 commit,自动给作者和维护者发邮件。他们是该 bug 最相关的人,往往第一眼就看出问题出在哪里,并了解如何修复最为妥善。

借助图表

有一些 bug 直接看数字很难体现,或者数字位数大了,人眼就不敏感。这个时候就应该祭出画图大法,将数字转化为图形。

本科的时候学习控制理论,这其中就有一种思想是将超调,纯滞后等特性在图像上进行反映。工厂有经验的老师傅们,进行参数整定与优化的过程中,都是看着输出曲线,加以调整。

所以一个系统有监控的时候可以一眼看出有啥问题,没有监控的时候,就要翻日志来看了。借助gnuplot或者 excel,都能形象化看出一些变化。比如在客户端和服务端iperf以 400Mb 的带宽进行通信的 15 分钟里,有一段时间,流量瞬间小了下去。

而在 bug 修复后以 800Mb 速率进行 15 分钟左右的测试,可以看到,流量还是相对比较平稳,哪个有问题,一眼可以看出来。


奥卡姆剃刀

这个思想是从医生这里学来的。

医生诊断的时候一般遵循 “一元论” 原则,这个可以看做是奥卡姆剃刀法则 “如无必要勿增实体” 在临床医学上的一个应用。很多疾病的临床表现都不止一个,如果有一种表现就考虑一种疾病,病人有五个临床表现,就考虑五种疾病,那么,会让医生的判断非常混乱,也就容易犯错。所以,这个原则建议尽量用一种病或者病因来解释所观察到的临床现象

这个原理在调试 debug 的时候也适用。qa 工程师在测试初期一开始发现 bug 的时候,都会记录下复现这个 bug 所需要的路径,以及依赖。用通俗的话来说就是要怎么样 保持姿势 ,来复现这个 bug。例如,我发现先做 a->b, 在保持 c 运行的状态下,打开 d,就会触发一个 bug。

但是实际上,不先做 a->b, 直接保持 c 运行的再打开 d,就能触发这个 bug。这个时候,就是需要我们动用奥卡姆剃刀,根据原则 2 善用 diff 来过滤掉无关干扰项,或者说叫做裁剪问题原因样本空间。(我记得高中有一些应用题,经常会有变量既不已知,也不用求,你要去解这些变量就掉进陷阱里卡死了)

类比到我们这个案例里。这个变量就是容器,我因为是在测试容器套餐的问题,发现了这个流量 bug,所以这之后一直以为是容器的原因,导致流量统计异常,殊不知,其实云主机层面流量统计就是异常的!


百病缠身

但是为什么我会一直被 “容器” 这个点所困扰呢?这其实是有原因的。这就是上文提到的:

至于为啥云主机那边数据正确,而容器不正确呢?因为蜂巢用的云主机和其他公有云云主机不在一个域里面,openvswitch 版本不一致,老的版本没这个问题。

两边的环境不一致。而正是因为这个的不一致,才默认cns->ceilometer-> 计费模块数据链路的正确性。因此才会做出 iaas 层没问题,是容器这边使用问题的假设。

这个有点类似于之前 北医三院事件 中,医生因为重度子痫症状,注意力全被它所吸引,没想到还有一个 10 万分之一的主动脉夹层存在。所以最后才出现遗漏。

有时 debug 的确跟医生看病一样,根据症状去找原因。理想情况下当然是一元论,但有时也会因为百病缠身,其实有好几个问题,但是互相掩盖了。比医生幸运的是,我们至少还能有测试环境,还可以一次一次进行实验。医生要是搞个core dump,那可就不好了。


他山之石

还有一招,就是问人啦。或者 google,stackoverflow,或者求教身边的大牛。

很多情况下,有经验的程序员可以一眼看出很多问题,比如野指针,传入参数为null,连接池未关闭等等。

记得不久之前和荣超同学上线,就遇到过一个线上数据库连接池部分资源泄露导致偶现数据丢失的问题,后来在妍姐的帮助下紧急修复了问题,我也顺带学习到不少知识,如此波澜不惊的描述背后可有一个惊心动魄的故事,以后有机会可以写写。


尾声

但愿世间人物病,何妨架上药生尘。

但愿世间无bug,何妨我术为屠龙。

然而世事不总遂人愿,因此学学调试技巧——一种让一个系统按照我们期望的方式运行的方法,还是很有意义的。君不见,生财之法,回春之术,经世之学,治国之道,无不都在 debug!


更多阅读




网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者黄哲骁授权发布。