前端事务性任务多次请求的问题

项目上线的时候,遇到一个诡异的问题:预上线环境不能复现,生产环境必现。而预上线环境和生产环境最大的区别就在于:预上线是单点应用服务,生产环境则是集群部署。

问题描述

项目概况:后台采用Spring Cloud生态搭建的微服务体系,前端为Vue框架,做了前后端分离。
日常情况下,都是单节点部署,不过也使用Eureka实现了服务注册和发现的机制,可以通过serviceId访问相关服务。前端请求都通过网关进行鉴权和转发,网关使用Zuul进行路径映射,同时由Ribbon进行负载均衡控制。
上线比较匆忙,调试为集群方式后就部署了。
前端有一个需求,是上传一些数据到后台服务,然后由后台服务生成Excel文件后供前端下载。之所以要上传到后端,是因为前台数据不足以或者不方便做这一部分复杂的Excel操作,同时要利用一下后台的相关数据和模板。
单节点情况下可以正常下载,集群部署后就开始出现404错误,页面刷白的情况。

前端代码

request().excel(data, command, function(success) {
  vueObj.progressBarPercent = 100;
  vueObj.progressBarVisible = false;
  window.clearInterval(id);
  request().download(process.env.API_ROOT + "project/downloadexcel/" + success);
  vueObj.$message({
    message: '导出成功!',
    type: 'success',
    iconClass:'el-icon-circle-check1'
  });
});

可以看出,前端采用了两次请求的方式处理这个需求:
第一个请求为request().excel,该请求让后台生成Excel文件;
第二个请求为request().download,这个请求从后台下载Excel文件。
这个Excel文件是个一次性文件,无需保存留档。

后端代码

StringBuffer tempPath = new StringBuffer();
        tempPath.append(System.getProperty(Consts.JavaTempDir)).append(UUID.randomUUID().toString().replace("-", "")).append("/").append(catalog).append(suffix);
File file = new File(tempPath.toString());
Files.createParentDirs(file);
// 设置输出流
FileOutputStream fOut = new FileOutputStream(file);
// 将模板的内容写到输出文件上
templatewb.write(fOut);
fOut.flush();
// 操作结束,关闭文件
fOut.close();

暂且不讨论上面未考虑windows与linux的系统兼容(路径斜杠),可以看到处理逻辑是在tmp目录下生成两个一个临时目录下的文件。在download之后会清理掉这个临时文件。
表面上看起来应该没什么问题:文件生成,路径下发给前端,前端请求,获得文件后写入输出流。
然后,集群环境下稳定复现的问题还是让我们不得不深究一下存在的漏洞。

原因分析

后端在生成excel的时候,生成在了当前系统的临时文件夹下。
在单节点的情况下,当然没有问题,因为生成文件的请求和下载文件的请求肯定都会发送到该服务节点。
然后,在集群环境下,网关做了负载均衡,就意味着生成文件的请求和下载文件的请求可能被分发到不同的服务节点上。而这两个节点如果不在同一台虚拟机上的话,下载请求必定找不到对应的文件。
我们在网关的地方通过Ribbon执行了负载均衡策略,默认使用轮询策略,而且没有类似Nginx那样的IP映射策略。所以这个Bug稳定复现也就不足为怪了。

由于之前的项目都比较小,单节点基本能够满足需求,所以大家在写代码的时候就忘了考虑集群的情况。甚至以客户端软件的思维来写代码,对于文件操作时特别容易出现问题。

解决方案

解决方案其实有两个办法:
改造前端:在同一个请求中完成所有的事情,不要把文件生成和文件下载分开。
改造后端:不能利用本地文件系统,要利用文件服务器处理。
和负责前端的同事讨论后,说是前端改造可能有问题,会下载不到。因为目前前端的下载还是通过一个form表单的方式来处理的。
那么我们考虑之后选择了文件服务的方式来处理,正好我们也有一个文件服务。让文件生成到文件服务器上(我们的文件服务是采用Mongo搭建的,可以处理小文件),让文件下载也去请求文件服务器。虽然文件服务也是集群部署的,但是MongoDB数据库目前是单节点部署的(仅做了实时备份),所以就不会再出现这个问题了。

经验教训

涉及共享型的操作,一定要摒弃单机思维。

猜你喜欢

转载自blog.csdn.net/achang07/article/details/80548547
今日推荐