如题, 最近在弄容器化部署, 将项目都上线到k8s上, 就在验证功能是否已正常的时候, 发现ftp下载都失败了.
和运维一起定位问题花了一天
起初以为是因为容器所在服务器连不上ftp server, 后面发现并不是这样, 后面自然又看了下是否由于代码原因, 但是在普通机器上跑的服务下载正常, 这就基本能断定是因为docker容器导致的问题.

下面是定位问题的过程, 写的比较啰嗦, 关注解决方法的看最后

定位问题过程

  1. 确定是偶发还是必然
    首先反复在docker容器和普通机器上测试, 确定这不是偶发性的问题, 而是确确实实在docker容器上的就无法下载. 查看堆栈日志, 是因为 retrieveFileStream() 方法返回null
  2. 排除防火墙, ftp server 的ip限制等因素
    额外开启了一个centos容器, 在centos容器上下载了ftp工具, 连上以后测试发现正常. 排除ftp server那边的限制问题
  3. 更详细的日志
    将apache的包路径设置debug日志级别, 在client的下载方法后打印返回码和返回信息
try (
      InputStream is = client.retrieveFileStream(new String(filename.getBytes("UTF-8"), "ISO-8859-1"));
    ) {
      if (is == null) {
        log.error("下载文件【{}】失败,流为空.|{}|{}", absoluteFilePath,client.getReplyCode(),client.getReplyString());
      }

到这里已经是第二天了, 终于有点进展, 看到错误码是 500, 信息是illegal port command. 根据这个信息大概了解是主动模式和被动模式的原因. 生产中大多都是启用被动模式, 我这个自然不例外, 我的代码中也确实加上了client.enterLocalPassiveMode();

  1. 主动模式和被动模式
    分别将ftp server 设置成 只允许主动模式, 和只允许被动模式, 然后分别 在容器中, 在普通机器java代码中, 在k8s代码中测试. 并增加了k8s启动新容器测试
仅主动仅被动
普通机器java代码正常失败, 报500 | illegal port command
容器ftp工具-A启动主动模式, 正常-P启动被动模式正常
k8s中ftp工具-A启动主动模式, 失败, 报500 | illegal port command|ftp: bind: Address already in use-P启动被动模式正常
k8s中java代码失败失败

由上可知, k8s由于自身nat转换和防火墙等原因导致无法使用主动模式连接, 也就是我们只能使用被动模式

这就很奇怪了,只能继续捕捉信息

  1. 抓包dumptcp
    dumptcp命令在ftp server机器上抓包.
    首先我们要知道:
    • 主动模式下, 客户端会开启一个>1024的随机端口, 服务端会从20端口传输数据
    • 被动模式下, 客户端发送PASV命令给服务端, 服务端返回227表示切换被动模式, 并开启一个随机端口给客户端传输数据, 不会动20端口

通过抓包, 发现使用ftp工具开启被动模式, 确实如上面所说, 看到227返回, 使用主动模式, 也能看到21端口.

但是普通机器上java代码运行, 虽然能正常下载, 但是抓到的包中, 既没有20端口打印(表示主动模式), 也没有227返回(被动模式)

并且根据上面测试, 当ftp server只开启被动模式的时候, 普通机器java代码也不能正常下载
虽然还是不太懂为什么没有看到使用20端口, 但是能够确定的是我们以为代码开启了被动模式, 但实际上还是用的主动模式

找到问题, 源码分析

首先看看原本写的代码:

client.setConnectTimeout(ftpConf.getConnectTimeoutMills());
    //设置从数据连接读取时要使用的超时(以毫秒为单位)。此超时将在打开数据连接后立即设置,前提是值为 > 0
    client.setDataTimeout(ftpConf.getDataTimeoutMills());
    client.setDefaultTimeout(ftpConf.getSocketTimeoutMills());
    client.setControlEncoding(ftpConf.getEncoding());

	//开启被动模式
    client.enterLocalPassiveMode();
    //解决中文乱码问题
    client.setAutodetectUTF8(true);

    client.connect(ftpConf.getHost(), ftpConf.getPort());
    client.login(ftpConf.getUsername(), ftpConf.getPassword());

错误就错误在enterLocalPassiveMode这个方法, 不能在connect之前调用

client.connect(ftpConf.getHost(), ftpConf.getPort());
    client.login(ftpConf.getUsername(), ftpConf.getPassword());
    if (!FTPReply.isPositiveCompletion(client.getReplyCode())) {
      log.error(
              "未连接到ftp({}:{}),用户名或密码错误|{}",
              ftpConf.getUsername(),
              ftpConf.getPassword(),
              client.getReplyString()
      );
      client.disconnect();
    } else {
      log.info("ftp({}:{})连接成功", ftpConf.getHost(), ftpConf.getPort());
    }
    //在connect后
    client.enterLocalPassiveMode();

为什么呢? 因为enterLocalPassiveMode方法并没有做很多事情, 只是修改了一个属性, 一个配置

   public void enterLocalPassiveMode()
    {
        __dataConnectionMode = PASSIVE_LOCAL_DATA_CONNECTION_MODE;
        // These will be set when just before a data connection is opened
        // in _openDataConnection_()
        __passiveHost = null;
        __passivePort = -1;
    }

那connect()八成就是把这个属性重置了. 我点进去一看, 果不其然

先根据__dataConnectionMode找下重置的方法:

    private void __initDefaults()
    {
    //定义成主动模式了
        __dataConnectionMode = ACTIVE_LOCAL_DATA_CONNECTION_MODE;
...
}

这个方法又被调用:

    @Override
    protected void _connectAction_(Reader socketIsReader) throws IOException
    {
        super._connectAction_(socketIsReader); // sets up _input_ and _output_
        __initDefaults();

这个方法又被connect调用:

    public void connect(InetAddress host, int port)
    throws SocketException, IOException
    {
			....
        _connectAction_();
    }

所以我们以为我们使用的是被动模式. 其实是主动模式

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