背景
有产品服务S,有内外部接口转发服务P。
P有接口 /user_name,输入 user,输出 name
P有接口 /push,转发S的/push_from_p接口
S有接口 /push_from_p,输入id,请求P的/user_name后入库
现象
数据库中有重复数据插入user_name,基本都是两条
排查
查看S日志,发现有些 /push_from_p 接口耗时5000ms+,有些耗时20ms。
查看P日志,发现有请求S的/push_from_p发生timeout,报错处代码发现timeout=5。
起初怀疑P请求S/push_from_p的timeout设置小了,S接口中请求P/user_name耗时太长,应该讲timeout设置大一些。
但为什么P/user_name耗时这么长,而且恰好每次都在5000ms,让我开始怀疑P是不是只有一个线程资源。
然后查看P的启动代码CMD ["gunicorn", "--reload", "--bind", "0.0.0.0:8111", "server:app"]
然后查看gunicorn
的默认并发数量,发现workers
和threads
的默认值都是1
gunicorn --help
-w INT, --workers INT
The number of worker processes for handling requests.
[1]
--threads INT The number of worker threads for handling requests.
[1]
如果P只有一个线程,那就可以解释通为什么P/user_name耗时总是5000ms了。
某个服务X调用了P/push,P接着调用S/push_from_p,S接着调用P/user_name,但是这次请求会一直pending,因为P唯一的线程被X调用P/push给占用,直到过了5秒,P调用S/push_from_p超时,然后P/push返回失败的结果,释放了线程,然后线程分配给了P/user_name请求,虽然客户端单方面报错接口超时,但是服务端仍然会继续请求执行,所以S把查到的user_name入库。
为什么有些请求是20ms呢?查看S的代码发现,S把/user_name的结果放入redis了,所以当X重试P/push时,S并不会再次调用P/user_name
解决方法
- 修改P服务的并发数量
- 修改/push_from_p接口,将user_name结果直接放进入,避免S再次查询,造成潜在死锁问题
感想
服务间调用设计时,要避免流程上的嵌套调用。我调用你的接口中不能包含你调用我的流程。
服务间调用问题难排查,这实际上是个死锁问题,但表面上只是一个timeout问题。