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的作用,减少连接消耗。
- 显式配置AWS SDK连接池参数,确保连接可复用或及时释放:
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自动扩容。
- 使用Node.js的
内容的提问来源于stack exchange,提问作者Terry Windwalker
相关产品推荐
相关产品推荐

