記一次sftp工具類導致jvm頻繁GC事件

背景

項目中需要使用sftp進行遠程文件的讀取,入庫。原有代碼中存在一個SFTPUtil類,底層使用的是jsch庫調用方法。

經過

  1. 事件前一天正好進行了發版,上線版本中,我修改了原有SFTPUtil中的一個問題:原有代碼讀取完成了之後,沒有關閉連接,導致讀取了文件之後,連接一直保持着。我在此次版本中關閉了連接。
  2. 當天中午,當我正好中午出去吃飯時,產線上突然出現了Cat預警。報錯:jvm eden區頻繁GC,2分鐘內ConcurrentMarkSweepCount達到63次,超過預警值10次
  3. 然後趕緊回來排查。這段時間正好是sftp讀取文件的任務在執行,所以確定是sftp讀取文件有問題。但是以前這段代碼一直運行正常,沒有出現過問題,此次突然出現問題,所以初步覺得可能是自己修改關閉連接的代碼有問題。但是這段代碼我是經過了多次測試的,不應該出現問題的!

結果

因爲產線一直在報警,所以緊急通過刪除遠程文件的方式,終止了文件的讀取。爲了不影響當天的業務進行,在尚不確定問題的情況下,只能先進行版本回退。

原因

後續我們去找業務碰了一下,發現此次讀取的文件比較大,有500M,比以前的都要大!事故前正好此任務被添加上去了,所以每間隔幾分鐘調度執行時,都會出現Cat預警!

那麼爲什麼讀取文件時,會出現出現頻繁GC異常呢?!難道是直接把文件讀取到內存中了?

帶着疑問,我們重新審視了一下這個SFTPUtil類,底層讀取文件使用的是這個方法。

public void get(String src, OutputStream dst) throws SftpException{
    get(src, dst, null, OVERWRITE, 0);
  }

SFTPUtil工具類new了一個空的ByteArrayOutputStream,作爲參數傳入此方法。get拿到了outputStream之後,外層層層包裝成了BufferedReader,然後利用BufferedReader.readLine()逐行讀取。因爲拿到outputStream時,sftp連接已經被關閉了,所以推斷jsch中的get方法應該是將文件全部讀取到outputStream了
順着get方法,進入源碼中真正執行讀取的地方查看

