返回

解决 EC2 上传 S3 大文件 60 秒超时重置难题

Linux

解决 AWS EC2 上传大文件到 S3 时遭遇的 Connection Reset 难题

上传大文件到 S3?别让 60 秒 Connection Reset 拦住你!

兄弟,你是不是也碰到了这个头疼的问题:用 Node.js(跑在 EC2 上)+ multer-s3 往 AWS S3 传文件,小文件顺畅无比,进度条赏心悦目;可一旦文件大了点(比如几十 MB 到 1GB),进度条刚跑个大概 60 秒就卡住不动,再等个半分钟,浏览器控制台无情地甩给你一个 net::ERR_CONNECTION_RESET

你可能已经像提问的朋友一样,翻遍了 Stack Overflow,试了各种 multerlimits 配置、AWS SDK 的 httpOptions.timeout、甚至改了 EC2 的 tcp_fin_timeout,结果还是原地踏步。更让人迷惑的是,同样的代码,在本地(Localhost)跑,哪怕限速跑个七八分钟也能成功;换到 Google Cloud 的 VM 上也一路绿灯。唯独在 AWS EC2 或者 Lightsail 上,就像被施了魔法,稳定地在 60 秒左右断开连接。

这情况,确实很容易让人怀疑人生,甚至得出“AWS 对 VM 的 TCP 连接有时长限制”这样的结论。但别急,这口锅可能不该 AWS 的基础网络或者 EC2/Lightsail 本身背。问题很可能出在请求到达你的 Node.js 应用之前的某个中间环节。

刨根问底:为啥连接会被重置?

根据里的排查过程(本地和 Google Cloud 成功,AWS EC2/Lightsail 失败),并且失败时间点惊人地稳定在 60 秒左右,我们可以把怀疑的重点放在 AWS 环境中,位于客户端和你的 EC2 实例之间的网络组件上。

你想啊,net::ERR_CONNECTION_RESET 这个错,意思是 TCP 连接被对方(或中间某个环节)强制关闭了。在文件上传这种长时间持续传输数据的场景下,最常见的原因就是 空闲超时 (Idle Timeout)

很多网络设备或软件,比如负载均衡器、反向代理,为了防止连接长时间占用资源但不活动,都会设置一个空闲超时时间。如果在设定时间内,连接两端没有任何数据传输,这个连接就会被“咔嚓”掉。

为什么大文件上传会触发这个?上传过程中,虽然整体看数据在流动,但网络传输并不是绝对平滑的,可能因为网络波动、服务器处理能力等原因,出现短暂的数据传输间隙。如果这个间隙超过了中间环节设定的空闲超时阈值(常见的默认值就是 60 秒!),那连接就会被断开。

尽管你调整了 Node.js 应用层面(如 aws.config.httpOptions.timeout)或操作系统层面(如 tcp_fin_timeout)的超时设置,但如果请求是经过了例如 AWS 的 Elastic Load Balancer (ELB/ALB) 或者你自己配置的 Nginx/Apache 反向代理,那么这些中间件的超时设置会先生效。它们在 60 秒时掐断了连接,你的后端应用和操作系统的设置再长也没用武之地。

对症下药:解决方案来了!

既然推测是中间环节的超时捣鬼,那咱们就顺藤摸瓜,逐一排查并调整。

方案一:检查并调整负载均衡器 (ELB/ALB) 空闲超时

