Node.js应用启动方式差异致S3流式解压文件损坏问题排查
问题背景
我遇到了一个异常问题:以下是我的Node.js函数,用于流式解压存储在AWS S3存储桶中的ZIP文件,无需将整个ZIP文件下载到本地文件系统,直接将压缩内容提取到本地。
流式解压函数
const streamUnzipFromS3ToLocal = async ( objectBucket: string, objectKey: string, extractDir: string, ) => { try { const data = await S3.getFile(objectBucket, objectKey); // data.Body is a readable stream; try { await pipeline(data.Body, unzipper.Extract({ path: extractDir })); } catch (error) { logger.error(error); } logger.info(`${objectKey} is decompressed to ${extractDir}`); } catch (error) { logger.error(`Failed to stream unzip file ${objectKey}: ${error}`); throw new Error('StreamUnzipFromS3Error'); } };
S3文件获取函数
import { s3Client } from 'utils/AWS/s3Client'; import { GetObjectCommand} from '@aws-sdk/client-s3'; export const getFile = async (bucket_name, object_key) => { let res; const params = { Bucket: bucket_name, Key: object_key, }; try { const data = await s3Client.send(new GetObjectCommand(params)); res = data; } catch (err) { logger.error(`Failed when s3Controller getFile: ${err}`); } return res; };
解压完成后,我通过执行md5sum extracted_files/00001.dcm获取提取文件的哈希值。
三种启动方式及结果
yarn start:提取的文件md5sum哈希值每次都不同(测试3次得到3个不同值),且用.dcm工具解析时提示文件损坏。sudo yarn start:提取的文件哈希值一致,解析正常。sudo systemctl start myapp.service:myapp.service配置如下,该方式提取的文件哈希值一致,解析正常。
[Unit] Description=myapp backend api After=network.target [Service] Type=simple WorkingDirectory=/home/ubuntu/myapp/code/api Restart=always RestartSec=1 User=ubuntu ExecStart=yarn start [Install] WantedBy=multi-user.target
yarn start对应的实际命令为:nodemon --exec ts-node --files src/index.ts
环境信息
ubuntu@ip-123-123-123-123:~/myapp/api$ sudo which node /usr/bin/node ubuntu@ip-123-123-123-123:~/myapp/api$ sudo node -v v10.19.0 ubuntu@ip-123-123-123-123:~/myapp/api$ which node /home/ubuntu/.nvm/versions/node/v21.2.0/bin/node ubuntu@ip-123-123-123-123:~/myapp/api$ node -v v21.2.0 ubuntu@ip-123-123-123-123:~/myapp/api$ yarn -v 1.22.19 ubuntu@ip-123-123-123-123:~/myapp/api$ which yarn /usr/bin/yarn
疑问
- 上述三种启动方式的差异是什么,导致第一种方式失败而另外两种正常?
sudo yarn start与sudo systemctl start myapp.service的差异是什么?后者是否是以sudo权限、ubuntu用户执行yarn start?
解答
问题1:三种启动方式的差异及失败原因
三种方式的核心差异在于运行环境的Node.js版本、权限以及进程运行上下文:
1. yarn start的运行逻辑
- 使用普通用户
ubuntu的shell环境,调用nvm管理的v21.2.0版本Node.js,这是较新的非LTS版本,可能在stream管道处理、文件IO缓冲机制上存在未验证的bug,导致从S3流式读取的数据写入本地时出现截断、乱序或重复,最终文件损坏、哈希值不一致。 - 同时,普通用户可能面临系统级IO资源限制(如文件描述符数量、缓冲区大小),进一步加剧流式写入的不稳定性。
2. sudo yarn start的运行逻辑
- 以root权限运行,调用系统全局安装的
v10.19.0版本Node.js(旧LTS稳定版)。root权限拥有更高的系统资源配额,且该版本的stream模块经过长期生产验证,流式处理逻辑稳定,不会出现数据写入异常。
3. sudo systemctl start myapp.service的运行逻辑
- 虽然用
sudo触发服务启动,但服务配置中指定了User=ubuntu,实际运行用户是ubuntu,但进程处于systemd的受控环境:- systemd使用标准化的系统环境变量,不继承用户shell中的nvm配置,默认调用系统全局的
v10.19.0版本Node.js,避免了高版本的兼容性问题。 - systemd会为进程设置稳定的资源限制和运行上下文,避免用户shell中的异常信号、资源竞争干扰流式写入。
- systemd使用标准化的系统环境变量,不继承用户shell中的nvm配置,默认调用系统全局的
总结:第一种方式失败的核心原因是使用了存在stream兼容问题的高版本Node.js(v21.2.0),而另外两种方式实际使用稳定的旧版本Node.js,同时运行上下文更可靠。
问题2:sudo yarn start与sudo systemctl start myapp.service的差异
1. 运行用户与权限
sudo yarn start:直接以root用户运行,拥有完全系统权限。sudo systemctl start myapp.service:sudo仅用于触发systemd启动服务,服务实际以配置中的User=ubuntu运行,权限为ubuntu用户权限,并非root权限。
2. 运行上下文
sudo yarn start:依赖当前shell的环境,继承shell的所有变量(如nvm配置),关闭当前shell会导致进程终止。sudo systemctl start myapp.service:进程由systemd托管,运行在独立的系统上下文,不依赖用户shell,会在后台持续运行,systemd还负责进程的自动重启、日志管理等。
3. Node.js版本
- 两者均调用系统全局的
v10.19.0版本Node.js,但原因不同:前者是root用户环境默认路径,后者是systemd的标准环境路径不包含nvm目录,无法调用v21.2.0。
内容的提问来源于stack exchange,提问作者Kid_Learning_C
相关产品推荐
相关产品推荐

