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。
更多文章
- socks5 协议详解
- zerotier简明教程
- 搞定面试中的系统设计题
- 用peewee代替SQLAlchemy
- frp 源码阅读与分析(一):流程和概念
- Golang(Go语言)中实现典型的fork调用
- DNSCrypt简明教程
- 一个Gunicorn worker数量引发的血案
- Golang validator使用教程
- Docker组件介绍(一):runc和containerd
- Docker组件介绍(二):shim, docker-init和docker-proxy
- 使用Go语言实现一个异步任务框架
- 协程(coroutine)简介 - 什么是协程?
- SQLAlchemy简明教程
- Go Module 简明教程