private void _get(String src, OutputStream dst,
                      SftpProgressMonitor monitor, int mode, long skip) throws SftpException {
        //System.err.println("_get: "+src+", "+dst);

        byte[] srcb = Util.str2byte(src, fEncoding);
        try {
            sendOPENR(srcb);

            ChannelSftp.Header header = new ChannelSftp.Header();
            header = header(buf, header);
            int length = header.length;
            int type = header.type;

            fill(buf, length);

            if (type != SSH_FXP_STATUS && type != SSH_FXP_HANDLE) {
                throw new SftpException(SSH_FX_FAILURE, "");
            }

            if (type == SSH_FXP_STATUS) {
                int i = buf.getInt();
                throwStatusError(buf, i);
            }

            byte[] handle = buf.getString();         // filename

            long offset = 0;
            if (mode == RESUME) {
                offset += skip;
            }

            int request_max = 1;
            rq.init();
            long request_offset = offset;

            int request_len = buf.buffer.length - 13;
            if (server_version == 0) {
                request_len = 1024;
            }

            loop:
            while (true) {

                while (rq.count() < request_max) {
                    sendREAD(handle, request_offset, request_len, rq);
                    request_offset += request_len;
                }

                header = header(buf, header);
                length = header.length;
                type = header.type;

                ChannelSftp.RequestQueue.Request rr = null;
                try {
                    rr = rq.get(header.rid);
                } catch (ChannelSftp.RequestQueue.OutOfOrderException e) {
                    request_offset = e.offset;
                    skip(header.length);
                    rq.cancel(header, buf);
                    continue;
                }

                if (type == SSH_FXP_STATUS) {
                    fill(buf, length);
                    int i = buf.getInt();
                    if (i == SSH_FX_EOF) {
                        break loop;
                    }
                    throwStatusError(buf, i);
                }

                if (type != SSH_FXP_DATA) {
                    break loop;
                }

                buf.rewind();
                fill(buf.buffer, 0, 4);
                length -= 4;
                int length_of_data = buf.getInt();   // length of data 

                /**
                 Since sftp protocol version 6, "end-of-file" has been defined,

                 byte   SSH_FXP_DATA
                 uint32 request-id
                 string data
                 bool   end-of-file [optional]

                 but some sftpd server will send such a field in the sftp protocol 3 ;-(
                 */
                int optional_data = length - length_of_data;

                int foo = length_of_data;
                while (foo > 0) {
                    int bar = foo;
                    if (bar > buf.buffer.length) {
                        bar = buf.buffer.length;
                    }
                    int data_len = io_in.read(buf.buffer, 0, bar);
                    if (data_len < 0) {
                        break loop;
                    }

                    dst.write(buf.buffer, 0, data_len);

                    offset += data_len;
                    foo -= data_len;

                    if (monitor != null) {
                        if (!monitor.count(data_len)) {
                            skip(foo);
                            if (optional_data > 0) {
                                skip(optional_data);
                            }
                            break loop;
                        }
                    }

                }
                //System.err.println("length: "+length);  // length should be 0

                if (optional_data > 0) {
                    skip(optional_data);
                }

                if (length_of_data < rr.length) {  //
                    rq.cancel(header, buf);
                    sendREAD(handle, rr.offset + length_of_data, (int) (rr.length - length_of_data), rq);
                    request_offset = rr.offset + rr.length;
                }

                if (request_max < rq.size()) {
                    request_max++;
                }
            }
            dst.flush();

            if (monitor != null) monitor.end();

            rq.cancel(header, buf);

            _sendCLOSE(handle, header);
        } catch (Exception e) {
            if (e instanceof SftpException) throw (SftpException) e;
            if (e instanceof Throwable)
                throw new SftpException(SSH_FX_FAILURE, "", (Throwable) e);
            throw new SftpException(SSH_FX_FAILURE, "");
        }
    }

注意到其中這段代碼,while循環將buffer寫入到了outputStream中了

while (foo > 0) {
    int bar = foo;
    if (bar > buf.buffer.length) {
        bar = buf.buffer.length;
    }
    int data_len = io_in.read(buf.buffer, 0, bar);
    if (data_len < 0) {
        break loop;
    }

    dst.write(buf.buffer, 0, data_len);

    offset += data_len;
    foo -= data_len;

    if (monitor != null) {
        if (!monitor.count(data_len)) {
            skip(foo);
            if (optional_data > 0) {
                skip(optional_data);
            }
            break loop;
        }
    }

}

好吧,原因找到了。確實是直接寫入到outputStream中了。
這就不難解釋爲什麼讀取文件時出現JVM頻繁GC的問題了,文件太大了,全部讀取到內存中,加上配置的eden區也不大,只有700多M,所以就GC了。。。

反思

  • 文件中內容太大,原先肯定沒有考慮到這一點,導致測試不充分,沒有覆蓋到這種場景
  • 對於jsch不熟悉,就直接進行使用,以爲從outputStream讀取出來,進行包裝後逐行讀取就不會一次性全部讀取到內存,太naive了。知其理方能善其器,矇眼狂奔肯定會出事的
  • 對於遠程sftp文件讀取,究竟是直接讀取到內存,還是先拉取到本地,值得好好考慮。直接讀取固然簡單,但是存在隱患。最好還是先拉取到本地,然後按照文件讀取規範進行讀取。jsch中get方法存在直接讀取到本地的重載,是逐行讀取、寫入的,可以直接使用

好了,到這裏就結束了。順便吐槽一句,項目中原有的SFTPUtil問題好多,大爺們別在挖坑了~~~

發佈了132 篇原創文章 · 獲贊 88 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章