这是最可能的原因,特别是当你使用了 AWS 的负载均衡服务将流量分发到 EC2 实例时。

  • 原理: AWS Application Load Balancer (ALB) 和 Classic Load Balancer (CLB) 都有一个“空闲超时” (Idle Timeout) 设置,默认值就是 60 秒。这个设置决定了连接在没有数据传输的情况下可以保持多久。大文件上传时间长,网络稍有停顿就容易超过这个默认值。
  • 操作步骤:
    1. 登录 AWS 管理控制台。
    2. 导航到 EC2 服务页面。
    3. 在左侧导航栏找到“负载均衡”下的“负载均衡器”。
    4. 选择你用于转发请求到目标 EC2 实例的那个负载均衡器。
    5. 对于 Application Load Balancer (ALB):
      • 在下方的“属性”(Attributes) 标签页(或对于旧版控制台,可能是“”Description 标签页),找到“空闲超时”(Idle timeout) 设置。
      • 点击“编辑”(Edit),将其值从默认的 60 秒调高到一个足够覆盖你最大文件上传所需时间的值,比如 600 秒(10 分钟)或更高。注意 :别设置得太离谱,几千秒通常足够了。
    6. 对于 Classic Load Balancer (CLB):
      • 在下方的“属性”(Attributes) 标签页,找到“空闲连接超时”(Idle Connection Timeout) 设置。
      • 点击“编辑”(Edit),调高该值。
    7. 保存更改。这个更改通常会立即生效,无需重启实例。
  • 安全建议: 虽然调高超时能解决问题,但过长的超时也意味着潜在的空闲连接会占用负载均衡器的资源更久。根据你的业务场景,设置一个合理的最大值,比如 1800 秒(30分钟)通常足够覆盖大部分场景,避免无限等待。
  • 进阶使用: 确保你的客户端(比如浏览器/Axios)和服务器端(Node.js http/https server)都配置了适当的 Keep-Alive 设置,这有助于维持连接活跃,但通常 ELB 的 Idle Timeout 是更直接的瓶颈。

方案二:核查 Nginx/Apache 反向代理超时 (如果使用)

如果你的架构中,EC2 实例前面还架设了 Nginx 或 Apache 作为反向代理,那么它们的超时配置也可能是“凶手”。

  • 原理: Nginx 和 Apache 作为反向代理时,也有自己控制与后端服务器(你的 Node.js 应用)连接以及与客户端连接的超时参数。
  • 操作步骤:
    1. SSH 登录到你的 EC2 实例。
    2. 找到你的 Nginx 或 Apache 配置文件。
      • Nginx: 通常在 /etc/nginx/nginx.conf/etc/nginx/conf.d/default.conf/etc/nginx/sites-available/your-site.
      • Apache: 通常在 /etc/httpd/conf/httpd.conf, /etc/apache2/apache2.conf, 或相关的虚拟主机配置文件中 (sites-available)。
    3. 编辑配置文件,查找并调整以下(或类似的)指令:
      • Nginx:
        • proxy_connect_timeout: Nginx 与后端服务器建立连接的超时时间。
        • proxy_send_timeout: Nginx 向后端服务器发送请求的超时时间。
        • proxy_read_timeout: Nginx 等待后端服务器响应数据的超时时间。 这个是最可能需要调高的,因为它关系到后端处理和返回数据(包括上传确认)的时间。 默认通常是 60 秒。
        • client_body_timeout: 读取客户端请求体的超时。
        • keepalive_timeout: 与客户端的 Keep-Alive 连接超时。
          将这些值(尤其是 proxy_read_timeout)调整到一个合适的大小,例如 300s (5分钟) 或更高。
      • Apache:
        • Timeout: Apache 等待 I/O 操作完成的通用超时时间,默认可能是 300 秒,但有时会被改小。
        • ProxyTimeout: 如果使用 mod_proxy,这个指令专门设置代理请求的超时,需要调高。
        • KeepAliveTimeout: Keep-Alive 连接的超时。
          调整这些值到足够大的秒数。
    4. 保存配置文件。
    5. 非常重要: 重载或重启你的 Nginx/Apache 服务使配置生效。
      • sudo systemctl reload nginxsudo service nginx reload
      • sudo systemctl reload apache2sudo service apache2 reload (或 httpd)
  • 代码示例 (Nginx):
    # 在你的 http, server, 或 location 块中
    proxy_connect_timeout 300s;
    proxy_send_timeout 600s;    # 允许发送较慢
    proxy_read_timeout 600s;     # 关键:允许后端处理/S3上传时间更长
    client_body_timeout 600s;    # 允许客户端发送请求体时间更长
    # 如果文件特别大,还需要检查 client_max_body_size
    client_max_body_size 1024M; # 允许上传最大 1GB 文件
    
  • 安全建议: 和 ELB 一样,避免设置不切实际的超长超时。同时,如果调整了 client_max_body_size (Nginx) 或 LimitRequestBody (Apache),确保它大于等于你 multer 中设置的 fileSize 限制。
  • 进阶使用: 考虑为特定的上传路径 (location) 设置不同的超时值,而不是全局修改,这样更精细化。

方案三:确认 Node.js 服务器本身的超时设置 (再次检查)

