<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          [性能] IP 定位緩存該如何做?

          共 5487字,需瀏覽 11分鐘

           ·

          2021-03-03 00:43

          背景

           

          我們有一個(gè)站點(diǎn)服務(wù),暴露 HTTP 接口,對(duì)接外部流量,類似網(wǎng)關(guān)。上線后發(fā)現(xiàn) Full GC 頻率比較高,老年代內(nèi)存使用情況如下圖。從圖上可以看出平均 3個(gè)小時(shí)左右會(huì)進(jìn)行一次 Full GC;內(nèi)存逐步上升,說(shuō)明每次 YGC 都有一些對(duì)象熬過(guò)了多次 YGC 并且晉升老年代;另外還可以注意到一點(diǎn),夜里的時(shí)候增長(zhǎng)速度比白天慢,說(shuō)明和流量相關(guān),請(qǐng)求越多,增長(zhǎng)越快。那到底是什么對(duì)象可以不斷的晉升老年代,而同時(shí)又可以被 Full GC 掉。

           

          排查

           

          先通過(guò) jstat -gcutil 觀察一下 GC 情況:

            S0     S1     E     O      M     CCS   YGC     YGCT    FGC   FGCT     GCT  4.00  0.00  94.34  21.54 93.44  87.29  28738 1107.341    53  18.695 1126.036  0.00  4.70   4.01  21.60 93.44  87.29  28739 1107.378    53  18.695 1126.073 

          可以看出 YGC 時(shí)會(huì)有一些對(duì)象晉升老年代。并且注意到 YGC 耗時(shí) 37ms,也比較長(zhǎng)。

           

          使用命令 jmap -histo 可以看出堆上類的統(tǒng)計(jì)信息,包含實(shí)例數(shù)量和占用內(nèi)存。jmap -histo 有一個(gè)選項(xiàng)是live,如果添加了該參數(shù),只會(huì)統(tǒng)計(jì)存活的對(duì)象,代價(jià)是進(jìn)行一次 Full GC。如果沒(méi)有添加該選項(xiàng),就會(huì)統(tǒng)計(jì)當(dāng)前全部的對(duì)象。


          想看出哪些對(duì)象被回收了,可以在內(nèi)存占用比較高的時(shí)候,分別執(zhí)行兩次 jmap -histo,第一次統(tǒng)計(jì)全部的對(duì)象,第二次觸發(fā) Full GC 只統(tǒng)計(jì)存活的對(duì)象,然后對(duì)比兩份數(shù)據(jù)可以看出是哪些對(duì)象被回收了。


          在線上進(jìn)行上述操作,發(fā)現(xiàn) LocalCache 相關(guān)的類從 600 多萬(wàn)變成了 100萬(wàn)整。存活對(duì)象的統(tǒng)計(jì)信息如下:

           num    #instances         #bytes  class name----------------------------------------------   1:        40514       89987016  [B   2:      1574061       78521184  [C   3:      1000000       48000000 com.google.common.cache.LocalCache$StrongAccessEntry   4:      1568771       37650504  java.lang.String   5:      1000000       16000000 com.google.common.cache.LocalCache$StrongValueReference

          通過(guò)這個(gè)信息確定與使用 Guava Cache 相關(guān),看下代碼發(fā)現(xiàn)是 IP 定位緩存,服務(wù)會(huì)使調(diào)用方傳遞的 IP 調(diào)用公司內(nèi)部服務(wù)獲取地域編碼,并且使用 Guava Cache 進(jìn)行緩存,Cache創(chuàng)建代碼如下,可以看到最大容量是 100萬(wàn)。

            CacheBuilder.newBuilder()    .expireAfterAccess(1, TimeUnit.HOURS)    .maximumSize(1000000)    .recordStats()    .build(); 

          由于設(shè)置了 1 個(gè)小時(shí)的過(guò)期時(shí)間,所以不斷的會(huì)有緩存過(guò)期,產(chǎn)生可回收的對(duì)象。到這里已經(jīng)確認(rèn)了問(wèn)題出現(xiàn)在 IP 到地域編碼的緩存上,那 IP到地域編碼的緩存還有優(yōu)化空間嗎?

           

          IP 定位屬于基礎(chǔ)服務(wù),公司內(nèi)部有很多調(diào)用方,所以緩存也是一個(gè)通用的問(wèn)題。先找定位服務(wù)負(fù)責(zé)人請(qǐng)教一下緩存的經(jīng)驗(yàn),溝通未獲取到緩存方面更好的實(shí)踐,不過(guò)獲取到兩個(gè)很重要的信息,IP定位只用IP 的前三段,就是說(shuō)1.12.36.0~1.26.36.255 都會(huì)定位到同一個(gè)地域編碼;另外一個(gè)信息是國(guó)內(nèi)記錄數(shù)不到百萬(wàn),這說(shuō)明全量緩存是可行的。

           

          解決方案

           

          這個(gè)時(shí)候修改 Guava Cache 使用 IP 前三段作為 Key,可以實(shí)現(xiàn)全量緩存,可以想到結(jié)果就是命中率大幅提升,性能得到改進(jìn),但是還是會(huì)有垃圾產(chǎn)生。能不能優(yōu)化的更徹底?

           

          只使用三段,每段取值 256 ,那全量就是 256 * 256 * 256 = 16,777,216。可以創(chuàng)建一個(gè)三維 int 型數(shù)組,來(lái)存儲(chǔ) IP到地域編碼的映射,占用內(nèi)存為 64M,還是可以的。

           

          這時(shí)還有一個(gè)問(wèn)題就是過(guò)期時(shí)間怎么做,IP 到地域編碼每天會(huì)有少量的調(diào)整,所以要實(shí)現(xiàn)一個(gè)過(guò)期機(jī)制。考慮到該過(guò)期時(shí)間為了保證緩存可以更新,所以可以直接使用寫入時(shí)間,另外考慮到每天的調(diào)整很少,可以把過(guò)期時(shí)間設(shè)置的稍微長(zhǎng)一點(diǎn),這里給了四個(gè)小時(shí)。將三維 int 型數(shù)組調(diào)整為三維 long 型數(shù)組,高 32 位存儲(chǔ)寫入時(shí)間,低 32位存儲(chǔ)地域編碼。占用 128 M 內(nèi)存,由于大數(shù)組是 Free GC 的所以 Full GC 的問(wèn)題可以解決,另外就是數(shù)組的查找和讀寫性能都很好,所以對(duì)性能也有好處。這是一個(gè)空間換時(shí)間的實(shí)現(xiàn)。

           

          最終緩存的實(shí)現(xiàn)如下:

          public class FastIpLocalCache {   /**   * 過(guò)期時(shí)間,單位秒   */  private int expireTime;  private long[][][] store = newlong[256][256][256];  private int baseTime = (int)(System.currentTimeMillis() / 1000);   /**   * 過(guò)期時(shí)間,單位秒   *   * @param expireTime   */  public FastIpLocalCache(int expireTime) {    if (expireTime <= 0 || expireTime >24 * 60 * 60) {      throw newIllegalArgumentException("expireTime 不合法");    }    this.expireTime = expireTime;  }   /**   * 讀取   *   * @param ip   * @return   */  public Integer get(String ip) {    if (ip == null) {      return null;    }    // IP 轉(zhuǎn)數(shù)字    short[] s3 = toS3(ip);    if (s3 == null) {      return null;    }    // 讀取    long value = store[s3[0]][s3[1]][s3[2]];    if (value <= 0) {      return null;    }    // 獲取當(dāng)前時(shí)間    int currentTimeSecond = (int)(System.currentTimeMillis() / 1000);    // 獲取寫入時(shí)間    int writeTime = (int) (value >> 32);    // 判斷超時(shí)    if (currentTimeSecond - baseTime -writeTime > expireTime) {      return null;    }    // 獲取地域    int local = (int) (value & 0x7fffffff);    return local;  }   /**   * 保存   *   * @param ip   * @param local   */  public void put(String ip, Integer local) {    // IP 轉(zhuǎn)數(shù)字    short[] s3 = toS3(ip);    if (s3 == null) {      return;    }    // 獲取當(dāng)前時(shí)間    long current = System.currentTimeMillis() /1000 - baseTime;    // 拼接時(shí)間和地域    long value = current << 32 | local;    // 保存    store[s3[0]][s3[1]][s3[2]] = value;  }   /**   * IP 轉(zhuǎn)數(shù)字,只轉(zhuǎn)前 3 段   *   * @param ip   * @return   */  public short[] toS3(String ip) {    if (ip == null) {      return null;    }    short[] result = new short[3];    int index = 0;    int size = ip.length();    short temp = 0;    for (int i = 0; i < size; i++) {      char c = ip.charAt(i);      if (c >= '0' && c <= '9') {        temp = (short) (temp * 10 + (c - '0'));      } else if (c == '.' || (i + 1) == size) {        if (temp > 255 || temp < 0) {          return null;        }        result[index] = temp;        index++;        if (index == 3) {          break;        }        temp = 0;      } else {        return null;      }    }    if (index != 3) {      return null;    }    return result;  }}

           

          效果

           

          上線后老年代使用空間如下圖,從圖上可以看出 Full GC 頻率已經(jīng)從 3個(gè)小時(shí)左右變成了超過(guò) 24 小時(shí)一次,效果很明顯。還會(huì)有晉升是因?yàn)槲覀冞€有一些埋點(diǎn)統(tǒng)計(jì)等。

           

          再使用 jstat -gcutil 觀察一些 GC 情況:

            S0    S1     E      O     M     CCS    YGC    YGCT    FGC    FGCT    GCT  0.00  2.55  96.28  16.74 91.98  84.94  18063 309.983     4    0.797 310.780  2.51  0.00   5.92  16.75 91.98  84.94  18064 309.997     4    0.797 310.794

          可以看到 YGC 耗時(shí)變成了 14ms,相比最開(kāi)始的 37ms 也有了大幅的提升。


          全量緩存以及更長(zhǎng)的緩存時(shí)間也大幅減少了對(duì) IP 定位服務(wù)的調(diào)用量,從 12000 QPS 下降到 2400 QPS。


          更高效的緩存定位方案也會(huì)減少服務(wù)的平均耗時(shí),從平均 11 ms,下降到平均 10ms。雖然只有 1ms,但是比例接近 10%。

           

           

           

          推薦閱讀

          Top99 超時(shí)排查思路


          如何快速判斷一個(gè)用戶是否訪問(wèn)過(guò)我們的 APP?


          一起刷 leetcode 之螺旋矩陣(頭條和美團(tuán)真題)


          一起刷 leetcode 之螺旋矩陣(頭條和美團(tuán)真題)


          原創(chuàng)|如果懂了HashMap這兩點(diǎn),面試就沒(méi)問(wèn)題了


          面試官:如何用最少的老鼠試出有毒的牛奶?


          cpu使用率過(guò)高和jvm old占用過(guò)高排查過(guò)程


          老年代又占用100%了,順便發(fā)現(xiàn)了vertx-redis-client 的bug


          KafkaProducer源碼分析


          Kafka服務(wù)端之網(wǎng)絡(luò)層源碼分析


          Redis 的過(guò)期策略是如何實(shí)現(xiàn)的?


          原創(chuàng)|面試官:Java對(duì)象一定分配在堆上嗎?


          ThreadPoolExecutor 線程池"源碼分析"


          類加載器知識(shí)點(diǎn)吐血整理


          三面阿里被掛,幸獲內(nèi)推名額,歷經(jīng) 5 面終獲口碑 offer


          原創(chuàng)|ES廣告倒排索引架構(gòu)演進(jìn)與優(yōu)化


          廣告倒排索引架構(gòu)與優(yōu)化


          頻繁FGC的真兇原來(lái)是它


          原創(chuàng)|這道面試題,大部分人都答錯(cuò)了


          同事:把"重試"抽象出來(lái)做個(gè)工具類吧

          瀏覽 70
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91精品久久久久久久不卡 | 伊人久久青青操 | 成人淫色免费视频 | 一级乱伦视频 | 欧美黑人黄片 |