`

构建高性能服务(一)ConcurrentSkipListMap和链表构建高性能Java Memcached

 
阅读更多

 

场景

缓存服务器是互联网后端服务中常用的基础设施。

场景(一)图片服务器上存储了大量图片,为了提高图片服务的吞吐量,希望把热门的图片加载到内存中。

场景(二)分布式存储服务,为提高访问吞吐,把大量的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,查找插入时间复杂度和二叉树一致,但实现更简单。代码如下,

 

public boolean put(K key, byte[] value) {
	Map.Entry<Float, LocalMCSlab> entry = null;
	Float theSize = Float.valueOf(value.length);
	Stat.set("CacheSize=", ((getCurrentTotalCacheSize() / 1024f)) + "KB");
	
	// 以cache size为key,以chunks map为value,如果比这个cache size大得slab不存在,则创建一个
	// 否则,在大约cache size的slab中找一个最小的slab
	if((entry = slabs.tailMap(theSize).firstEntry()) == null) {
		Float floorKey = slabs.floorKey(theSize);
		float needSize = floorKey == null ? theSize : floorKey * scale;
				
		while(needSize < theSize) {
			needSize = needSize * scale;
		}
			
		LocalMCSlab<K, byte[]> slab = new LocalMCSlab<K, byte[]>((int) needSize);
		slab.put(key, value, false);
		slabs.put(needSize, slab);
		return true;
	}
	else {
		// 当当前全部cache size + 这个缓存的size > 分配给整个cache的initSize时,则需使用LRU策略
		boolean isLRU = getCurrentTotalCacheSize() + theSize > initSize;
		entry.getValue().put(key, value, isLRU);
		return true;
	}
}
 

 

每一个slab基于一个Map<K, V>实现。同时为实现LRU,实现了一个链表从头插入从尾部取出,这样链表尾部对象为last recent used,代码如下,

 

private static class LinkedListNode {
        public LinkedListNode previous;
        public LinkedListNode next;
        public Object object;
        /**
         * Constructs a new linked list node.
         * @param object   the Object that the node represents.
         * @param next     a reference to the next LinkedListNode in the list.
         * @param previous a reference to the previous LinkedListNode in the list.
         */
        public LinkedListNode(Object object, LinkedListNode next,
                              LinkedListNode previous) {
            this.object = object;
            this.next = next;
            this.previous = previous;
        }
...

}

public static class LinkedList {

        /**
         * The root of the list keeps a reference to both the first and last
         * elements of the list.
         */
        private LinkedListNode head = new LinkedListNode("head", null, null);

        /**
         * Creates a new linked list.
         */
        public LinkedList() {
            head.next = head.previous = head;
        }

        /**
         * Returns the first linked list node in the list.
         *
         * @return the first element of the list.
         */
        public LinkedListNode getFirst() {
            LinkedListNode node = head.next;
            if (node == head) {
                return null;
            }
            return node;
        }

        /**
         * Returns the last linked list node in the list.
         *
         * @return the last element of the list.
         */
        public LinkedListNode getLast() {
            LinkedListNode node = head.previous;
            if (node == head) {
                return null;
            }
            return node;
        }
        
        public LinkedListNode removeLast() {
            LinkedListNode node = head.previous;
            if (node == head) {
                return null;
            }
            
            head.previous = node.previous;
            return node;
        }

        /**
         * Adds a node to the beginning of the list.
         *
         * @param node the node to add to the beginning of the list.
         */
        public LinkedListNode addFirst(LinkedListNode node) {
        	node.next = head.next;
            head.next = node;
            node.previous = head;
            node.next.previous = node;
            return node;
        }
...
}

 

当LRU策略发生时,不再创建新的byte[],而是重写最老的一个byte[],并把这个cache移动到链表头部

 

if(removeLRU) {
 	LinkedListNode lastNode = ageList.removeLast();
       	Object lasthashKey = hashKeyMap.remove(lastNode.object);
        	
       	if(lasthashKey == null) {
       		return false;
       	}
        	
       	Stat.inc("eviction[" + this.chunkSize + "]");
       	CacheObject<byte[]> data = map.get(lasthashKey);
       	System.arraycopy(value, 0, data.object, 0, value.length);
       	data.length = value.length;
        	
       	// update key / hashkey mapping
       	hashKeyMap.put(key, lasthashKey);
        	
       	lastNode.object = key;
       	ageList.addFirst(lastNode);
}

 

注意使用了一个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管理内存的方式。实际测试中表现也有一定收益。可以基于这个版本开发线上服务。但是这个实现里面还没有很好的处理并发问题,对内存的使用也有一些坑。使用中如果遇到问题,欢迎大家一起讨论。

 

  • 大小: 10.2 KB
分享到:
评论
5 楼 hn5092 2016-07-19  
楼主 为什么是1.27F 求解释
4 楼 maoyidao 2012-12-16  
Hi flysnail,你说的haskKey是指的是hashKeyMap还是slabs的hashKey?
3 楼 flysnail 2012-12-09  
flysnail 写道
楼主,有一处不解,请问haskKey作用是?

2 楼 flysnail 2012-12-06  
楼主,有一处不解,请问haskKey作用是?
1 楼 lanseshuchong 2012-09-16  
楼主你厉害啊。。我研究看看

相关推荐

    Java里多个Map的性能比较(TreeMap、HashMap、ConcurrentSkipListMap)

    在Java编程中,Map接口是用于存储键值对的数据结构,而Java提供了多种Map的实现,包括TreeMap、HashMap和ConcurrentSkipListMap。本文主要比较了这三种Map的性能,尤其是在插入和查找操作上的效率。 1. **TreeMap**...

    Java concurrency集合之ConcurrentSkipListMap_动力节点Java学院整理

    Java concurrency集合之ConcurrentSkipListMap_动力节点Java学院整理,动力节点口口相传的Java黄埔军校

    JAVA高并发包介绍

    ConcurrentSkipListMap通过一种分层的多级链表来维护其内部结构,这使得它在高并发环境下不仅能够保持良好的读写性能,同时还能保持元素的有序性。 在介绍ConcurrentSkipListMap的实现原理之前,我们需要了解跳表的...

    Java 2 高级程序设计百事通

    理解线程的生命周期、同步机制(如synchronized关键字、wait/notify、Lock接口)以及并发工具类(如ExecutorService、Semaphore、CyclicBarrier)对于构建高性能应用至关重要。 2. **网络编程**:Java 2提供了丰富...

    java入门,面试圣经

    Java是一种广泛使用的面向对象编程语言,它具有跨平台、面向对象、分布式、解释执行、健壮安全、高性能、多线程以及可移植性等特点。在深入学习Java时,基础语法、面向对象、IO操作、线程处理和网络编程等方面是必须...

    ConcurrentSkipListMap源码1

    跳表是一种高效的动态数据结构,它在有序链表的基础上,通过构建多级索引来实现快速的查找、插入和删除操作。跳表利用概率统计的方法,使得查找效率接近于二分查找,从而在性能上优于传统的有序链表。 1. **跳表的...

    java高并发系统设计

    在Java领域,高并发系统设计是一项至关重要的技能,它涉及到如何构建能够同时处理大量请求的高效应用程序。在本文中,我们将深入探讨Java高并发系统设计的相关知识点,包括基础概念、核心技术以及最佳实践。 1. **...

    Java并发编程事件 mobi kindle版

    6. **并发设计模式**:介绍了一些在并发编程中常用的模式,如生产者消费者模型、读写锁策略、双检查锁定等,帮助开发者构建可扩展和高性能的并发程序。 7. **JVM与并发**:探讨了JVM对并发的支持,包括内存模型、...

    java 开发工具 jdk 1.4 免安装版

    4. **集合框架的改进**:包括`TreeMap`和`TreeSet`的性能提升,以及`HashMap`和`HashSet`的线程不安全版本`ConcurrentHashMap`和`ConcurrentSkipListMap`的引入,为多线程环境下提供了更好的数据结构支持。...

    java-api-1.8.zip

    Java API 1.8是Java开发的关键组成部分,它包含了丰富的类库和接口,为开发者提供了构建各种应用程序的基础。这个压缩包“java-api-1.8.zip”很可能包含了Java SE(标准版)8的所有公共API文档,这些文档详细阐述了...

    Java——JUC

    - 一个基于`ExecutorService`的增强版服务,用于管理一组异步任务的执行和结果收集。 8. **ScheduledExecutorService** - 支持定时及周期性任务执行的接口,如`ScheduledThreadPoolExecutor`,可以用来执行一次性...

    Java 常见并发容器总结

    Java 常见并发容器总结 JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。 - **`ConcurrentHashMap`** : 线程安全的 `HashMap` - **`CopyOnWriteArrayList`** : 线程安全的 `List`,在读多写少的场合性能...

    java1.6zhongwenban

    Java 1.6 版本,也被称为 Java SE 6,是 Sun Microsystems(后被 Oracle 收购)推出的一个重要版本,它在Java平台上引入了一系列的新特性、优化和改进,旨在提升开发效率、性能和安全性。这个压缩包文件"java1.6...

    java se 1.8 中文 api

    3. **流(Stream) API**:这是一个用于处理集合的新API,支持串行和并行数据处理,使得代码更简洁,性能更高。流API可以与lambda表达式结合,进行过滤、映射、减少等操作。 4. **日期和时间API的改进**:`java.time`...

    Java api 1.8 中文 帮助文档

    Java API 1.8中文帮助文档是Java开发者的重要参考资料,它包含了Java开发工具包(JDK)1.8版本的所有核心类库、接口和方法的详细说明。这些文档旨在帮助程序员理解和使用Java语言的各种功能,从而高效地进行软件开发...

    Java后端体系高级面试题

    Java后端体系高级面试题是针对Java开发人员的深度技术面试准备材料,涵盖了广泛的Java...这些知识点是Java后端开发人员在面试过程中可能会遇到的,深入了解并掌握这些内容,将有助于提升个人的技术水平和面试竞争力。

    Java方法排序五百万数据

    在实际应用中,我们还可以利用Java的并发库,如Fork/Join框架和ConcurrentSkipListMap等,它们提供了一种更高级的并行处理能力,可以有效应对大规模数据的排序。 总的来说,Java处理五百万数据的排序需要结合高效的...

    java-collection-all-in-one.pdf

    EnumMap是一种专门为枚举键设计的紧凑型Map实现,它比HashMap提供更高的性能和空间效率;EnumSet是为枚举类型元素设计的Set实现,高效且紧凑。SkipList(跳表)是一种可以在对数时间内插入、删除和搜索的有序数据...

Global site tag (gtag.js) - Google Analytics