虽然之前的测试表明问题不在 Node.js 本身,但为了完整性,也快速过一遍 Node.js HTTP 服务器的超时设置。

  • 原理: Node.js 内建的 httphttps 模块创建的服务器对象也有超时属性,可以控制连接保持时间和请求处理时间。

  • 操作步骤 & 代码示例:
    在创建 HTTP/HTTPS 服务器时进行设置:

    const http = require('http');
    const app = require('./your_express_app'); // 你的 Express 应用
    
    const server = http.createServer(app);
    
    // 设置服务器的总超时时间 (包括请求头和体)
    // 默认是 2 分钟 (120000 ms). 设置为 0 表示禁用。
    server.timeout = 0; // 或者设置一个很长的时间,比如 10 * 60 * 1000 (10分钟)
    
    // 设置 Keep-Alive 连接的超时时间
    // 默认是 5 秒 (5000 ms). 连接在这个时间内无活动则关闭。
    // 对于长时间上传,可能需要延长或与 ELB/Proxy 协调。
    server.keepAliveTimeout = 65 * 1000; // 比如设置成比 ELB 空闲超时略长一点点
    server.headersTimeout = 70 * 1000; // 处理请求头的时间,Node v11.3.0+
    
    const PORT = process.env.PORT || 3000;
    server.listen(PORT, () => {
      console.log(`Server listening on port ${PORT}`);
    });
    

    注意: server.timeout 在较新版本的 Node.js 中,行为有所调整,主要影响非活动连接。对于活动连接中的长时间处理(如上传),它可能不是直接瓶颈,但设置足够长或禁用(设为0)可以排除这个因素。keepAliveTimeout 与中间件(ELB/Proxy)的超时配合更重要。

  • 安全建议: 禁用超时 (server.timeout = 0) 需要谨慎,可能导致恶意连接或缓慢的连接长时间占用服务器资源。设置一个合理的上限通常更好。

  • 进阶使用: 理解 timeout, keepAliveTimeout, headersTimeout 各自的作用域和行为差异。

方案四:客户端 Axios 超时配置 (防御性检查)

虽然错误是 CONNECTION_RESET(通常是服务器端或中间件关闭连接),而不是客户端超时,但检查客户端 Axios 的配置也是好习惯。

  • 原理: Axios 也可以设置请求超时时间。如果设置得太短,客户端自己就会先放弃等待。
  • 操作步骤 & 代码示例:
    在发起 Axios 请求时,添加 timeout 选项(单位是毫秒):
    import axios from 'axios';
    
    const uploadFile = (file, onUploadProgress) => {
      const formData = new FormData();
      formData.append('file', file);
    
      // ... 其他 headers (如 path, pid, tofrom)
    
      return axios.post('/uploadResources/', formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
          // ... 其他 headers
        },
        onUploadProgress,
        // 设置一个足够长的超时时间,比如 30 分钟
        timeout: 30 * 60 * 1000, // 1800000 ms
      });
    };
    
  • 安全建议: 不要在客户端设置无限超时 (timeout: 0),这可能导致用户界面卡死。给用户一个合理的反馈或取消机制。
  • 进阶使用: 使用 Axios 的 CancelToken 允许用户主动取消上传,而不是仅仅依赖超时。

方案五:采用 AWS S3 预签名 URL + 前端直传 (架构调整)

