xylz,imxylz

          關注后端架構、中間件、分布式和并發編程

             :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理 ::
            111 隨筆 :: 10 文章 :: 2680 評論 :: 0 Trackbacks
          線上服務器負載過高發生了報警,同事找我求救。
          我看到機器的負載都超過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 |求賢若渴
          posted on 2012-03-15 18:30 imxylz 閱讀(11515) 評論(16)  編輯  收藏 所屬分類: J2EE

          評論

          # re: 一次簡單卻致命的錯誤[未登錄] 2012-03-15 22:06 Joey
          if (buf.toString().length() > 0) {
          buf.append("\r\n");
          }
          既然是toString() 方法 每個循環 都會生成新的String對象引起的為什么還要像上面那樣解決?  回復  更多評論
            

          # re: 一次簡單卻致命的錯誤 2012-03-15 22:10 xylz
          @Joey
          謝謝提醒,寫錯了。
          直接用文本復制粘貼手動修改錯了。去掉toString()就可以了。  回復  更多評論
            

          # re: 一次簡單卻致命的錯誤[未登錄] 2012-03-16 09:40 changedi
          嗯,在技術汪洋中,我們很容易陷入而忽略了那些基本的思考~  回復  更多評論
            

          # re: 一次簡單卻致命的錯誤 2012-03-16 11:03 小明
          寫出 if (StringUtils.isNotEmpty(buf.toString()))的程序員應該裁掉  回復  更多評論
            

          # re: 一次簡單卻致命的錯誤[未登錄] 2012-03-16 11:27 kevin
          @小明
          同意,為什么總有人喜歡寫StringUtils之類的東西,我在很多項目里都看到過。  回復  更多評論
            

          # re: 一次簡單卻致命的錯誤 2012-03-16 18:35 Saga
          @changedi
          確實,深有感受  回復  更多評論
            

          # re: 一次簡單卻致命的錯誤 2012-03-17 15:17 bescq
          在三分鐘內看出這個代碼問題,并給與解決方案的人,在貴司能給個什么級別?大概值多少米?   回復  更多評論
            

          # re: 一次簡單卻致命的錯誤 2012-03-19 19:36 路過
          @bescq
          8K-1W  回復  更多評論
            

          # re: 一次簡單卻致命的錯誤 2012-03-22 00:14 iamct
          親,偶爾瞅了一眼排行。第6:) 另外,為啥不把那個判斷放在循環外面呢?  回復  更多評論
            

          # re: 一次簡單卻致命的錯誤 2012-03-27 17:34 new comer
          "我發現線程棧里面的線程都RUNNABLE在32行。",大俠,能否講一下這個是怎樣發現的?  回復  更多評論
            

          # re: 一次簡單卻致命的錯誤 2012-03-27 17:35 new comer
          "我發現線程棧里面的線程都RUNNABLE在32行。"

          大俠能否講一下這個是怎樣發現的?  回復  更多評論
            

          # re: 一次簡單卻致命的錯誤[未登錄] 2012-03-30 19:23 y
          @new comer
          應該是線程棧 打出來 一眼掃過去 一部分線程正在執行的代碼都是一個地方  回復  更多評論
            

          # re: 一次簡單卻致命的錯誤 2012-12-20 11:53 dohkoos
          if (StringUtils.isNotEmpty(buf.toString()))
          寫出這斷代碼是對StringUtils中的方法還不熟悉,isNotEmpty中的參數是CharSequence類型,不需要轉換的。
            回復  更多評論
            

          # re: 一次簡單卻致命的錯誤 2015-01-18 12:40 風車
          同意@小明
            回復  更多評論
            

          # re: 一次簡單卻致命的錯誤 2015-06-16 09:49 高帆
          請交大俠!查看java線程是怎么看的  回復  更多評論
            

          # re: 一次簡單卻致命的錯誤 2015-11-16 11:52 shaw
          @高帆
          jstack 打印出來 線程棧信息,能看到 線程棧目前運行在那個地方,等待什么資源  回復  更多評論
            


          ©2009-2014 IMXYLZ
          主站蜘蛛池模板: 怀柔区| 白朗县| 枝江市| 佛坪县| 奉节县| 赤城县| 吉林省| 阿图什市| 许昌市| 江安县| 江川县| 清远市| 东兰县| 虎林市| 新昌县| 桑日县| 新津县| 清水河县| 克什克腾旗| 新竹市| 贵港市| 望都县| 醴陵市| 上饶市| 嘉峪关市| 禹城市| 剑阁县| 沧州市| 建瓯市| 会宁县| 泽州县| 武冈市| 房产| 右玉县| 诸暨市| 东海县| 酒泉市| 丹巴县| 冕宁县| 淮南市| 牙克石市|