前段时间,我们对一个遗留系统进行了改造,主要是把老的登录模块,换成新的统一登录,同时优化了一些页面跳转的逻辑。
测试环境测了很久,都没什么问题,我们就上线了。上线后,一开始也没什么问题,但是过了几个小时,客服开始反馈,有部分用户登录后,会随机跳转到错误的页面,有的跳到了首页,有的跳到了个人中心,有的甚至跳到了别人的页面,而且这个Bug很难复现,时有时无,同一个用户,有时候正常,有时候不正常。
这个Bug很诡异,我们一开始以为是缓存的问题,清了缓存,还是有问题;又以为是浏览器的问题,换了浏览器,还是有问题;又以为是用户的问题,但是不同的用户都有反馈,而且都是随机的。
没办法,我们只能开始排查,从晚上8点,一直排查到第二天早上6点,排查了一夜,终于找到了原因,原来是一个很小的细节导致的,说出来大家可能都不信,但是就是这么一个小细节,导致了线上的大Bug。
今天就来分享一下,这次线上Bug排查的全过程,以及我的经验和感悟,希望能给大家一些参考。
一、Bug的现象
先说说这个Bug的现象,真的很诡异。
我们的系统,是一个老的电商系统,用户登录后,应该跳转到"我的账户"页面,但是上线后,有部分用户反馈,登录后,会随机跳转到错误的页面:
- 有的用户,登录后,跳到了首页,而不是"我的账户"页面;
- 有的用户,登录后,跳到了个人中心的某个子页面,比如"我的订单"、"我的收藏";
- 有的用户,登录后,甚至跳到了别人的页面,能看到别人的订单信息,这就很严重了,涉及到用户隐私和安全;
- 还有的用户,登录后,页面空白,或者报错。
最诡异的是,这个Bug是随机的,时有时无:
- 同一个用户,有时候登录正常,有时候登录不正常;
- 同一个浏览器,有时候正常,有时候不正常;
- 有的用户,反馈了一次,之后就再也没出现过;
- 有的用户,每次登录都不正常。
而且,这个Bug,在测试环境完全复现不了,我们在测试环境测了很多次,都正常,一到线上就出问题,非常诡异。
这种随机的、难复现的Bug,是最难排查的,因为你不知道什么时候会出现,也不知道怎么触发,只能一点点猜,一点点试。
二、排查过程
接到反馈后,我们立刻开始排查,从晚上8点,一直排查到第二天早上6点,整整一夜,过程很曲折,下面说说我们的排查过程。
第一步:查看日志,没有发现异常
首先,我们查看了服务器的日志,包括Nginx日志、应用日志、错误日志,但是没有发现明显的异常,没有报错,没有异常堆栈,登录接口的返回值,看起来都是正常的,跳转的URL,也都是"我的账户"页面的URL,没有问题。
这就奇怪了,日志里显示跳转的URL是对的,但是用户实际看到的页面是错的,难道是浏览器的问题?
第二步:怀疑是浏览器缓存,清缓存,没用
我们一开始怀疑是浏览器缓存的问题,老系统有很多页面,缓存策略很乱,可能是缓存了旧的页面,导致跳转错误。
我们让用户清浏览器缓存,强制刷新,但是还是有问题,有的用户清了缓存,还是会跳转到错误的页面。
我们又检查了Nginx的缓存配置,发现有几个页面,配置了很长的缓存时间,可能是缓存了旧的跳转逻辑,我们把这些缓存都清了,但是还是有问题。
第三步:怀疑是CDN缓存,清CDN,没用
我们的系统,用了CDN,静态资源和部分页面,走了CDN,我们怀疑是CDN缓存了旧的页面,导致跳转错误。
我们清了CDN的缓存,甚至把CDN暂时关了,直接回源,但是还是有问题,用户还是会跳转到错误的页面。
这时候,我们有点慌了,缓存也清了,CDN也关了,还是有问题,到底是哪里的问题?
第四步:怀疑是Session共享的问题,检查Session,发现了线索
我们的系统,是多台服务器部署的,用了Redis做Session共享,登录后,用户的信息存在Session里。我们怀疑,是不是Session共享有问题,不同的服务器,Session不一致,导致跳转错误。
我们检查了Redis里的Session,发现了一个奇怪的现象:有的Session里,存储的跳转URL,是错误的,有的是首页,有的是"我的订单",有的甚至是别的用户的页面URL。
这就有线索了,跳转URL存在Session里,但是有的Session里的跳转URL是错的,这说明,在设置跳转URL的时候,就设置错了。
第五步:查看跳转逻辑的代码,发现了问题
我们立刻去查看跳转逻辑的代码,这是这次改造新加的逻辑,登录前,会把用户之前访问的页面URL,存在Session里,登录后,跳转到这个URL,如果没有,就跳转到"我的账户"页面。
代码大概是这样的:
// 登录前,保存当前页面URL到Session
$_SESSION['redirect_url'] = $_SERVER['REQUEST_URI'];
// 登录后,跳转到之前的页面
$redirectUrl = $_SESSION['redirect_url'] ?? '/account';
header('Location: ' . $redirectUrl);看起来没什么问题,但是仔细看,发现了一个问题:$SESSION['redirecturl'],没有做校验,也没有做清理,登录后,没有把这个Session删掉,下次登录的时候,如果这个Session还在,就会跳转到之前的URL。
但是,这也不能解释,为什么会跳转到别人的页面,因为Session是每个用户独立的,不会共享。
第六步:深入排查,发现了Session ID的问题
我们继续排查,发现了一个更奇怪的现象:有的用户的Session ID,和别的用户的Session ID,是一样的!也就是说,两个不同的用户,用了同一个Session ID,所以他们的Session是共享的,一个用户设置的跳转URL,另一个用户也能读到,所以会跳转到别人的页面。
这就很严重了,Session ID怎么会重复呢?Session ID应该是唯一的啊。
我们检查了Session ID的生成逻辑,发现了问题!这个遗留系统,Session ID不是PHP自动生成的,而是自己写的生成逻辑,代码大概是这样的:
function generateSessionId() {
$id = md5(uniqid(rand(), true));
return $id;
}看起来没问题,用了uniqid和rand,应该是唯一的。但是,我们仔细看,发现了问题:rand()函数,在多台服务器上,如果种子一样,生成的随机数是一样的!
这个遗留系统,在启动的时候,有一个地方,调用了srand(12345),把随机数种子固定成了12345,所以,每台服务器,生成的随机数序列,都是一样的!
而uniqid()函数,是基于时间的,如果两台服务器的时间一样,生成的uniqid也是一样的。
所以,在高并发的时候,两台服务器,在同一毫秒,生成的Session ID,是一样的!因为随机数种子一样,时间一样,所以md5的结果也一样。
这就是问题的根源!Session ID重复了,导致不同的用户,共享了同一个Session,所以A用户设置的跳转URL,B用户也能读到,登录后就跳转到了A用户的页面,这就是为什么会跳转到别人的页面,而且是随机的,因为只有在同一毫秒,两台服务器生成的Session ID才会重复,概率不高,但是用户量大了,就会出现。
而且,这个Bug,在测试环境复现不了,因为测试环境只有一台服务器,不会出现多台服务器Session ID重复的问题,只有线上多台服务器部署,才会出现,所以我们在测试环境测了很久,都没发现。
找到原因的时候,已经是早上6点了,我们排查了一夜,终于找到了这个诡异的Bug的根源,竟然是因为一个固定的随机数种子,导致Session ID重复,这么小的一个细节,导致了这么大的一个线上Bug,真的是细节决定成败啊。
三、解决方案
找到原因后,解决方案就很简单了:
- 去掉固定的随机数种子: 把
srand(12345)这行代码删掉,让PHP自动用时间作为随机数种子,这样每台服务器的随机数序列就不一样了。
- 用更安全的Session ID生成方式: 不用自己写的生成逻辑,用PHP自带的Session ID生成,或者用
randombytes()、opensslrandompseudobytes()等更安全的随机数生成函数,生成的Session ID更随机,更难重复。
- 校验跳转URL: 登录后的跳转URL,要做校验,只能是本站的URL,不能是外部URL,也不能是别的用户的页面,避免安全问题。
- 清理Session: 登录后,把跳转URL的Session删掉,避免下次登录的时候,还跳转到之前的页面。
- Session ID校验: 登录的时候,校验Session ID,如果发现Session ID对应的用户,和当前登录的用户不一致,就重新生成Session ID,避免Session劫持和共享。
我们把这些修复,紧急上线了,上线后,就再也没有用户反馈跳转错误的问题了,这个Bug终于解决了。
四、经验总结
这次排查,花了一夜的时间,虽然很辛苦,但是也学到了很多,总结了一些经验,分享给大家:
1. 线上Bug,要先看日志,但是不要只看日志: 排查线上Bug,首先要看日志,日志能提供很多线索,但是不要只看日志,因为有的Bug,日志里不会有明显的错误,比如这次的Bug,日志里一切正常,但是实际有问题,所以,还要结合现象,深入分析。
2. 难复现的Bug,要找规律: 随机的、难复现的Bug,是最难排查的,但是只要仔细观察,总能找到规律,比如,这次的Bug,虽然随机,但是都是多台服务器、高并发的时候出现,而且会跳转到别人的页面,这就提示我们,可能是Session共享的问题,顺着这个线索,就找到了原因。
3. 多台服务器部署,要注意共享和一致性: 现在的系统,基本都是多台服务器部署,要注意Session、缓存、数据的共享和一致性,不要想当然地认为,每台服务器的状态都是独立的,或者都是一致的,要仔细检查,特别是自己实现的一些逻辑,比如Session ID生成、随机数、缓存等,很容易在多台服务器上出问题。
4. 随机数,不要固定种子: 随机数,不要固定种子,除非你有特殊的需求,比如测试的时候,需要可复现的随机数。生产环境,一定要用真正的随机数,不要固定种子,否则多台服务器,或者多次运行,生成的随机数是一样的,会导致各种问题,比如Session ID重复、验证码重复、订单号重复等,很危险。
5. 自己实现的基础功能,要仔细测试: 很多遗留系统,喜欢自己实现一些基础功能,比如Session ID生成、加密、缓存、数据库连接池等,这些基础功能,看起来简单,但是很容易出问题,而且一旦出问题,就是大问题。自己实现的基础功能,一定要仔细测试,特别是多台服务器、高并发的场景,要测试充分,不要想当然地认为没问题。
6. 测试环境,要尽量和线上一致: 这次的Bug,在测试环境复现不了,因为测试环境只有一台服务器,而线上是多台服务器,所以,测试环境要尽量和线上一致,包括服务器数量、配置、架构、数据量等,这样才能在测试环境发现更多的问题,避免线上出Bug。
7. 跳转URL,一定要做校验: 登录后的跳转URL,一定要做校验,只能是本站的URL,不能是外部URL,也不能是用户的敏感页面,避免开放重定向漏洞,以及用户隐私泄露。很多系统,跳转URL都没有校验,很容易出安全问题。
8. 排查Bug,要有条理,不要乱试: 排查Bug的时候,要有条理,从现象到原因,一步步来,先假设,再验证,不要乱试,一会儿改这个,一会儿改那个,这样不仅找不到问题,还可能引入新的问题。这次排查,我们虽然走了一些弯路,但是整体还是有条理的,从缓存到CDN,到Session,到代码,一步步缩小范围,最后找到了原因。
9. 团队协作,很重要: 排查复杂的Bug,一个人很难,团队协作很重要,大家分工合作,有人看日志,有人看代码,有人查配置,有人复现问题,效率会高很多。这次排查,我们团队好几个人,一起排查了一夜,互相讨论,互相启发,才找到了原因,如果是一个人,可能要更久。
10. 上线后,要密切监控: 新功能上线后,要密切监控,包括日志、错误率、用户反馈等,发现问题,及时处理,不要等问题扩大了,才去处理。这次的Bug,上线后几个小时,就有用户反馈了,我们及时开始排查,没有造成太大的影响,如果等了很久才发现,可能会造成更严重的后果。
五、我的感悟
排查了一夜,虽然很累,但是也有很多感悟。
首先,我觉得,程序员这个职业,真的是细节决定成败,一个很小的细节,比如一行固定随机数种子的代码,可能看起来没什么,但是在特定的场景下,就会导致很大的线上Bug,影响很多用户,甚至造成安全问题。所以,写代码的时候,一定要仔细,注意细节,不要想当然,不要觉得"这行代码没关系",很多大Bug,都是由小细节引起的。
其次,我觉得,遗留系统的改造,真的很不容易,遗留系统,代码混乱,文档缺失,很多老代码,都不知道是谁写的,也不知道为什么这么写,改造的时候,很容易踩坑。这次的Bug,就是因为老代码里,有一行固定随机数种子的代码,我们改造的时候,没有注意到,就踩坑了。所以,改造遗留系统的时候,一定要仔细,要充分了解老代码的逻辑,不要想当然地认为老代码是对的,或者是没用的,很多老代码,虽然看起来奇怪,但是可能有它的原因,要仔细分析,再改造。
第三,我觉得,线上Bug排查,真的很考验一个程序员的能力,不仅要有扎实的技术基础,还要有清晰的逻辑思维,有条理的排查方法,还要有耐心,有抗压能力,面对诡异的Bug,不能慌,不能乱,要冷静分析,一步步来,总能找到问题所在。这次排查,虽然花了一夜,但是也锻炼了我的排查能力,积累了经验,以后再遇到类似的Bug,就能更快地找到原因。
第四,我觉得,测试真的很重要,很多线上Bug,都是因为测试不充分导致的。这次的Bug,在测试环境复现不了,因为测试环境和线上环境不一致,所以,测试环境要尽量和线上一致,要覆盖各种场景,包括多台服务器、高并发、异常情况等,这样才能在测试环境发现更多的问题,避免线上出Bug。当然,测试也不能发现所有的问题,但是充分的测试,能大大减少线上Bug的概率。
第五,我觉得,团队协作真的很重要,一个人的能力是有限的,团队协作,能发挥每个人的优势,提高效率。这次排查,我们团队好几个人,一起排查了一夜,互相讨论,互相启发,才找到了原因,如果是一个人,可能要更久,甚至找不到原因。所以,平时要和团队成员搞好关系,遇到问题,一起讨论,一起解决,不要一个人扛着。
最后,我觉得,程序员这个职业,就是在不断地踩坑,不断地填坑,不断地成长,每一次Bug排查,都是一次学习,一次成长,虽然过程很辛苦,但是解决问题之后,会很有成就感,也会学到很多东西。所以,不要害怕Bug,不要害怕踩坑,每一次踩坑,都是一次成长的机会,只要我们认真总结,吸取教训,就能越来越强,越来越厉害。
写在最后
线上出了个遗留系统改造的Bug,我排查了一夜。
这个Bug,很诡异,随机出现,难复现,我们排查了一夜,终于找到了原因,竟然是因为一行固定随机数种子的代码,导致多台服务器Session ID重复,这么小的一个细节,导致了这么大的一个线上Bug,真的是细节决定成败啊。
通过这次排查,我学到了很多,也总结了一些经验,比如,线上Bug要先看日志,但是不要只看日志;难复现的Bug,要找规律;多台服务器部署,要注意共享和一致性;随机数,不要固定种子;自己实现的基础功能,要仔细测试;测试环境,要尽量和线上一致;跳转URL,一定要做校验;排查Bug,要有条理,不要乱试;团队协作,很重要;上线后,要密切监控。
希望我的经历和经验,能给大家一些参考,遇到线上Bug的时候,不要慌,要有条理地排查,从现象到原因,一步步来,总能找到问题所在。也希望大家,写代码的时候,注意细节,仔细测试,尽量避免线上Bug,让系统更稳定,更可靠。
最后,用一句话结尾:
"细节决定成败,每一个看似无关紧要的细节,都可能导致严重的问题。写代码要仔细,排查Bug要有条理,不断踩坑,不断成长,才能成为更优秀的程序员。"
祝大家都能写出稳定的代码,少踩坑,遇到Bug也能快速解决,系统永远稳定运行!
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录