一次 FTP 上传引发 504 Gateway Timeout 的排查复盘

背景

生产环境有一个文件上传功能:用户在后台页面选择一个升级包,提交后服务端会把文件上传到 FTP 服务器,并在数据库中保存文件记录。

某天上传一个很小的文件时,页面返回了:

html 复制代码
504 Gateway Time-out
openresty

文件只有几百字节到 1KB 左右,所以一开始很容易误判为前端上传、网关超时配置或者文件解析问题。

现象

页面请求链路大致如下:

text 复制代码
浏览器
  -> openresty/nginx
  -> Tomcat/Spring MVC
  -> FTP 服务器

后端上传接口逻辑大致是:

java 复制代码
public String upload(MultipartFile file, FileInfo fileInfo, HttpServletRequest request) {
    fileInfo.setFileName(file.getOriginalFilename());
    boolean success = ftpFileService.insert(fileInfo, file.getInputStream());
    return success ? "ok" : "error";
}

服务层中会直接连接 FTP 并上传:

java 复制代码
FTPClient ftpClient = new FTPClient();
ftpClient.connect(host, port);
ftpClient.login(username, password);
ftpClient.changeWorkingDirectory(path);
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
ftpClient.storeFile(fileName, inputStream);

问题是,请求一直不返回,最终由 openresty 返回 504。

初步排查

先看后端日志,曾经出现过一次空指针:

text 复制代码
java.lang.NullPointerException
at XxxController.upload(...)

定位到是 MultipartFile file 为空时直接调用了:

java 复制代码
file.getOriginalFilename()

这确实是一个代码健壮性问题,所以先补了防御:

java 复制代码
if (file == null || file.isEmpty()) {
    log.error("文件上传失败:未收到上传文件,contentType={}", request.getContentType());
    return "error";
}

但这不是 504 的根因。因为实际页面正常提交了文件,文件也很小,真正的 504 发生在后端继续处理 FTP 上传时。

验证 FTP 状态

为了确认 FTP 服务本身是否正常,在应用服务器上执行了几类命令。

测试登录和目录列表:

bash 复制代码
curl -v --connect-timeout 10 --max-time 30 --list-only \
  --user '***:***' \
  'ftp://ftp.example.com/meter/'

测试上传:

bash 复制代码
echo 'ftp upload test' > /tmp/ftp_test.bin

curl -v --connect-timeout 10 --max-time 30 \
  --user '***:***' \
  --upload-file /tmp/ftp_test.bin \
  'ftp://ftp.example.com/meter/ftp_test.bin'

结果显示:

text 复制代码
230 Logged on
250 CWD successful
EPSV
229 Entering Extended Passive Mode
150 Opening data channel
226 Successfully transferred

这说明:

  • FTP 服务器可达
  • 账号密码正确
  • 目标目录存在
  • 目录有上传权限
  • 应用服务器到 FTP 的网络链路可用

但注意日志中的关键点:EPSV

curl 成功上传时使用的是 FTP 被动模式。

根因

问题出在 Java 代码使用 Apache Commons Net FTPClient 上传时,没有开启被动模式。

原代码类似:

java 复制代码
public boolean storeFile(InputStream inputStream, String fileName) throws IOException {
    // ftpClient.enterLocalPassiveMode();
    return ftpClient.storeFile(fileName, inputStream);
}

FTPClient 默认使用主动模式。

FTP 有两个连接:

  • 控制连接:登录、切目录、发送命令
  • 数据连接:传文件、列目录

主动模式下,FTP 服务器会反向连接应用服务器的数据端口。生产环境通常存在防火墙、云安全组、NAT、容器网络或代理层,这种反向连接很容易失败或卡住。

所以当时的真实情况是:

text 复制代码
控制连接正常:connect/login/changeWorkingDirectory 成功
数据连接异常:storeFile 建立数据通道失败或长时间等待
请求线程阻塞:Tomcat 一直没有响应
网关超时:openresty 返回 504

这也解释了为什么文件只有 1KB 仍然超时。问题不是传输慢,而是 FTP 数据通道建立方式不适合当前生产网络环境。

修复

核心修复是在上传前启用 FTP 被动模式:

java 复制代码
ftpClient.enterLocalPassiveMode();
boolean result = ftpClient.storeFile(fileName, inputStream);

