一、问题的产生
最近这几天公司的一个线上的系统出现了故障,操作人员导入了一份excel后,由于处理时间比较长,前端界面一直显示在loading,等到最后就报“接口不可用“了,然后后台的开发人员去查询数据库,发现生成了多条的导入处理任务,这时候,后台、前端、运维人员开始争吵了,各说不是我的问题,最后问题推给了“中间件”。
二、分析问题
1、 系统现状
既然要分析问题,那先要了解这个系统的部署及网络的情况,从用户操作到服务器的过程。来看下这个系统的部署图,这个图是主要的部分,次要的部分就没画出来了。大致情况就是,用户在网页上操作,数据通过网络传递回服务端,这个过程使用了CDN,然后到负载器,在分发到nginx,nginx在分发到后端的服务上,在到数据库。
另外,这是一个前后端分离的项目,前端采用Vue开发,后台采用java开发,数据库用mysql。
2、 排查问题
后台写入数据重复,后端的开发一想就是前端重复提交的问题,找前端的同事看,前端说我没有问题呀,操作上还加了loading,用户不可能点第二次,后端开发还是不信,那就打开F12吧,我靠,没有重复的网络请求。
后端开发找到运维人员,运维说服务器我没动过呀,还是原来那样子。一堆人讨论了半天,有的说是CDN,有的说是nginx,各有各的说法。
3、问题超乎想象
看大家都没有头绪,我说来看下nginx的access log吧,通过简单的grep命令,找到接口对应的请求记录,MD,一看吓一跳,一个接口处理要60s?
找到对应的开发人员询问情况,它说后台处理确实是要超60s的,在问下前端超时是多久,前端说是30s,这里只能说“我服了”。 正常情况下,前端操作的超时要大于后端的超时,不然后端处理完了,就无法返回给到前端了,前端都超时了,用户可能另起操作或者关闭了。(PS:这种任务就应该做成异步,不知当初是怎么设计的)。
在仔细地看下access log,发现同个接口请求之间间隔的时间是差不多的,不像是前端用户手动操作导致,那会是什么问题?
脑海里闪现了是不是nginx的重次机制导致的呢,于是打开这个系统对应的location的conf文件,发现这个API对应的location中并未作个性化配置,都是默认配置。隐约记得location中有几个超时的配置项目,用于控制与后端的连接,读取,发送的。
三、解决问题
1、 nginx的超时、重试机制
找到nginx中用于配置location的超时的配置项,一共有三个:
- proxy_connect_timeout time:与后端/上游服务器建立连接的超时时间,默认为60s。
- proxy_read_timeout time:设置从后端/上游服务器读取响应的超时时间,默认60s。此超时时间指的是2次成功读操作时间间隔,而不是读取整个响应体的超时时间。如果超时时间内没有任何响应,则Nginx将关闭连接。
- proxy_send_timeout time:设置往后端/上游服务器发送请求的超时时间,默认60s。此超时时间指的是2次成功读操作时间间隔,而不是读取整个响应体的超时时间。如果超时时间内没有任何响应,则Nginx将关闭连接。
失败重试机制设置
- proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_503 | http_504 | http_403 | http_404 | non_idempotent | off ... :配置什么情况下需要向上游服务器进行重试。默认为:error timeout。error表示读写出错;timeout表示超时;invalid_header表示头信息有误;non_idempotent表示RFC-2616定义的非幂等HTTP方法(POST、LOCK、PATCH),也可以在失败后重试(默认幂等方法GET、HEAD、PUT、DELETE、OPTIONS、TRACE);off表示禁用重试。
- proxy_next_upstream_tries number:设置重试次数,默认0表示不限制。这里的次数包含第一次请求。
- proxy_next_upstream_timeout time:设置重试最大超时时间,默认0表示不限制。
上面2个值的关系是且的关系,在限制时间内或者重试次数达到一个值就会结束重试,并返回客户端响应。
2、 结合现状分析
从官方的文档中发现:指令 proxy_connect_timeout 或 proxy_read_timeout 为超时状态时,都会触发 proxy_next_upstream 的 timeout 条件,当条件成立时,就会执行proxy_next_upstream_tries中配置的重试次数。
再来看下当前的nginx,location中都是默认值,而后端服务的处理时长却超过60s,这里已经达到触发proxy_next_upstream的条件,请求自动进入重试,从而导致后端服务处理了多次请求,写入了重复的记录。
3、 解决当前问题
既然已经确定问题,那调整相关的参数,在location中加入上面的几个参数,并调整下时长,如下示例。
location /xxx {
proxy_connect_timeout 5s;
proxy_read_timeout 100s;
proxy_send_timeout 70s;
proxy_next_upstream error timeout;
proxy_next_upstream_timeout 70;
proxy_next_upstream_tries 1;
proxy_pass http://backend_server;
add_header upstream_addr $upstream_addr;
}
重新reload下nginx的配置,让用户在重新操作一次,虽然界面还是在loading,但观察数据库中的记录,已经没有重复写入了,并且前端响应也变快了。
至些问题解决。
四、总结
虽然解决了问题,但好像高兴不起来,MD,处理要超过60s,这是什么概念,要求开发排查到底是那里慢。如果确实是要这么久,要做成异步来处理,不能这样一直loading,体验不好。
另外,这是通过中间件来解决重复的问题,其实这个说白了,是接口幂等性的问题,应该采用幂等性的处理规则来解决。比如在前端的请求加上一个唯一标识,后台通过对标识的加锁,或者数据库唯一主键、或者文件的MD5码等来解决。