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

createh53个月前 (01-07)技术教程38

本文预计阅读时间: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.nextBytes(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倍。


某团基础架构部搬砖工,专注于高并发、高可靠系统研发。本公号主要素材来自于个人日常工作、思考,偶尔也有前沿新闻、国外译文。关注我就对了= =

往期文章:

为什么面试必问线程状态?你的回答满分了吗

21个最常见Java并发编程面试题

线上服务宕机问题排查思路

吃瓜吃瓜!今年应届生有点牛

相关文章

Java 中的静态字段和静态方法

还记得我们写的第一个 Java 代码吗?public class Main { public static void main(String[] args) { System....

面试官:为什么java中静态方法不能调用非静态方法和变量?

这个可能很多人之前学习jvm的时候都会遇到,属于一个小问题,写这篇文章的原因是我在看java相关的面试题目中遇到的,因此顺手总结一下:一、例子我们先看效果:我们在静态方法main中调用非静态变量或者是...

详解Java中的静态代理和动态代理

代理是一种设计模式在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。目的:为其他...

教你如何在Java中更好的定义常量

关于Java中常量的话题似乎有很多困惑。有些人使用整数或字符串来定义常量,而另一些人则使用枚举。我还遇到了在它们自己的接口中定义的常量——在接口中,使用常量的类必须实现接口。这种策略通常被称为接口常量...

手把手教你Java反射的入门

一、什么是反射(Reflection)?简单的来讲就是可以从内存中直接获取到运行的class文件并且能够知道这个类的所有属性和方法;对于任意一个Java对象,都能够调用到它的任意一个方法或属性;这种动...

Java静态内部类、匿名内部类、成员式内部类和局部内部类

内部类可以是静态(static)的,可以使用 public、protected 和 private 访问控制符,而外部类只能使用 public,或者默认。成员式内部类在外部类内部直接定义(不在方法内部...