debug故事之:事务让生活更美好

最近遇到一个故障,接手的项目是一个类似RBAC结构的鉴权系统,用于公司内部的各组织内的管理系统,但是某个接口间歇性返回空数据, 这会导致第三方系统判定为无权限,从而让用户从管理后台退出登录。

这个故障一直存在,但是以前出现的频率并不高,一直到我此前把本地数据库的数据缓存删除之后,几乎每几次请求就会遇到一次,然而, 测试环境却无法复现。

这个接口原先的逻辑是这样的:

  • 首先确定是否有调用权限
  • 将token去用户服务换成user id
  • 去另一个服务把人员的岗位取出来(并且在本地缓存,加上一些逻辑来判断是否刷新数据:即离上次刷新超过24h)
  • 将岗位去另外一个服务取出组织架构相关的信息
  • 将上一步的返回结果在本地数据库进行查询另外一些信息,然后返回

我接手之后,将本地数据缓存这一步去掉了,如果每个微服务都要在本地缓存一份数据的话,那就没有微服务的意义了,但是这个改动 却引入了另一个bug,也就是最开始说的,有几分之一的概率,会返回空数据。

起初我怀疑是调用第三方系统的返回结果有问题,因为第三方系统经常gg,这是既往经验,免不了被束缚,通过加日志观察之后发现 不是,返回空数据时,第三方请求都没有走。于是跳到代码里一看,最上面有一个逻辑,当xxx长度为0时,直接返回。而这个xxx,就是 上述第三步返回的岗位。

此前说了,去除本地缓存之后,会每次都刷新岗位信息,而刷新岗位信息的逻辑是:

  • 先删除所有岗位
  • 再把目前的岗位加上

简单粗暴但是效果良好,但是有一个致命问题,就是没有使用事务,当并发出现时,A请求先将岗位删除,此时B请求调用获取岗位的 接口时,就取不到数据了,然后A才将现有的岗位加上去,然而这个时候第三方系统已经判断没有对应的权限而将用户登出了。

这种偶发性bug,由于难于稳定复现,因此非常难debug,加上事务之后,由于事务内,接口看到的数据总是稳定的,因此就不会有这种 问题了。

呼(长叹一口气)!此时不得不感叹,SQLAlchemy默认一个session就是一个事务,真是太明智了,虽然这样耗费点性能,但是比手动 开事务,真的可以避免很多bug,就像Go帮我自动管理了内存,写起来出错率自然是要比手动管理内存的C更低的。

因此,应当在web框架中,收到web请求时,开事务,请求结束时,提交或回滚,当然,这只是针对简单的web应用,若是复杂的,自然 就当根据实际情况来定制了。

当然,这样并非全无问题,无法通过数据库中间件自动做主从分离,就是它的问题所在了,详见 这里


经验:若生产环境能出现,而测试环境极难或极少出现的话,多半是和并发有关系,比如并发情况下,由于没有事务导致的竞争问题, 或者是内存泄漏,测试环境由于正常情况下请求量是很少的,因此很难复现。

此时为了debug,常见的手段是加日志、测试环境加压测、生产debug。


更多文章
  • HTTP 路由的两种常见设计形式
  • Golang的short variable declaration
  • Greenlet和Stackless Python
  • 写一个简单的ORM
  • 从源码看Python的descriptor
  • Python字符串格式化
  • Gunicorn 简明教程
  • Raft 论文阅读笔记
  • 什么是 Golang Comparable Types
  • GFS 论文阅读
  • MapReduce 论文阅读
  • 一起来做贼:Goroutine原理和Work stealing
  • Go语言的defer, panic和recover
  • 再读vim help:vim小技巧
  • 再读 Python Language Reference