1. 程式人生 > >構建高效能服務(一)ConcurrentSkipListMap和連結串列構建高效能Java Memcached

構建高效能服務(一)ConcurrentSkipListMap和連結串列構建高效能Java Memcached

1、對常用功能建立快取模組 
2、網頁儘量靜態化 
3、使用單獨的圖片伺服器,降低伺服器壓力,使其不會因為圖片載入造成崩潰 
4、使用映象解決不同網路接入商和不同地域使用者訪問差異 
5、資料庫叢集圖表雜湊 
6、加強網路層硬體配置,硬的不行來軟的。 
7、終極辦法:負載均衡

場景

快取伺服器是網際網路後端服務中常用的基礎設施。

場景(一)圖片伺服器上儲存了大量圖片,為了提高圖片服務的吞吐量,希望把熱門的圖片載入到記憶體中。

場景(二)分散式儲存服務,為提高訪問吞吐,把大量的meta資訊儲存在記憶體中。

問題

但是使用Java語言開發快取服務,不可避免的遇到GC問題。無論使用ehcache是基於Map實現的快取,都會產生大量Minor GC無法回收的物件,最終導致CMS或Full GC,對系統吞吐造成影響。通過觀察這類服務產生的GC日誌,可以觀察到頻繁的CMS。這裡簡單介紹下CMS的過程即對系統的影響,CMS兩階段標記,減少stop the world的時間,如圖紅色部分為STW(stop the world)。


CMS日誌如下:

