记录一次解决大文件上传偶尔失败问题

1.问题现象

经常被反馈,服务器后台上传一个大文件往往上传到快结束的时候,前端进度条不走了,但是也不是每次都出现,上传大文件在本次环境是一次都不会复现问题的。只出现在线上服务器,服务器用的是nginx。

2.排查过程

2.1 思路分析

大文件上传肯定用的是分段上传,首先前端拿到文件后分割成5M每个的包然后计算出需要分多少个上传,然后传递包的序号,服务器每次收到一个这个样的包都保存在tmp目录下,并且按照一定的规则命名,命名规则包含了文件名和分包的位次信息,方便在传递完成最后一个分包的时候组合。服务器如果收到的分包如果不是最后一个,就会在收到后告诉客户端返回成功,如果是最后一个会把之前的所有分包都取出来,然后合并为一个完整的文件并放在指定目录后,删除tmp的文件,再告诉客户端合并成功,并把最终文件存储目录告诉客户端。

2.2 线上实验

上传一个2G的文件,发现确实会分包,但是上传到最后一步的时候回出现超时,超时的原因有两种,第一种是服务器超时,另一种是客户端超时。服务器超时要么是php执行的时间过长,如果真是这样的话服务器会返回php超时信息,但是服务器返回的是504.说明是nginx的配置问题,是nginx等待php处理的时间超过了预定值,所以尝试更改nginx配置。

fastcgi_connect_timeout 60;
fastcgi_send_timeout 600;
fastcgi_read_timeout 600;

前端不在提示504了,但是还是没有得到正常信息,控制台请求提示

Provisional headers are shown

显示这个的原因其实曾经出现过,就是前端的请求框架自己设置了一个超时时间,超过后就不在等待。为了验证最后合并文件到指定目录到底多长时间,自己在上传过程中手动改了服务器代码,在2.5G文件上传后,并没有将分片信息删除,然后又撤销了代码改动,所以tmp目录下面会有很多分片文件,每个5M。

image

为了测试合并文件到底需要多少时间我写了个接口,我在接口里面分别测试了把合成文件放在tmp目录下和指定目录下所需要的时间,发现时间差别巨大,如果在tmp目录总体需要不到6s,在指定的目录下需要1分14秒。后来才知道指定的目录是阿里云的NAS盘,读写文件文件速度差异巨大。所以我希望前端看看有没有设置超时时间,有的话改的长一点,后来应该是改了,反正消停了一段时间,而且为了稳定还把nginx服务器改成了apache。但是后来又出问题了,症状没有变。而且似乎还更严重了,一旦失败就把整个后台服务器拖死,服务器卡死的原因是CPU被占满,虽说服务器配置不高,但是一个上传文件导致这样的结果还是不正常的。不得不彻底查查了。

自己不断的测试,发现在上传失败的时候确实网页几乎卡住,没有反应,但是我清cookie就好了。所以在出问题的时候我在服务器上看下消耗CPU最多的进程,

找出消耗cpu最多的进程前10名,apache果然在前面

ps auxw|head -1;ps auxw|sort -rn -k3|head -10
image.png

查看进程的调用栈

pstack 30965
image.png

函数调用栈看不出有什么用。

但是执行

strace -p 30965

其实就会发现线程一直在尝试打开一个分片文件,但是文件不存在。所以死循环了。那就代码找出可能包含死循环的部分。

 // 检查分片是否都已上传成功
 do {
      for ($i = 0; $i < $chunks; $i++) {
           $filePath = $tmpDir . "{$chunkName}_{$i}";
           if (!file_exists($filePath)) {
                  $unChecked = true;
                  break;
           } else {
                  $unChecked = false;
            }
       }
 } while($unChecked);

就是这段代码,外面的do while其实是造成死循环的主要原因,do while的目的就是希望能够等待客户端把文件全部都传递上来,因为如果客户端开多线程上传的话,包的顺序可能有先后不一致的情况。等待是合理的,但是如果传递的文件过大的话,chunks比较大,循环里面对每个文件都调用了file_exist这个函数,这样会造成操作系统不断的尝试打开文件,对资源占用过高,存在需要优化的地方。另一个地方就是do while可能会造成死循环,尤其是一个客户端上传某个分片确实失败的时候就永远死循环,这样就导致了浏览器无反应,但是清cookie后有相当于主动断开了服务器的连接,而恢复正常的现象。其实如果在服务器端kill掉这个进程浏览器也会恢复正常的,否则一旦进入死循环不进浏览器卡死,服务器端cpu也会逐渐消耗殆尽。

所以尝试对本段代码进行了优化,不在死循环,而且仅仅对不存在的文件进行检查。另一个是增加了分片信息缺失后删除本次所有分片的代码,之前没有删除本次全部的分片,会导致tmp目录空间越来越小,越大的文件越容易失败,在tmp目录下留下的垃圾文件越大,越失败越上传,所以恶性循环。

 $notExistFiles = [];
            for ($i = 0; $i < $chunks; $i++) {
                $filePath = $tmpDir . "{$chunkName}_{$i}";
                if (!file_exists($filePath)) {
                    $notExistFiles[] = $filePath;
                }
            }
            $time = 0;
            do{
                $tempFiles = [];
                foreach ($notExistFiles as $notExistFile){
                    if (file_exists($notExistFile)){
                        $tempFiles[] = $notExistFiles;
                    }
                }
                $notExistFiles = array_diff($notExistFiles,$tempFiles);//
                $time++;
            }while(count($notExistFiles) > 0 && $time < 100);

            if (count($notExistFiles) > 0){
                for ($n = 0; $n < $chunks; $n++) {
                    $file = $tmpDir . "{$chunkName}_{$n}";
                    if (file_exists($file)){
                        unlink($file);
                    }
                }
                throw new Exception('上传的文件分片缺失,需要重新上传');
            }

这里的time <1000是自己定的,其实可以使用sleep的,让操作系统主动悬挂当前进程,过段时间上传的分片应该会都全了,之所以这样所,是因为php的进程是php-fpm管理的,不是我们代码手动创建的。客户端每次发起一个连接,都会被转发给其他子进程,其实代码都允许在每个子进程中,至于其他子进程是否处理完文件本进程并不知情,所以执行尝试等待,这样的情况主要是应对客户端开多线程分片上传,为了确保正常,和前端沟通,让他只有在前一个上传成功后才传递第二个分包,单线程上传,进一步减少出错的可能性。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,642评论 18 139
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,915评论 2 89
  • 概述 一说到文件上传,想必大家都并不陌生,更何况是利用AFNetworking(PS:后期统称AF)来做,那更是小...
    CoderMikeHe阅读 13,475评论 48 133
  • 1、第八章 Samba服务器2、第八章 NFS服务器3、第十章 Linux下DNS服务器配站点,域名解析概念命令:...
    哈熝少主阅读 3,721评论 0 10
  • 谁将诅咒撒满星空 那面目狰狞的面孔 犹如 疯狂的野兽 黑暗的魔鬼 张牙舞爪 愤怒咆哮 言语讽刺 喋喋不休 日复一日...
    春雨夜絮阅读 236评论 4 2