如果你面临的问题确实难以通过调整超时解决,或者想优化大文件上传的性能和服务器负载,强烈推荐考虑这种方式。

  • 原理: 后端 Node.js 应用不再接收整个文件数据,而是扮演一个“授权者”的角色。前端先向后端请求一个有时效性的、包含特定权限的 S3 URL(预签名 URL, Presigned URL)。拿到这个 URL 后,前端可以直接使用这个 URL 将文件数据通过 HTTP PUT 请求发送到 S3,完全绕过你的 EC2 服务器。这极大减轻了服务器负担,也避开了服务器及其中间件的网络瓶颈和超时限制。
  • 操作步骤:
    1. 后端 (Node.js): 添加一个 API 端点,用于生成 S3 预签名 URL。需要使用 AWS SDK 的 getSignedUrlPromise (或 getSignedUrl) 方法。
    2. 前端 (Axios/Fetch):
      • 先调用后端的 API 获取预签名 URL。
      • 使用获取到的 URL,通过 PUT 方法直接上传文件内容。注意,请求头通常需要设置 Content-Type 为文件的 MIME 类型。
    3. S3 Bucket 配置: 需要配置 CORS (Cross-Origin Resource Sharing) 规则,允许来自你的前端域名的 PUT 请求,并允许相关的 Header (如 Content-Type)。
  • 代码示例 (后端 Node.js 生成 URL):
    const aws = require('aws-sdk');
    
    aws.config.update({ region: 'your-region' }); // 配置 AWS 区域
    const s3 = new aws.S3({ apiVersion: '2006-03-01' });
    
    app.get('/generate-presigned-url', async (req, res) => {
      const { fileName, fileType } = req.query;
      const bucketName = 'your-bucket-name';
      const objectKey = `uploads/${Date.now()}_${fileName}`; // 定义 S3 中的存储路径
    
      const params = {
        Bucket: bucketName,
        Key: objectKey,
        ContentType: fileType,
        Expires: 3600, // URL 有效期,单位秒,比如 1 小时
        // ACL: 'public-read', // 如果你需要公共读取权限
      };
    
      try {
        const signedUrl = await s3.getSignedUrlPromise('putObject', params);
        res.json({ signedUrl, key: objectKey }); // 返回 URL 和 Key 给前端
      } catch (err) {
        console.error("Error generating presigned URL", err);
        res.status(500).send("Failed to generate upload URL.");
      }
    });
    
  • 代码示例 (前端 Axios 上传):
    async function uploadDirectlyToS3(file) {
      try {
        // 1. 从你的后端获取预签名 URL
        const response = await axios.get('/generate-presigned-url', {
          params: { fileName: file.name, fileType: file.type }
        });
        const { signedUrl, key } = response.data;
    
        // 2. 使用预签名 URL 直接 PUT 文件到 S3
        await axios.put(signedUrl, file, {
          headers: {
            'Content-Type': file.type // 必须匹配生成 URL 时的 ContentType
          },
          onUploadProgress: progressEvent => {
            const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
            console.log(`Upload Progress: ${percentCompleted}%`);
            // 更新你的进度条
          }
        });
    
        console.log('File uploaded successfully! S3 Key:', key);
        // (可选) 文件上传成功后,通知你的后端进行记录或后续处理
        // await axios.post('/notify-upload-complete', { key });
    
      } catch (error) {
        console.error('Upload failed:', error);
      }
    }
    
  • 安全建议:
    • 预签名 URL 的 Expires 时间设置要尽可能短,够用就行。
    • 务必在 S3 Bucket 上配置好 CORS 规则,只允许必要的来源域名和 HTTP 方法。
    • 后端生成 URL 时,应严格校验请求参数(如文件类型、大小限制等),防止滥用。
    • 考虑在生成 URL 前进行用户身份验证和授权。
  • 进阶使用: 对于非常大的文件(超过 5GB),S3 强制要求使用分片上传 (Multipart Upload)。你可以使用预签名 URL 来为每个分片生成上传签名,前端库(如 aws-sdk 的浏览器版或第三方库 Uppy)可以帮助管理分片上传的复杂性。

深入理解:multer-s3 与分片上传

关于最初提到的“multer-s3 是否应该处理分片上传”,你的理解是对的。multer-s3 内部使用了 AWS SDK for JavaScript 的 ManagedUpload 功能。这个功能确实会自动根据文件大小,在需要时(通常是大于 5MB 时,具体阈值可配置)切换到分片上传模式,并在后台处理分片的上传、并发控制和完成。

所以,理论上,multer-s3 在帮你处理大文件的分片上传的。但即使它在做分片上传,每一片数据的传输仍然是一个网络连接的过程,如果中间环节(如 ELB、Nginx)的空闲超时设置过短,依然可能在某个分片传输的间隙、或者单个分片传输时间过长时,导致连接被重置。

因此,问题根源很可能不是 multer-s3 本身,而是它赖以运行的网络路径上的某个超时限制卡住了数据流。

排查这些超时设置,特别是 ELB/ALB 的 Idle Timeout,有很大机会能帮你搞定这个恼人的 Connection Reset 问题。如果依然不行,或者你希望寻求更优的架构,预签名 URL 直传 S3 是个非常值得考虑的方案。