xylz,imxylz

          關(guān)注后端架構(gòu)、中間件、分布式和并發(fā)編程

             :: 首頁 :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理 ::
            111 隨筆 :: 10 文章 :: 2680 評論 :: 0 Trackbacks
          線上服務(wù)器負(fù)載過高發(fā)生了報警,同事找我求救。
          我看到機器的負(fù)載都超過20了,查看java進(jìn)程線程棧,找到了出問題的代碼。

          下面是其代碼片段,實際情況錯誤處理比這更壞。
           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 

          思考下,這段代碼有什么致命問題么?(這里不追究業(yè)務(wù)邏輯處理的正確性以及細(xì)小的瑕疵)
          .
          ..
          ...
          現(xiàn)在回來。
          我發(fā)現(xiàn)線程棧里面的線程都RUNNABLE在32行。
          這一行看起來有什么問題呢?StringBuilder.toString()不是轉(zhuǎn)換成String么?Apache commons-lang里面的StringUtils.isNotEmpty使用也沒問題啊?
          看代碼,人家的邏輯其實是判斷是否是第一行,如果不是第一行那么就增加一個換行符。

          既然CPU在這里運行,那么就說明這個地方一定存在非常耗費CPU的操作,導(dǎo)致CPU非常繁忙,從而系統(tǒng)負(fù)載過高。
          看詳細(xì)堆棧,其實CPU在進(jìn)行內(nèi)存的拷貝動作。
          看下面的源碼。
          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ù):
              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);
              }

          看出來了么?
          問題的關(guān)鍵在于String構(gòu)造函數(shù)的最后一行,value并不是直接指向的,而是重新生成了一個新的字符串,使用系統(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;
              }

          好了,再回頭看邏輯代碼32行。
          if (StringUtils.isNotEmpty(buf.toString())) {
              buf.append("\r\n");
          }
          這里有問題的地方在于每次循環(huán)一行的時候都生成一個新的字符串。也就是說如果HTTP返回的結(jié)果輸入流中有1000行的話,將額外生成1000個字符串(不算StringBuilder擴容生成的個數(shù))。每一個字符串還比前一個字符串大。


          我們來做一個簡單的測試,我們在原來的代碼上增加幾行計數(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。
          這是一次輸出的結(jié)果。
          1169 -> 66958 -> 39356387
          也就是1169行的網(wǎng)頁,一共是66958字節(jié)(65KB),結(jié)果額外生成的內(nèi)存大小(不算StringBuilder擴容占用的內(nèi)存大小)為39356387字節(jié)(37.5MB)!!!
          試想一下,CPU一直頻繁于進(jìn)行內(nèi)存分配,機器的負(fù)載能不高么?我們線上服務(wù)器是2個CPU 16核,內(nèi)存24G的Redhat Enterprise Linux 5.5,負(fù)載居然達(dá)到幾十。這還是只有訪問量很低的時候。這就難怪服務(wù)頻繁宕機了。

          事實上我們有非常完善和豐富的基于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);

          當(dāng)然了,最后緊急處理線上問題最快的方式就是將有問題的代碼稍微變通下即可。
              if (buf.length() > 0) {
                  buf.append("\r\n");
              }


          這個問題非常簡單,只是想表達(dá)幾個觀點:
          • 團隊更需要合作,按照規(guī)范來進(jìn)行。自造輪子不是不可以,但是生產(chǎn)環(huán)境還是要限于自己熟悉的方式。
          • 即使非常簡單的代碼,也有可能有致命的陷阱在里面。善于思考才是王道。
          • 學(xué)習(xí)開源的代碼和常規(guī)思路,學(xué)習(xí)解決問題的常規(guī)做法。這個問題其實非常簡單,熟悉輸入輸出流的人非常熟練就能解決問題。


          ©2009-2014 IMXYLZ |求賢若渴
          posted on 2012-03-15 18:30 imxylz 閱讀(11516) 評論(16)  編輯  收藏 所屬分類: J2EE

          評論

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

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

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

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

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

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

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

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

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

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

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

          大俠能否講一下這個是怎樣發(fā)現(xiàn)的?  回復(fù)  更多評論
            

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

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

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

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

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


          ©2009-2014 IMXYLZ
          主站蜘蛛池模板: 南皮县| 逊克县| 修文县| 曲松县| 德兴市| 喜德县| 丰原市| 长沙市| 保康县| 乐昌市| 郴州市| 漳平市| 武强县| 门头沟区| 凤冈县| 上杭县| 通渭县| 茂名市| 通化县| 乐业县| 新密市| 三江| 亳州市| 文安县| 寿宁县| 广南县| 杭锦后旗| 淮阳县| 彩票| 文水县| 汤阴县| 铅山县| 兴国县| 张家港市| 虞城县| 长泰县| 鸡东县| 阳曲县| 云林县| 衡东县| 三都|