9.780: [GC [1 CMS-initial-mark: 507883K(507904K)] 521962K(521984K), 0.0029230 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

Total time for which application threads were stopped: 0.0029970 seconds

CMS第一次標記,stop the world。以下各個步驟則不影響Java Threads工作,即併發模式。

9.783: [CMS-concurrent-mark-start]

9.913: [CMS-concurrent-mark: 0.130/0.130 secs] [Times: user=0.26 sys=0.00, real=0.13 secs] 

9.913: [CMS-concurrent-preclean-start]

9.914: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

9.914: [CMS-concurrent-abortable-preclean-start]

9.914: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

Application time: 0.1317920 seconds

9.914: [GC[YG occupancy: 14079 K (14080 K)]9.914: [Rescan (parallel) , 0.0023580 secs]9.917: [weak refs processing, 0.0000060 secs] 

[1 CMS-remark: 507883K(507904K)] 521962K(521984K), 0.0024100 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 

Total time for which application threads were stopped: 0.0025420 seconds

Rescan為第二次標記,STW。

解決方案

構造和Memcached slab/chunk類似的Java記憶體管理方式。為快取的物件分配一組chunck,相同Size的Chunk合成一組Slab。初始slab設為100B,如果快取物件小於100B,放入100B slab,如果大於100B,小於 100B * Growth Factor = 1.27 = 127B,則放入127B slab。因此需要一個快速排序的資料結構來實現slab。我用ConcurrentSkipListMap實現slab,查詢插入時間複雜度和二叉樹一致,但實現更簡單。程式碼如下,

Java程式碼  收藏程式碼
  1. public boolean put(K key, byte[] value) {  
  2.     Map.Entry<Float, LocalMCSlab> entry = null;  
  3.     Float theSize = Float.valueOf(value.length);  
  4.     Stat.set("CacheSize=", ((getCurrentTotalCacheSize() / 1024f)) + "KB");  
  5.     // 以cache size為key,以chunks map為value,如果比這個cache size大得slab不存在,則建立一個  
  6.     // 否則,在大約cache size的slab中找一個最小的slab  
  7.     if((entry = slabs.tailMap(theSize).firstEntry()) == null) {  
  8.         Float floorKey = slabs.floorKey(theSize);  
  9.         float needSize = floorKey == null ? theSize : floorKey * scale;  
  10.         while(needSize < theSize) {  
  11.             needSize = needSize * scale;  
  12.         }  
  13.         LocalMCSlab<K, byte[]> slab = new LocalMCSlab<K, byte[]>((int) needSize);  
  14.         slab.put(key, value, false);  
  15.         slabs.put(needSize, slab);  
  16.         return true;  
  17.     }  
  18.     else {  
  19.         // 噹噹前全部cache size + 這個快取的size > 分配給整個cache的initSize時,則需使用LRU策略  
  20.         boolean isLRU = getCurrentTotalCacheSize() + theSize > initSize;  
  21.         entry.getValue().put(key, value, isLRU);  
  22.         return true;  
  23.     }  
  24. }  

每一個slab基於一個Map<K, V>實現。同時為實現LRU,實現了一個連結串列從頭插入從尾部取出,這樣連結串列尾部物件為last recent used,程式碼如下,

Java程式碼  收藏程式碼
  1. private static class LinkedListNode {  
  2.         public LinkedListNode previous;  
  3.         public LinkedListNode next;  
  4.         public Object object;  
  5.         /** 
  6.          * Constructs a new linked list node. 
  7.          * @param object   the Object that the node represents. 
  8.          * @param next     a reference to the next LinkedListNode in the list. 
  9.          * @param previous a reference to the previous LinkedListNode in the list. 
  10.          */  
  11.         public LinkedListNode(Object object, LinkedListNode next,  
  12.                               LinkedListNode previous) {  
  13.             this.object = object;  
  14.             this.next = next;  
  15.             this.previous = previous;  
  16.         }  
  17. ...  
  18. }  
  19. public static class LinkedList {  
  20.         /** 
  21.          * The root of the list keeps a reference to both the first and last 
  22.          * elements of the list. 
  23.          */  
  24.         private LinkedListNode head = new LinkedListNode("head"nullnull);  
  25.         /** 
  26.          * Creates a new linked list. 
  27.          */  
  28.         public LinkedList() {  
  29.             head.next = head.previous = head;  
  30.         }  
  31.         /** 
  32.          * Returns the first linked list node in the list. 
  33.          * 
  34.          * @return the first element of the list. 
  35.          */  
  36.         public LinkedListNode getFirst() {  
  37.             LinkedListNode node = head.next;  
  38.             if (node == head) {  
  39.                 return null;  
  40.             }  
  41.             return node;  
  42.         }  
  43.         /** 
  44.          * Returns the last linked list node in the list. 
  45.          * 
  46.          * @return the last element of the list. 
  47.          */  
  48.         public LinkedListNode getLast() {  
  49.             LinkedListNode node = head.previous;  
  50.             if (node == head) {  
  51.                 return null;  
  52.             }  
  53.             return node;  
  54.         }  
  55.         public LinkedListNode removeLast() {  
  56.             LinkedListNode node = head.previous;  
  57.             if (node == head) {  
  58.                 return null;  
  59.             }  
  60.             head.previous = node.previous;  
  61.             return node;  
  62.         }  
  63.         /** 
  64.          * Adds a node to the beginning of the list. 
  65.          * 
  66.          * @param node the node to add to the beginning of the list. 
  67.          */  
  68.         public LinkedListNode addFirst(LinkedListNode node) {  
  69.             node.next = head.next;  
  70.             head.next = node;  
  71.             node.previous = head;  
  72.             node.next.previous = node;  
  73.             return node;  
  74.         }  
  75. ...  
  76. }  

當LRU策略發生時,不再建立新的byte[],而是重寫最老的一個byte[],並把這個cache移動到連結串列頭部

Java程式碼  收藏程式碼
  1. if(removeLRU) {  
  2.     LinkedListNode lastNode = ageList.removeLast();  
  3.         Object lasthashKey = hashKeyMap.remove(lastNode.object);  
  4.         if(lasthashKey == null) {  
  5.             return false;  
  6.         }  
  7.         Stat.inc("eviction[" + this.chunkSize + "]");  
  8.         CacheObject<byte[]> data = map.get(lasthashKey);  
  9.         System.arraycopy(value, 0, data.object, 0, value.length);  
  10.         data.length = value.length;  
  11.         // update key / hashkey mapping  
  12.         hashKeyMap.put(key, lasthashKey);  
  13.         lastNode.object = key;  
  14.         ageList.addFirst(lastNode);  
  15. }  

注意使用了一個hashKeyMap,它的key是這次put的cache物件的key,value作為byte[]的key,在第一次建立byte[]時建立。這樣做也是為了不重新建立物件。

全部程式碼及測試類見附件。

測試

測試引數

java -Xms2g -Xmx2g -Xmn128m -XX:+UseConcMarkSweepGC -server -XX:SurvivorRatio=5 -XX:CMSInitiatingOccupancyFraction=80 -XX:+PrintTenuringDistribution -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime  -Xloggc:./gc.log test.TestMain

測試表現穩定,記憶體全部在Minor GC階段回收。

分配cache=1G,實際CacheSize==1048625.2KB;

各個slab chunk個數:

Chunk[100.0] count==5

Chunk[209758.16] count==1231

Chunk[165163.9] count==4938

總結

本來想寫一個虛擬碼的,後來覺得Java中還是有不少比較好的資料結構,比如ConcurrentSkipListMap和LRUMap還是想介紹給大家。因此就寫了這個比較粗糙的版本,基本可以反映出類似Memcached slab/chunk管理記憶體的方式。實際測試中表現也有一定收益。可以基於這個版本開發線上服務。但是這個實現裡面還沒有很好的處理併發問題,對記憶體的使用也有一些坑。使用中如果遇到問題,歡迎大家一起討論。

原文:http://maoyidao.iteye.com/blog/1559420