You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Express服务器连接S3时出现504 GATEWAY_TIMEOUT问题求助

问题

我在EC2上运行Node.js Express服务器,通过以下函数从S3存储桶获取文件。服务器刚启动时API运行正常,但运行数小时后加载时间变得极长,随后抛出504 GATEWAY_TIMEOUT错误,重启服务器后恢复正常。由于问题仅在运行一段时间后出现,且数小时后我的网络会断开与AWS的连接,无法复现问题查看日志,但每次运行数小时都会触发。此前仅用getObject从未出现问题,现在为了流式处理需求改用headObject,怀疑这是诱因。

原实现代码

S3操作函数

export const headObject = async (key) => {
    try {
        const params = {
            Bucket: BUCKET_NAME,
            Key: key,
        };
        const object = await s3.headObject(params);
        // console.log('object: ', object);
        return object;
    } catch (err) {
        throw new Error(`${err}`);
    }
};

export const getObject = async (key, range) => {
    try {
        console.log('range: ', range);
        if (!range) {
            range = null;
        }
        const params = {
            Bucket: BUCKET_NAME,
            Key: key,
            Range: range
        };
        const object = await s3.getObject(params);
        // console.log('object: ', object);
        return object;
    } catch (err) {
        throw new Error(`${err}`);
    }
};

API路由(流式处理版本)

router.get('/files/:key', async (request, response) => {
    console.log('api: GET /general/files/:key');
    const key = request.params.key;
    console.log('key: ', key);
    return headObject(key)
    .then(async (data) => {
        // console.log('request.headers: ', request.headers);
        var range = request.headers.range;
        const { ContentType: type, ContentLength: length} = data;
        if (data.ETag) response.setHeader("ETag", data.ETag);
        getObject(key, range)
        .then(object => {
            // console.log('object: ', object);
            if (typeof object !== 'number') {
                // response.setHeader('filename', key);
                if (range) {
                    // console.log("Serving range", range);
                    const parts = range.replace("bytes=", "").split("-");
                    const start = parseInt(parts[0], 10);
                    const end = parts[1]? parseInt(parts[1], 10): length -1;
                    let headers  = {
                        "Content-Range": `bytes ${start}-${end}/${length}`,
                        "Accept-Ranges": "bytes",
                        "Content-Type": type,
                        "Content-Length": end - start + 1,
                        "Last-Modified": data.LastModified,
                     };
        
                     if (request.headers["if-range"]) {
                         console.log("Setting if-range to", request.headers["if-range"]);
                         headers["If-Range"] = request.headers["if-range"];
                     }
        
                    response.writeHead(206, headers);
                }
                else {
                    console.log("Whole file");
                    response.setHeader("Accept-Ranges", "bytes");
                    response.setHeader("Content-Type", type);
                    response.setHeader("Content-Length", length);
                    response.setHeader("Last-Modified", data.LastModified);
                }
                object.Body.pipe(response);
            }
            else {
                response.sendStatus(object);
            }

        })
        .catch((err) => {
            console.error('getObject failed, error: ', err);
            console.log('key: ', key);
            response.sendStatus(404);
        });
    })
    .catch((err) => {
        console.error('listObjectsByRoute failed, error: ', err);
        console.log('key: ', key);
        generalApiResponseSender(response, 404);
    });
})

更新后的代码(取消流式处理)

router.get('/files/:key', async (request, response) => {
    console.log('api: GET /general/files/:key');
    const key = request.params.key;
    console.log('key: ', key);
    const object = await getObject(key).catch((err) => {
        console.error('getObject failed, error: ', err);
        console.log('key: ', key);
        return 404;
    });
    if (typeof object !== 'number') {
        response.setHeader('filename', key);
        object.Body.pipe(response);
    }
    else {
        response.sendStatus(object);
    }
})
可能的原因及解决方案

1. HTTP连接池耗尽(最可能诱因)

AWS SDK默认维护HTTP连接池,使用headObject+getObject组合时,每个请求会发起两次S3调用。若请求量较大,未正确释放的连接会逐渐占满连接池,导致后续请求无法获取连接,引发超时。

  • 解决方法:
    • 显式配置AWS SDK连接池参数,确保连接可复用或及时释放:
      const s3 = new AWS.S3({
        httpOptions: {
          maxSockets: 50, // 根据并发量调整
          keepAlive: true
        }
      });
      
    • 去掉多余的headObject调用,getObject返回的响应已包含ContentType、ContentLength等元数据,可直接替代headObject的作用,减少连接消耗。

2. Promise处理不规范导致资源泄漏

原路由代码混合使用async/await和.then(),存在嵌套Promise未正确处理的情况,可能导致事件循环阻塞或内存堆积未完成的Promise,长时间运行后引发性能下降。

  • 解决方法:
    • 统一使用async/await写法,避免嵌套.then(),确保所有异步操作被正确等待和处理:
      router.get('/files/:key', async (request, response) => {
        console.log('api: GET /general/files/:key');
        const key = request.params.key;
        console.log('key: ', key);
        try {
          const range = request.headers.range;
          const object = await getObject(key, range);
          const { ContentType: type, ContentLength: length, ETag, LastModified } = object;
          
          if (ETag) response.setHeader("ETag", ETag);
          
          if (range) {
            const parts = range.replace("bytes=", "").split("-");
            const start = parseInt(parts[0], 10);
            const end = parts[1] ? parseInt(parts[1], 10) : length - 1;
            const headers = {
              "Content-Range": `bytes ${start}-${end}/${length}`,
              "Accept-Ranges": "bytes",
              "Content-Type": type,
              "Content-Length": end - start + 1,
              "Last-Modified": LastModified,
            };
            
            if (request.headers["if-range"]) {
              console.log("Setting if-range to", request.headers["if-range"]);
              headers["If-Range"] = request.headers["if-range"];
            }
            
            response.writeHead(206, headers);
          } else {
            console.log("Whole file");
            response.setHeader("Accept-Ranges", "bytes");
            response.setHeader("Content-Type", type);
            response.setHeader("Content-Length", length);
            response.setHeader("Last-Modified", LastModified);
          }
          
          object.Body.pipe(response);
        } catch (err) {
          console.error('getObject failed, error: ', err);
          console.log('key: ', key);
          response.sendStatus(404);
        }
      });
      

3. 流式响应未处理错误

原代码中object.Body.pipe(response)若出现网络中断等错误,未添加错误监听,会导致请求挂起、占用连接资源,长时间积累后引发超时。

  • 解决方法:
    • 为流添加错误监听,及时结束响应:
      object.Body.pipe(response).on('error', (err) => {
        console.error('Stream error:', err);
        if (!response.headersSent) {
          response.sendStatus(500);
        } else {
          response.end();
        }
      });
      

4. EC2资源耗尽

长时间运行后,内存泄漏或CPU占用过高也会导致请求超时。可通过AWS CloudWatch监控EC2的CPU、内存指标,确认是否存在资源瓶颈。

  • 解决方法:
    • 使用Node.js的--expose-gc参数配合内存监控工具,排查内存泄漏点;
    • 根据业务负载调整EC2实例规格,或启用Auto Scaling自动扩容。

内容的提问来源于stack exchange,提问作者Terry Windwalker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 22:44:56