背景
項目中需要使用sftp進行遠程文件的讀取,入庫。原有代碼中存在一個SFTPUtil類,底層使用的是jsch庫調用方法。
經過
- 事件前一天正好進行了發版,上線版本中,我修改了原有SFTPUtil中的一個問題:原有代碼讀取完成了之後,沒有關閉連接,導致讀取了文件之後,連接一直保持着。我在此次版本中關閉了連接。
- 當天中午,當我正好中午出去吃飯時,產線上突然出現了Cat預警。報錯:jvm eden區頻繁GC,2分鐘內ConcurrentMarkSweepCount達到63次,超過預警值10次。
- 然後趕緊回來排查。這段時間正好是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問題好多,大爺們別在挖坑了~~~