線上服務器負載過高發生了報警,同事找我求救。
我看到機器的負載都超過20了,查看java進程線程棧,找到了出問題的代碼。
下面是其代碼片段,實際情況錯誤處理比這更壞。
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
思考下,這段代碼有什么
致命問題么?(這里不追究業務邏輯處理的正確性以及細小的瑕疵)
.
..
...
現在回來。
我發現線程棧里面的線程都RUNNABLE在32行。
這一行看起來有什么問題呢?StringBuilder.toString()不是轉換成String么?Apache commons-lang里面的StringUtils.isNotEmpty使用也沒問題啊?
看代碼,人家的邏輯其實是判斷是否是第一行,如果不是第一行那么就增加一個換行符。
既然CPU在這里運行,那么就說明這個地方一定存在非常耗費CPU的操作,導致CPU非常繁忙,從而系統負載過高。
看詳細堆棧,其實CPU在進行內存的拷貝動作。
看下面的源碼。
java.lang.StringBuilder.toString()
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
接著看java.lang.String的構造函數:
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);
}
看出來了么?
問題的關鍵在于String構造函數的最后一行,value并不是直接指向的,而是重新生成了一個新的字符串,使用系統拷貝函數進行內存復制。
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;
}
好了,再回頭看邏輯代碼32行。
if (StringUtils.isNotEmpty(buf.toString())) {
buf.append("\r\n");
}
這里有問題的地方在于每次循環一行的時候都生成一個新的字符串。也就是說如果HTTP返回的結果輸入流中有1000行的話,將額外生成1000個字符串(不算StringBuilder擴容生成的個數)。每一個字符串還比前一個字符串大。
我們來做一個簡單的測試,我們在原來的代碼上增加幾行計數代碼。
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);
我們記錄下行數lines以及額外發生的字符串拷貝大小malloc。
這是一次輸出的結果。
1169 -> 66958 -> 39356387
也就是1169行的網頁,一共是66958字節(65KB),結果額外生成的內存大小(不算StringBuilder擴容占用的內存大小)為39356387字節(37.5MB)!!!
試想一下,CPU一直頻繁于進行內存分配,機器的負載能不高么?我們線上服務器是2個CPU 16核,內存24G的Redhat Enterprise Linux 5.5,負載居然達到幾十。這還是只有訪問量很低的時候。這就難怪服務頻繁宕機了。
事實上我們有非常完善和豐富的基于Apache commons-httpclient的封裝,操作起來也非常簡單。對于這種簡單的請求,只需要一條命令就解決了。
String platform.utils.HttpClientUtils.getResponse(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);
當然了,最后緊急處理線上問題最快的方式就是將有問題的代碼稍微變通下即可。
if (buf.length() > 0) {
buf.append("\r\n");
}
這個問題非常簡單,只是想表達幾個觀點:
- 團隊更需要合作,按照規范來進行。自造輪子不是不可以,但是生產環境還是要限于自己熟悉的方式。
- 即使非常簡單的代碼,也有可能有致命的陷阱在里面。善于思考才是王道。
- 學習開源的代碼和常規思路,學習解決問題的常規做法。這個問題其實非常簡單,熟悉輸入輸出流的人非常熟練就能解決問題。
©2009-2014 IMXYLZ
|求賢若渴