線上服務(wù)器負(fù)載過高發(fā)生了報(bào)警,同事找我求救。
我看到機(jī)器的負(fù)載都超過20了,查看java進(jìn)程線程棧,找到了出問題的代碼。
下面是其代碼片段,實(shí)際情況錯(cuò)誤處理比這更壞。
思考下,這段代碼有什么致命問題么?(這里不追究業(yè)務(wù)邏輯處理的正確性以及細(xì)小的瑕疵)
.
..
...
現(xiàn)在回來。
我發(fā)現(xiàn)線程棧里面的線程都RUNNABLE在32行。
這一行看起來有什么問題呢?StringBuilder.toString()不是轉(zhuǎn)換成String么?Apache commons-lang里面的StringUtils.isNotEmpty使用也沒問題啊?
看代碼,人家的邏輯其實(shí)是判斷是否是第一行,如果不是第一行那么就增加一個(gè)換行符。
既然CPU在這里運(yùn)行,那么就說明這個(gè)地方一定存在非常耗費(fèi)CPU的操作,導(dǎo)致CPU非常繁忙,從而系統(tǒng)負(fù)載過高。
看詳細(xì)堆棧,其實(shí)CPU在進(jìn)行內(nèi)存的拷貝動(dòng)作。
看下面的源碼。
java.lang.StringBuilder.toString()
看出來了么?
問題的關(guān)鍵在于String構(gòu)造函數(shù)的最后一行,value并不是直接指向的,而是重新生成了一個(gè)新的字符串,使用系統(tǒng)拷貝函數(shù)進(jìn)行內(nèi)存復(fù)制。
好了,再回頭看邏輯代碼32行。
我們來做一個(gè)簡單的測試,我們?cè)谠瓉淼拇a上增加幾行計(jì)數(shù)代碼。
這是一次輸出的結(jié)果。
試想一下,CPU一直頻繁于進(jìn)行內(nèi)存分配,機(jī)器的負(fù)載能不高么?我們線上服務(wù)器是2個(gè)CPU 16核,內(nèi)存24G的Redhat Enterprise Linux 5.5,負(fù)載居然達(dá)到幾十。這還是只有訪問量很低的時(shí)候。這就難怪服務(wù)頻繁宕機(jī)了。
事實(shí)上我們有非常完善和豐富的基于Apache commons-httpclient的封裝,操作起來也非常簡單。對(duì)于這種簡單的請(qǐng)求,只需要一條命令就解決了。
即使非要自造輪子,處理這種簡單的輸入流可以使用下面的代碼,就可以很好的解決問題。
當(dāng)然了,最后緊急處理線上問題最快的方式就是將有問題的代碼稍微變通下即可。
這個(gè)問題非常簡單,只是想表達(dá)幾個(gè)觀點(diǎn):
©2009-2014 IMXYLZ
求賢若渴
|
我看到機(jī)器的負(fù)載都超過20了,查看java進(jìn)程線程棧,找到了出問題的代碼。
下面是其代碼片段,實(shí)際情況錯(cuò)誤處理比這更壞。
1 package demo;
2
3 import java.io.BufferedReader;
4 import java.io.InputStream;
5 import java.io.InputStreamReader;
6 import java.net.HttpURLConnection;
7 import java.net.URL;
8 import java.net.URLConnection;
9 import org.apache.commons.lang.StringUtils;
10
11 /**
12 * @author adyliu (imxylz#gmail.com)
13 * @since 2012-3-15
14 */
15 public class FaultDemo {
16
17 /**
18 * @param args
19 */
20 public static void main(String[] args) throws Exception {
21 final String tudou = "http://v.youku.com/v_playlist/f17170661o1p9.html";
22
23 URL url = new URL(tudou);
24 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
25 conn.connect();
26 try {
27 InputStream in = conn.getInputStream();
28 BufferedReader br = new BufferedReader(new InputStreamReader(in, "utf-8"));
29 StringBuilder buf = new StringBuilder();
30 String line = null;
31 while ((line = br.readLine()) != null) {
32 if (StringUtils.isNotEmpty(buf.toString())) {
33 buf.append("\r\n");
34 }
35 buf.append(line);
36 }
37 //do something with 'buf'
38
39 } finally {
40 conn.disconnect();
41 }
42
43 }
44
45 }
46
2
3 import java.io.BufferedReader;
4 import java.io.InputStream;
5 import java.io.InputStreamReader;
6 import java.net.HttpURLConnection;
7 import java.net.URL;
8 import java.net.URLConnection;
9 import org.apache.commons.lang.StringUtils;
10
11 /**
12 * @author adyliu (imxylz#gmail.com)
13 * @since 2012-3-15
14 */
15 public class FaultDemo {
16
17 /**
18 * @param args
19 */
20 public static void main(String[] args) throws Exception {
21 final String tudou = "http://v.youku.com/v_playlist/f17170661o1p9.html";
22
23 URL url = new URL(tudou);
24 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
25 conn.connect();
26 try {
27 InputStream in = conn.getInputStream();
28 BufferedReader br = new BufferedReader(new InputStreamReader(in, "utf-8"));
29 StringBuilder buf = new StringBuilder();
30 String line = null;
31 while ((line = br.readLine()) != null) {
32 if (StringUtils.isNotEmpty(buf.toString())) {
33 buf.append("\r\n");
34 }
35 buf.append(line);
36 }
37 //do something with 'buf'
38
39 } finally {
40 conn.disconnect();
41 }
42
43 }
44
45 }
46
思考下,這段代碼有什么致命問題么?(這里不追究業(yè)務(wù)邏輯處理的正確性以及細(xì)小的瑕疵)
.
..
...
現(xiàn)在回來。
我發(fā)現(xiàn)線程棧里面的線程都RUNNABLE在32行。
這一行看起來有什么問題呢?StringBuilder.toString()不是轉(zhuǎn)換成String么?Apache commons-lang里面的StringUtils.isNotEmpty使用也沒問題啊?
看代碼,人家的邏輯其實(shí)是判斷是否是第一行,如果不是第一行那么就增加一個(gè)換行符。
既然CPU在這里運(yùn)行,那么就說明這個(gè)地方一定存在非常耗費(fèi)CPU的操作,導(dǎo)致CPU非常繁忙,從而系統(tǒng)負(fù)載過高。
看詳細(xì)堆棧,其實(shí)CPU在進(jìn)行內(nèi)存的拷貝動(dòng)作。
看下面的源碼。
java.lang.StringBuilder.toString()
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
接著看java.lang.String的構(gòu)造函數(shù):// Create a copy, don't share the array
return new String(value, 0, count);
}
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.offset = 0;
this.count = count;
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.offset = 0;
this.count = count;
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
看出來了么?
問題的關(guān)鍵在于String構(gòu)造函數(shù)的最后一行,value并不是直接指向的,而是重新生成了一個(gè)新的字符串,使用系統(tǒng)拷貝函數(shù)進(jìn)行內(nèi)存復(fù)制。
java.util.Arrays.copyOfRange(char[], int, int)
public static char[] copyOfRange(char[] original, int from, int to) {
int newLength = to - from;
if (newLength < 0)
throw new IllegalArgumentException(from + " > " + to);
char[] copy = new char[newLength];
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
int newLength = to - from;
if (newLength < 0)
throw new IllegalArgumentException(from + " > " + to);
char[] copy = new char[newLength];
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
好了,再回頭看邏輯代碼32行。
if (StringUtils.isNotEmpty(buf.toString())) {
buf.append("\r\n");
}
這里有問題的地方在于每次循環(huán)一行的時(shí)候都生成一個(gè)新的字符串。也就是說如果HTTP返回的結(jié)果輸入流中有1000行的話,將額外生成1000個(gè)字符串(不算StringBuilder擴(kuò)容生成的個(gè)數(shù))。每一個(gè)字符串還比前一個(gè)字符串大。buf.append("\r\n");
}
我們來做一個(gè)簡單的測試,我們?cè)谠瓉淼拇a上增加幾行計(jì)數(shù)代碼。
int lines =0;
int count = 0;
int malloc = 0;
while ((line = br.readLine()) != null) {
lines++;
count+=line.length();
malloc += count;
if (StringUtils.isNotEmpty(buf.toString())) {
buf.append("\r\n");
}
buf.append(line);
}
System.out.println(lines+" -> "+count+" -> "+malloc);
我們記錄下行數(shù)lines以及額外發(fā)生的字符串拷貝大小malloc。int count = 0;
int malloc = 0;
while ((line = br.readLine()) != null) {
lines++;
count+=line.length();
malloc += count;
if (StringUtils.isNotEmpty(buf.toString())) {
buf.append("\r\n");
}
buf.append(line);
}
System.out.println(lines+" -> "+count+" -> "+malloc);
這是一次輸出的結(jié)果。
1169 -> 66958 -> 39356387
也就是1169行的網(wǎng)頁,一共是66958字節(jié)(65KB),結(jié)果額外生成的內(nèi)存大小(不算StringBuilder擴(kuò)容占用的內(nèi)存大?。?9356387字節(jié)(37.5MB)!!!試想一下,CPU一直頻繁于進(jìn)行內(nèi)存分配,機(jī)器的負(fù)載能不高么?我們線上服務(wù)器是2個(gè)CPU 16核,內(nèi)存24G的Redhat Enterprise Linux 5.5,負(fù)載居然達(dá)到幾十。這還是只有訪問量很低的時(shí)候。這就難怪服務(wù)頻繁宕機(jī)了。
事實(shí)上我們有非常完善和豐富的基于Apache commons-httpclient的封裝,操作起來也非常簡單。對(duì)于這種簡單的請(qǐng)求,只需要一條命令就解決了。
String platform.utils.HttpClientUtils.getResponse(String)
String platform.utils.HttpClientUtils.postResponse(String, Map<String, String>)
String platform.utils.HttpClientUtils.postResponse(String, Map<String, String>)
即使非要自造輪子,處理這種簡單的輸入流可以使用下面的代碼,就可以很好的解決問題。
InputStream in = 
ByteArrayOutputStream baos = new ByteArrayOutputStream(8192);
int len = -1;
byte[] b = new byte[8192];//8k
while ((len = in.read(b)) > 0) {
baos.write(b, 0, len);
}
baos.close();//ignore is ok
String response = new String(baos.toByteArray(), encoding);

ByteArrayOutputStream baos = new ByteArrayOutputStream(8192);
int len = -1;
byte[] b = new byte[8192];//8k
while ((len = in.read(b)) > 0) {
baos.write(b, 0, len);
}
baos.close();//ignore is ok
String response = new String(baos.toByteArray(), encoding);
當(dāng)然了,最后緊急處理線上問題最快的方式就是將有問題的代碼稍微變通下即可。
if (buf.length() > 0) {
buf.append("\r\n");
}
buf.append("\r\n");
}
這個(gè)問題非常簡單,只是想表達(dá)幾個(gè)觀點(diǎn):
- 團(tuán)隊(duì)更需要合作,按照規(guī)范來進(jìn)行。自造輪子不是不可以,但是生產(chǎn)環(huán)境還是要限于自己熟悉的方式。
- 即使非常簡單的代碼,也有可能有致命的陷阱在里面。善于思考才是王道。
- 學(xué)習(xí)開源的代碼和常規(guī)思路,學(xué)習(xí)解決問題的常規(guī)做法。這個(gè)問題其實(shí)非常簡單,熟悉輸入輸出流的人非常熟練就能解決問題。