同时建议补上超时配置,避免 FTP 异常时拖到网关超时:

java 复制代码
private static final int FTP_CONNECT_TIMEOUT = 10_000;
private static final int FTP_DEFAULT_TIMEOUT = 10_000;
private static final int FTP_DATA_TIMEOUT = 30_000;
private static final int FTP_SOCKET_TIMEOUT = 30_000;

private void setFtpTimeouts(FTPClient client) {
    client.setConnectTimeout(FTP_CONNECT_TIMEOUT);
    client.setDefaultTimeout(FTP_DEFAULT_TIMEOUT);
    client.setDataTimeout(FTP_DATA_TIMEOUT);
}

连接时设置:

java 复制代码
FTPClient ftpClient = new FTPClient();
setFtpTimeouts(ftpClient);
ftpClient.connect(host, port);
ftpClient.setSoTimeout(FTP_SOCKET_TIMEOUT);

上传时设置:

java 复制代码
ftpClient.enterLocalPassiveMode();
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);

boolean result = ftpClient.storeFile(remoteFileName, inputStream);
if (!result) {
    log.error("FTP 上传失败,replyCode={}, replyString={}",
            ftpClient.getReplyCode(), ftpClient.getReplyString());
}

服务层也应该判断 FTP 是否连接成功:

java 复制代码
if (!ftpUtil.connectFtp(ftpConfig)) {
    log.error("FTP 连接失败,host={}, port={}, path={}",
            ftpConfig.getHost(), ftpConfig.getPort(), ftpConfig.getPath());
    return false;
}

为什么 openresty 报 504

504 并不一定代表 openresty 本身有问题。

这里的含义是:

text 复制代码
openresty 已经把请求转发给后端
但后端在网关允许的时间内没有返回响应
所以 openresty 返回 504

后端没有返回响应的原因,是请求线程同步等待 FTP 上传,而 FTP 数据通道卡住。

排查经验

这类问题可以按下面顺序排查:

  1. 看后端日志,确认请求是否进入 Controller。
  2. 看是否有空指针、上传解析失败、文件为空等基础问题。
  3. 确认文件大小,排除 client_max_body_size、Tomcat maxPostSize 等限制。
  4. 在应用服务器上用 curl 测 FTP 登录、目录列表和上传。
  5. 注意 curl -v 日志中的 PASV/EPSV,这说明测试走的是被动模式。
  6. 检查 Java FTP 客户端是否也开启了被动模式。
  7. 给 FTP 连接、数据传输补超时,避免网关先超时。
  8. connectlogincwdstoreFile、数据库入库前后加阶段日志,快速定位卡点。

总结

这次问题的根因不是文件大、不是账号错、也不是 FTP 服务不可用,而是 Java FTP 客户端默认主动模式与生产网络环境不匹配。

最终修复点很小:

java 复制代码
ftpClient.enterLocalPassiveMode();

但定位过程很有价值。看到 504 Gateway Timeout 时,不要只盯着网关配置;如果后端请求中包含外部系统调用,比如 FTP、HTTP、数据库、消息队列,也要重点检查后端线程是否阻塞在外部依赖上。

相关推荐
用户2986985301411 分钟前
Word 转 PDF 的 3 种自动化实现:从桌面操作到后端服务集成
java·人工智能·后端
风雨_8319 分钟前
轻量级 Nginx 日志分析和可视化平台 NginxPulse安装
运维·nginx
若无情我 纸小铭29 分钟前
Nginx学习笔记(二) Nginx--connection&request
笔记·学习·nginx
Zane199429 分钟前
线程池进阶:如何科学设置线程数(结合实际业务场景复盘)
java·后端
SeaTunnel35 分钟前
Apache SeaTunnel 提交一个任务都经过了什么?
java·大数据·服务器·apache·etl·seatunnel
xixingzhe237 分钟前
net::ERR_INCOMPLETE_CHUNKED_ENCODING解决
nginx
mifengxing38 分钟前
Java TreeSet
java·开发语言
编程风暴1 小时前
零基础数据分析项目模板+5条避坑红线
java·数据挖掘·数据分析
IKUN家族1 小时前
常见的依懒
java·服务器·数据库
我命由我123451 小时前
Jetpack Compose - Material Design 断点范围、WindowSizeClass、针对不同屏幕尺寸创建预览、四类导航栏
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime