您的位置 首页 java

Java避坑指南——高并发场景下的性能隐藏杀手“UUID”

本文预计阅读时间:10分钟


最近开发了一个新需求,要求对项目做压测,很奇怪,单机达到20万QPS之后就怎么也上不去了,增加线程之后,性能反而下降的厉害。经过一番分析,发现处理线程会block在UUID的一个地方,跟踪源码才发现了这个大坑。

先来介绍下它吧,UUID (Universally Unique Identifier) 大家都很熟悉,它是由一组32位数的16进制数字所构成,采用如下编码规则

1-8位采用系统时间,在系统时间上精确到毫秒级保证时间上的惟一性;9-16位采用底层的IP地址,在服务器集群中的惟一性;17-24位采用当前对象的HashCode值,在一个内部对象上的惟一性;25-32位采用调用方法的一个随机数,在一个对象内的毫秒级的惟一性。 

通过以上4种策略能够保证在整个分布式系统中的惟一性。

使用非常简单,Java提供了简易的api。

    public String createUUID() {
        UUID uuid = UUID.randomUUID();
        return uuid.toString();
    } 

但是,就是这短短的两句,成为了系统的性能瓶颈。

我们先来看下压测。。

机器配置

CPU: 16核 2.20GHz 
Memory: 16G 
JDK: 1.8 
VM: CentOS 6 
GC: G1 

测试代码

 while (true) {
     UUID.randomUUID();
 } 

压测结果

很奇怪,常理来说,增大线程数都会带来性能的提升,但是在UUID这里行不通了。使用jstack发现,线程都block在了这里

test-thread    BLOCKED    blocked on java.security.SecureRandom@3b194842 owned by "test- Thread " Id=358
at java.security.SecureRandom.next bytes (SecureRandom.java:468)
at java.util.UUID.randomUUID(UUID.java:145)
.....
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:617)
at java.lang.Thread.run (Thread.java:745) 

看到这里,基本已经知道问题了,UUID.randomUUID()是一个静态方法,内部使用一个静态成员变量SecureRandom,在SecureRandom内部有一个方法级别的锁,所以在锁竞争非常强烈的时候性能会下降的特别厉害。

public static UUID randomUUID() {
    SecureRandom ng = Holder.numberGenerator;

    byte[] randomBytes = new byte[16];
    ng.nextBytes(randomBytes);
    randomBytes[6]  &= 0x0f;  /* clear version        */    randomBytes[6]  |= 0x40;  /* set to version 4     */    randomBytes[8]  &= 0x3f;  /* clear variant        */    randomBytes[8]  |= 0x80;  /* set to IETF variant  */    return new UUID(randomBytes);
} 

我们再看下SecureRandom的Java doc

 * Note: Depending on the implementation, the {@code generateSeed} and * {@code nextBytes} methods may block as entropy is being gathered, * for example, if they need to read from /dev/random on various Unix-like * operating systems. 

意思是SecureRandom的generateSeed和nextBytes这两个方法可能会block,依赖随机数的产生,如果随机数不够了,它有可能就会堵塞在那边。比如随机数的产生是读取unix类系统的/dev/random文件。为什么是比如呢?事实上SecureRandom是使用SPI做扩展的。

public void nextBytes(byte[] bytes) {
   secureRandomSpi.engineNextBytes(bytes);
} 

那么/dev/random又是什么呢?Unix-Like或者Linux系统有两个随机伪设备:/dev/random和/dev/urandom,他们提供永不为空的随机字节数据流。/dev/random和/dev/urandom是Linux系统中提供的随机伪设备,这两个设备的任务,是提供永不为空的随机字节数据流。

/dev/random的random pool依赖于 系统中断 ,因此在系统的中断数不足时,/dev/random设备会一直封锁,尝试读取的进程就会进入等待状态,直到系统的中断数充分够用, /dev/random设备可以保证数据的随机性。

而/dev/urandom不依赖系统的中断,也就不会造成进程忙等待,但是数据的随机性比/dev/random低,在不是对随机性要求特别高的场景下,可以提供更高的性能

在java启动项中增加-Djava.security.egd=file:/dev/./urandom 配置项之后,再测试一下, 性能提升了1.5倍。

往期文章:

文章来源:智云一二三科技

文章标题:Java避坑指南——高并发场景下的性能隐藏杀手“UUID”

文章地址:https://www.zhihuclub.com/171972.shtml

关于作者: 智云科技

热门文章

网站地图